Literate AI

Repository documentation

Declared-trace sidecar format: design pass and prototype results

Artifacts in this spike

Relationship to the sibling 0.6.0-scxml-prototype/ spike

The design doc already recorded, on 2026-08-19, that the placement decision for a trace sidecar was made and prototyped once already (docs/roadmap/0.6.0-scxml-prototype/media_player.trace.json + trace_conformance.py): a separate file next to the .scxml document, JSON (not inline authoring Markdown, not an XML processing instruction inside the .scxml itself), with a full stateful replay checker that tracks <history> across steps. That prior spike answered where the sidecar lives and proved a stateful interpreter can validate it end to end.

This spike answers a narrower, still-open question that prior spike did not attempt: what the sidecar's own schema should look like as a reusable format (field names, how it expresses simultaneity under <parallel>, how it annotates history re-entry, how it handles ordering/timing), and whether a lighter-weight, purely static structural check — the kind a litai spec scxml-review-shaped verb could run cheaply, without needing a full interpreter — is sufficient to catch the most common authoring mistakes in a hand-written sidecar document. It is deliberately validated against a second, independently-authored .scxml sample (connection_handshake.scxml, a different domain than media_player.scxml) so the format is shown to generalize, not merely fit the one chart it was designed against.

The SCXML sample

connection_handshake.scxml — a connection-handshake protocol. connect enters a <parallel id="negotiating"> state with two independent regions (auth, caps); a drop event suspends the connection mid-negotiation; reconnecting resumes via <history id="conn_history" type="deep"> instead of restarting both regions. 14 declared state/parallel/final/history nodes: disconnected, connecting, conn_history, negotiating, auth, auth_pending, auth_failed, auth_done, caps, caps_pending, caps_done, suspended, established, closed. Uses SCXML's standard auto-raised done.state.negotiating event (fired once every region of a <parallel> reaches its own <final>) as the ancestor-declared transition off the parallel state, the same "event bubbling" pattern the sibling media_player.scxml sample already validated.

The proposed sidecar format

One sidecar file per set of related traces, JSON, conventionally named <component>.trace-<purpose>.json next to (or, for a single combined file, matching) the .scxml document — consistent with the placement the parent design doc already decided (separate file, not inline Markdown, not an XML processing instruction).

{
  "$schema": "litai-scxml-trace-sidecar/v1",
  "scxml": "connection_handshake.scxml",
  "traces": [
    {
      "id": "unique-within-file",
      "description": "prose, why this trace exists",
      "steps": [
        {
          "event": "connect",              // required; the triggering event
          "auto": false,                    // optional; true = SCXML-auto-raised
                                             //   (e.g. done.state.<id>), not sent
                                             //   by an external caller
          "expect_active": ["auth_pending", "caps_pending"],
                                             // required; the FULL set of atomic
                                             //   leaf state ids active after this
                                             //   event -- one entry per <parallel>
                                             //   region currently active, plus
                                             //   whatever's active outside any
                                             //   <parallel>
          "expect_regions": {                // optional; human-readable annotation
            "auth": "auth_pending",           //   mapping a <parallel> region's
            "caps": "caps_pending"            //   root id -> its active leaf;
          },                                 //   cross-checked against
                                             //   expect_active, not authoritative
                                             //   on its own
          "via_history": "conn_history",     // optional; names the <history>
                                             //   node this transition is expected
                                             //   to resolve through (documents
                                             //   intent; validator confirms the
                                             //   id names a real <history> node)
          "note": "free text"                // optional; documents WHY this
                                             //   step matters (e.g. "this is the
                                             //   assertion that distinguishes
                                             //   default-entry from recorded
                                             //   history-entry")
        }
      ]
    }
  ]
}

(a) Expressing simultaneous activity under <parallel>

expect_active is a flat set of every atomic leaf state id active at once — the natural representation of an SCXML "configuration," which is literally defined by the spec as a set of simultaneously-active atomic states. This is the authoritative field. expect_regions is an optional, purely diagnostic sibling: a map from a <parallel> region's root id to its currently-active leaf, useful for readability when a chart has several regions and a reviewer wants to see "which region is in which state" at a glance without mentally re-deriving it from the flat leaf set. The validator cross-checks expect_regions against expect_active and against the chart's actual parallel/region structure, so the two can never silently drift apart; if only one is worth authoring, expect_active is the one that matters. Static structural well-formedness of a <parallel> configuration means: every region of every implied <parallel> ancestor has exactly one active leaf represented in the set (see _validate_parallel_region_coverage in trace_sidecar_validator.py) — no region left uncovered, no region double-covered, no leaf listed together with its own ancestor.

(b) Expressing history re-entry

No new step type is needed — a history-driven transition is authored exactly like any other step (an event, an expect_active). The one addition is the optional via_history annotation, which names the <history> pseudostate the author expects this transition to route through. It is intentionally documentation, not the primary assertion: the primary assertion is still expect_active declaring the exact post-re-entry configuration, which is what actually distinguishes correct history behavior (restoring the last-active sub-configuration) from a history-blind implementation (falling back to the chart's default-entry configuration). connection_handshake.trace-history.json is built specifically to make that distinction observable: after caps finishes (caps_done) but auth is still pending, a drop followed by connect must restore {auth_pending, caps_done}, not the default entry configuration {auth_pending, caps_pending} a naive re-implementation would produce. via_history lets a static structural check confirm the referenced id is genuinely a <history> node (catching a typo'd or wrong-id reference); confirming the value asserted after it is correct requires the stateful interpreter the sibling 0.6.0-scxml-prototype/ trace_conformance.py spike already prototyped — see "Known limitations" below for why that split is deliberate, not an oversight.

(c) Ordering and timing

Steps are an ordered list; sequence in the array is the declared event order — no separate ordering field is needed for the common case. The auto flag documents which steps are SCXML-internal auto-raised events (most importantly done.state.<id> on <parallel> completion) versus events an external caller/environment actually sends, which matters for a future test-generation consumer of this format (an auto: true step should become an assertion in generated test code, not a call the test harness makes). Genuine wall-clock timing (e.g. <send delay="...">) is deliberately not modeled as a first-class field in this version of the format: SCXML's own timing model is already fully delegated to generated code per the parent design doc's "minimum viable adapter" answer (litai is a parser/static-analyzer, not an interpreter), and a delay assertion would require exactly the stateful, time-aware interpretation this format is designed to avoid needing. The note free-text field is sufficient for documenting timing-sensitive intent in this version; a min_delay_ms/ max_delay_ms per-step field is a plausible v2 addition if a real Component ever needs it (see Known limitations).

Prototyped trace documents

Three sidecar documents against connection_handshake.scxml, each proven structurally valid by trace_sidecar_validator.py:

The prototype validator

trace_sidecar_validator.py — stdlib-only (xml.etree.ElementTree + json), standalone (does not import from the sibling 0.6.0-scxml-prototype/ spike, per the task's instruction to keep this spike self-contained). It parses the .scxml chart's state/parallel/ final/history structure and validates a sidecar document structurally:

Confirmed by direct testing: all three prototyped trace documents pass against connection_handshake.scxml. Deliberately broken variants (an unknown state id, an incomplete <parallel> region set, a via_history pointing at a non-<history> node, and an expect_regions entry inconsistent with the chart's actual structure) were each constructed and confirmed to fail with a specific, actionable error message rather than a crash or a silent pass — proving the checks are load-bearing, not vacuous.

Run it yourself:

python3 trace_sidecar_validator.py connection_handshake.scxml \
  connection_handshake.trace-straight-line.json \
  connection_handshake.trace-parallel.json \
  connection_handshake.trace-history.json

Known limitations

Recommendation

Ready for the eventual production SCXMLProvider, not needing a further speculative design iteration before implementation. The schema is small, each field earns its place against a concrete need surfaced by one of the three prototyped traces (not speculatively added), and the two-tier oracle split (cheap static structural check here; stateful replay check already proven in the sibling spike) matches the parent design doc's existing "two complementary oracles, not a single either/or" conclusion exactly — this spike fills in the previously-unspecified schema for the static side of that split with something now validated against a second, independently-authored chart. What remains is ordinary implementation work, already named in the parent design doc's "Maturity / next-action assessment": porting this shape (plus the sibling spike's stateful replay logic) from throwaway ElementTree scripts into the real SCXMLProvider, the registry refactor shared with DMN, and the new litai spec scxml-review-shaped verb that would actually invoke both checks. No further open design questions were found for the sidecar format itself.

Read the Markdown sourceConference homePublished 1.3.0 · Patch 1.3.1 in progress · Development 1.4