# `StatifierUI.EventLog`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/event_log.ex#L1)

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.

# `t`

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

# `build`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
