ADR-0007: Text-first authoring

Copy Markdown View Source

Status: accepted (2026-08-16)

Context

Every authoring tool for statecharts has to answer one question first: what does the author actually edit? The candidates here are the SCXML text, a diagram canvas that generates SCXML, or both at once with synchronization between them.

The research doc (docs/research/260816-sui-kua-gui-research-and-direction.md) evaluated the diagram-first path concretely and watched it fail. Lucidchart - the strongest general-purpose canvas candidate - has a real extension SDK but no stable structured export: the only structural read-back outside a live editor session is an endpoint Lucid disclaims as internal and unstable, so diagram-to-SCXML would ride on undocumented internals. That is the round-trip problem in its general form: the moment a diagram is the editing surface, the diagram's own model becomes a second source of truth, and every edit must survive a translation into SCXML and back without loss. Anything the diagram model cannot represent - an attribute it has no field for, a comment, an element ordering, a namespace - is silently dropped or mangled on the trip. The research verdict was "viewer at best, not a foundation," and the same structural problem disqualifies any diagram-as-editor design, not just Lucidchart: Stately Studio removed SCXML interop entirely rather than keep maintaining a lossy translation.

Meanwhile the engine side already pays text a large dividend. Statifier compiles SCXML text and retains what tooling needs to point back into it: source locations on states, transitions, and executable content, stable document-order identities (state indexes, t_index, c_index), and expression-level spans through predicator's span tables (statifier ADR-0012 and statifier ADR-0014, both adopted here by ADR-0002). Its validator reports errors and warnings against that same text. And its compatibility contract is a conformance corpus of plain .scxml files (statifier ADR-0006). All of that vocabulary is text-anchored; none of it exists for a diagram model.

Decision

All modification is via SCXML text. The visualization is a read-only rendering of that text. SCXML is the single source of truth; the diagram is an output, never an editor backed by its own model, and nothing in this repository generates or rewrites SCXML on the author's behalf from a canvas gesture.

Why, stated as the four things this buys:

  • One source of truth. There is no shadow representation to keep synchronized with the text and no translation layer whose losses accumulate. What the author sees in the editor is exactly what the engine compiles.
  • The round-trip problem is avoided entirely, not solved. The failure mode that disqualified Lucidchart - edits passing through a diagram model that cannot faithfully regenerate the XML - cannot occur in a design where no edit ever originates in the diagram. A read-only rendering can be lossy in what it chooses to draw without corrupting anything, because it is never asked to write.
  • Statifier's diagnostics become editor diagnostics for free. The validator's errors and warnings are already stated against the text the author is editing, with the source locations the compiled Machine retains. The editor pane surfaces them as lint gutters and squiggles at those locations; expression-level findings underline the failing subexpression via predicator span tables (statifier ADR-0014). That underlining is the one part of this not yet free: a predicator span is relative to the expression string, and composing it with the enclosing attribute's location is neither the arithmetic the engine's own moduledocs describe nor sound against entity references in the source. The engine owes a resolver; sui-czr mirrors it, and nothing here reimplements the composition locally (ADR-0002). No diagnostic is translated into diagram coordinates and back. A validator finding that arrives without a usable location would be an engine gap - filed as an st- bead per ADR-0002, never patched around here.
  • The conformance corpus stays the compatibility contract. Charts are .scxml files, so the corpus statifier conforms to (statifier ADR-0006) is directly this tool's input domain: anything the engine runs, the editor edits and the viewer renders. A diagram-native model would need its own parallel corpus and its own notion of conformance.

The hover/selection sync contract. Read-only does not mean disconnected: the editor pane and the SVG viewer hover- and selection-sync in both directions, and the mechanism is a contract worth stating precisely.

  • For one compiled Machine build, the engine's document-order identities (state indexes, t_index for transitions, c_index for executable content) and its identity-to-source-location tables are the shared vocabulary (statifier ADR-0012; the same tables ADR-0005's session.start message carries on the wire).
  • The viewer stamps each rendered SVG element with the identity of the chart entity it draws, as data attributes (data-state-index, data-t-index, data-c-index). The SVG carries identities only - never its own notion of chart structure.
  • Sync is a lookup in each direction through the same tables: pointer over an SVG element reads its stamped identity, resolves it to a source range, and highlights that range in the editor; cursor or hover in the editor resolves the position to the innermost enclosing entity's identity and highlights the SVG element stamped with it.
  • Identities are valid only against the build that produced them. Document-order indexes shift when anything above them is edited (the instability ADR-0006 already ruled them out as a fixture-matching key), so stamps and tables are only ever used as a matched pair from one compile, and both are regenerated together on every successful recompile. Between an edit and the next successful compile the pair is stale as a pair - never mixed with the new text's positions.
  • Hover targets go finer than one element. Statifier retains per-attribute source locations at the Document layer: every Statifier.Document.Transition carries attribute_locations, a map keyed by attribute-name atom with distinct value spans for cond, event, target, and type, and an entry exists only for an attribute actually written in the source - key presence distinguishes an authored attribute from a lowering-applied default. cond and event on one transition are separate hover targets today, with no gap at that layer (statifier ADR-0012, statifier ADR-0014). The granularity has a layering constraint, though: the full per-attribute map lives on the Document layer, not the compiled Machine layer - Machine.Transition keeps location and cond_location only, with no event_location or target_location, and that selectivity is the Machine's documented convention rather than an oversight (it distills the spans with a runtime diagnostic use; Machine.Invoke is the escape hatch that carries the whole map when per-field distillation stops paying). Attribute-level hover targets are therefore read from the %Statifier.Document{} tree, and the Machine's t_index / c_index and state indexes serve as the join to runtime trace effects, not as the source of the locations themselves. Reading only the Machine would silently lose event and target granularity. Whether the map should be carried to the Machine after all is an upstream question, not one this record settles - sui-qay mirrors it.

This contract is also what makes the viewer honest about being an output: the diagram's only identities are the engine's, so there is nothing diagram-side for an edit to originate against.

What this rides on, named as the dependency it is. The sync mechanism and the diagnostics pipeline both assume the engine retains what they need: locations on states, transitions, and executable content, stable document-order identities, and expression spans. Statifier ADR-0012 and statifier ADR-0014 commit the engine to exactly that, and ADR-0005 already plans the identity tables onto the wire - so today this is an adopted premise, not a hope. Where a gap surfaces in practice - an element kind whose location is not retained, a hover target needing finer granularity than the engine keeps - that is engine work, an st- bead under ADR-0002's rule, and the UI feature waits on it.

What this decision does not do:

  • It does not preclude the viewer being interactive. Hover, selection, pan/zoom, collapsing a compound state, choosing a layout - all are view state, fine precisely because they never touch the SCXML.
  • It does not preclude text edits made through UI affordances, so long as the affordance edits the text itself, visibly, in the editor pane - a rename refactor or a quick-fix on a diagnostic is text-first; a canvas drag that regenerates the document is not.
  • It does not decide the rendering stack. elkjs layout, plain SVG, no React is the research doc's extracted direction, owned by its own bead (sui-p61); this record constrains only the direction of data flow.
  • It does not forbid canvas editing forever. The research doc's phasing lists it last, "only if users ever demand it"; if that day comes, it is a superseding ADR that must answer the round-trip problem this record avoids, not a drift.
  • It does not decide how the viewer presents the interval between an edit and the next successful compile. The Consequences below already fix the substance - stamps and identity tables regenerate only on a successful compile, so a document that will not compile has last-good-build sync, and that stale pair is never mixed with the new text's positions. What is left open is only the visual treatment of that state - freeze, dim the viewer, suppress highlights, a banner - which is presentation, owned by the first viewer consumer, the same way the rendering stack above is its own bead.

Consequences

  • A diagram-as-editor is ruled out, and with it the entire class of translation-loss bugs, diagram-model migrations, and "the canvas and the XML disagree" states. This is the record plans and reviews cite when a feature sketch has an edit originating in the viewer.
  • Authors who think best on a canvas pay the cost: there is no drag-to-add state, and structural edits require XML fluency. The mitigation is editor intelligence (completions, diagnostics, fixtures-driven hover per ADR-0003), not diagram edits.
  • The renderer must be able to draw everything the corpus contains - compound and parallel states, cross-hierarchy transitions - because the input domain is all conformant SCXML, not the subset a palette produces. This is why Mermaid's cross-hierarchy limitation was disqualifying for execution-accurate rendering (research doc) and elkjs was chosen.
  • Sync quality is bounded by what the engine retains. That keeps this repository honest (no local position bookkeeping to drift) at the price of cross-repo latency when a granularity gap needs an st- bead before a UI feature can land.
  • Recompile-on-edit becomes load-bearing: stamps and tables refresh only on a successful compile, so a document that will not compile has only last-good-build sync. Accepted - a chart that does not compile has bigger problems, and diagnostics (which point at the current text) are the affordance that covers the gap.
  • The SVG gains a small public surface: the data-state-index / data-t-index / data-c-index attributes are a contract consumers (tests included) may rely on, and rendering tests assert on stamped structure rather than pixels, per this repo's "verified by what it renders" convention.
  • Attribute-level hover locations live one layer higher than the identities on the wire. attribute_locations is a field of %Statifier.Document.Transition{}; ADR-0005 specifies session.start's identity tables as built from "the compiled Machine" (docs/adr/0005-language-neutral-trace-wire-format.md:90), and Machine.Transition carries only location and cond_location - no event_location or target_location. A consumer that wants cond-versus- event hover granularity has to read the Document tree directly rather than the wire's identity tables alone, which would silently lose that granularity. Not a contradiction of ADR-0005 - the wire format was never asked to carry per-attribute spans - but a real constraint on how a consumer reads locations, and one this record leaves for ADR-0005 or its successor to take up rather than settling here. Tracked as sui-qay.

Alternatives considered:

  • Diagram-first on an existing canvas (Lucidchart): no stable structured export, so SCXML generation rides on internals Lucid disclaims; no semantic enforcement; no hierarchical layout. Disqualified by the research doc; kept only as a possible one-way export target for sharing pictures.
  • Bidirectional sync (edit either surface): inherits the round-trip problem in both directions plus a merge problem when the surfaces diverge, for the benefit of canvas edits nobody has yet demanded. Rejected; it is the expensive path the phasing defers until demand exists, and it would supersede this record.
  • Diagram-first with SCXML as an export format: Stately Studio's shape, and the reason it is unusable here - its SCXML support was dropped rather than maintained, demonstrating where the maintenance burden of a lossy translation ends up. Rejected.
  • Invented stable ids stamped into the SCXML (annotating elements so the diagram can track identity across edits): decorates the source of truth with tool bookkeeping the engine ignores, the same move ADR-0006 rejected for fixture matching, and unnecessary once identities are per-build. Rejected.