Folds a [StatifierUI.Trace.Message.t()] (in any order) into a readable
log keyed by (macrostep, round), per the sui-t36.5 plan.
build/1 is a pure regrouping, not an interpretation: it decides where
each message goes, never what it means. Ordering comes entirely from the
stamps the producer put on each message, never from arrival order or
seq alone - docs/wire-format.md:91-94 names (macrostep, round) as
the format's own timeline key. Feeding build/1 the same messages in a
different order returns an identical struct.
Bucketing, not sorting
Buckets are ordered by (macrostep, round), and only within a bucket
do messages get a further order, by {microstep, seq}. This is two
levels, not one flat {macrostep, round, microstep, seq} sort key: a
round is a bucket a message either belongs to or does not, so a message
stamped at a lower microstep than an earlier round's messages still
lands in its own round's bucket rather than sorting ahead of them. seq
is itself a producer stamp (docs/wire-format.md:84-89), so using it as
the final tiebreak is still ordering from the stamps.
Routing effect.* messages
The routing rule is "does this message carry a round", never a check
against the "effect." prefix. When this module was written only
trace.* messages and effect.budget_exhausted carried round, so the
other effect.* types landed in their macrostep's effects; sui-67d
then propagated round onto every effect.* type, and - exactly as
planned - that was a pure data change needing no change here: an
effect.* message carrying a round is routed to its round bucket, and
effects still catches any message without one (an older recorded
stream, for example - the must-ignore rule cuts both ways).
Errors
docs/wire-format.md:96-102 forbids merging two sessions' stamps onto
one timeline (statifier ADR-0050 lets one mailbox carry an invoke tree of
related sessions), so build/1 refuses a message list naming more than
one session id rather than corrupting the log.
Reading a past configuration
configuration_at/2 answers "what was the configuration at macrostep
n" by looking up the stamps already in the log, never by re-deriving
one. The engine wrote every trace.macrostep_stable and trace.done
payload this reads, and a stream reconstructed through catch-up got
there by statifier ADR-0034 replay, which re-drives the core rather than
rewinding a live session (ADR-0002's inherited clause). Time travel here
is therefore a read of replay output as data - nothing in this repo
re-implements Appendix D to answer it.
Two message types stamp a configuration, not one (sui-dc7). A macrostep
that reaches quiescence stamps trace.macrostep_stable; the macrostep
that halts the run by entering a top-level <final> never reaches
quiescence and stamps trace.done instead. docs/wire-format.md gives
both the same field, the same shape, and the same authority -
trace.done's configuration is "the full configuration as it stood at
exit" - so reading it here is one more read of an engine stamp, not a
new interpretation of one.
Summary
Functions
Folds messages into an t().
The configuration in force at macrostep, read from the log's own
trace.macrostep_stable and trace.done stamps.
Types
@type t() :: %StatifierUI.EventLog{ macrosteps: [StatifierUI.EventLog.Macrostep.t()], session: String.t() | nil, session_messages: [StatifierUI.Trace.Message.t()], truncated?: boolean() }
Functions
@spec build([StatifierUI.Trace.Message.t()]) :: {:ok, t()} | {:error, {:mixed_sessions, [String.t()]}}
Folds messages into an t().
Returns {:error, {:mixed_sessions, sorted_ids}} when messages names
more than one distinct session. An empty list returns an empty, default
log with session: nil.
@spec configuration_at(t(), non_neg_integer()) :: {:quiescent, [non_neg_integer()]} | {:final, [non_neg_integer()]} | {:carried, non_neg_integer(), [non_neg_integer()]} | :before_first
The configuration in force at macrostep, read from the log's own
trace.macrostep_stable and trace.done stamps.
Four outcomes, kept distinct because a caller that collapsed them would present a carried configuration as a measured one:
{:quiescent, configuration}- macrostepmacrostepitself reached quiescence and this is the configuration it settled in.{:final, configuration}- macrostepmacrostephalted the run by entering a top-level<final>, so it never stabilized; this is the configurationtrace.donestamped at exit (sui-dc7).{:carried, from, configuration}- macrostepmacrostepstamped neither (it is still in flight, or its stamp was dropped), so the newest configuration at or below it is returned along with the macrostepfromthat stamped it.:before_first- no macrostep at or belowmacrostepstamped a configuration at all. The caller decides what to show; the inspector falls back to the session's initial configuration.
A macrostep naming no bucket in this log is not an error: it resolves
the same way, against whatever buckets sit below it.