Files
AILang/design/contracts/roundtrip-invariant.md
T
Brummel f683f1aec8 iter design-md-rolesplit.tidy (DONE 7/7): resolve milestone-close audit drift
Gate-first TDD: widened design_index_pin.rs clause-3 to a hand-rolled
FAITHFUL Sweep-1 superset (case-sensitive digit-anchored line anchors
+ Sweep-1's ^[^/]* path-excluded date + the audit-named
decision-record phrases, case-insensitive); no regex dep; the blanket
iter-detector rejected as unworkable. Sentence-level strip of
faithfully-migrated history/decision-record prose out of 5 contract
files (the audit's 3 spot-checked + roundtrip-invariant.md +
data-model.md the exhaustive scan found) into the decision-record
journal, each replaced by its present-tense contract equivalent;
float-semantics.md stale 'see Str ABI below' -> str-abi.md;
architect_sweeps honesty sweeps re-scoped to design/contracts only
(models/ is the narrative tier) + ailang-architect.md lockstep.
Invariant: clause-3 GREEN => Sweep-1 clean in contracts/.

The prior dispatch correctly BLOCKED on a real plan defect (iso_date
lacked Sweep-1's path-exclusion, over-firing on legit
docs/specs/2026-.. citations); per the two+-defects-in-one-iteration
discipline the audit Resolution mechanism+scope were corrected
upstream in lockstep (f2cdd67) before re-dispatch, not patched a
third time.

Boss-verified independently: cargo test --workspace 646/0,
design_index_pin 4/4 (clause-3 RED->GREEN), architect_sweeps.sh exit
0 'All five sweeps clean' (acceptance criterion 9 met), acceptance
grep CLEAN, 3 docs_honesty_pin pinned runs each exactly 1 contiguous
match. Zero spec/quality re-loops. FINAL design-md-rolesplit
iteration — milestone functionally complete, audited, drift-resolved,
hard gate enforces the honesty spirit.
2026-05-19 13:44:37 +02:00

4.7 KiB

Roundtrip Invariant

Roundtrip Invariant

Every well-formed AILang module has a canonical .ail.json representation (the hashable, content-addressed JSON-AST) and a hand-authored .ail representation (Form A, the authoring projection). The JSON-AST is derived from .ail in-process by consumers that need it; the round-trip is now the property of that derivation, not the byte-level agreement of two on-disk forms.

Concretely:

  1. Parse-determinism. For every well-formed .ail text t, ailang_surface::parse(t) produces a unique AST. The parser is a pure function of input — no randomness, no time dependence, no environment leak. Hashing canonical::to_bytes(parse(t)) is therefore well-defined for any .ail source.

  2. Idempotency under print. For every well-formed .ail text t, canonical(parse(t)) == canonical(parse(print(parse(t)))). The printer is a left-inverse of the parser modulo canonical form: print-then-parse is a no-op on the canonical bytes.

  3. CLI-pipeline idempotency. For every .ail fixture, the public CLI pipeline ail parse | ail render | ail parse is byte-identical to direct ail parse of the source .ail. Pins drift that crate-internal tests cannot see.

  4. Carve-out anchor. Seven .ail.json-only fixtures (subject-matter rejection tests) survive in the corpus by structural necessity. They participate in their own dedicated rejection-shape tests, not in the round-trip gate. (The prelude is embedded as examples/prelude.ail in ailang-surface and parsed at compile time via ailang_surface::parse_prelude.)

Hashing is the consequence the language depends on: BLAKE3 of the canonical bytes is well-defined for any .ail source via parse- determinism. An LLM author writes .ail; the build derives the canonical hash without ambiguity.

Float literals are inside the invariant

Float literals carry an IEEE-754 bit pattern, not a decimal approximation. The canonical encoding is {"kind":"float","bits":"<16-lowercase-hex>"} and the surface emits the same bits-hex string. NaN, ±Inf, signed zero, and subnormals all round-trip exactly because the JSON-number path is bypassed (see §"Float semantics", "Form-A serialisation"). Floats are not an exception to the invariant — the bits-hex encoding is the mechanism that keeps them inside it.

Enforcement

The invariant is workspace-CI-enforced by five tests, each operating on the examples/ corpus via dynamic read_dir collection (no hardcoded fixture list, so newly added fixtures inherit the gate automatically):

  • crates/ailang-surface/tests/round_trip.rs::parse_is_deterministic_over_every_ail_fixture — for every .ail, parse twice and assert canonical-byte equality between the two ASTs. Direction 1 above.
  • crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ail_fixture — for every .ail text t, parse(t) and parse(print(parse(t))) produce canonical-byte-equal AST. Direction 2 above.
  • crates/ail/tests/roundtrip_cli.rs::cli_parse_then_render_then_parse_is_idempotent — for every .ail, the public CLI pipeline ail parse | ail render | ail parse reproduces canonical bytes byte-identical to direct ail parse. Pins drift internal tests cannot see.
  • crates/ailang-core/tests/schema_coverage.rs::every_ast_variant_is_observed_in_the_fixture_corpus — every variant of Def, Term, Pattern, Literal, Type, and ParamMode appears in at least one .ail fixture. New AST variants fail compile until the visitor and corpus are extended in lockstep.
  • crates/ailang-core/tests/carve_out_inventory.rs::examples_ail_json_inventory_matches_carve_outs — exactly the seven named carve-out files exist under examples/*.ail.json at any commit. A new .ail.json or a missing carve-out fails the test.

A new fixture or a new AST variant that violates the invariant fails one of these tests; the fix is in render or parse code, never by relaxing the test.

Why this is anchored at top level

The invariant is a property of the language identity, not of any one surface-design Decision. Decision 6 introduces the .ail surface and lists round-trip-as-property as one of its constraints, but the property is load-bearing for every downstream concern that treats the two forms as exchangeable: content-addressed hashing, the LLM-author's choice of authoring form, the integrity of fixture cross-references, and the prerequisite for empirical cross-model authoring-form studies. Lifting it out of Decision 6 makes the property quotable on its own and reviewable by the architect agent at every milestone close.

Ratified by: crates/ailang-surface/tests/round_trip.rs.