All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Entries for unreleased work are not written here directly. Each issue drops a
fragment in changelog.d/; the fragments are assembled
into a version section at release. See that README for the format and for when a
change warrants an entry at all.
[0.9.1] 2026-09-06
A patch. StatifierUI.Diagram.render/3 now escapes the : in a transition
label, so a chart whose event descriptors carry a prefixed name renders
through any Mermaid client instead of failing to parse. The expression
picklist offers every value of a {:one_of, _} path whose values are floats.
And the event log marks the selected macrostep in wording that stays correct
in a host composing the log with no diagram mounted. No surface changes and
no dependency changes - the trace wire format is untouched, so a consumer
taking ~> 0.9 re-pins nothing.
Changed
StatifierUI.Live.event_log/1marks the selected macrostep- selectedinstead of- shown in the diagram, so the mark reads correctly in a host that composes the log with no diagram mounted.
Fixed
StatifierUI.Live's scrubber renders the note for a{:final, n}resolution, so scrubbing back from the live tip of a run that has halted names the configuration the run exited in instead of raisingFunctionClauseErrorand remounting the host LiveView.StatifierUI.Diagram.render/3escapes:in a transition label as Mermaid's#58;entity code, so a chart whose event descriptors carry a prefixed name (myapp:authorize), a lifted edge, or a deep initial marker now renders instead of failing to parse.- The expression picklist's value select offers every value of a
{:one_of, _}path whose values are floats, instead of rendering only the value the author's own expression already carries.
[0.9.0] 2026-09-06
A minor, and the release that gives this package a required runtime
dependency: statifier_datamodel ~> 0.1, which a host adding
statifier_ui 0.9.0 pulls in. The expression editor takes a decoded datamodel
:document and projects its declared path types instead of needing the host to
assemble :path_types by hand. StatifierUI.Live.State.configuration_ids/1
answers the selected configuration as the chart's own state ids rather than
wire-format indexes. StatifierUI.Kino.inspect_trace/3 is a stepper over a
persisted trace, with a pane saying what each macrostep changed in the
datamodel. And the diagram's active-configuration highlight takes an
:active_style, so a host under a dark theme can restyle it. The trace wire
format is untouched - version 1, 25 types - so a consumer taking ~> 0.9
re-pins nothing.
Added
StatifierUI.Kino.inspect_trace/3is a stepper rather than a snapshot: the reopened trace gets the First / Prev / Next / Live scrubber, a Jump to select listing every macrostep by number and event, and a pane saying what the selected macrostep changed in the datamodel.StatifierUI.Inspector.datamodel_diff/2: what one macrostep changed in the datamodel, as a Markdown table.StatifierUI.DatamodelExplorer.Diff.between/2andStatifierUI.DatamodelExplorer.Diff.Markdown.render/2: the pure comparison behind that pane, over two datamodel explorer panes. A slot missing on one side is:absent, soniland:undefinedstay values.StatifierUI.Diagram.render/3takes:active_style, which decides how the active-configuration highlight is styled::default(the shipped light palette, unchanged),:none(noclassDefat all, so the host's own stylesheet or Mermaid theme reaches theactiveclass the nodes still carry), or aclassDefbody as a binary. A host under a dark theme no longer has to post-process the Mermaid source to restyle the highlight.StatifierUI.Inspector.diagram/3accepts the same:active_styleoption, andStatifierUI.Live.diagram/1andops_view/1the sameactive_styleattribute.StatifierUI.Live.State.configuration_ids/1andStatifierUI.Inspector.active_configuration_ids/2: the configuration the current selection implies, as the chart's own state ids rather than the wire format's document-order indexes, resolved through the stream's ownsession.startmanifest. A host drawing its own diagram no longer has to parse that manifest. A stream carrying none - the late-attach case - answers{:error, :no_manifest}; the index reads are unchanged there.StatifierUI.Inspector.active_invokes/2: the invocations live at that same point, as{state_id, invoke_type | nil}, folded from theeffect.invokeandeffect.cancel_invokethe engine already stamps.StatifierUI.Live.ExpressionInput.expression_input/1takes a:documentassign: a decoded datamodel document, projected throughStatifierDatamodel.Index.path_types/1for the kinds:path_typeswould otherwise carry. A non-empty:path_typeswins over it.StatifierUI.Expression.declared_kind/0admits:number, the tag that projection answers for a document'sintegeranddecimalalike.
Changed
StatifierUI.Inspector.datamodel/2takes the:selectionevery other fold there takes, so the datamodel pane can show the values as they stood at a selected macrostep.datamodel/1is unchanged.- Adds a required dependency on
statifier_datamodel ~> 0.1(see ADR-0004's 2026-09-06 note). - "Add clause" seeds the new row's literal from the path's declared kind -
0for a number,truefor a boolean, today for a date, the first value of a{:one_of, _}, aCONTAINSclause for a list - instead of always seeding an empty string.
Fixed
- Adding a clause on a path declared
:numberno longer produces a row that immediately carries a value-kind advisory about its own seed.
[0.8.0] 2026-09-05
A minor. The expression editor's clause builder takes a path_types
assign: a clause's operator list and value control come from the kind the
host declares for the path rather than from the literal in the source, and
a date-declared path offers relative-date candidates. The expression
field's two mode switches also stop rendering run-together on a host page
that ships no stylesheet. The trace wire format is untouched - version 1,
25 types - so a consumer taking ~> 0.8 re-pins nothing.
Added
StatifierUI.Live.ExpressionInput.expression_input/1takes a:path_typesassign,%{path => kind | {:list, kind} | {:one_of, values}}, which decides a clause's operator list and value control from the kind the host declares rather than from the literal in the source.StatifierUI.Expression.simple/2takes a:path_typesoption, and each row it returns carries:declared_kind,:control_kindand:advisories.StatifierUI.Expression.relative_date_candidates/0returns the relative dates a date-declared path offers.
Fixed
- The expression field's two mode switches no longer render run-together on a
host page with no stylesheet: their container carries an inline
display: inline-flex; gap: 0.5rem, the one layout default this package ships.
[0.7.0] 2026-09-05
A minor. The wire vocabulary grows to 25 types with
trace.conds_evaluated, which carries a selection round's guard outcomes
so a guarded chart's stream can say why a branch fired; session.start's
data rows stop falling back to the element's own span for
value_location; StatifierUI.Trace.Replay.recording/3 becomes public
surface; and the predicator floor rises to ~> 9.4. The wire format
version stays 1. A consumer that asserts the vocabulary size rather
than reading it re-pins 24 to 25 when it takes ~> 0.7.
Added
trace.conds_evaluated, a new trace message carrying a selection round's guard outcomes: one entry per evaluation, with the guarded transition'st_index, anoutcomeof"enabled","disabled"or"error", and, on"error", the samereasonerror object anerror.executionevent carries. Entries are in the engine's walk order, so the nth"error"entry pairs with the ntherror.executionthe round raised. A guarded chart's stream can now say why a branch fired or failed instead of showing two identical timelines (ADR-0018).Under a projected stream the entry's
reason.reasonis redacted unconditionally and itsreason.expressionfollowsallow_source, exactly as on an event's error object;t_indexandoutcomeare never projected.Before this, the producer skipped the engine's guard-evaluation effect rather than mapping it, so the outcomes reached no consumer at all.
StatifierUI.Trace.Replay.recording/3, which builds theStatifier.Session.Recordinga host's stored inputs describe and returns it. It is the foldfrom_events/4already ran to reach its own recording, now reachable on its own - forStatifier.Replay.run/1directly, forStatifier.Session.Recording.to_binary/1, or to compare against a recording you already hold. An unrecognized entry shape is{:error, {:unknown_entry, entry}}, the same fail-closed answerfrom_events/4gives;:traceand:session_idare not checked here, because they are requirements of the message stream rather than of the recording.docs/ops-embedding.md's "From a persisted event log" now carries the table saying which stored row becomes which of the six entry shapes, and states the rule a fired delayed send turns on: it is{:timer, send_id, event, routes}, never{:event, event, routes}, because only the timer shape draws the pending-timer credit thesend_idmatches. Naming a firing as an event costs the{:unscheduled_timer_firing, send_id}check and leaves the credit outstanding for a later cancel to move into the raced pool.
Changed
The wire vocabulary is 25 types, up from 24. The format version stays
1: a newtypeis additive, and ADR-0005's must-ignore-unknown rule means a consumer that has never heard oftrace.conds_evaluatedskips it. The conformance clause's MUST list is unchanged - it names the nine Appendix D phase-boundarytrace.*types, and this one is a seam inside selection, so it joins the MAY half beside theeffect.*families.Nothing breaks at runtime, but a consumer that asserts the vocabulary size rather than reading it fails until it is re-pinned: an embedder pinning 24 types today moves to 25 when it takes
~> 0.7.session.start'sdatarows omitvalue_locationwhen the<data>element wrote no value, instead of falling back to the element's own span. A consumer that compared the two spans before slicingsourcecan now test for the key's presence instead; one that sliced without comparing stops getting the whole element's text presented as a value. The wire format version stays 1.The predicator requirement is
~> 9.4, up from~> 9.2: a host on 9.2 or 9.3 has to move up, and getsPredicator.Simple.value_kind/1, whichStatifierUI.Expressionnow asks what kind a clause value is instead of keeping its own table.StatifierUI.Expression.simple_available?/0also requires the resolved:predicator_simplemodule to exportvalue_kind/1. A host that points that key at its own module has a fourth function to provide; every published predicator at the new floor already carries it.
[0.6.1] 2026-09-05
A hotfix. StatifierUI.Trace.Normalizer.normalize/2 refused statifier 2.5's
Statifier.Effect.Trace.CondsEvaluated, which meant no chart with a guarded
transition could be inspected from a persisted trace; the normalizer now
skips it, and the statifier floor rises to ~> 2.5. A caller matching only
{:ok, _} and {:error, _} on normalize/2 needs a :skip clause.
Changed
- Raises the
statifierfloor to~> 2.5. StatifierUI.Trace.Normalizer.normalize/2has a third return,:skip, for an engine trace effect the v1 wire format deliberately does not carry. A caller matching only{:ok, _}and{:error, _}needs a clause for it; neither a message nor aseqis produced.
Fixed
StatifierUI.Trace.Normalizer.normalize/2no longer refuses statifier 2.5'sStatifier.Effect.Trace.CondsEvaluated, so a chart with a guarded transition can be replayed throughStatifierUI.Trace.Replay.from_events/4instead of failing closed on its first branch.
[0.6.0] 2026-09-05
A failure becomes something the wire format can carry, and a trace can be
produced without a live run. StatifierUI.Trace.Replay.from_events/4 builds
the v1 wire format from a persisted event log, so a finished session can be
inspected offline. The wire error object gains a discriminated reason arm,
which is what lets an error.execution or error.communication event reach
a consumer at all instead of being dropped in normalization.
StatifierUI.Live.ExpressionInput.display_label/1 is removed; the grammar's
display phrases replaced the only work it did.
Added
StatifierUI.Trace.Replay.from_events/4produces the v1 trace wire format from a persisted session event log, with no liveStatifier.Session, no process, and no clock - the same message streamStatifierUI.Trace.Subscriberproduces from a live run,session.terminatedexcepted. A recording made withouttrace: trueis refused rather than replayed into a silentlytrace.*-free stream. See ADR-0017.- The wire
errorobject gains aclassdiscriminator ("expression"or"reason"), and on the reason arm akindtoken derived from the term's tag, a human-readablereasonstring, and acontent_pathnaming the enclosing<if>or<foreach>when the failure was raised inside one. Adding fields is not a format version bump, so the format version stays1. See ADR-0014 anddocs/wire-format.md.
Changed
error.reasonis redacted under every projection profile, besidesession.terminated'sreason: it isinspect/1of a whole engine term and can embed a datamodel value verbatim.class,kind, andcontent_pathare never projected.
Removed
StatifierUI.Live.ExpressionInput.display_label/1. It lowercased a word-shaped lexeme so a dropdown could readinwhere the decompiler wroteIN. Since operator labels became the grammar's own display phrases, delivered byStatifierUI.Expression.operators/1, every label it could be handed was already display-cased and it returned its argument unchanged. Operator options now carry the grammar's phrase verbatim, which leaves one spelling of a display phrase in the system and it is the vocabulary's.
Fixed
- A
trace.event_dequeuedfor anerror.executionorerror.communicationevent now reaches a consumer. The engine puts an unconstrained reason term in those events' data, which is not a value, so the whole message used to fail to normalize and be dropped - the inspector showed a run in which the failure simply was not there.
[0.5.0] 2026-09-05
Operator eligibility moves to the grammar. StatifierUI.Expression asks
Predicator.Simple.operators/1 which operators a value kind admits instead of
answering from a table of its own, so a picklist offers what the grammar
offers and lists it in the grammar's order. An operator entry now carries the
source spelling and the display phrase separately, which is the one migration
in this release.
Added
StatifierUI.Expression'st:value_kind/0gains:float, so a float-valued clause is offered the operators a number is offered once the resolved predicator admits float literals to the subset.
Changed
StatifierUI.Expression.operators/1reads per-value-kind eligibility fromPredicator.Simple.operators/1instead of a table kept here, so the operators offered beside a value are the ones the grammar admits for that kind. Every scalar kind but:stringnow also offers===,!==andCONTAINS, which:stringalready offered, and a string gains the ordered comparisons.- Each entry
StatifierUI.Expression.operators/1returns gains:lexeme, the source spelling the expression will carry, and its:labelis now the grammar's display phrase ("is at least"for">=") rather than the spelling. Read:lexemewhere you were reading:labelto build source text; nothing stored changes, since a row is still written back throughPredicator.Simple.to_source/1. StatifierUI.Live.ExpressionInput's operator dropdown shows those phrases. Thevalueattribute on every option is unchanged: it is still the writer's own untouched output.
Fixed
- A picklist control now shows the choice that was just made. LiveView skips
patching a
<select>that has focus when the option list is unchanged, so after an operator, field or connective edit the control kept displaying the previous selection until the field was re-rendered from scratch; the hook now restores it from the source string the server rendered.
[0.4.0] 2026-09-04
Structured authoring reaches the expression field. StatifierUI.Expression
gains a read and write pass over the picklist-renderable subset of
predicator's grammar, and StatifierUI.Live.ExpressionInput renders that
subset as one row of dropdowns per clause - field, operator, value - keeping
the text field for anything outside it. The source string stays the single
representation: picklists are a rendering of it and never a second form of it
(ADR-0007), and every value a dropdown offers is a complete expression the
writer produced.
Added
StatifierUI.Expression.simple/2classifies a source string against the picklist-renderable subset, returning{:ok, rows, connective}for source a row of dropdowns can draw,:outsidefor a valid expression it cannot, and{:error, error}for source that does not parse. Each row carries the field path, the operator, the value, and the value's kind in both structural and source form.StatifierUI.Expression.operators/1returns the operators a picklist offers beside a value of a given kind, andStatifierUI.Expression.value_candidates/2normalizes the values a host declares for a path.StatifierUI.Expression.simple_available?/0reports whether the resolved predicator exposesPredicator.Simple. A host on an older predicator gets:outsidefor every source string rather than an error.StatifierUI.Live.ExpressionInputrenders a picklist mode: a source string inside the picklist-renderable subset draws one row of dropdowns per clause - field, operator, value - with a connective toggle, an add-clause button and a remove-clause button per row. A valid expression outside the subset, and source that does not parse, render the text input as before. The component never refuses a source string and never rewrites one.- Source that does not parse now renders the text input alongside the parse
error's message and position, stamped as
data-error-position, rather than falling back silently. - A switch to text mode is always offered; the switch to picklists appears
only while the current text is inside the subset.
:mode(:auto,:text,:picklist) sets which mode renders first, and:value_candidatessupplies the values a host offers per clause path. StatifierUIExpressionPicklist, a second hook, shipped as source alongside the completion popup and exported fromStatifierUIHooks.StatifierUI.Live.ExpressionInput.picklist_hook_name/0names it, anddisplay_label/1is the one place an operator label is cased for display. A host that registers no hook gets the text field alone.StatifierUI.Expression.source/2writes a source string back from the rowssimple/2returned,value_source/2spells one clause value on its own, andsegments/1reads a declared path into the structural form a clause carries. Together they are the write half of the same round trip throughPredicator.Simple, which is what keeps the source text the single representation: every picklist option's value is a complete expression the writer produced, and no quoting, escaping, list punctuation or operator spelling is repeated in JavaScript.
[0.3.0] 2026-09-02
The ops surface arrives. StatifierUI.Live ships read-only LiveView
components a host mounts over one trace stream, live or persisted; the
Livebook inspector gains a scrubber that moves the diagram to any macrostep
in the run; and StatifierUI.Live.ExpressionInput offers predicator's own
grammar as completion, dropping into statifier_blocks' editor seam - which
makes this the first release to ship JavaScript. Around them the trace wire
format reserves otel for W3C Trace Context correlation, with a producer
and an APM deep-link consumer on either side of it, gains per-attribute
source spans on the identity tables, and StatifierUI.Trace.Projection
redacts values out of a stream so a capture can travel without carrying
any.
Added
- The Livebook inspector's diagram can be moved to any macrostep in the
event log and back, pairing the two panes into a comprehension surface
for one point in a run rather than only the live tip (sui-3gg). Four
buttons above the diagram - |< First, < Prev, Next >,
Live - drive it; the selected macrostep's entry in the log is
opened and marked
- shown in the diagram, and a note above the diagram names the point on screen. StatifierUI.EventLog.configuration_at/2returns the configuration in force at a macrostep, distinguishing{:quiescent, configuration}from{:carried, from, configuration}(the macrostep never settled, so an earlier one's configuration is shown) and:before_first. A carried configuration is never presented as a measured one - which is what surfacessui-dc7's halting macrostep out loud instead of silently.StatifierUI.Inspectorgained the:selectionoption (:liveor{:macrostep, n}), pluspoints/1,step/3,resolution/2, andselection_note/2. Every decision the scrubber makes lives here, so a LiveView or other host gets the same behaviour without Kino.StatifierUI.EventLog.Markdown.render/2gained:selected, which suffixes one macrostep's summary with- shown in the diagram.
Nothing here re-derives a configuration: every one 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. Time travel is a read
of replay output as data (ADR-0002's inherited clause), so selecting a
macrostep neither touches the session nor needs anything new from the
engine.
StatifierUI.Trace.DeepLinkbuilds a URL into a host's APM backend from the wire format'sotelcorrelation key (ADR-0013), the consuming half of the producer shipped withsui-6e4. The URL template is host configuration -"https://apm.example.com/trace/{trace_id}?span={span_id}", with{trace_id},{span_id},{session}, and{macrostep}available and substituted values percent-encoded - because only the host knows which backend its spans went to.StatifierUI.EventLog.DeepLink, the rendering seam over anStatifierUI.EventLog.t():from_opts/1reads a renderer's:deep_linkoption,for_macrostep/2andfor_log/2answer per macrostep, andmarkdown/3renders the inline Markdown link.StatifierUI.EventLog.Markdown.render/2and everyStatifierUI.Inspectorfold take the same:deep_linkoption. A macrostep whose messages carryotelgets a[trace](...)link at the end of its event-log summary line, andInspector.selection_note/2gains an[open trace](...)link for the macrostep on screen.docs/telemetry.mdgains a section on the consuming end of theotelkey.
A malformed template raises where the option is read, so a typo cannot
quietly produce URLs that resolve nowhere. A step with no correlation - no
template configured, no otel key on the stream, or ids that are not W3C
Trace Context hex - renders no link and no error, which is the normal case
for every stream captured with no bridge attached, and every pane then
renders exactly what it rendered before. This package still calls
no OpenTelemetry API and gains no dependency.
StatifierUI.Trace.Subscriberaccepts an:otel_contextresolver -(session_id, macrostep -> {:ok, %{trace_id: binary, span_id: binary}} | :none)- and stamps the wire format'sotelenvelope key from it ontrace.*andeffect.*messages, the producer half of ADR-0013. The ids come from the host; this package still calls no OpenTelemetry API and gains no dependency. A resolver answering:none, returning a malformed or half pair, or raising leaves the key absent rather than failing the trace, and a projected stream carries the key unchanged.StatifierUI.Trace.Otel, the pure module deciding which messages may carryoteland whether an answer is well-formed W3C Trace Context hex.docs/telemetry.mdis published with the other guides on hexdocs.
Without an :otel_context resolver nothing changes: the key is absent
everywhere and golden traces stay byte-comparable.
StatifierUI.DatamodelExplorercarriessession.start'sprojectionheader on the pane struct and exposes it asprojected?/1andprojection_profile/1, so a host can surface the profile name alongside the mode (ADR-0012).StatifierUI.DatamodelExplorer.edit_disabled_reason/1and/2, with the boolean formseditable?/1andeditable?/2: ADR-0012's rule that no value-editing affordance may be offered over a projected stream, or over a redacted slot, as a function a write path can consult. The reason is a sentence written to be shown, naming the profile or the slot. The pane still has no write path of its own; this is the guard whatever builds one asks first.StatifierUI.DatamodelExplorer.Markdownrenders that reason under a live pane's header when the stream is projected, instead of leaving the reader to guess why every value reads(redacted).
StatifierUI.EventInjection is explicitly excluded from the same rule
and its moduledoc records why: its palette is composed from a fixtures
bundle the operator already holds in full, never from observed values, so
projection cannot reach it.
- Such an event now carries an
errorobject naming the failure kind, the expression, the span within it, and the absolute, pre-resolved document location of the failing subexpression - so a consumer underlines it directly with no span composition of its own.docs/wire-format.mdnow states the end-exclusive convention for spans and locations explicitly. StatifierUI.EventLog.configuration_at/2gained a fourth outcome,{:final, configuration}- macrostepnhalted the run, and this is the configuration it exited in. It is kept distinct from{:quiescent, configuration}for the same reason{:carried, ...}is: a configuration the chart exited in is not one it settled in, andStatifierUI.Inspector.selection_note/2words the two differently ("at the final configuration the run halted in").StatifierUI.EventLog.Macrostepgainedfinal_configurationandstamped/1, andStatifierUI.Inspector.points/1gainedfinal?besidequiescent?- a halting macrostep is not quiescent, and a scrubber saying so is not the same as one saying it has nothing to show.StatifierUI.Trace.Projectionprojects a trace stream down to structure, transitions, outcomes and ordering, replacing datamodel and payload values with the reserved{"$redacted": true}sentinel (ADR-0012). Build a profile withprofile/2and pass it toStatifierUI.Trace.Subscriber.start_link/1as:projection; every message is projected before it is buffered or fanned out, so a projected stream can be rendered, encoded or persisted without any of those having held a value.- A profile allows values back by path prefix (
allow_paths, matchingeffect.datamodel_change'slocation_pathencoding) or by naming an unlocated position (allow_positions). A prefix longer than a write's own path descends into the written value, so an allowed leaf passes while its withheld siblings are redacted.allow_source: falseadditionally redactssession.start's chart source. session.startcarries aprojectionheader naming the mode and profile whenever the stream is projected, so a projected capture is always distinguishable from a full one.StatifierUI.Valuedecodes{"$redacted": true}to the new:redactedatom and encodes it back;StatifierUI.Shapegains a matching:redactedshape that renders asredacted.StatifierUI.Live- read-only LiveView function components for a host application's ops views:ops_view/1composingstatus/1,scrubber/1,diagram/1, andevent_log/1over one trace stream in wire format v1, live or persisted. Compiled only when the optional:phoenix_live_viewdependency is present (ADR-0004); without it every component raises with instructions.docs/ops-embedding.mdis the embedding guide.StatifierUI.Live.State- the pure read model a host keeps in its socket:new/2over a persisted message list,push/2for a live subscriber's fan-out (dropping any message whoseseqis not newer than the newest one held, so theadd_listenerthensync/2overlap costs nothing),sync/2to pull a subscriber's buffer and stats in one call, andscrub/2/select/2overStatifierUI.Inspector's selection.- The event log renders as HTML rather than Markdown in a LiveView host, so clicking a macrostep entry moves the diagram to it - the link the Livebook pane cannot have, because a Markdown document has no click target.
docs/ops-embedding.md- embedding the ops view in a host LiveView, live and persisted, with the styling hooks and the Mermaid client the diagram pane expects a host to supply.StatifierUI.Trace.Capturemakes recording, saving, and reloading a trace one call each:record/3off a liveStatifier.Session,save/2to a JSON Lines file,load/1back into a message list.StatifierUI.Trace.Json.decode/1anddecode_lines/1read the wire format back intoStatifierUI.Trace.Messagestructs, over the newStatifierUI.Trace.Message.from_map/1.docs/ops-embedding.mdhas citeddecode/1since it was written; it now exists.StatifierUI.Kino.inspect_trace/3reopens a saved trace in the Livebook inspector, recompiling the chart from the SCXML the trace carries when the caller does not supply a machine.StatifierUI.Inspector.persisted_status/1renders the status header for a stream read from storage, which has no subscriber counts to report.docs/wire-format.mdspecifies JSON Lines as the file framing and states the v1 round-trip law: decoding and re-encoding a conformant stream reproduces its bytes.session.start'sstatesandtransitionsidentity tables now carry anattribute_locationsobject per row, mapping an attribute's name to that attribute's own value span. Key presence is the contract: an entry exists only for an attribute the author actually wrote, soattribute_locations["type"]being absent is how a consumer tells a transition that defaulted to external from one writtentype="external"- a question the loweredtypevalue cannot answer. A consumer wanting hover precision on a transition'seventortarget, or a state'sidorinitial, now has it fromsession.startalone; reading the%Statifier.Document{}tree for it is no longer necessary. Requires a statifier that carriesattribute_locationson the compiled Machine (statifier 2.0.0 and later).StatifierUI.Live.ExpressionInput- an expression field with completion, offering predicator's own grammar (operators, keywords, literal words, duration units, and every function the host's providers resolve) alongside the datamodel paths the host declares. It is the affordance statifier_blocks ADR-0005 defers to this package, and it drops into that editor'sexpression_componentseam as&StatifierUI.Live.ExpressionInput.expression_input/1(sui-wqr). The field carries no event of its own: it renders an<input>with thenameit was given, so edits - typed or completed - arrive through the host form's existingphx-change.StatifierUI.Expression- the completion source behind it, pure and without LiveView:completions/2returns{label, insert, kind, detail}entries read fromPredicator.Vocabularyand the supplied paths, so a host can render its own control over the same list.- This package now ships JavaScript, as source under
assets/, per ADR-0009. A host adds"statifier_ui": "file:../deps/statifier_ui/assets"to itsassets/package.jsonand spreadsStatifierUIHooksfromjs/index.jsinto itsLiveSockethooks. The one hook,StatifierUIExpressionInput, upgrades the field from a native<datalist>to a caret-aware completion list; it imports nothing, and a host that registers no hook keeps a working field. Hook names and export names are public API. See the ADR-0009 note of 2026-09-02 for the layout.
Changed
StatifierUI.Inspector.event_log/1is nowevent_log/2, taking the same options as the other fold functions. The one-argument call is unchanged in behaviour.- A transition written
type="internal"now carries an[internal]marker in the diagram. SCXML'sexternaldefault stays unmarked, so the two no longer render identically. StatifierUI.Diagram's moduledoc gained a "Known limits of this projection" section: lifted-edge geometry (including edges between two regions of one parallel state), self-edge notation for internal transitions, pseudo-states drawn as ordinary nodes, shallow-versus-deep history distinguishable only by label, executable content not drawn, and layout left entirely to Mermaid. These are the accepted limits of the Mermaid backend rather than defects; ADR-0008's elkjs renderer is where they are addressed.- The trace wire format reserves
otelas an envelope key: an optional object carrying the W3C Trace Contexttrace_idandspan_idof the OpenTelemetry span covering a message's macrostep, legal ontrace.*andeffect.*messages only (ADR-0013). No producer emits it yet, the format version stays1, and a stream with no correlation context attached is byte-unchanged - but a payload may no longer useotelas a key. - The wire format's reserved one-key
$-prefixed shape admits$redacted, making five reserved forms rather than four. The format version stays1: no existing stream changes, and full fidelity remains the default and is byte-unchanged. - Nothing existing moves. Every row keeps its element-level
locationunchanged, andattribute_locationsis{}for an element that wrote no attributes, for the synthesized initial transition, and for a Machine compiled by an older engine - in each case a consumer falls back tolocation, the granularity this format offered before. The format version stays1; the addition is additive per ADR-0005. cond_locationis retained rather than superseded. It falls back to the transition's ownlocationwhen a guard was written without a recorded span, whereattribute_locationssimply omits the key, so the two answer different questions. Preferattribute_locations["cond"]for new work.contentsanddatarows are unchanged and carry noattribute_locations.predicatoris now a direct dependency at~> 9.1, the release that carriesPredicator.Vocabulary. It arrived only throughstatifierbefore; the completion source reads the vocabulary itself, and a host on a predicator without it gets its declared paths and no grammar entries rather than an error (sui-vsx).
Fixed
StatifierUI.Diagram.render/2no longer drops transitions the Mermaid projection has no obvious notation for. A targetless transition - the spec-legal way to run executable content without changing configuration - was rendered as nothing at all, so a state that handles an event read as one that ignores it; it is now drawn as a self-edge marked[internal]. A history state's default transition, which lives inhistory_defaultrather than in the selectabletransitionslist, was dropped the same way, leaving the(H)/(H*)label naming a pseudo-state whose fallback target was invisible; it is now drawn with a[default]marker.An event whose data is an expression-evaluation failure no longer fails to normalize. Previously
StatifierUI.Value.encode/1rejected the failure payload and the whole trace message was dropped, so the diagnostic a consumer most needs never reached the wire.The configuration pane now shows a halted chart's final configuration (sui-dc7). A run that ends by entering a top-level
<final>never reaches quiescence in its last macrostep, so it emits notrace.macrostep_stablefor it;StatifierUI.Inspectorread only that message type and therefore kept highlighting the state the chart had left, while the datamodel pane showed the assignment that moved it out. Both stamping messages are read now, newest wins.The wire format is what settles that
trace.donemay be read this way: itsconfigurationfield is defined as "the full configuration as it stood at exit, a genuine set, sorted ascending" - the same shape and the same authority as atrace.macrostep_stablepayload. Nothing here re-derives an exit configuration from the exit sets that precedetrace.done; the format version is unchanged and the engine is untouched.StatifierUI.EventLog.Markdownno longer raisesProtocol.UndefinedErrorwhen asession.terminatedmessage carries a non-stringreason. It and the datamodel explorer now render a withheld value as(redacted)rather than as unbound or as a literal one-key map.docs/wire-format.mdno longer sayssession.start'sdata.value_locationis present only when the compiler recorded a value span. A conformant producer always emits it, falling back to the element's own span, exactly as the surrounding prose already said - so a consumer need not handle its absence.
[0.2.0] 2026-08-27
Fixtures become executable. ADR-0006 adds named datasets and free-standing
expressions to a fixture bundle, StatifierUI.TruthTable evaluates the two
against each other into a result matrix, and a bundle can now travel with a
single reusable chart fragment rather than with a whole chart - so a palette
entry carries its own worked examples and a host can run them in its own
suite.
Added
StatifierUI.Fixturesgainsdatasetsandexpressionsfields (ADR-0006): named example datamodels for evaluating expressions against, and named free-standing predicator expressions carrying anexpectmap keyed by dataset name.StatifierUI.Fixtures.Sourcegains optionaldatasets/0andexpressions/0callbacks so a host can supply the two new maps from Elixir alongsidescenarios/0andexample_events/0.StatifierUI.Fixtures.Lintreports an expression matching no compiled guard and anexpectkey naming no dataset, both as warnings.StatifierUI.Fixtures.Expectationsruns everyexpectentry against its named dataset and reports whether the stated value held, for wiring into a host's own test suite.- Depends directly on
predicator(~> 9.0) rather than only transitively throughstatifier. StatifierUI.TruthTableevaluates a bundle's expressions across its datasets and returns the ADR-0006 result matrix, one cell per(expression, dataset)pair. A cell's verdict is:satisfied,:unsatisfied,:undefined,:value,:error, or:missing_dataset- deliberately nottrue/false, so predicator's three-valuedundefinedcannot be collapsed into false by Elixir truthiness.StatifierUI.TruthTable.Markdownrenders that matrix as Markdown, with datasets down the rows and expressions across the columns by default, or transposed withorientation: :expressions_as_rows. Every cell spells its value out as a word and adds emphasis on top, so the three truth values stay distinct in plain text.StatifierUI.Kino.truth_table/2wraps the rendered matrix in aKino.Markdownwidget for a Livebook cell. It needs no session and no Phoenix; without the optional:kinodependency the stub points at the pure renderer instead.StatifierUI.Fixtures.Bundlelets an ADR-0003/ADR-0006 fixture bundle travel with one reusable chart fragment instead of with a whole chart, so a palette entry can carry its own executable examples. A fragment supplies its bundle as aStatifierUI.Fixturesstruct, an atom-keyed Elixir map, a string-keyed sidecar map, or a path to a.fixtures.jsonfile; all four route through the existing validation and converge on one struct.StatifierUI.Fixtures.Bundle.discover/2loads every entry's bundle across a palette of modules, anddiscover_dir/2does the same for a directory of<fragment>.fixtures.jsonfiles. Neither is all-or-nothing: a fragment that ships no examples is reported as an absence, and one malformed bundle is reported against its own name while the rest still load.StatifierUI.Fixtures.Bundle.Markdownrenders a fragment's "test this step" panel - its truth table and its expectation results together - andrender_discovery/2renders a whole palette's worth. The expectations summary reports four counts rather than a pass or a fail, becauseExpectations.check/2andFixtures.Lintdeliberately disagree about whether anexpectkey naming no dataset is a failure or a warning.StatifierUI.Kino.test_panel/2andStatifierUI.Kino.palette_panel/2wrap those renderings asKino.Markdownwidgets. Liketruth_table/2they need no session and no chart; without the optional:kinodependency the stubs point at the pure renderers instead.docs/fixture-bundles.mddocuments the convention and walks an embedder through wiring a palette entry's bundle, discovering a whole palette, and running every fragment's expectations in a host suite.
[0.1.1] 2026-08-24
Documentation-only release: brings the hexdocs to the shared fleet standard. No code changes.
Changed
- Unpublishes the ADRs from hexdocs; they remain in the repository under
docs/adr/. - Fixes the five broken links in the published docs - repo-relative references (research doc, ADRs, inspector notebook, architecture's research link, LICENSE) now resolve as absolute GitHub URLs or badges.
- Adds a badge row (CI, Hex version, downloads, hexdocs, license) and a Documentation index to the README, and corrects the stale claim that the project has no CI.
mix docsnow builds with zero warnings.
[0.1.0] 2026-08-22
First release: authoring, observing, and debugging components for the statifier statechart engine, consuming its effect stream through the language-neutral trace wire format (format version 1). The Livebook inspector is the first assembled frontend; the panes underneath it are pure folds any other frontend can render.
Added
StatifierUI.Diagram.render/2renders a compiled machine and an active configuration as MermaidstateDiagram-v2source forKino.Mermaid, with composite nesting, parallel regions, active-state highlighting, and cross-hierarchy transitions lifted to the composite level with a[lifted: ...]marker.StatifierUI.EventLog.build/1folds a trace message stream into a log grouped by(macrostep, round), ordered by the producer's stamps rather than arrival, andStatifierUI.EventLog.Markdown.render/2renders it as collapsible Markdown forKino.Markdown, with wire-format indexes resolved to state and transition names byStatifierUI.EventLog.Labels.StatifierUI.EventInjection.build/1turns an ADR-0003 fixture bundle (ornil) into the event-injection pane model: a sorted palette of editable event buttons viaStatifierUI.EventInjection.Palette, afree_form_only?flag for the fixture-less degraded mode, andsend/2/send_draft/3to deliver aStatifierUI.EventInjection.DraftthroughStatifier.Session.send_event/2- the ordinary recordable input path, per statifier ADR-0029.StatifierUI.DatamodelExplorer.build_authoring/3andbuild_live/2build a read-only datamodel tree - document<data id>declarations, spec 5.10.1 system variables, predicator provider functions in scope, and either a fixture scenario or a live session's datamodel with entries markedchanged?per macrostep - andStatifierUI.DatamodelExplorer.Markdown.render/2renders it as Markdown forKino.Markdown.StatifierUI.Kino.inspect/3assembles the Livebook inspector: the configuration diagram, datamodel explorer, event injection, and event log panes composed over one shared subscriber, live-updating, detaching cleanly on cell re-evaluation. Compiled only when the optional:kinodependency is present.StatifierUI.Trace.Subscriber.attach/3acceptscatch_up: true: on a session started withrecord: truethe missed prefix is replayed into the buffer atomically with the subscription (statifier ADR-0049); an unrecorded session falls back to live delivery with a:not_recordeddiagnostic the inspector surfaces as "Live-only".StatifierUI.Inspector- the pure pane-assembly fold the Kino shell renders, usable by any other frontend.notebooks/inspector.livemd- the demo notebook, doubling as the milestone's manual acceptance test.- Serializes statifier's
DatamodelChangeeffect as theeffect.datamodel_changewire type, so consumers can observe datamodel values as they are written instead of only the variable namessession.datamodelcarries. New types are additive under the wire format's must-ignore rule. - Every
effect.*wire message carries the engine'sroundstamp in its envelope, alongsidemacrostepandmicrostep; consumers reading older recorded streams must still tolerateeffect.*messages without the key.