StatifierUI.EventLog (StatifierUI v0.7.0)

Copy Markdown View Source

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

t()

@type t() :: %StatifierUI.EventLog{
  macrosteps: [StatifierUI.EventLog.Macrostep.t()],
  session: String.t() | nil,
  session_messages: [StatifierUI.Trace.Message.t()],
  truncated?: boolean()
}

Functions

build(messages)

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

configuration_at(event_log, macrostep)

@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} - macrostep macrostep itself reached quiescence and this is the configuration it settled in.
  • {:final, configuration} - macrostep macrostep halted the run by entering a top-level <final>, so it never stabilized; this is the configuration trace.done stamped at exit (sui-dc7).
  • {:carried, from, configuration} - macrostep macrostep stamped neither (it is still in flight, or its stamp was dropped), so the newest configuration at or below it is returned along with the macrostep from that stamped it.
  • :before_first - no macrostep at or below macrostep stamped 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.