Repository documentation
SCXML specification_provider — design spike (MEDIUM(c))
- Status: historical
- Owning queue item: SPEC-HIERARCHY-SCXML-001
- Completion / archival evidence: provider implementation and focused/full-suite evidence
- Release target: 0.6.0 — the design spike below is complete; the declared-trace sidecar format's own design pass and the actual provider implementation are targeted for 0.6.0, not this cycle.
This is not committed 0.5.0 scope
This document is the design spike docs/roadmap/0.5.0-specification-hierarchy.md's
MEDIUM(c) breakdown calls for — it exists so a future SCXML specification_provider
can be scoped precisely before any code is written. Nothing in this document is
promised 0.5.0 delivery. See 0.5.0-scope.md for what is actually committed this
cycle, and 0.5.0-specification-hierarchy.md for why MEDIUM(a)/(b)/(c)/LOW are
planning, not commitments. A sibling design spike for MEDIUM(b) (DMN) is running
concurrently on this same branch; where this document's answer to a shared question is
identical to what DMN would need, it says so explicitly rather than re-deriving it.
The problem, in one sentence
OpenSpec's WHEN/THEN format is a poor fit for state-machine/protocol/workflow logic —
the issue underlying 0.5.0-specification-hierarchy.md expects this to be the common
case among Components with state-machine-shaped behavior — and SCXML (the W3C State
Chart XML standard) is proposed as a second non-literate-markdown specification_provider
to carry that shape natively, the same way DMN is proposed for decision-table logic.
What already exists — confirmed by direct code audit (2026-08-19)
The specification_provider seam is real and already has two implementations
ComponentAuthoring.specification_provider: str
(src/literate_ai/contracts/component_locking/authoring.py:378) is validated only as a
non-empty string (string_value(...), no closed enum) — the contract layer is
already open by design. Two providers exist today, both under
src/literate_ai/adapters/specifications/:
OpenSpecProvider(openspec.py:110,provider_id = "specification-provider:openspec@1") — parses### Requirement:/#### Scenario:/- **GIVEN/WHEN/THEN**Markdown into aSpecificationSet. Itssnapshot()(openspec.py:160) does bounded, adversarial-input-safe reading: path traversal/symlink rejection (_safe_relative, line 42), per-artifact and total byte ceilings, duplicate-path rejection, UTF-8 enforcement.load()(line 134) wraps that in aLoadedOpenSpecwith.specification_setand.require_unchanged(root)(re-verifies byte-for-byte identity — line 82 — used to guard against a spec changing mid-operation).LiterateMarkdownProvider(literate_markdown.py:102,provider_id = "specification-provider:literate-markdown@1") — parses a more general hierarchical Markdown node format (kind/parent/referencesfrontmatter) and projects it into the sameSpecificationSetcontract. Critically,_node_context_requirements()(literate_markdown.py:345-374) does not author real WHEN/THEN prose — it manufactures one syntheticSpecificationScenarioper node with a fixedwhen=("the specification node participates in generation",)/then=("its complete Markdown artifact is supplied under its exact content identity",)pair, with the comment: "This projection gives otherwise free-form VFI-style nodes a stable machine handle without forcing Requirement/Scenario boilerplate into every authored document." This is a working precedent for exactly the pattern a non-WHEN/THEN provider (DMN or SCXML) needs — not relaxing the contract, but wrapping non-behavioral-scenario content in a synthetic, structurally valid scenario while the real normative content stays in the content-addressed artifact itself.
SpecificationScenario mandates when/then at the contract level — this is load-bearing
src/literate_ai/contracts/specifications.py:51-94 — SpecificationScenario.__post_init__
(line 59) fail()s if when or then is empty. SpecificationSet (line 151) and
SpecificationRequirement (line 97) both require at least one scenario / requirement.
There is no per-provider variance point in this contract — every provider's output must
resolve to one-or-more SpecificationRequirements each with one-or-more
SpecificationScenarios, each with non-empty when/then. Both existing providers
satisfy this by either literally authoring WHEN/THEN (OpenSpec) or synthesizing it
(literate-markdown). An SCXML provider needs the same synthesis strategy — see the
acceptance-oracle answer below.
Provider dispatch is a closed if/elif, not a registry — this is the actual provider-neutrality gap
Three call sites resolve specification_provider by string comparison, found by direct
search (grep -rn "specification_provider ==" src/literate_ai/):
_specification_references()(src/literate_ai/adapters/component_lock_planning.py:1573-1610) —if authoring.specification_provider == "openspec": ... elif == "literate-markdown": ... else: raise ComponentLockPlanningError("component_lock.specification_provider_unsupported", ...). This is thelitai lockpath.src/literate_ai/adapters/legacy_generation_catalog.py:601-613— the same shape,else: raise ..."Component specification_provider must be openspec or literate-markdown". This is alitai generate-path call site.src/literate_ai/self_hosting.py:842—if authoring.specification_provider != "literate-markdown":— this one is a deliberate narrower restriction (literate-ai's own self-hosted authoring is pinned to literate-markdown only) and should stay that way; it is not a gap to close.
Both real providers already share the same duck-typed shape by convention (not a formal
Protocol): .load(root, paths, **kwargs) returning an object with
.specification_set: SpecificationSet and .require_unchanged(root: Path) -> None.
generation_preparation.py:434 only passes specification_provider through — it does
not dispatch on it.
Generation-key binding already operates on raw artifact bytes, not parsed semantics
_specification_references() builds a ContentReference per declared
specification_roots URI as a raw SHA-256 of file bytes (_reference(...)), then
separately validates that the provider's parsed specification_set.artifacts identities
match those same raw hashes (component_lock_planning.py:1603-1609,
component_lock.specification_changed on mismatch). The generation key is bound to
exact artifact bytes, uniformly across providers — it does not depend on what the bytes
mean. This is true today for both OpenSpec Markdown and literate-Markdown, and needs
no change to also be true for an SCXML XML document.
spec derive/spec review/spec accept/spec coverage are a different, WHEN/THEN-shaped pipeline
This is a load-bearing distinction the open questions' phrasing risks blurring, so it is
worth stating precisely. litai spec derive|review|accept|coverage
(src/literate_ai/cli/source_to_specification.py, backed by
src/literate_ai/source_to_specification/contracts.py) is a reverse pipeline: it
derives a specification from existing source code (DraftStatement/DraftScenario,
BehaviorObservation/BehaviorSurface/CoverageState), not a forward-authored input to
litai lock/litai generate. Its core scenario type is hardwired, not
provider-neutral: DraftScenario (source_to_specification/contracts.py:450-458) has
plain when: str / then: str fields with no provider dimension at all — there is no
specification_provider concept anywhere in this pipeline's contracts. accept
(cli/source_to_specification.py:2731) renders an accepted draft into either OpenSpec
Markdown (synthesis.py:render_openspec_markdown/render_component_openspec_markdown)
or literate-Markdown artifacts (synthesis.py:render_literate_markdown_artifacts) — the
same two providers again, as targets this reverse pipeline writes into, not as
providers this pipeline consumes generically. spec coverage/spec review as they
exist today have no path for consuming a hand-authored SCXML document at all — they
operate on source-derived WHEN/THEN drafts, and a hand-authored SCXML chart is a
forward-authored specification input, structurally on the other side of this pipeline.
See the direct answer to this open question below.
Grounding in the SCXML standard itself
SCXML (W3C State Chart XML: State Machine Notation for Control Abstraction) documents
are well-formed XML with a root <scxml> element (namespace
http://www.w3.org/2005/07/scxml, version="1.0", initial="<state-id>") containing:
<state id="...">— a simple or compound (nested) state.<parallel id="...">— concurrent regions, all active simultaneously while the parallel state is active; the parallel state completes when every region reaches a<final>.<final id="...">— a terminal state (of its containing compound/parallel scope).<history id="..." type="shallow|deep">— remembers the last active sub-state(s) of its parent for re-entry.<transition event="..." cond="..." target="...">— an edge:eventis the trigger (may use.wildcards, e.g.error.*),condis a boolean guard expression in the chart's declared<datamodel>expression language,targetis one or more state IDs. A transition may be eventless (noevent, taken immediately whencondis true).<onentry>/<onexit>— executable content (<assign>,<raise>,<send>,<script>,<if>/<elseif>/<else>,<foreach>,<log>) run on state entry/exit.<datamodel>/<data id="..." expr="...">— the chart's typed/untyped extended state;<datamodel type="...">names the expression language (e.g.ecmascript,null).<invoke>/<send>— external service invocation and event dispatch (the SCXML Event I/O Processor model), including delayed (<send delay="...">) and targeted (<send target="...">) events.
Real SCXML tooling (Apache Commons SCXML, SCION, the W3C's own reference test suite)
treats a chart as a single, self-contained document — there is no standard
multi-file <xi:include>-equivalent in the base spec (some engines add one as an
extension); this matters for the authoring-shape answer below.
Open questions — answered
1. Authoring/discovery shape for an SCXML document as a litai spec source
Answer: ComponentAuthoring.specification_provider = "scxml",
specification_roots = ("spec/<name>.scxml",) — a single, complete, well-formed SCXML
document per Component, mirroring how OpenSpec/literate-markdown roots are declared
today. Concretely:
- New module
src/literate_ai/adapters/specifications/scxml.py,SCXMLProviderclass,provider_id = "specification-provider:scxml@1", matchingOpenSpecProvider's public shape exactly:.load(root, paths, *, baseline_id=None, active_change_id=None) -> LoadedSCXML(a.specification_set+.require_unchanged(root)pair, same asLoadedOpenSpec), and a bounded.snapshot(root, paths)doing the identical path-safety/size-ceiling/duplicate/UTF-8 checksOpenSpecProvider.snapshot()already does. Recommend extracting that bounded-read logic (_safe_relative, the per-artifact/total byte ceiling loop, symlink/traversal rejection) out ofopenspec.pyinto a small shared helper (e.g.adapters/specifications/_bounded_snapshot.py) once a second and third provider both need it verbatim — it is generic file-safety code, not WHEN/THEN-specific, and today only exists insideOpenSpecProvider. - Single-file-per-Component is the MVP scope: SCXML has no first-party multi-file
inclusion, so there is no natural "roots" plural the way OpenSpec's Markdown corpus
has;
specification_rootsstays a tuple for contract-shape consistency with the other providers but is expected to hold exactly one.scxmlpath in the common case. - Parsing must not use a naive
xml.etree.ElementTree.parse()on untrusted input — litai's other providers are explicit about treating specification artifacts as bounded, adversarial input (OpenSpecError's size/path limits;openspec.py's module docstring: "does not claim to replace the OpenSpec provider's own strict CLI validation"). XML additionally carries XXE/entity-expansion risk that Markdown does not. This is a concrete new-dependency decision a future implementation needs to make explicitly: either vendor a hardened parse path (disable DTD/external-entity resolution on stdlibElementTree, which is possible but easy to get wrong) or take ondefusedxmlas a new dependency. Flag as an implementation-time decision, not resolved here. - Discovery of the parsed structure: one
SpecificationRequirementper<state>(including<parallel>/<final>/<history>)id, title = the state'sid, statement = a rendered one-line summary (e.g. "Statecheckout.paidwith N outgoing transitions"), with synthetic scenarios per outgoing<transition>— see the next answer for exact shape. The complete chart stays in the content-addressed.scxmlartifact itself, exactly as literate-markdown's_node_context_requirements()precedent already established for its own free-form nodes.
2. Acceptance-oracle shape for a state machine
The open question names three candidates: reachability/coverage of states and transitions, conformance to declared scenario traces, or something else. Answer: both of the first two, as two independent, complementary oracles — not a single either/or — because they check different things and only one of them is genuinely SCXML-specific.
- Structural soundness (SCXML-specific, static, no execution required). Every
<transition target="...">resolves to a declared stateidin the same document; every state is reachable frominitialby at least one transition path (pure graph reachability over the declared<state>/<parallel>/<final>/<history>graph);<parallel>regions are each independently well-formed;<history>targets resolve to an ancestor's sub-state set. This is the SCXML analogue of a linter/compiler's unreachable-code check — it is checking the chart itself is internally consistent, not that any implementation of it behaves correctly. This is what aspec review-shaped command for SCXML should check (see thespec coverage/spec reviewanswer below for why this is a new SCXML-specific command, not a reuse of today'slitai spec review). - Declared-trace conformance (the scenario-shaped oracle, SCXML's closest analogue to
WHEN/THEN). A trace is an ordered sequence of
(event, expected-target-state)pairs — "starting frominitial, receiving eventpaytransitions tocheckout.paid; receivingcancelfrom there transitions tocheckout.cancelled." SCXML has no standard wire format for declaring example traces (unlike, say, a DMN table's own input/output rows, which are part of the DMN document itself) — this would be a litai-defined sidecar convention, most naturally a small YAML/JSON list alongside the.scxmlfile, or additional structured content inside the Component's authoring Markdown. This is the one genuinely novel format decision in this design — flagged explicitly in the maturity assessment below as needing its own narrower design pass rather than being settled here. - Both oracles are checkable without an interpreter: structural soundness is pure graph analysis of the parsed chart; trace conformance against generated code is the generated implementation's own test's job (see the next answer) — litai's role is only to guarantee each declared trace got turned into a generated test case, not to execute the state machine itself to verify it.
3. Minimum viable adapter — parser, not interpreter
Answer: litai needs an SCXML parser and static-structure analyzer, not an SCXML
interpreter. Generation is keyed off the static chart structure — the declared states,
transitions, guard expressions (cond), and executable content (onentry/onexit
actions) are extracted as data and handed to generation the same way DMN's table rows or
OpenSpec's WHEN/THEN clauses are handed to generation today — with actual runtime
execution, <datamodel> expression evaluation, the SCXML event-queue algorithm, and
<send>/<invoke> external communication left entirely to the generated code, using
whatever idiomatic state-machine pattern or library the target language's Flavor
prescribes. This is explicitly the "structured-asset treatment, not a live spec engine"
the roadmap issue anticipated, and it parallels assets:'s existing treatment of
literal reference data (MEDIUM(a)) — the SCXML document is consumed as declarative
structure, the same category of input assets: already treats non-prose reference data
as. It also mirrors OpenSpecProvider's own explicit non-claim ("does not claim to
replace the OpenSpec provider's own strict CLI validation") — SCXMLProvider should
likewise not claim to be (or need to become) a W3C SCXML conformance-test-suite-grade
interpreter. Building a full interpreter would mean reproducing genuinely hard runtime
semantics (the SCXML processor algorithm, expression-language sandboxing, the event I/O
processor) that litai has no product need for, since the actual execution happens in
generated target-language code, not inside litai itself.
4. Generation-key binding, and spec coverage/spec review treatment
Generation-key binding: no change needed, same answer as DMN needs. As established
above, _specification_references()'s content-hash mechanism operates uniformly on raw
artifact bytes regardless of what those bytes mean — an SCXML .scxml file's SHA-256
becomes part of a Component's generation key exactly the way an OpenSpec .md file's
hash does today, with zero new code at the identity/hashing layer. This is a direct
consequence of the seam's existing design, not something either MEDIUM(b) or MEDIUM(c)
needs to add.
spec coverage/spec review treatment: these commands, as they exist today, do not
apply to a hand-authored SCXML document at all, and extending them is the wrong move —
same conclusion DMN needs, reasoned here from SCXML's own code paths. As shown above,
litai spec coverage/litai spec review belong to the reverse, source-derivation
pipeline (DraftScenario's hardwired when/then, BehaviorSurface/CoverageState)
— they derive a spec from existing source, then optionally render it into OpenSpec
or literate-Markdown. A hand-authored SCXML chart is the opposite direction: a
forward-authored input consumed before generation, structurally unrelated to what this
reverse pipeline's contracts model. Two paths were considered:
(a) Generalize
DraftScenario/BehaviorSurfaceto be provider-neutral so the reverse pipeline could, in principle, derive-and-render SCXML too. Rejected for this spike: this is a deeper change to the reverse pipeline's own core contracts (which have no provider dimension whatsoever today), well beyond what "add a specification provider" requires, and not evidently useful — deriving a state machine from unstructured source code is a much harder, more speculative capability than parsing a hand-authored one.(b) Give SCXML its own, separate structural-review verb, distinct from
litai spec review. Recommended. E.g. a newlitai spec scxml-review <path>(or a provider-aware flag on a new, explicitly-scoped review command) that performs exactly the structural-soundness checks named in the acceptance-oracle answer above (transition-target resolution, reachability frominitial,<parallel>/<history>consistency) plus, if the declared-trace sidecar format from that same answer is adopted, a check that every declared trace is realizable against the parsed graph. This keepslitai spec derive/review/accept/coverage's existing WHEN/THEN-shaped reverse-pipeline meaning intact and gives SCXML an oracle shaped for what a state chart actually needs checked, rather than forcing an ill-fitting reuse.
5. litai lock/litai generate provider-neutrality
Answer once, for SCXML specifically confirming the general shape: yes, a change is
needed, and it is small and mechanical — the contract layer is already open; only the
imperative dispatch is closed. As shown above, ComponentAuthoring.specification_provider
is validated only as a non-empty string at the contract layer — no schema change is
needed to add "scxml" as a value. The actual work is replacing the two closed
if/elif chains (component_lock_planning.py:_specification_references,
legacy_generation_catalog.py:601-613) with a small registry
(dict[str, SpecificationProviderFactory] or equivalent) keyed by provider_kind
string, mapping to a provider conforming to the shape both existing providers already
share by convention: .load(root, paths, **kwargs) returning .specification_set +
.require_unchanged(root). Optionally formalize that duck-typed shape as a Protocol
in application/ports.py (which already hosts other provider-shaped Protocols, e.g. the
inspect(...) Protocol at line 16) so a third/fourth provider is checked structurally
by the type system rather than only by convention. self_hosting.py:842's narrower
literate-markdown-only restriction is deliberate project-bootstrap policy and should
not be touched by this change. This is confirmed to be a small refactor (two call
sites + one registry + one new provider module), not an architecture change — consistent
with the roadmap document's framing that specification_provider is "an extension of an
established extension point." This answer is identical for DMN — say so rather than
re-deriving it per the task's own instruction: whichever of the two lands first should
do this registry refactor once, and the second provider should land as a pure registry
addition with no further dispatch-site changes.
Why TLA+/PlusCal is deliberately out of scope, and confirmation this design doesn't accidentally need it
TLA+/PlusCal's hard problem is verifying safety and liveness properties of concurrent, independently-scheduled processes under every possible interleaving — TLC model checking explores the combinatorial state space produced by multiple actors each progressing nondeterministically, to prove properties like mutual exclusion or eventual consistency hold under every schedule, not just the ones a developer anticipated. That is a fundamentally different — and harder — problem than validating one hierarchical/parallel state chart's own internal consistency.
Base SCXML, including its <parallel> construct, does not require solving that
problem: <parallel> models concurrent regions within a single chart — all regions
are active simultaneously and the parallel state completes when every region reaches a
<final> — which is closed, structural, single-document semantics, not multiple
independently-scheduled external processes whose relative ordering is unbounded. Every
oracle proposed in this design is one of:
- Static graph analysis of one declared chart (reachability, transition-target
resolution,
<parallel>/<history>well-formedness) — no state-space exploration across independent actors, no combinatorial interleaving, no temporal-logic property language. - Conformance of specific, individually-declared traces against that same static graph — checking realizability of concrete sequences, not proving a property holds under every possible schedule.
- Delegation of actual runtime execution to generated code's own tests — exactly the "minimum viable adapter" answer above; litai never simulates the chart's runtime behavior itself, so it never needs to reason about interleavings at all.
None of this reaches for a model checker, a temporal-logic property language, or
interleaving exploration across multiple independently-scheduled processes — confirming
this design does not accidentally take on TLA+'s harder problem to handle ordinary
sequential (or single-chart-parallel) state machines. If a real Component's needs ever
require proving properties across genuinely independent, concurrently-scheduled
processes (e.g. a distributed consensus protocol, not a single workflow's state chart),
that is precisely the narrower, not-yet-justified TLA+/PlusCal case
0.5.0-specification-hierarchy.md already reserves separately — nothing in this SCXML
design should be stretched to cover it, and this design finds no evidence it needs to
be.
Maturity / next-action assessment
Update (2026-08-19): the three follow-on steps below have now been prototyped.
Artifacts live in docs/roadmap/0.6.0-scxml-prototype/:
media_player.scxml— a real, valid SCXML 1.0 document, deliberately non-trivial: a compoundonstate containing both a deep<history>pseudostate and a<parallel>state with two independently-progressing regions (playback,volume), plus transitions declared on an ancestor state (on) that apply to every descendant configuration.scxml_structural.py— a throwawayxml.etree.ElementTree-based parser plus the two structural-soundness checks (transition-target resolution, reachability frominitial), runnable directly (python3 scxml_structural.py media_player.scxml).media_player.trace.json+trace_conformance.py— a prototype declared-trace sidecar and a stateful replay checker.
Finding 1 (confirms the design, with one correction): reachability and
transition-target resolution are mechanical, but only if the reachability walk treats
<parallel> fan-out and ancestor-declared transitions correctly — a naive
"per-state <transition> children only" graph undercounts real reachable behavior.
SCXML transitions declared on a compound/parallel ancestor state are inherited by
every descendant configuration (the spec's event-bubbling semantics) — in the
prototype chart, power and standby are declared once on <state id="on"> and must
be honored from every leaf underneath it (playing, paused, unmuted, muted,
suspended), not just from on's immediate children. The original design's open
question 2 described this as "pure graph reachability over the declared
<state>/<parallel>/<final>/<history> graph," which is directionally right but
undersells the work: a correct implementation must walk each active leaf's full
ancestor chain to collect enabled transitions (Chart.enabled_transitions in the
prototype), and must fan a <parallel> state's entry out into all regions'
initial sub-states simultaneously when computing what configuration a transition
leads to (Chart.entry). Both are still static, mechanical, no-interpreter-needed
graph operations — the design's core claim holds — but "mechanical" undersold how much
of SCXML's actual entry/exit algorithm a "just check reachability" implementation
has to reproduce correctly to avoid false unreachable/dangling-target reports.
Finding 2 (an interaction the design didn't call out): the two structural checks are
not independent — reachability must be resilient to dangling targets, not just report
them separately. A first prototype pass crashed (KeyError) walking reachability
through a transition whose target didn't resolve, because the reachability walker
tried to compute the entry-configuration of a nonexistent state. The fix is simple
(skip transitions with any unresolved target during the reachability walk; they are
already reported by the target-resolution check) but this ordering/robustness
requirement was not mentioned in the original design and would have produced a crash,
not a clean diagnostic, in a first implementation attempt. Confirmed by deliberately
introducing both a dangling transition target and an orphan unreachable state into a
copy of the sample chart and re-running the checker: after the fix, both are reported
as clean FAIL lines (not a traceback), and reverting the breakage restores a clean
PASS.
Finding 3 (the declared-trace sidecar, now decided against the concrete sample):
a separate file alongside the .scxml document, prototyped as JSON. Of the three
placement options the design named:
- Inline in the Component's authoring Markdown was rejected: it would force the trace's structured event/state data through prose-oriented Markdown parsing for no benefit, and breaks the pattern every other provider uses of keeping structured specification content in its own content-addressed artifact.
- XML processing instructions inside the
.scxmldocument itself was rejected: it would make the.scxmlfile a litai-specific dialect that other SCXML tooling (Apache Commons SCXML, SCION, the W3C reference suite) would have to specially tolerate or ignore, undermining the whole point of adopting a standard format instead of inventing one. - Separate file (
media_player.trace.jsonnext tomedia_player.scxml) won: it keeps the.scxmlartifact byte-for-byte standard, mirrors how OpenSpec/DMN artifacts are already independent files, and slots intospecification_rootsexactly the way the design already expects (specification_rootsstays a tuple — a real implementation would hold two paths,spec/<name>.scxmlandspec/<name>.trace.json, both covered by the same bounded-snapshot/path-safety logic). JSON (not YAML) was used for the prototype specifically to avoid taking on a new dependency (PyYAML is not in this project's dependency set today); a real implementation could reasonably prefer YAML for authoring ergonomics — that is a smaller, separate decision from the placement decision this prototype resolves.
Finding 4 (the one place the two oracles must NOT share an implementation): trace
conformance requires real, stateful <history> tracking; structural reachability
must NOT. The structural checker's Chart.entry() deliberately resolves a
<history> pseudostate via its default transition only (there is no "last visited"
state to fall back on when merely asking "can this state ever be reached at all") —
that simplification is correct and sufficient for reachability/target-resolution,
which only need a yes/no per state. Trace conformance is a different question — "does
this specific run reach this exact configuration" — and requires a real
interpreter that records each <history> node's last-active sub-configuration
across steps and restores it on re-entry, not its default. The prototype's trace
deliberately exercises this: power (off→on, no history yet) → pause → mute
(now in {paused, muted}, inside the <parallel> region) → power (on→off, history
must record {paused, muted}) → power (off→on again). The trace asserts the final
power restores {paused, muted}, not the chart's default entry-configuration
({playing, unmuted}) — confirmed both ways: the real checker (apply_with_history,
which records history before exiting) passes; a deliberately-history-blind variant of
the same replay fails exactly that step with expected ['muted', 'paused'], got ['playing', 'unmuted'], proving the trace is actually exercising history state and
not trivially satisfied by the simpler entry algorithm. Consequence for the
provider design: SCXMLProvider's structural-review path and its trace-conformance
path are not the same code walking two different inputs — the latter needs its own
small stateful stepper, and that stepper (not the structural reachability walker)
is the piece any future implementation should treat as the more delicate one to get
right.
Shared bounded-snapshot/path-safety extraction (open question 3): still recommend
deferring to whichever provider lands first, now slightly favoring DMN-or-shared
timing. This prototype touched only parsing/graph logic, not the snapshot/path-safety
layer at all — nothing here changes the original recommendation to extract
_bounded_snapshot.py once, informed by both providers, rather than overfitting to
one. Check the sibling DMN spike's own findings before deciding: if DMN's parsing
prototype needed no format-placement decision as delicate as the trace-sidecar
question above, DMN may be the simpler provider to land first and do the extraction
as part of that first landing, with SCXML's SCXMLProvider (plus its now-decided
.trace.json sidecar convention) arriving second as a pure registry addition per open
question 5's answer.
Overall: closer to implementation-ready than the original spike, but not there yet.
The trace-sidecar format is now concretely decided (separate JSON file,
spec/<name>.trace.json) and validated end-to-end against a chart that actually has
<parallel> and <history> in it, which was this design's one explicitly-named open
piece. What remains before code, beyond ordinary implementation work: (a) porting the
prototype's Chart/reachability/trace-replay logic from a throwaway
ElementTree-based script into the real SCXMLProvider shape (bounded snapshot,
SpecificationSet projection, the XXE-hardening decision the original design flagged
and this prototype deliberately did not need to resolve, since it only ever parses a
trusted local fixture), and (b) the still-open registry refactor (open question 5,
shared with DMN) and the new litai spec scxml-review-shaped verb (open question 4)
that would actually invoke these checks. Those are implementation tasks with a
concrete shape now, not further open design questions.
Update (2026-08-20): the trace-sidecar format's own schema is now designed
and prototyped, closing the "Next action" this document previously left
open. The 2026-08-19 update above decided where the sidecar lives
(a separate JSON file next to the .scxml document) and proved a stateful
replay checker works end to end. It did not yet specify a reusable schema
for that file. docs/history/roadmap/spikes/scxml-trace-prototype/RESULTS.md
does exactly that: a small JSON schema ($schema, scxml, traces[].steps[]
with event/expect_active/expect_regions/via_history/auto/note)
validated against a second, independently-authored .scxml sample
(connection_handshake.scxml, a connection-handshake protocol with two
<parallel> regions and a deep <history>, deliberately a different
domain from media_player.scxml so the format is shown to generalize
rather than fit one fixture) and three prototyped trace documents
(straight-line, parallel-region-independence, history-re-entry). A
standalone trace_sidecar_validator.py proves the schema supports a cheap
static structural check — every referenced state/region/history id
resolves, every <parallel> configuration covers each region exactly
once — as a fast first-pass complement to the sibling spike's stateful
replay checker, confirmed to catch four distinct classes of malformed
sidecar (unknown state id, incomplete parallel-region coverage, a
via_history reference to a non-history node, an expect_regions entry
inconsistent with chart structure) with specific errors rather than
crashing or silently passing. Recommendation: this format is
implementation-ready; no further design iteration was found necessary.
Summary table
| Question | Answer | Confidence |
|---|---|---|
| Authoring/discovery shape | Single .scxml file per Component via specification_provider = "scxml"; new SCXMLProvider mirroring OpenSpecProvider's bounded-snapshot shape |
High — direct precedent exists |
| Acceptance-oracle shape | Two oracles: static structural soundness (reachability/transition-target resolution) + declared-trace conformance (separate .trace.json sidecar, prototyped) |
Structural: high, prototype-validated (with ancestor-inheritance and dangling-target-robustness corrections — see Maturity section). Trace format: decided and prototype-validated against a <parallel>+<history> sample; needs normal design review, not a further open question |
| Minimum viable adapter | Parser + static analyzer, no interpreter; execution stays in generated code | High — mirrors assets: treatment and OpenSpec's own non-claim |
| Generation-key binding | No change needed; already byte-hash-based and provider-agnostic | High — same answer DMN needs |
spec coverage/spec review treatment |
Today's commands don't apply (reverse WHEN/THEN pipeline); needs its own SCXML-specific review verb | High — same answer DMN needs |
litai lock/litai generate provider-neutrality |
Contract already open; replace two if/elif dispatch sites with a registry, once, for all future providers | High — same answer DMN needs |
| TLA+/PlusCal scope | Confirmed not needed; every oracle here is static-graph or delegated-to-generated-code, never interleaving/model-checking | High |
| Overall maturity | Prototype-validated; trace-sidecar format decided (separate .trace.json); remaining work is ordinary implementation (provider module, registry refactor, review verb), not further open design questions |
— |