StatifierUI.Trace.Normalizer (StatifierUI v0.1.0)

Copy Markdown View Source

The pure mapping from Statifier.Effect.t() (and the session's own lifecycle messages) to StatifierUI.Trace.Message.t(), matching docs/wire-format.md field for field. No process, no session, no %Statifier.Machine{} - the whole vocabulary is testable from a struct literal.

Value handling

Anything from the engine that occupies a value position - Event.data, Log.value, Trace.Done.donedata, Effect.Done.donedata, Send.data, SendDelayed.data, Invoke.params, Invoke.content, DatamodelChange.new_value, DatamodelChange.prior_value - goes through StatifierUI.Value.encode/1; its {:error, _} is propagated, never rescued. Everything structural is reduced first (decision 6): MapSets to sorted lists (configuration/1), atoms to "kind"-tagged objects (origin/1, owner/1). This ordering matters - StatifierUI.Value.encode/1 rejects any term outside predicator's closed value domain with {:error, {:unsupported_value, term}}, so handing it a raw tuple or a bare atom would produce a wrong error (:unsupported_value instead of a normalizer-specific one) rather than the structural, purpose-built handling this module gives those shapes.

Absence

_event.data keeps ADR-0005's three-way rule unchanged: :undefined omits the key, nil is present as JSON null, %{} is present as {} (put_defined/3). DatamodelChange.new_value and DatamodelChange.prior_value follow the same three-way rule, because they share _event.data's property: the engine spells "unbound" as :undefined there (ADR-0037), so nil genuinely means a stored null. Every other nullable field in this vocabulary has no engine-side way to distinguish "no value" from "a genuinely null value" - Statifier.Effect.Log.value and the two donedata fields default to plain nil for "nothing here", never :undefined - so put_present/3 and put_value/3 generalize the rule (decision 5) by treating nil and :undefined alike as absence for those fields, applied uniformly instead of remembered clause by clause.

Summary

Types

What normalize/2 needs beyond the effect itself: the emitting session and the seq to stamp.

Everything normalize/2 accepts: a bare effect, the {:effect, _} wrapper the session sends its subscribers, and the two lifecycle messages. The outer {:statifier, session_id, _} envelope is never accepted here - unwrapping it is the subscriber's job, and keeping it out is what makes this module testable from a struct literal.

Functions

Normalizes one input() into a %Message{} stamped with ctx's session and seq.

The closed, sorted list of every type string this format defines - the vocabulary's single definition site in code. 24 entries: 9 trace.*, 10 effect.* (the nine core effects plus effect.datamodel_change, from Statifier.Effect.DatamodelChange), and 5 session.* (the four lifecycle types plus session.datamodel, emitted once per session from Statifier.Effect.DatamodelInit). docs/wire-format.md's type index table is the same 24, and test/statifier_ui/trace/wire_format_spec_test.exs asserts the two sets are equal.

Types

ctx()

@type ctx() :: %{session: String.t(), seq: non_neg_integer()}

What normalize/2 needs beyond the effect itself: the emitting session and the seq to stamp.

input()

@type input() ::
  Statifier.Effect.t()
  | {:effect, Statifier.Effect.t()}
  | {:halted, :done | :cancelled | :budget_exhausted}
  | {:unroutable, Statifier.Effect.t()}

Everything normalize/2 accepts: a bare effect, the {:effect, _} wrapper the session sends its subscribers, and the two lifecycle messages. The outer {:statifier, session_id, _} envelope is never accepted here - unwrapping it is the subscriber's job, and keeping it out is what makes this module testable from a struct literal.

Functions

normalize(effect, ctx)

@spec normalize(input(), ctx()) ::
  {:ok, StatifierUI.Trace.Message.t()} | {:error, term()}

Normalizes one input() into a %Message{} stamped with ctx's session and seq.

Returns {:error, {:unknown_effect, tag}} for a tag this module does not know, rather than skipping it - a new engine effect appearing silently is exactly what ADR-0005's Consequences warn a drift test must catch. It is the caller's job to decide what to do with the error.

types()

@spec types() :: [String.t()]

The closed, sorted list of every type string this format defines - the vocabulary's single definition site in code. 24 entries: 9 trace.*, 10 effect.* (the nine core effects plus effect.datamodel_change, from Statifier.Effect.DatamodelChange), and 5 session.* (the four lifecycle types plus session.datamodel, emitted once per session from Statifier.Effect.DatamodelInit). docs/wire-format.md's type index table is the same 24, and test/statifier_ui/trace/wire_format_spec_test.exs asserts the two sets are equal.