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.