Status: Accepted — implemented; TIEBREAKER ROLE SUPERSEDED by
docs/semantics/bluebook-semantics.md + spec/corpus/semantics/
(the clause-numbered semantics and its frozen corpus are the reference
for MEANING now; Ruby remains the reference IMPLEMENTATION, and both
runtimes answer to the corpus rather than to each other). The conformance corpus
(spec/corpus/rust_conformance/, banking.json, chess.json) is now frozen data too: each
fixture carries its expect, and Ruby (spec/conformance_corpus_spec.rb) and Rust
(spec/rust_conformance_spec.rb) are both held to it, not diffed against a live Ruby replay.
The differential harness this ADR specifies is real and runs in CI: spec/rust_conformance_spec.rb,
spec/codegen_parity_spec.rb, and spec/parser_parity_spec.rb.
Supersedes the “wait for IR stability” entry criterion in
0007’s
original Phase 5 placement.
0007 committed Rust to being a code generator over canonical IR, not a hand-written interpreter — closing the failure mode that killed the retired Rust runtime (a second parser/dispatcher/evaluator kept in differential parity by hand, forever, as Ruby changed). Having settled that, the roadmap still gated Rust behind Phases 2–4 (Governance, Identities, act_as, UL projection, a proven canonical corpus) landing first, reasoning that starting codegen against IR that’s still gaining shape would mean regenerating and recompiling repeatedly against a moving target.
That reasoning conflated two different risks. Eras mean canonical IR always keeps changing — that’s the premise the whole system is built on, not a temporary condition to wait out. The actual cost the retired runtime paid wasn’t “the target changed”; it was “a human had to notice the target changed and manually re-derive the second implementation’s behavior, with nothing mechanical checking they’d gotten it right.” A generator, by construction, doesn’t have that problem the same way: regenerating from new IR is cheap and mechanical. What was still missing was a way to know, cheaply and continuously, whether the generated result actually behaves like Ruby — without that, “regenerate and hope” is barely better than the thing 0007 already rejected.
The fix is to make Ruby do double duty it’s already positioned for: it’s the existing, working implementation, so it can serve as both the spec an implementer reads to know what to build, and the oracle a differential test suite checks the result against. Once that oracle exists and runs continuously, IR churn stops being a reason to wait — it becomes a normal, bounded, immediately-verified increment: extend the generator (and, when a change is behavioral rather than shape-only, the small hand-written kernel), run the harness against the existing corpus, fix whatever it flags.
Ruby is the reference implementation for every other runtime this project builds, in two roles at once:
spec/corpus/*.json) and the fuzzer (Fuzzing::SequenceGenerator/Fuzzing::Replay) — into both Ruby and the second runtime, and diffs accepted/refused outcomes, refusal wording, emitted events, and resulting record state. This harness is the actual gate for shipping or extending a second runtime — not a roadmap phase boundary.This replaces 0007’s original “Rust doesn’t start until Phases 2–4’s IR is stable and corpus-proven” gate. Rust — and any future non-Ruby runtime — can start now, against whatever IR exists today, and extend incrementally in lockstep as later phases land: each new Ruby-side feature (Governance, act_as, UL projection, …) gets a fast-following update to the generator and, where the change is behavioral rather than purely shape, the hand-written kernel — verified by the same harness before it counts as done. Rust never falls silently behind; there is no “wait until everything settles, then build it all at once” cliff.
act_as, UL projection/provenance, and beyond) now has an implicit, standing obligation: extend the second runtime and re-run the harness in the same or a fast-following change, not as separately-scheduled catch-up work.