# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Entries for unreleased work are not written here directly. Each issue drops a
fragment in [`changelog.d/`](changelog.d/README.md); 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.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/2` returns 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
  surfaces `sui-dc7`'s halting macrostep out loud instead of silently.
- `StatifierUI.Inspector` gained the `:selection` option (`:live` or
  `{:macrostep, n}`), plus `points/1`, `step/3`, `resolution/2`, and
  `selection_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/2` gained `: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.DeepLink` builds a URL into a host's APM backend from
  the wire format's `otel` correlation key (ADR-0013), the consuming half of
  the producer shipped with `sui-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 an
  `StatifierUI.EventLog.t()`: `from_opts/1` reads a renderer's `:deep_link`
  option, `for_macrostep/2` and `for_log/2` answer per macrostep, and
  `markdown/3` renders the inline Markdown link.
- `StatifierUI.EventLog.Markdown.render/2` and every `StatifierUI.Inspector`
  fold take the same `:deep_link` option. A macrostep whose messages carry
  `otel` gets a `[trace](...)` link at the end of its event-log summary
  line, and `Inspector.selection_note/2` gains an `[open trace](...)` link
  for the macrostep on screen.
- `docs/telemetry.md` gains a section on the consuming end of the `otel`
  key.

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.Subscriber` accepts an `:otel_context` resolver -
  `(session_id, macrostep -> {:ok, %{trace_id: binary, span_id: binary}} |
  :none)` - and stamps the wire format's `otel` envelope key from it on
  `trace.*` and `effect.*` 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
  carry `otel` and whether an answer is well-formed W3C Trace Context hex.
- `docs/telemetry.md` is 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.DatamodelExplorer` carries `session.start`'s `projection`
  header on the pane struct and exposes it as `projected?/1` and
  `projection_profile/1`, so a host can surface the profile name alongside
  the mode (ADR-0012).
- `StatifierUI.DatamodelExplorer.edit_disabled_reason/1` and `/2`, with the
  boolean forms `editable?/1` and `editable?/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.Markdown` renders 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 `error` object 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.md` now
  states the end-exclusive convention for spans and locations explicitly.
- `StatifierUI.EventLog.configuration_at/2` gained a fourth outcome,
  `{:final, configuration}` - macrostep `n` halted 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, and
  `StatifierUI.Inspector.selection_note/2` words the two differently
  ("at the final configuration the run halted in").
- `StatifierUI.EventLog.Macrostep` gained `final_configuration` and
  `stamped/1`, and `StatifierUI.Inspector.points/1` gained `final?`
  beside `quiescent?` - 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.Projection` projects 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 with `profile/2` and pass it to
  `StatifierUI.Trace.Subscriber.start_link/1` as `: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`, matching
  `effect.datamodel_change`'s `location_path` encoding) 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: false` additionally redacts
  `session.start`'s chart source.
- `session.start` carries a `projection` header naming the mode and profile
  whenever the stream is projected, so a projected capture is always
  distinguishable from a full one.
- `StatifierUI.Value` decodes `{"$redacted": true}` to the new `:redacted`
  atom and encodes it back; `StatifierUI.Shape` gains a matching `:redacted`
  shape that renders as `redacted`.
- `StatifierUI.Live` - read-only LiveView function components for a host
  application's ops views: `ops_view/1` composing `status/1`, `scrubber/1`,
  `diagram/1`, and `event_log/1` over one trace stream in wire format v1,
  live or persisted. Compiled only when the optional `:phoenix_live_view`
  dependency is present (ADR-0004); without it every component raises with
  instructions. `docs/ops-embedding.md` is the embedding guide.
- `StatifierUI.Live.State` - the pure read model a host keeps in its socket:
  `new/2` over a persisted message list, `push/2` for a live subscriber's
  fan-out (dropping any message whose `seq` is not newer than the newest one
  held, so the `add_listener` then `sync/2` overlap costs nothing),
  `sync/2` to pull a subscriber's buffer and stats in one call, and
  `scrub/2` / `select/2` over `StatifierUI.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.Capture` makes recording, saving, and reloading a trace
  one call each: `record/3` off a live `Statifier.Session`, `save/2` to a
  JSON Lines file, `load/1` back into a message list.
- `StatifierUI.Trace.Json.decode/1` and `decode_lines/1` read the wire format
  back into `StatifierUI.Trace.Message` structs, over the new
  `StatifierUI.Trace.Message.from_map/1`. `docs/ops-embedding.md` has cited
  `decode/1` since it was written; it now exists.
- `StatifierUI.Kino.inspect_trace/3` reopens 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/1` renders the status header for a
  stream read from storage, which has no subscriber counts to report.
- `docs/wire-format.md` specifies 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`'s `states` and `transitions` identity tables now carry an
  `attribute_locations` object 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, so
  `attribute_locations["type"]` being absent is how a consumer tells a
  transition that defaulted to external from one written `type="external"` -
  a question the lowered `type` value cannot answer. A consumer wanting
  hover precision on a transition's `event` or `target`, or a state's `id`
  or `initial`, now has it from `session.start` alone; reading the
  `%Statifier.Document{}` tree for it is no longer necessary. Requires a
  statifier that carries `attribute_locations` on 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's `expression_component` seam as
  `&StatifierUI.Live.ExpressionInput.expression_input/1` (sui-wqr). The field
  carries no event of its own: it renders an `<input>` with the `name` it was
  given, so edits - typed or completed - arrive through the host form's
  existing `phx-change`.
- `StatifierUI.Expression` - the completion source behind it, pure and
  without LiveView: `completions/2` returns `{label, insert, kind, detail}`
  entries read from `Predicator.Vocabulary` and 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 its `assets/package.json` and spreads `StatifierUIHooks` from
  `js/index.js` into its `LiveSocket` hooks. 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/1` is now `event_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's `external` default 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 `otel` as an envelope key: an optional
  object carrying the W3C Trace Context `trace_id` and `span_id` of the
  OpenTelemetry span covering a message's macrostep, legal on `trace.*` and
  `effect.*` messages only (ADR-0013). No producer emits it yet, the format
  version stays `1`, and a stream with no correlation context attached is
  byte-unchanged - but a payload may no longer use `otel` as a key.
- The wire format's reserved one-key `$`-prefixed shape admits `$redacted`,
  making five reserved forms rather than four. The format version stays `1`:
  no existing stream changes, and full fidelity remains the default and is
  byte-unchanged.
- Nothing existing moves. Every row keeps its element-level `location`
  unchanged, and `attribute_locations` is `{}` 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 to
  `location`, the granularity this format offered before. The format
  version stays `1`; the addition is additive per ADR-0005.
- `cond_location` is retained rather than superseded. It falls back to the
  transition's own `location` when a guard was written without a recorded
  span, where `attribute_locations` simply omits the key, so the two answer
  different questions. Prefer `attribute_locations["cond"]` for new work.
- `contents` and `data` rows are unchanged and carry no
  `attribute_locations`.
- `predicator` is now a direct dependency at `~> 9.1`, the release that
  carries `Predicator.Vocabulary`. It arrived only through `statifier`
  before; 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/2` no 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 in `history_default`
  rather than in the selectable `transitions` list, 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/1` rejected 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 no
  `trace.macrostep_stable` for it; `StatifierUI.Inspector` read 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.done` may be read this way:
  its `configuration` field is defined as "the full configuration as it
  stood at exit, a genuine set, sorted ascending" - the same shape and the
  same authority as a `trace.macrostep_stable` payload. Nothing here
  re-derives an exit configuration from the exit sets that precede
  `trace.done`; the format version is unchanged and the engine is
  untouched.
- `StatifierUI.EventLog.Markdown` no longer raises `Protocol.UndefinedError`
  when a `session.terminated` message carries a non-string `reason`. 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.md` no longer says `session.start`'s `data.value_location`
  is 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.Fixtures` gains `datasets` and `expressions` fields (ADR-0006):
  named example datamodels for evaluating expressions against, and named
  free-standing predicator expressions carrying an `expect` map keyed by
  dataset name.
- `StatifierUI.Fixtures.Source` gains optional `datasets/0` and
  `expressions/0` callbacks so a host can supply the two new maps from
  Elixir alongside `scenarios/0` and `example_events/0`.
- `StatifierUI.Fixtures.Lint` reports an expression matching no compiled
  guard and an `expect` key naming no dataset, both as warnings.
- `StatifierUI.Fixtures.Expectations` runs every `expect` entry 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
  through `statifier`.
- `StatifierUI.TruthTable` evaluates 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 not `true` / `false`, so predicator's three-valued
  `undefined` cannot be collapsed into false by Elixir truthiness.
- `StatifierUI.TruthTable.Markdown` renders that matrix as Markdown, with
  datasets down the rows and expressions across the columns by default, or
  transposed with `orientation: :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/2` wraps the rendered matrix in a
  `Kino.Markdown` widget for a Livebook cell. It needs no session and no
  Phoenix; without the optional `:kino` dependency the stub points at the
  pure renderer instead.
- `StatifierUI.Fixtures.Bundle` lets 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 a `StatifierUI.Fixtures` struct, an atom-keyed Elixir map, a
  string-keyed sidecar map, or a path to a `.fixtures.json` file; all four
  route through the existing validation and converge on one struct.
- `StatifierUI.Fixtures.Bundle.discover/2` loads every entry's bundle across
  a palette of modules, and `discover_dir/2` does the same for a directory
  of `<fragment>.fixtures.json` files. 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.Markdown` renders a fragment's "test this
  step" panel - its truth table and its expectation results together - and
  `render_discovery/2` renders a whole palette's worth. The expectations
  summary reports four counts rather than a pass or a fail, because
  `Expectations.check/2` and `Fixtures.Lint` deliberately disagree about
  whether an `expect` key naming no dataset is a failure or a warning.
- `StatifierUI.Kino.test_panel/2` and `StatifierUI.Kino.palette_panel/2`
  wrap those renderings as `Kino.Markdown` widgets. Like `truth_table/2`
  they need no session and no chart; without the optional `:kino`
  dependency the stubs point at the pure renderers instead.
- `docs/fixture-bundles.md` documents 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 docs` now builds with zero warnings.

## [0.1.0] 2026-08-22

First release: authoring, observing, and debugging components for the
[statifier](https://hex.pm/packages/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/2` renders a compiled machine and an active
  configuration as Mermaid `stateDiagram-v2` source for `Kino.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/1` folds a trace message stream into a log
  grouped by `(macrostep, round)`, ordered by the producer's stamps rather
  than arrival, and `StatifierUI.EventLog.Markdown.render/2` renders it as
  collapsible Markdown for `Kino.Markdown`, with wire-format indexes
  resolved to state and transition names by `StatifierUI.EventLog.Labels`.
- `StatifierUI.EventInjection.build/1` turns an ADR-0003 fixture bundle
  (or `nil`) into the event-injection pane model: a sorted palette of
  editable event buttons via `StatifierUI.EventInjection.Palette`, a
  `free_form_only?` flag for the fixture-less degraded mode, and
  `send/2`/`send_draft/3` to deliver a `StatifierUI.EventInjection.Draft`
  through `Statifier.Session.send_event/2` - the ordinary recordable input
  path, per statifier ADR-0029.
- `StatifierUI.DatamodelExplorer.build_authoring/3` and `build_live/2` build
  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
  marked `changed?` per macrostep - and
  `StatifierUI.DatamodelExplorer.Markdown.render/2` renders it as Markdown
  for `Kino.Markdown`.
- `StatifierUI.Kino.inspect/3` assembles 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 `:kino`
  dependency is present.
- `StatifierUI.Trace.Subscriber.attach/3` accepts `catch_up: true`: on a
  session started with `record: true` the 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_recorded`
  diagnostic 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 `DatamodelChange` effect as the
  `effect.datamodel_change` wire type, so consumers can observe datamodel
  values as they are written instead of only the variable names
  `session.datamodel` carries. New types are additive under the wire
  format's must-ignore rule.
- Every `effect.*` wire message carries the engine's `round` stamp in
  its envelope, alongside `macrostep` and `microstep`; consumers reading
  older recorded streams must still tolerate `effect.*` messages without
  the key.
