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
@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.
@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
@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.
@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.