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 - shown in the diagram 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.
Summary
Types
Options shared by the fold functions.
One selectable point, for a caller building a scrubber.
Which point in the run the panes show: the live tip, or one macrostep.
Functions
The configuration messages and opts[:selection] imply.
The configuration active_configuration/2 answers, as state IDs.
The invocations live at the point opts[:selection] names, as
{state_id, invoke_type | nil} - the invoke half of
active_configuration_ids/2.
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.
Markdown for the datamodel diff pane: what macrostep n changed in the
datamodel, as a table (sui-2uz).
Mermaid stateDiagram-v2 source for the configuration pane:
StatifierUI.Diagram.render/3 over active_configuration/2.
Markdown for the event log pane: StatifierUI.EventLog.build/1 rendered
collapsible. The selected macrostep is opened and marked - shown in the diagram; 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.
The status header for a stream read back from storage rather than watched live.
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 [].
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).
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.
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).
Moves a selection one scrubber step over points, purely - the widget
layer holds the selection, this decides where a move lands.
Types
@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.
@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.
@type selection() :: :live | {:macrostep, non_neg_integer()}
Which point in the run the panes show: the live tip, or one macrostep.
Functions
@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.
@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}
@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, []}
@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.
@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 opt/0's, plus StatifierUI.DatamodelExplorer.Diff.Markdown's
:title and :empty_note, which are forwarded.
@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.
@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 - shown in the diagram; 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.
@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.
@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 [].
@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).
@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.
@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).
Moves a selection one scrubber step over points, purely - the widget
layer holds the selection, this decides where a move lands.
:livereturns:livefrom anywhere.:firstselects the oldest macrostep in view.:prevfrom:livepins 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.:nextselects the oldest macrostep above the current one, and returns to:liveonce 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.