API Reference Statifier v#2.1.0

Copy Markdown View Source

Modules

Flags likely violations of the mechanically-checkable ADRs in docs/adr/.

Judges the current branch's diff against a registry of judged ADRs, each carrying its own path scope, ADR text, and failure vocabulary, using two independent model calls per ADR: one proposes violations, a second is prompted to refute each one. Only a proposed violation the refute pass fails to overturn becomes a finding - a single pass reporting whatever it first notices is exactly what the adversarial-verification requirement on this check rules out, because a false positive here blocks a commit (CLAUDE.md: "never go green by weakening the check" means the fix for a bad finding has to be "the check was wrong," not "disable the check" - so the bar to reach gate-failure status is higher than an FYI).

Finds changes to the quality gate's own configuration that no one has justified in writing.

Reads and writes test/passing_tests.json, the regression ratchet registry.

The public entry point for statifier-ex.

The versioned binary contract for a chart - a Statifier.Machine.t() reduced to the inputs that reproduce it: its SCXML source, the persisted subset of the options it was compiled with, and its Statifier.Machine.Identity.t(). No compiled term is written - from_binary/1 rebuilds a Machine.t() by recompiling the stored source with the stored options through Statifier.compile/2, the same pipeline any other caller runs, rather than by deserializing compiler output directly.

The fourth arrow of the parser pipeline: a validated %Statifier.Document{} in, {:ok, %Statifier.Machine{}} | {:error, [Statifier.Compiler.Error.t()]} out (docs/architecture.md:47-51). Nothing downstream of this pass ever sees a %Statifier.Document{} again - the interpreter accepts only a Machine (docs/architecture.md principle 4).

The compiler's own error shape: {reason, message, location}, character-identical in style to Statifier.Lowering.Error (lib/statifier/lowering/error.ex:21-36).

The expression-compilation seam: compiles raw predicator source - a cond, an expr, or a <content> text body - into the single Machine.expr() sum type, on its own, before either of its two consumers (the transition pass's transitions, the executable-content pass's content/donedata) so neither lands a raw-string field and changes its type afterwards.

The typed parse target: what lowering produces, and what the validator and the Machine compiler consume.

An <assign> executable-content element: spec 5.4's deep-path datamodel write.

One <onentry> or <onexit> element: an ordered list of executable content plus the location of the element that contains it.

A <cancel> element: spec 6.3's instruction to cancel a delayed <send>.

A <content> element: static text, an expr, markup, or (representably, if not validly) any combination.

A <data> element: an id, and at most one of expr, src, or child text as its value source.

A <datamodel> element: an ordered list of <data> children.

A <donedata> element: the optional payload a :final state's done.state.* event carries (spec 5.7).

A <foreach> executable-content element (spec 4.6): iterates over an array, binding item (and, optionally, index) in the datamodel for each element, then running its own child content once per iteration.

An <if> executable-content element (spec 4.3): the partitioned form of <if>/<elseif>/<else>, quoting spec 4.3.2 exactly

One partition of an <if> - the executable content between one partitioning tag (<if>, <elseif>, or <else>) and the next, per Statifier.Document.If's moduledoc.

An <initial> element: spec 3.6's way to name a compound state's default entry point via a <transition> rather than an initial attribute.

An <invoke> element: spec 6.4's instruction to create an instance of an external service.

A <log> executable-content element: spec 4.7's diagnostic output point.

A <param> element under <donedata>: spec 5.7's key-value alternative to <content>.

A <raise> executable-content element: spec 4.2's way to enqueue an internal event.

A <script> element (spec 5.8): a predicator statement program body, run for its side effects on the datamodel (ADR-0026) - not an expression-bearing node like <log>/<content>, and compiled through Statifier.Compiler.Expressions.compile_program/3 rather than compile/3. Legal both as executable content (<onentry>, <onexit>, a transition, an <if> branch, a <foreach> body) and, spec 5.8, as a direct child of <scxml>, evaluated at document load time - Statifier.Lowering.Builders.place/3 sorts the two by parent.

A <send> element: spec 6.2's instruction to send an event, immediately or after a delay.

One <state>, <parallel>, <final>, or <history> element - a single struct with a kind atom rather than four per-kind structs.

A <transition> element: spec 3.5, matched against events and guarded by a condition to move the configuration from one set of states to another.

The one place this engine turns an SCXML duration designation - <send delay>'s attribute string, or a delayexpr result - into milliseconds. Wraps Predicator.Duration rather than reimplementing CSS2-style duration parsing.

The effect vocabulary (ADR-0003) plus the nine trace effects (docs/observability.md constraint 2) - one @type t() union, in this one module, that every interpreter function emits from and every consumer pattern-matches against, including Statifier.Session, which drives <send>/<cancel>/<invoke>. This module defines the vocabulary and the trace gate; it never emits an effect itself.

Payload for {:autoforward, %__MODULE__{}} - spec 6.4's autoforward attribute, Appendix D's if inv.autoforward: send(inv.id, externalEvent) in mainEventLoop's finalize/autoforward pass (:152-158). invoke_id names the invocation to forward to; state_index is the constraint-3 identity of the invoking state; event is the external event that triggered the pass, carried verbatim; macrostep/microstep/round are the counters as they stood at the moment of the pass.

Payload for {:budget_exhausted, %__MODULE__{}} - ADR-0019's outcome when Statifier.Interpreter.macrostep/1's fold spends Statifier.MachineState.max_macrostep_rounds without reaching quiescence.

Payload for {:cancel, %__MODULE__{}} - spec 6.3's <cancel>. send_id is the sendid/sendidexpr attribute naming the delayed send to cancel; resolving it against the pending SendDelayed timers is Statifier.Session's job.

Payload for {:cancel_invoke, %__MODULE__{}} - the cancellation Appendix D's exitStates and exitInterpreter both perform with for inv in s.invoke: cancelInvoke(inv) when a state carrying live invocations exits. invoke_id names the invocation to cancel; state_index is the constraint-3 identity of the state that owned it. macrostep/microstep/ round are the counters as they stood at the moment of the cancel.

Payload for {:datamodel_change, %__MODULE__{}} - one successful datamodel write, carrying enough to reconstruct the datamodel from the effect stream alone, without ever calling Session.snapshot/1 (plan decision 1). Emitted for every successful write write_location/4 performs (decision 2); a failed write emits nothing, since the datamodel did not change and the failure is already observable on the error channel (decision 9). One exception: a <send>'s idlocation write that ADR-0047 goes on to reject (an invalid target or unsupported type, classified after the write lands) emits no effect either, because the composite error return that carries the rejection has no effects slot. The write is in the datamodel regardless - a consumer reading it back through _event.sendid sees it - and live and replay agree because both derive from the core.

Payload for {:datamodel_init, %__MODULE__{}} - the datamodel's starting map, emitted once per Statifier.Interpreter.initialize/2, before the first <data> value is evaluated (plan decision 1).

Payload for {:done, %__MODULE__{}} - the terminal effect exit_interpreter (Appendix D) produces once, after top-level final entry, when Statifier.MachineState.status becomes :done. donedata is the top-level final's resolved <donedata> content, or nil when the final carries none. configuration is the full configuration (ADR-0005, ancestors included) as it stood at exit: the full set, so a consumer can observe the terminal position without switching tracing on, since MachineState.configuration is empty by then and Statifier.active_leaf_states/1 correctly reports nothing active. See Statifier.Interpreter.exit_interpreter/1.

Payload for {:invoke, %__MODULE__{}} - spec 6.4's <invoke>. Fields are the element's own attribute names: invoke_id (id/idlocation, generated when the element has none), type, src, params (the resolved <param>/namelist payload), content (the resolved <content> payload), autoforward (the autoforward attribute).

Payload for {:log, %__MODULE__{}} - spec 4.7's <log>. label is the label attribute (nil when the element omits it); value is the resolved expr (or nil for a label-only <log>).

Payload for {:send, %__MODULE__{}} - spec 6.2's <send>, fired immediately (no delay/delayexpr; that variant is Statifier.Effect.SendDelayed). Fields are the element's own attribute names so the not-yet-implemented session/invoke support, which will give this effect its semantics, can extend rather than rename them: event is the event name being sent, target and type are the target/type attributes (nil when the element omits them), data is the resolved payload, send_id is the id attribute (generated when the element has none).

Payload for {:send_delayed, %__MODULE__{}} - spec 6.2's <send> when delay/delayexpr is present. Carries every field Statifier.Effect.Send does, plus delay_ms, the resolved delay in milliseconds. The timer that fires this send is Statifier.Session's to schedule; this module only defines the shape it schedules.

Trace payload for {:trace, %__MODULE__{}} - emitted when a block of executable content ran (docs/observability.md constraint 2's "content executed" row). c_indexes are the run content nodes' identities (constraint 3), in execution order.

Trace payload for {:trace, %__MODULE__{}} - emitted alongside Statifier.Effect.Done at top-level final entry / exit_interpreter (docs/observability.md constraint 2's "done" row). donedata and configuration mirror the core :done effect's payload - both effects are built from the same configuration_at_exit binding in Statifier.Interpreter.exit_interpreter/1, so they can never disagree. This effect exists for the observability row (ADR-0012, docs/observability.md:68), not because it is the only carrier of the configuration.

Trace payload for {:trace, %__MODULE__{}} - emitted with compute_entry_set's result, before any state is entered (docs/observability.md constraint 2's "entry set" row). indexes are the states to be entered (constraint 3, integer indexes), in entry order.

Trace payload for {:trace, %__MODULE__{}} - emitted when an external or internal event is selected for processing (docs/observability.md constraint 2's "event dequeued" row). event is the dequeued Statifier.Event.t(); from names which queue it came off.

Trace payload for {:trace, %__MODULE__{}} - emitted before any state is exited (docs/observability.md constraint 2's "exit set" row). indexes are the states to be exited (constraint 3, integer indexes), in exit order.

Trace payload for {:trace, %__MODULE__{}} - emitted once per external event, at the end of the finalize/autoforward pass: for state in configuration: for inv in state.invoke: if inv.invokeid == externalEvent.invokeid: applyFinalize(inv, externalEvent); if inv.autoforward: send(inv.id, externalEvent) (Appendix D, Statifier.Interpreter's own moduledoc section "The finalize/autoforward pass runs once per external event, inside handle_event/2, before transition selection"). This is a phase boundary Appendix D itself names, just not one docs/observability.md's constraint-2 table enumerated when it was written - <invoke> did not exist in this interpreter yet (ADR-0012's parenthetical is illustrative, not a closed list).

Trace payload for {:trace, %__MODULE__{}} - emitted once the invoke pass finishes: for state in statesToInvoke.sort(entryOrder): for inv in state.invoke.sort(documentOrder): invoke(inv) followed by statesToInvoke.clear() (Appendix D, Statifier.Interpreter's own moduledoc section "The invoke pass runs at the end of every fold, inside main_event_loop/3"). This is a phase boundary Appendix D itself names, just not one docs/observability.md's constraint-2 table enumerated when it was written - <invoke> did not exist in this interpreter yet (ADR-0012's parenthetical is illustrative, not a closed list).

Trace payload for {:trace, %__MODULE__{}} - emitted when the configuration reaches quiescence, i.e. the macrostep's microstep loop has no more eventless transitions or internal events to drain (docs/observability.md constraint 2's "macrostep stable" row). configuration is the full configuration (ADR-0005, ancestors included) as it stood at quiescence.

Trace payload for {:trace, %__MODULE__{}} - emitted whenever select_transitions returns, including the empty set (docs/observability.md constraint 2's "transitions selected" row). t_indexes are the selected transitions' t_index identities (constraint 3, never %Statifier.Machine.Transition{} structs), in the order selection returned them. event is the event the selection matched against, or nil for an eventless round.

The evaluation half of docs/datamodel.md's evaluation contract (ADR-0014): one module, one evaluate/2 over both arms of Statifier.Machine.expr(), built against a context this module's own context/1 produces. Mirrors Statifier.Compiler.Expressions - one module per side of the compile/evaluate seam.

Statifier.Evaluator.evaluate/2's own error value (ADR-0014 item 4).

The Predicator.FunctionProvider carrying In/1 (spec 5.9.1), replacing Statifier.Evaluator's former private closure-building helper.

Spec 5.10's system variables, as the plain maps Statifier.MachineState.datamodel carries them in. Two functions, so that neither Statifier.MachineState nor Statifier.Interpreter grows spec 5.10 knowledge of its own - MachineState.new/2 calls initial/2 once, and MachineState.put_event/2 calls event/1 on every write.

The value every queue holds and every selection round matches against - spec 5.10.1's event, name plus data plus type, with the constraint-4 cause slot (docs/observability.md) for events the platform raised itself.

Why an internally raised event exists - docs/observability.md constraint

docs/datamodel.md's standing commitment discharged: one function, with defined rules, that normalizes a value into _event.data per spec B.2.8.1. <param> and <content> (under <donedata>) call it here; namelist, <send>, and <invoke> extend it rather than reinventing it when they land.

The one dispatch point every executable-content node implements. There is no central case/cond anywhere in this tree that switches on a node's kind (docs/architecture.md:101-105's named v1 mistake); a node's runtime behavior lives in exactly one place, this protocol's defimpl for that node's struct, in the same file as the struct itself (Statifier.Machine.Content.Raise, Statifier.Machine.Content.Log, Statifier.Machine.Content.If, Statifier.Machine.Content.Foreach). A future datamodel element (<script>) or session element (<send>/<cancel>/<invoke>) adds a struct and a defimpl, never a clause here or in the block runner.

The second argument every Statifier.ExecutableContent.execute/2 call receives, alongside the node itself.

Appendix D's outer loop, ported function for function (ADR-0002), with its loop state reified onto %Statifier.MachineState{} per docs/observability.md constraint 1: any %MachineState{} value is a complete, resumable interpreter position, and this module keeps nothing of that position on the call stack.

The block runner - spec 4.9's rule for a block of executable content, ported once here rather than at each of Statifier.Interpreter.ExitEntry's four call sites: a block runs its nodes in document order; if a node errors, the rest of the block does not run, and the error becomes an error.* event on the internal queue - error.execution for every failure but one, and error.communication for a <send> the core judges undeliverable (ADR-0048) - the errors-are-events conversion happens here and only here, never in a leaf node's own Statifier.ExecutableContent.execute/2 implementation. Other blocks are unaffected: a block that errors leaves every other block - another <onentry> on the same state, an <onexit>, a transition's own content - to run exactly as if nothing had happened, since each call to execute_block/3 is independent.

Datamodel creation and early/top-level binding - the interpret preamble hook (Statifier.Interpreter.initialize/2).

What Statifier.Interpreter.Datamodel.write_location/4 wrote - a report of a write that already happened, never an instruction to perform one. path is the resolved Predicator.ContextLocation.location_path(), new_value is what was written, and prior_value is what stood at that full path immediately before the write - :undefined when nothing did, per ADR-0037's single spelling for an unbound value. This does conflate "the path was absent" with "the path held :undefined" already; that is accepted, since prior_value exists for diffing and undo, never for reconstruction.

Appendix D's exit and entry blocks, ported function for function (ADR-0002) - the half of the algorithm that changes the configuration. Statifier.Interpreter.Selection answers "which transitions fire and what would they leave"; this module actually leaves and enters states.

Spec 3.13 event descriptor matching - Appendix D's nameMatch, under ADR-0002's predicate-naming amendment.

Appendix D transition selection, ported function for function (ADR-0002).

The extension seam docs/datamodel.md has always pointed hosts at ("real computation belongs in the host application, reached through <invoke> handlers") but never had until ADR-0051: a behaviour a host implements to serve <invoke type="..."> values beyond the built-in scxml handler (Statifier.Invoke.Handler.Scxml, sitting beside this module the way Statifier.Invoke.Source does).

The built-in type=scxml (and long-URI, http://www.w3.org/TR/scxml/) Statifier.Invoke.Handler - Statifier.Session.Effects.plan_invoke/3's default entry for the built-in type set, not a name it special-cases (ADR-0051 decision 4).

Turns an %Statifier.Effect.Invoke{}'s content/src pair into a Statifier.Machine.t() ready to start as a child session - the seam ADR-0038 decided: the library never dereferences src.

A caller-declared, point-in-time claim about which <invoke type> values this deployment implements beyond the built-in scxml handler (ADR-0051 decision 2) - a plain value in Statifier.Invoke's namespace, mirroring Statifier.Send.Routes's own shape and moduledoc posture.

The second arrow of the parser pipeline: a generic %Statifier.Parser.DOM.Element{} tree in, a typed %Statifier.Document{} tree out (docs/architecture.md, "Parser: DOM first, then lowering").

The four attribute operations every builder in Statifier.Lowering.Builders needs: read a raw value, split a whitespace-separated value into a list, map a value onto a known atom with a default, and record a value span into attribute_locations.

One build_* function per supported SCXML element, reached through Statifier.Lowering's dispatch map - the structural fix for v1's 903-line, 73-clause state_stack.ex (docs/architecture.md, "adding an element touches one builder").

Lowering's own error shape: never raised, always collected into a list.

Prefix scope resolution for Statifier.Lowering's walk.

The compiled, valid-by-construction interpreter input (docs/architecture.md:47-51, principle 4). Statifier.Compiler.compile/1 is the only producer; the interpreter is the only consumer that matters - every other layer stops at %Statifier.Document{}.

One compiled <onentry> or <onexit> element - the interned counterpart to Statifier.Document.Block, built by the compiler's executable-content pass.

Namespace for the compiled executable-content node family: one struct per node kind, Statifier.Machine.Content.Raise, Statifier.Machine.Content.Log, Statifier.Machine.Content.Assign, Statifier.Machine.Content.If, Statifier.Machine.Content.Foreach, Statifier.Machine.Content.Script, Statifier.Machine.Content.Send, and Statifier.Machine.Content.Cancel, the interned counterpart to Statifier.Document.Raise / Statifier.Document.Log / Statifier.Document.Assign / Statifier.Document.If / Statifier.Document.Foreach / Statifier.Document.Script / Statifier.Document.Send / Statifier.Document.Cancel. This module owns the family's shared vocabulary - owner/0 and the t() union - and no longer a struct itself: an Elixir protocol dispatches on the struct module, so each node kind needs its own struct for Statifier.ExecutableContent to implement without a central case on a kind field. Each future executable-content node gets its own struct here too, and its own Statifier.ExecutableContent implementation, never a clause added to this module.

A compiled <assign> executable-content node (spec 5.4, 5.9.2) - the interned counterpart to Statifier.Document.Assign.

A compiled <cancel> executable-content node (spec 6.3) - the interned counterpart to Statifier.Document.Cancel.

A compiled <foreach> executable-content node (spec 4.6) - the interned counterpart to Statifier.Document.Foreach. array is the compiled Machine.expr() the iteration source evaluates once, before any iteration (spec 4.6.3's shallow copy - see step 3 in execute/2's doc below for why one evaluation is enough). item/index are the raw attribute strings - bare variable names by 4.6.2, so neither is compiled or resolved any earlier than execute/2. content is the dense, document-order c_index list of <foreach>'s own body, resolved through Statifier.Machine.content/2 at runtime rather than carried inline (Statifier.Compiler's Decision 2).

A compiled <if> executable-content node (spec 4.3, 4.4, 4.5) - the interned counterpart to Statifier.Document.If. branches is the document-order partition list: each Statifier.Machine.Content.If.Branch holds its compiled cond (nil for the <else> branch - spec 4.5.1: "<else> ... is equivalent to an <elseif> with a 'cond' that always evaluates to true"), the diagnostic span that cond compiled against, and the dense c_index list assigned to that partition's own content (Statifier.Compiler's Decision 2), resolved through Statifier.Machine.content/2 at runtime rather than carried inline.

One compiled <if>/<elseif>/<else> partition - a small struct alongside Statifier.Machine.Content.If (Decision 8 of the plan cited on the parent module), parallel to Statifier.Document.If.Branch without being identical to it: this struct carries no c_index of its own (a branch is not itself a dispatchable content node, only its own content is), and its cond is already compiled.

A compiled <log> executable-content node (spec 4.7) - the interned counterpart to Statifier.Document.Log. label is the optional diagnostic label; expr is the optional Machine.expr() to evaluate and log; expr_location is attribute_locations[:expr]'s value span, nil when expr was never written.

A compiled <raise> executable-content node (spec 4.2) - the interned counterpart to Statifier.Document.Raise. event is the literal event name being enqueued - never tokenized, unlike a <transition>'s event attribute (lib/statifier/document/raise.ex:6-13).

A compiled <script> executable-content node (spec 5.8, ADR-0026) - the interned counterpart to Statifier.Document.Script.

A compiled <send> executable-content node (spec 6.2) - the interned counterpart to Statifier.Document.Send.

One compiled <data> element - the interned counterpart to Statifier.Document.Data, dense from d_index 0 across the whole machine (ADR-0012 item 3), read via Statifier.Machine.data/2.

One compiled <donedata> element - the interned counterpart to Statifier.Document.Donedata, built by the compiler's executable-content pass, reachable only through its owning :final state as elem(machine.states, i).donedata.

A chart's identity: a content hash taken over the SCXML source Statifier.compile/2 received, plus an optional embedder-supplied name and version. Statifier.compile/2 stamps one onto every Statifier.Machine.t() it produces (Statifier.Machine.identity/1).

One compiled <invoke> element (spec 6.4) - the interned counterpart to Statifier.Document.Invoke, reachable only through its owning state as elem(machine.states, i).invoke, in document order.

One compiled <param> element - the interned counterpart to Statifier.Document.Param. A %Param{} is not reachable only through Statifier.Machine.Donedata.params: Statifier.Machine.Content.Send and Statifier.Machine.Invoke each own a namelist and a params list of Param.t() too.

One compiled <scxml>, <state>, <parallel>, <final>, or <history> element - the interned, indexed counterpart to Statifier.Document.State (and, for index 0, to Statifier.Document itself).

One compiled <transition> element - the interned counterpart to Statifier.Document.Transition, built by the compiler's transition pass.

The reified interpreter position - every Appendix D global and loop variable this core keeps, per docs/observability.md constraint 1 (ADR-0012): any %MachineState{} value is a complete, inspectable, resumable position. Statifier.Machine is the compiled document this struct walks; interpreter modules alias Statifier.MachineState as MachineState and Statifier.Machine.State as State, never abbreviating either further, because the two names are one character apart and mean very different things - a compiled state versus a runtime position.

XML source in, generic DOM tree out.

The generic document tree Statifier.Parser.parse/1 produces, and the three accessors every lowering builder would otherwise re-implement.

One attribute of an element, carrying two spans rather than one.

A generic XML element: a name, an ordered attribute list, an ordered child list, and the source span it occupies.

A run of character data between two markup constructs.

The Saxy.Handler that builds a Statifier.Parser.DOM tree, attaching the positions Saxy does not supply.

A source span: 1-based line/column (columns counted in Unicode codepoints), 0-based byte offsets, exclusive end - the shape ADR-0014 fixed for expression spans, used here for XML nodes so the two compose.

A total, left-to-right scan of an XML source binary that produces the positions Saxy does not.

The parser's own error shape: never %Saxy.ParseError{} directly, so callers get one error type regardless of what failed inside.

The versioned binary contract for a position - a Statifier.MachineState.t() with the compiled chart it walks stripped out and its Statifier.Machine.Identity.t() carried alongside instead.

Re-drives a Statifier.Session.Recording through the pure core, with no process and no timer (ADR-0034).

A caller-declared, point-in-time claim about which <send> routes are live (ADR-0048 decision 1) - a plain value in Statifier.Send.Target's neutral namespace (ADR-0047 decision 3), carrying exactly what Statifier.Session.deliver/5 resolves today: the set of session ids reachable by {:session, sid}, whether a parent exists for :parent, and the set of live invoke ids reachable by {:invoke, id}.

Spec 6.2.2's target attribute and C.1's special-target vocabulary, parsed into a route, plus the supported-type predicate 6.2.5 asks for. One pure module, no process, no effect calls - Mix.Statifier.AdrGuard has nothing to say about it and no exemption is needed. It has two consumers: the core's <send> node, which rejects a {:invalid, _} target or an unsupported type under ADR-0047, and Statifier.Session.Effects, which applies the same classification at Statifier.Session.interpret/2's boundary (ADR-0029).

The GenServer effect interpreter (ADR-0003): the outer while running loop, the waiting external events, the delayed-send timers, and the fan-out of the effect stream to subscribers. The pure core decides; this module performs.

Turns the effect list the pure core returns into an ordered list of instructions for Statifier.Session to perform (ADR-0003). Deciding is here, where it is a pure function of the effect list; performing is there, where the process is.

The waiting external events, plus Appendix D's isCancelEvent check.

The parent-held invocation table, as a value (ADR-0027 decision 3): invokeid -> {child_session_id, pid, monitor_ref}, plus the autoforward flag the delivery half ({:forward, invoke_id, event}) reads. A reverse index, pid -> invoke_id, is carried alongside so a child's own :DOWN can be resolved with no scan.

The four-input replay recording (ADR-0029), as a value.

The Statifier.Session-pinned view of Statifier.Telemetry (ADR-0067 decision 2): every function here forwards to the caller-agnostic emitter with driver: :session pinned, so the nine functions 2.0.0 published keep their arities and keep working unchanged for existing consumers.

The pending delayed-send timers, as a value.

The ADR-0027 session runtime: a module-based supervisor holding the library's named session registry and the flat dynamic supervisor sessions started through Statifier.start_session/2 land on.

The :telemetry bridge (ADR-0040, amended by ADR-0067) - the single authoritative reference for every [:statifier, :session, ...] event any stepping driver can emit. This module is the caller-agnostic emission half of the ADR-0003 effect-interpreter role: it holds no state, drives no core function, and is called from wherever a driver steps a chart - Statifier.Session included, through the Statifier.Session.Telemetry facade that pins driver: :session.

Test case template for the SCION and W3C conformance corpora.

Detects SCXML features used in documents so the test harness can fail precisely on unsupported features.

A reusable conformance case for Statifier.Invoke.Handler implementations

The third arrow of the parser pipeline, and the only gate in front of the Machine compiler (docs/architecture.md principle 4): a %Statifier.Document{} in, {:ok, document, warnings} | {:error, errors, warnings} out. The interpreter only ever accepts a compiled Machine, so there is no "validate if not already validated" fallback anywhere downstream - this pass is the single place a malformed document is ever caught.

Spec 5.4.2: "A conformant SCXML document MUST specify either 'expr' or children of <assign>, but not both." Reports {:assign_expr_and_text, expr} at the <assign>'s own element span.

Checks 9 and 10 (spec 3.2.1), which close the boilerplate gap that relaxed parsing left open. Lowering accepts a boilerplate-free <scxml> fragment unconditionally (Statifier.Lowering.Namespace.scxml_vocabulary?/1 treats a nil namespace as SCXML's own vocabulary), so this is the only place in the pipeline that ever refuses one.

Spec 6.3.1's single constraint on <cancel>: "A conformant SCXML document MUST specify exactly one of sendid or sendidexpr." Reports {:cancel_sendid_and_sendidexpr} when both are present and {:cancel_no_sendid} when neither is, both at the <cancel> element's own location.

Spec 5.6: a <content> element must not specify both an expr attribute and inline content, whether that content is text or markup (ADR-0041). Reports {:content_expr_and_text, expr} at the <content> element's own location - same reason, same shape, regardless of which form the content takes.

Check 13: <datamodel> and <data> structural rules that lowering leaves representable on purpose (lib/statifier/document/data.ex, lib/statifier/document/datamodel.ex), following the same division of labour Checks.Content and Checks.Donedata already have with their own document nodes.

Check 7 (spec 3.3): a compound <state> with no initial attribute and no <initial> element falls back to entering its first child in document order (spec 3.3). That fallback is only legal when the first child is itself enterable - a :history pseudo-state is not, since spec 3.10 makes it entered only by a transition that targets it explicitly - "enterable" here means "not a history pseudo-state".

The sub-check checks 4 and 5 share: an <initial> element's and a :history state's whole content model is one constrained <transition> - required, exactly one, a non-null target, no event, no cond. Spec 3.6 states this for <initial>; spec 3.10 states it for <history>'s default transition, more strictly than a looser "when present" reading - the spec's stricter wording wins.

Check 8 (spec 3.7, 5.7): <donedata> is only legal on a :final state. Reports {:donedata_not_on_final, id} for any non-:final state carrying a non-nil donedata, at the donedata's own location - lib/statifier/document/donedata.ex names this check as the layer that reports the shape rather than one that refuses to build it.

Out-of-range enumerated attribute values (spec 3.2.1 and 3.5).

Check 6 (spec 3.7): a :final state's content model is onentry, onexit, and donedata - nothing else. Two reasons, each reported once per offending child and at the child's own location rather than the <final>'s, so the caret lands on the element that should not be there

Check 12 (spec 3.7, Appendix D enterStates): a <final> state's parent carries no id. Entering a non-top-level <final> raises done.state.{parent.id} (lib/statifier/interpreter/exit_entry.ex's raise_parent_completion/3), so a parent with no written id has a completion event the spec names after a name it does not have.

Check 5 (spec 3.10): a :history state's placement, default transition, and type. Four independent facts, per :history state in the document

Check 1 (spec 3.14): an ID-typed attribute's value is unique across the document - not only a state's id, but a <data>'s too. 3.14: "all attributes of type "ID" MUST have unique values within an SCXML document", and <data>'s id is type ID (5.3.1) - so a <data id="s1"> colliding with a <state id="s1"> is exactly as much a violation as two states sharing one id, and both join one uniqueness set rather than two separate ones. nil ids are excluded (lib/statifier/document/state.ex - a nil id means "the author omitted an optional attribute", not "no id"; <data> has no nil id, since id is required to build a Data struct at all), and document scope, not session scope, is what this layer can see.

Spec 4.3.2: "In a conformant SCXML document, <else> MUST occur after all <elseif> tags." Statifier.Lowering.Builders.build_if/2 has already partitioned an <if>'s children into branches by the time a %Statifier.Document.If{} reaches the validator, so this check reads branch order directly rather than re-parsing <elseif>/<else> element names.

Check 4 (spec 3.3, 3.6): a state's own <initial> element.

Check 3 (spec 3.2, 3.3, 3.6): every initial reference - a state's initial attribute, a state's <initial> element, and the document's own root initial attribute - resolves and lands on a legal target. Three reasons, in this precedence

Spec 6.4.1's five mutual-exclusion constraints on <invoke>'s attributes and children, each reported at the <invoke> element's own location

Spec 5.7: "A conformant SCXML document MUST specify either the 'expr' attribute of <param> or the 'location' attribute, but MUST NOT specify both." Reports {:param_expr_and_location, name} when both are present and {:param_no_value, name} when neither is, both at the <param> element's own location.

Spec 5.8.2: "A conformant SCXML document MUST specify either the 'src' attribute or child content, but not both." The src-and-content pair is unreachable through this codebase's lowering (Statifier.Lowering. Builders.build_script/2 already refuses to build a struct when src is written, per ADR-0026 decision 2 - src is rejected regardless of any child text), so this check has exactly one remaining job: a <script> with neither src nor non-blank text is an empty script, which the spec's MUST also forbids. Reports {:script_no_src_or_text} at the <script>'s own element span.

Spec 6.2.2's mutual-exclusion constraints on <send>'s attributes and children, each reported at the <send> element's own location

Check 2 (spec 3.5): every <transition>'s target id resolves to a state in the document. Walks Context.transitions - every owner (:plain, :initial, :history) in one traversal - and reports {:unresolved_target, id} per unresolved id, at that transition's own target span (falling back to the transition's own span when unwritten).

The throwaway index the checks share, built once per Statifier.Validator.validate/2 call and discarded afterward. It must never be returned, cached, or handed to the compiler: Statifier.Compiler builds the real interned index, and sharing this one would couple two layers the architecture keeps independent. Correctness over speed here, per the bead's own design note.

The validator's own error shape: never raised, always collected into a list. Mirrors Statifier.Lowering.Error's shape (reason, message, location) with character-identical field names, so a future common diagnostic protocol can adopt both without either layer's reason union leaking into the other's. The two layers' error lists are never observed together - Statifier.Validator.validate/2 only ever receives a document lowering already accepted - so sharing the shape rather than the type is enough.

The validator's non-fatal finding shape: never raised, always collected into a list, and never gating compilation. Mirrors Statifier.Validator.Error's shape character for character - the same three enforced fields (reason, message, location), a closed reason union declared once in full, one public constructor per variant, and a code/1 tag extractor. Error's own moduledoc already states that it and Statifier.Lowering.Error share a shape, not a type, precisely so a later diagnostic struct can join the family without any layer's reason union leaking into another's; this module is that later member.

Mix Tasks

Reports lines the branch adds that look like violations of ADR-0002 (Appendix D naming), ADR-0003 (pure core with effects), ADR-0004 (predicator as the datamodel), ADR-0008 (generated identifier formats), or ADR-0018 (process artifacts are not code comments) - plus, since ADR-0058, three checks that are not about the diff at all: docs/adr/ numbers colliding, docs/adr/README.md's table falling out of bijection with the directory, and a branch-added ADR number that already exists on the base ref under a different filename.

Reports likely violations of any judged ADR (Mix.Statifier.AdrJudge's registry) in the current branch's diff against its base, each one proposed by one model call and required to survive an independent second call prompted to refute it.

Reports changes to the quality gate's own configuration that the branch does not also record in docs/quality-gate-changes.md.

Runs mix quality and attests that what it ran was the full gate.

Finds conformance tests that pass but are not yet in the regression registry, and adds them once they are verified.

Runs exactly the tests listed in test/passing_tests.json.