Statifier inspector
Mix.install([
{:kino, "~> 0.14"},
{:statifier_ui, path: Path.join(__DIR__, "..")}
])
What this notebook is
The Livebook inspector end to end, driving a small card-authorization
chart - the same domain, and the same pending -> authorized -> captured
spine, the README's worked example uses. It is also the milestone's
manual acceptance test: each numbered step below says what to do and
what you should see, so walking it top to bottom verifies the assembled
widget (sui-t36.8) against a live session.
Livebook evaluates cells in order; use "Evaluate" on each code cell as you reach it.
1. Compile the chart
Text-first: the SCXML below is the source of truth, and everything the inspector shows is read from it or from the session's trace effects.
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" initial="pending" version="1.0">
<datamodel>
<data id="amount_cents" expr="1999"/>
<data id="captured_cents" expr="0"/>
<data id="authorization_attempts" expr="0"/>
</datamodel>
<state id="pending">
<transition event="authorize.declined" target="pending">
<assign location="authorization_attempts" expr="authorization_attempts + 1"/>
</transition>
<transition event="authorize.approved" target="authorized">
<assign location="authorization_attempts" expr="authorization_attempts + 1"/>
</transition>
</state>
<state id="authorized">
<transition event="capture.settled" target="captured">
<assign location="captured_cents" expr="amount_cents"/>
</transition>
</state>
<final id="captured"/>
</scxml>
"""
{:ok, machine} = Statifier.compile(xml)
One deliberate difference from the README's chart: a declined
authorization here returns to pending rather than reaching a final
declined state, so the card can be retried. The walk below needs one
event that takes a real step without moving the highlight, and that
self-transition is it.
Expect: {:ok, %Statifier.Machine{...}}. A compile error here means
the chart text was edited into something invalid - fix it before going on.
2. Fixtures for the injection palette
One sample payload per event name (ADR-0003). These become the one-click buttons in the injection pane.
{:ok, fixtures} =
StatifierUI.Fixtures.new(
events: %{
"authorize.approved" => %{"amount_cents" => 1999, "currency" => "USD"},
"authorize.declined" => %{"reason" => "insufficient_funds"},
"capture.settled" => %{"captured_cents" => 1999}
}
)
3. Start a recorded session
trace: true puts the run on the wire; record: true is what lets the
inspector catch up on anything it missed (statifier ADR-0049). Without it
the inspector still works but labels itself Live-only. Replay cost
grows with run length, so a very long-lived session makes the inspector
cell slower to evaluate - not a concern at this notebook's scale.
{:ok, session} = Statifier.Session.start_link(machine, trace: true, record: true)
4. Open the inspector
StatifierUI.Kino.inspect(session, fixtures, source: xml)
Expect, immediately:
- Status header - the session id,
attached, a message count, and no "Live-only" warning. - Scrubber row - four buttons, |< First, < Prev, Next >, Live - and under them the line Showing the live tip.
- Configuration diagram (left) - the chart as a Mermaid state
diagram with
pendinghighlighted. The initialize burst happened before this cell ran; catch-up is why the diagram still knows about it. - Datamodel explorer (right) -
amount_centsat1999,captured_centsat0, andauthorization_attemptsat0. - Injection pane - one button per fixture event, then a free-form name/payload form.
- Event log - macrostep 1 (the initialize burst), collapsed except the last macrostep, and nothing marked "- selected" while the scrubber is live.
5. Walk the chart from the palette
Do these in order, watching the panes after each click:
-
Click authorize.declined. Expect: feedback line "Sent
authorize.declined..."; the highlight stays onpending, and that is correct - the self-transition still exits and re-enters, so the datamodel showsauthorization_attemptsas1with a changed marker and the event log grows a macrostep whose cause isauthorize.declined. A step the diagram cannot show is exactly what the log is for. -
Click authorize.approved. Expect: highlight moves to
authorized;authorization_attemptsreads2; the log's newest macrostep namesauthorize.approved. -
Click capture.settled. Expect:
captured_centsreads1999with a changed marker, copied fromamount_centsby the transition's<assign>; the log's newest macrostep namescapture.settledand carries the entry ofcaptured; the status header still saysattached- a halted chart's session process is alive, and the inspector keeps its whole trace readable.Expect the highlight to move to
captured. Entering a top-level<final>halts the run, so this macrostep never reaches quiescence and ends intrace.donerather thantrace.macrostep_stable.trace.donecarries the configuration the run exited in, andStatifierUI.Inspector.active_configuration/2reads it (sui-dc7) - so the diagram shows where the chart ended, not the state it left.
6. Scrub back through the run
The scrubber moves the diagram to any macrostep in the log while the
log itself stays whole. Nothing is recomputed: every configuration it can
show was stamped by the engine on a trace.macrostep_stable, and a
caught-up stream got there through statifier ADR-0034 replay.
- Click |< First. Expect: the note reads Showing macrostep 1
(initialize), at its quiescent configuration, the diagram highlights
pending, and macrostep 1's entry in the log is open and suffixed - selected. Exactly one entry carries that suffix. - Click Next > repeatedly. Expect: the note and the marked log entry
advance together, macrostep by macrostep; the highlight follows what
each macrostep settled in (so it sits on
pendingthrough the declined attempt, then moves toauthorized). - Keep clicking Next > past the newest macrostep. Expect: the note returns to Showing the live tip, no entry is marked, and the diagram is following the run again.
- Click < Prev from the live tip. Expect: the newest macrostep is pinned - the same picture as the live tip, but no longer following it, and its log entry now carries the marker. Live clears both.
- Select the macrostep that entered
captured- the halting one. Expect the note to say at the final configuration the run halted in, and the diagram to highlightcaptured. The halting macrostep stamped its configuration ontrace.donerather than on atrace.macrostep_stable(sui-dc7), and the note keeps the two readings apart: a configuration the chart exited in is never worded as one it settled in.
The datamodel explorer in this widget does not move with the
scrubber - it always shows the current values. The per-macrostep fold
exists (StatifierUI.Inspector.datamodel/2 takes the same :selection
every other read here does, sui-2uz), and the persisted stepper in step
10 is built on it; wiring it into the live inspector's own pane is a
separate change. Here the diagram and the log are the pair this walk
exercises.
7. The form, including its error path
- In the free-form fields, enter name
authorize.declinedand payload{"reason": "manual"}, press Send. Expect: "Sent" feedback, and no new trace message anywhere - not a macrostep, not even an event-received line. The chart halted when it enteredcaptured, so the run is over and the event is dropped rather than processed. "Sent" is honest about what the injection pane did (it handed the event to the session); the empty log is honest about what the session did with it. Worth seeing both, because they are two different facts. - Now enter payload
{not json. Expect: Not sent, with the JSON error ({:invalid_json, ...}) - the session was never touched, because the draft failed to build before anything was sent.
8. Re-evaluate the inspector cell
Re-evaluate the StatifierUI.Kino.inspect(...) cell (step 4).
Expect: the previous widget's processes are terminated with the old
cell evaluation (that is the clean detach - the session simply drops the
dead subscriber), and the fresh widget shows the entire history
again: every macrostep you drove above is in the log,
authorization_attempts is 2, captured_cents is 1999, and the
diagram highlights captured - the halted run's final configuration,
read from trace.done (sui-dc7).
That round trip is catch-up doing its job: a fresh widget over the same
recorded session reproduces the previous one exactly, halt included.
To see the honest degraded mode: start a session without
record: true, inspect it, and note the status header's Live-only
warning - the inspector refuses to present a partial stream as whole.
9. Session death
Process.exit(session, :kill)
Expect: the status header moves to terminated, the log's footer
gains a session.terminated line, and every pane keeps rendering the
buffered trace - death is an observation, not a reset. (Because the
session was started from this notebook with start_link, killing it may
also take down the cell's evaluator; re-evaluate from step 3 to go
again.)
10. Step through the run after it is over
Save the recording and reopen it. This is StatifierUI.Trace.Capture's
record / save / reload, and the reopened widget is a stepper rather
than a snapshot.
{:ok, messages} = StatifierUI.Trace.Capture.record(session, machine, source: xml)
path = Path.join(System.tmp_dir!(), "authorization.jsonl")
:ok = StatifierUI.Trace.Capture.save(messages, path)
StatifierUI.Kino.inspect_trace(path)
Expect: the status header says persisted and reports no subscriber
counts (there is no subscriber), and above the diagram sit the same four
scrubber buttons plus a Jump to select listing every macrostep by
number and event.
- Pick a macrostep from Jump to. Expect: the note, the diagram, the event log's marked entry, and the datamodel pane all move to that point together - unlike step 6, the values shown are the ones that macrostep ended with, not the ones the run finished with.
- Read the Datamodel changes in macrostep N table below the panes. Expect: one row per slot the step moved, with the value before and after. A step that assigned nothing says so in words rather than showing an empty table.
- Step to the initialize macrostep. Expect: each declared
<data expr>shows as a change from:undefinedto its declared value - the evaluation of the declaration is something the run did, and the diff says so. - Click Next > past the newest macrostep. Expect: the selection returns to the end of the recording. A file has no tip that moves, so "Live" here means the last thing that happened.
Nothing in this step asks the engine for anything. Every configuration and every value came out of the file, and the file's contents were produced by statifier ADR-0034 replay re-driving the core at capture time. Stepping is a read of a shorter prefix of the same messages.
Where to go next
StatifierUI.Trace.Subscriber- the one process behind all four panes.Statifier.Session.invocations/1plus:inherit_observers(statifier ADR-0050) - attaching one subscriber to a whole invoke tree; the inspector composes per session today.docs/wire-format.md- every message type the panes fold over.