# iter rt.1 — Roundtrip-Invariant audit tests **Date:** 2026-05-12 **Started from:** 10ccd1406c886270d105cd7d5a08ec4b447030cd **Status:** DONE **Tasks completed:** 4 of 4 ## Summary Three new tests landed; one existing test (handwritten exhibits) was replaced with a dynamic counterpart. No production code or DESIGN.md changes (those would be later iterations if any gap surfaced). All three new tests passed on first observation — the corpus carries enough breadth that the spec-intended audit deliverable is the clean signal "no current gap" rather than a defect list. ## Per-task notes - iter rt.1.1: `crates/ailang-surface/tests/round_trip.rs` — replaced static three-pair `handwritten_exhibits_match_json_fixtures` with dynamic `every_ailx_fixture_matches_its_json_counterpart` (and helper `list_ailx_fixtures`). Module doc rewritten to drop the "three hand-written exhibits" phrasing. - iter rt.1.2: `Cargo.toml` — `tempfile = "3"` added to `[workspace.dependencies]`. `crates/ail/Cargo.toml` — new `[dev-dependencies]` section with `tempfile.workspace = true` and `blake3.workspace = true`. - iter rt.1.3: `crates/ailang-core/tests/schema_coverage.rs` — new file. Exhaustive AST visitor (`Def`, `Term`, `Pattern`, `Literal`, `Type`, `ParamMode`) against the fixture corpus; flat `VariantTag` enum + `EXPECTED_VARIANTS` array; assertion that every variant is observed at least once. No `_` wildcards in outer match arms — AST drift fails the build, not the test. - iter rt.1.4: `crates/ail/tests/roundtrip_cli.rs` — new file. Subprocess pipeline `ail render → tempfile → ail parse`, BLAKE3 hash identity on canonical bytes, for every `examples/*.ail.json`. ## Audit findings - `every_ailx_fixture_matches_its_json_counterpart`: PASS. `.ailx cross-check ok (57 paired, 0 parse-only)`. All 57 `.ailx` fixtures parse and produce canonical bytes byte-equal to their same-stem `.ail.json` counterpart. Today no `.ailx` exists without a JSON counterpart. - `every_ast_variant_is_observed_in_the_fixture_corpus`: PASS. `schema coverage ok: 34 variants observed across 136 fixtures`. All 34 expected `VariantTag` entries (5 `Def` + 13 `Term` + 4 `Pattern` + 5 `Literal` + 4 `Type` + 3 `ParamMode`) are exercised by at least one fixture. No load failures across the 136-file corpus. - `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`: PASS. `CLI roundtrip ok for 136 fixtures`. Every `examples/*.ail.json` survives `ail render → tempfile → ail parse` with BLAKE3 hash identity on canonical bytes. - `cargo test --workspace --quiet` (Step 4.5): PASS, no regressions in the pre-existing test suite. ## Concerns - Plan-text errata in Task 3's visitor scaffold. The plan in `docs/plans/rt.1.md` Step 3.2 destructured AST nodes with field names that do not match `crates/ailang-core/src/ast.rs` (the authoritative source). Step 3.4 of the plan explicitly anticipated this and prescribed inline correction by reading `ast.rs` and adjusting the destructuring. The corrections applied: - `Def::Fn(fd)`: dropped the `for m in &fd.params { visit_param_mode(m, …) }` loop (`fd.params: Vec` is parameter names, not modes). - `Def::Const(cd)`: walks `cd.value` (the actual field name), not `cd.body`. - `Def::Class(cd)`: walks `m.ty` plus optional `m.default: Option` on each `ClassMethod`; no `m.body`/`m.params` fields exist on `ClassMethod`. - `Def::Instance(id)`: walks `id.type_: Type` (singular) and `id.methods`; no `id.args: Vec` field exists. `InstanceMethod` has only `name` and `body`. - `Term::App`: `{ callee, args, .. }` (not `{ f, args }`). - `Term::Let`: `{ value, body, .. }` (not `{ bound, body }`). - `Term::LetRec`: `{ ty, body, in_term, .. }` (per actual AST: `name, ty, params, body, in_term`), not the plan's `{ bindings, body }`. - `Term::If`: `{ cond, then, else_ }` (not `{ cond, then_b, else_b }`). - `Term::Do`: `{ args, .. }` only (no `stmts`/`ret` fields). - `Term::Lam`: `{ param_tys, ret_ty, body, .. }` (no `modes` field exists on `Lam`). - `Term::Seq`: `{ lhs, rhs }` (not `{ stmts, ret }`). - ParamMode discovery routed through `visit_type` on `Type::Fn { param_modes, ret_mode, .. }` — the only place `ParamMode` appears in the AST. The plan tried to visit ParamMode from `Def::Fn.params` (wrong type) and `Term::Lam.modes` (non-existent field). The spec-compliance intent of Step 3.2 ("every AST enum variant is visited at least once via an exhaustive match with no `_` wildcard") is preserved; only the destructuring patterns adapted to the actual AST. All 34 expected variants observe coverage in the corpus, and the build remains a tripwire for future AST additions. - Plan template note: a future `planner`/`brainstorm` pass on tests-against-AST might cheaply read `ast.rs` and emit a destructuring-correct scaffold instead of relying on the implementer to repair under the Step 3.4 escape hatch. Not rt.1 work. ## Known debt - None added by this iter. The three new tests are pure readers and add no production-code surface; their existence pins three invariants that future iters can use to detect regression rather than rediscover. ## Files touched - `Cargo.toml` — workspace dep `tempfile = "3"`. - `Cargo.lock` — auto-updated by cargo for `tempfile`. - `crates/ail/Cargo.toml` — new `[dev-dependencies]` section. - `crates/ailang-surface/tests/round_trip.rs` — module doc rewrite; `handwritten_exhibits_match_json_fixtures` → `every_ailx_fixture_matches_its_json_counterpart` (+ helper). - `crates/ailang-core/tests/schema_coverage.rs` — new file. - `crates/ail/tests/roundtrip_cli.rs` — new file. ## Stats bench/orchestrator-stats/2026-05-12-iter-rt.1.json