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.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.