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

Pure pane assembly for the Livebook inspector: folds a compiled
`Statifier.Machine` and a `StatifierUI.Trace.Subscriber` message list
into the render source each pane displays - Mermaid source for the
configuration diagram, Markdown for the event log and the datamodel
explorer, and a status line from the subscriber's `stats/1` snapshot.

No Kino, no process, no session: everything here is testable from a
message list, exactly like the pane modules it composes. The Kino shell
(`StatifierUI.Kino`) maps these strings into widgets and owns nothing
else.

The active configuration is read from the newest message that stamped
one - a `trace.macrostep_stable`, the quiescent configuration of the
last completed macrostep, or a `trace.done`, the configuration a halted
run exited in (`docs/wire-format.md`). Before either has arrived in
view, the caller-supplied initial configuration (typically
`Statifier.Session.snapshot/1`'s) is used instead.

## A halted chart's final configuration (sui-dc7)

A run that ends by entering a top-level `<final>` never stabilizes in
its last macrostep: quiescence is never reached, so no
`trace.macrostep_stable` is emitted for it and the engine stamps the
configuration on `trace.done` instead. Reading only
`macrostep_stable` therefore left the diagram highlighting the state the
chart *left*, while the datamodel pane showed the assignment that moved
it out.

Both stamps are read, and the wire format is what settles that they may
be: its `trace.done` row defines `configuration` as "the full
configuration as it stood at exit, a genuine set, sorted ascending" -
the same field, shape, and authority as a `macrostep_stable` payload.
Nothing here re-derives an exit configuration from the exit sets that
precede `trace.done`; that would be re-implementing Appendix D, which
this repo does not do (ADR-0002, `StatifierUI.EventLog`). The two
readings stay labelled apart, so a configuration the chart *exited* in
is never presented as one it *settled* in.

## Selecting a past macrostep (sui-3gg)

Every fold function takes a `:selection`, which is `:live` (the default,
the behaviour above) or `{:macrostep, n}`. Under a selection the diagram
shows the configuration macrostep `n` settled in, the event log marks
that macrostep's entry `- selected` and opens it, and
`selection_note/2` renders the line that says which point is on screen.
That is the whole of the link between the two panes.

Nothing here re-derives a configuration. Every configuration this module
can show was stamped by the engine on a `trace.macrostep_stable` or a
`trace.done`, and a
stream reconstructed by catch-up got there through statifier ADR-0034
replay, which re-drives the core in a pure fold rather than rewinding a
live session. Time travel is a read of replay output as data (ADR-0002's
inherited clause), so selecting a macrostep neither touches the session
nor asks the engine for anything new.

A selected macrostep that stamped no configuration of its own - in
flight, or its stamp dropped - has none to draw. Rather than draw
nothing, the newest configuration at or below it is shown and
`selection_note/2` says which macrostep it was carried from - a carried
configuration is never presented as a measured one.

## The configuration as state ids (sui-hq0)

`active_configuration_ids/2` answers the same configuration as
`active_configuration/2`, resolved through the stream's own
`session.start` manifest into the chart's ids, and `active_invokes/2`
does the same for the invocations live at that point. Both are pure
reads a host can call from a `render/1` to drive a diagram of its own
without parsing the manifest itself; both refuse with
`{:error, :no_manifest}` on a stream that carries none.

## Stepping a persisted trace (sui-2uz)

`datamodel/2` takes the same `:selection` every other fold here does, so
the datamodel pane can show the values as they stood at a selected
macrostep rather than only at the live tip, and `datamodel_diff/2`
renders what one macrostep changed. Both are the same stream cut short
and folded again by `StatifierUI.DatamodelExplorer.build_live/2`; the
comparison itself is `StatifierUI.DatamodelExplorer.Diff`. Nothing
re-runs the chart and nothing re-implements an assignment - a persisted
stream is a recording, and stepping it is a read of a shorter prefix.

## Linking out to a trace (sui-4w2)

Every fold function also takes `:deep_link`, the host's APM URL template
(ADR-0013). With one configured, the event log's macrostep summaries and
`selection_note/2` gain an `[open trace](...)` link for each macrostep
whose messages carry the wire format's `otel` key. Without the option, or
on a macrostep carrying no correlation, every pane renders exactly as it
did - including the carried-forward wording above, which the link is
appended to rather than replacing.

# `opt`

```elixir
@type opt() ::
  {:initial_configuration, Enumerable.t()}
  | {:selection, selection()}
  | {:deep_link, String.t() | StatifierUI.Trace.DeepLink.t() | nil}
  | {:active_style, StatifierUI.Diagram.active_style()}
```

Options shared by the fold functions.

# `point`

```elixir
@type point() :: %{
  macrostep: non_neg_integer(),
  event: String.t() | nil,
  quiescent?: boolean(),
  final?: boolean()
}
```

One selectable point, for a caller building a scrubber.

`quiescent?` says the macrostep settled, `final?` that it halted the run
(sui-dc7). They are never both true, and a macrostep with a
configuration to draw is one where either is - which is why `final?` was
added beside `quiescent?` rather than widening it: a halting macrostep
is not quiescent, and a scrubber saying so is not the same as one saying
it has nothing to show.

# `selection`

```elixir
@type selection() :: :live | {:macrostep, non_neg_integer()}
```

Which point in the run the panes show: the live tip, or one macrostep.

# `active_configuration`

```elixir
@spec active_configuration([StatifierUI.Trace.Message.t()], [opt()]) :: [
  non_neg_integer()
]
```

The configuration `messages` and `opts[:selection]` imply.

On `:live` (the default): the `configuration` payload of the newest
`trace.macrostep_stable` or `trace.done`, whichever arrived last, or
`opts[:initial_configuration]` (default `[]`) when neither is in view.

On `{:macrostep, n}`: the configuration macrostep `n` settled in - or,
for the macrostep that halted the run, exited in - per
`StatifierUI.EventLog.configuration_at/2`, falling back to the newest
one below it when `n` stamped neither, and to
`opts[:initial_configuration]` when nothing at or below `n` stamped one.
A message list the log refuses (mixed sessions) degrades to the live
reading rather than raising, same policy as `event_log/2`.

# `active_configuration_ids`

```elixir
@spec active_configuration_ids([StatifierUI.Trace.Message.t()], [opt()]) ::
  {:ok, [String.t()]} | {:error, :no_manifest}
```

The configuration `active_configuration/2` answers, as state IDs.

The indexes that function returns are the wire format's own document-order
positions, which mean nothing outside the stream that produced them. A host
that draws its own diagram - the debugger canvas in a block editor, say -
works in the chart's ids, so this resolves them through the stream's own
`session.start` manifest and hands back the names.

The resolution rule is `StatifierUI.EventLog.Labels.state/2`, the one the
event log already renders indexes with: a state's `"id"`, `"<scxml>"` for
the synthesized root, and `"#<index>"` for a state the document left
anonymous. There is no second answer to what an index is called.

A stream carrying no `session.start` returns `{:error, :no_manifest}`
rather than a list of `"#<index>"` strings. That is the late-attach case
(`StatifierUI.Trace.Subscriber` emits no manifest when it joins a session
that already has one), and a caller marking a canvas needs to know it got
no names rather than to mark nothing and call it an empty configuration.
`active_configuration/2` still answers the indexes there.

Nothing here re-derives a configuration or parses the manifest itself:
both halves are reads of what the engine already stamped.

## Examples

    iex> {:ok, machine} =
    ...>   Statifier.compile(~s(<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="pending"><state id="pending"/><state id="authorized"/></scxml>))
    iex> {:ok, manifest} = StatifierUI.Trace.Manifest.build(machine, "sess_ops")
    iex> StatifierUI.Inspector.active_configuration_ids([manifest], initial_configuration: [1])
    {:ok, ["pending"]}

    iex> StatifierUI.Inspector.active_configuration_ids([], initial_configuration: [1])
    {:error, :no_manifest}

# `active_invokes`

```elixir
@spec active_invokes([StatifierUI.Trace.Message.t()], [opt()]) ::
  {:ok, [{String.t(), String.t() | nil}]} | {:error, :no_manifest}
```

The invocations live at the point `opts[:selection]` names, as
`{state_id, invoke_type | nil}` - the invoke half of
`active_configuration_ids/2`.

An invocation is live from the `effect.invoke` that started it until the
`effect.cancel_invoke` that ends it, both of which the engine stamps with
the owning `state_index` (`docs/wire-format.md`). Nothing else is inferred:
a stream that never reached the `<invoke>` seam answers `{:ok, []}`, and
the pairing is by `invoke_id`, the format's own identity for an
invocation. The list is in start order.

## What the second element is, and is not

The wire format defines **no outcome field for an invocation** - its only
`outcome` is `trace.conds_evaluated`'s guard discriminator - so this pair's
second element is the `invoke_type` an `effect.invoke` carries, `nil` when
the `<invoke>` element set neither `type` nor `typeexpr`. A caller wanting
to know how an invocation *ended* reads the `done.invoke.*` event in the
event log; answering that here would mean this module deciding what
"finished" means, which is the engine's call and not a read (ADR-0002's
inherited clause).

`{:error, :no_manifest}` for the same reason and in the same case as
`active_configuration_ids/2`.

## Examples

    iex> {:ok, machine} =
    ...>   Statifier.compile(~s(<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="pending"><state id="pending"/></scxml>))
    iex> {:ok, manifest} = StatifierUI.Trace.Manifest.build(machine, "sess_ops")
    iex> StatifierUI.Inspector.active_invokes([manifest])
    {:ok, []}

# `datamodel`

```elixir
@spec datamodel([StatifierUI.Trace.Message.t()], [opt()]) :: String.t()
```

Markdown for the datamodel explorer pane:
`StatifierUI.DatamodelExplorer.build_live/1` over `messages`, rendered
with the default markers. A build failure renders as a visible error
line, same policy as `event_log/1`.

Under `opts[:selection]` of `{:macrostep, n}` the pane folds the stream
as it stood at the end of macrostep `n` rather than at the live tip
(`sui-2uz`) - the same cut every other selection-aware read here takes,
so the datamodel a caller shows beside a selected configuration is the
one that configuration was reached with. On `:live` (the default) the
whole list folds, byte-identical to what `datamodel/1` always rendered.

Nothing is re-derived: the cut is a shorter prefix of the same messages,
folded by the same `build_live/2`.

# `datamodel_diff`

```elixir
@spec datamodel_diff([StatifierUI.Trace.Message.t()], [opt()]) :: String.t()
```

Markdown for the datamodel diff pane: what macrostep `n` changed in the
datamodel, as a table (`sui-2uz`).

The two sides are the same stream cut twice - everything below macrostep
`n`, and everything through it - folded by
`StatifierUI.DatamodelExplorer.build_live/2` and compared by
`StatifierUI.DatamodelExplorer.Diff.between/2`. "Adjacent" is therefore
the cut, not a pair of buckets: a macrostep the log holds no entry for
still diffs correctly against whatever precedes it, and macrostep `n`'s
own diff against the session's seeded datamodel is what the first
macrostep shows.

`opts[:selection]` picks `n`. On `:live` the newest macrostep in view is
diffed, so a pane following the tip answers "what did the step that just
happened change". A stream with no macrostep at all has nothing to diff
and says so.

Options are `t:opt/0`'s, plus `StatifierUI.DatamodelExplorer.Diff.Markdown`'s
`:title` and `:empty_note`, which are forwarded.

# `diagram`

```elixir
@spec diagram(Statifier.Machine.t(), [StatifierUI.Trace.Message.t()], [opt()]) ::
  String.t()
```

Mermaid `stateDiagram-v2` source for the configuration pane:
`StatifierUI.Diagram.render/3` over `active_configuration/2`.

`opts[:active_style]` is passed through unchanged, so a host under a dark
theme can drop the shipped `classDef` and style the `active` class from
its own stylesheet - see `StatifierUI.Diagram`.

# `event_log`

```elixir
@spec event_log([StatifierUI.Trace.Message.t()], [opt()]) :: String.t()
```

Markdown for the event log pane: `StatifierUI.EventLog.build/1` rendered
collapsible. The selected macrostep is opened and marked `- selected`;
with no selection the last macrostep is opened, as before. A
build failure renders as a visible error line rather than raising - the
inspector keeps showing the other panes.

# `persisted_status`

```elixir
@spec persisted_status([StatifierUI.Trace.Message.t()]) :: String.t()
```

The status header for a stream read back from storage rather than
watched live.

A persisted stream has no `StatifierUI.Trace.Subscriber` behind it, so
there is no status, no buffered or dropped count, and no diagnostics -
and this says `persisted` rather than inventing them.
`StatifierUI.Live`'s status pane takes the same position for the same
reason (`status_kind(nil)`), so both surfaces describe a reloaded trace
the same way.

What a persisted stream *can* still say is whether it is projected: the
`projection` header rides on its own `session.start` message (ADR-0012),
so a reloaded capture that withholds values still announces that it
does.

# `points`

```elixir
@spec points([StatifierUI.Trace.Message.t()]) :: [point()]
```

The macrosteps a caller can select, oldest first - the scrubber's own
vocabulary, so a UI never has to fold the log itself. A log that refuses
to build yields `[]`.

# `resolution`

```elixir
@spec resolution([StatifierUI.Trace.Message.t()], [opt()]) ::
  :live
  | {:quiescent, non_neg_integer()}
  | {:final, non_neg_integer()}
  | {:carried, non_neg_integer(), non_neg_integer()}
  | {:before_first, non_neg_integer()}
```

How the selection resolved, for a caller that needs to say so: `:live`,
`{:quiescent, n}`, `{:final, n}` (macrostep `n` halted the run, so the
configuration it exited in is shown), `{:carried, n, from}` (macrostep
`n` stamped no configuration, so macrostep `from`'s is shown), or
`{:before_first, n}` (nothing at or below `n` stamped a configuration,
so the initial configuration is shown).

# `selection_note`

```elixir
@spec selection_note([StatifierUI.Trace.Message.t()], [opt()]) :: String.t()
```

The one-line note naming the point on screen, for display above the
diagram: which macrostep and its event, whether the configuration was
measured there or carried from an earlier macrostep, and - on `:live` -
that the diagram is following the tip.

# `status`

```elixir
@spec status(StatifierUI.Trace.Subscriber.stats()) :: String.t()
```

The one-line (plus warnings) status header: session id, subscriber
status, message and drop counts, and one blockquote line per diagnostic.
A `:not_recorded` or `:catch_up_failed` diagnostic is what labels the
whole inspector live-only - a partial stream is never presented as
whole (statifier ADR-0049; this bead's precondition note).

# `step`

```elixir
@spec step(selection(), [point()], :live | :first | :prev | :next) :: selection()
```

Moves a selection one scrubber step over `points`, purely - the widget
layer holds the selection, this decides where a move lands.

  * `:live` returns `:live` from anywhere.
  * `:first` selects the oldest macrostep in view.
  * `:prev` from `:live` *pins* the newest macrostep (the same picture,
    no longer following the tip); from a macrostep it selects the
    newest one below it, staying put at the oldest.
  * `:next` selects the oldest macrostep above the current one, and
    returns to `:live` once there is none - so a run driven forward
    while the scrubber sits at the tip keeps following it.

With no points at all every move returns `:live`: there is nothing else
to show.

---

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