# Iter form-a.1 — Big-bang Migration + Milestone Close — Implementation Plan > **Parent spec:** `docs/specs/0025-form-a-default-authoring.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Render every non-carve-out `examples/*.ail.json` to its `.ail` sibling, delete the JSON-AST counterparts, restate the roundtrip-CI invariant, refactor all test and production-code consumers to read `.ail` (via library `ailang_surface::parse` or the extension-dispatching `ailang_surface::load_workspace`), update CLAUDE.md and DESIGN.md doctrine to reflect Form-A as the authoring surface, and close the milestone with a WhatsNew entry and a roadmap strike. Iter form-a.0 (prelude pilot) and the §C4 (b) amendment (prelude embed as compile-time exception) are the load-bearing inputs. **Architecture:** The iter sequences in an additive-then-subtractive pattern: new T1/T4 tests added first (cover the existing 58 `.ail` corpus), then bulk render of the 99 missing `.ail` files, then consumer migrations (~26 test files + bench drivers + production-code test relocations), then `.ail.json` deletion of the 156 non-carve-outs, then obsolete-test retirement and corpus flips, finally the doctrine edits and milestone close. The `cargo test --workspace` baseline (558 tests + 3 new T1/T4 tests = 561) must hold or rise green at every task boundary; non-green at task close is a defect to repair before the next task starts. **Tech Stack:** `ail render` / `ail parse` CLI; `ailang_surface::parse` and `ailang_surface::load_workspace` (library entry points); the roundtrip-CI tests at `crates/ailang-surface/tests/round_trip.rs`, `crates/ail/tests/roundtrip_cli.rs`, `crates/ailang-core/tests/schema_coverage.rs`; the 26 fixture-loading test files; 5 bench drivers; CLAUDE.md + DESIGN.md doctrine paragraphs. **Files this plan creates or modifies:** - Create: `crates/ailang-core/tests/carve_out_inventory.rs` — §T4 carve-out coverage check, hardcoded eight-file list. - Create: 99 new `examples/.ail` files (rendered from their `.ail.json` counterparts; recon-verified inventory). - Create: `crates/ailang-core/tests/hash_pin.rs` — relocated `#[cfg(test)] mod tests` from `crates/ailang-core/src/hash.rs:110-346` (uses `ailang_surface` dev-dep). - Create: `crates/ailang-core/tests/workspace_pin.rs` — relocated non-carve-out `#[cfg(test)] mod tests` from `crates/ailang-core/src/workspace.rs` (uses `ailang_surface` dev-dep). - Create: `crates/ailang-codegen/tests/eq_primitives_pin.rs` — relocated `#[cfg(test)] mod tests` from `crates/ailang-codegen/src/lib.rs:3723-3782` (uses `ailang_surface` dev-dep). - Modify: `crates/ailang-surface/tests/round_trip.rs` — add `parse_is_deterministic_over_every_ail_fixture`; later retire `print_then_parse_round_trips_every_fixture` and `every_ail_fixture_matches_its_json_counterpart`. - Modify: `crates/ail/tests/roundtrip_cli.rs` — add `cli_parse_then_render_then_parse_is_idempotent`; retire `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`. - Modify: `crates/ailang-core/tests/schema_coverage.rs:324-410` — flip corpus walker from `.ail.json` to `.ail`. - Modify: 14 Group-A test files (`ailang_core::load_workspace` consumers) — switch to `ailang_surface::load_workspace` + `.ail` paths. - Modify: 12 Group-B test files (subprocess consumers via `assert_cmd`) — suffix change at call sites. - Modify: `crates/ailang-prose/tests/snapshot.rs:28` — switch to `.ail` + `ailang_surface::load_module`. - Modify: `crates/ail/tests/e2e.rs:695-735` — re-author the synthesised-diff test to derive `sum.ail.json` on-the-fly via `ail parse`. - Delete (cargo-internal `#[cfg(test)] mod tests` block bodies, replaced by integration-test counterparts above): `crates/ailang-core/src/hash.rs:110-346`, `crates/ailang-core/src/workspace.rs` non-carve-out test bodies, `crates/ailang-codegen/src/lib.rs:3723-3782`. - Modify: `bench/compile_check.py:43,101`; `bench/cross_lang.py:48,80`; `bench/mono_dispatch.py:10,112`; `bench/run.sh:68,182,183` — fixture-stem suffix changes from `.ail.json` to `.ail`. - Modify: `CLAUDE.md:5-6` — replace doctrine sentence per spec §A4. - Modify: `docs/DESIGN.md:465-466` — replace first sentence per spec §A4 (second sentence preserved). - Modify: `docs/DESIGN.md:2027-2109` — restate §"Roundtrip Invariant" per spec §C3. - Modify: `docs/WhatsNew.md` — append milestone-close entry. - Modify: `docs/roadmap.md` — strike `[milestone] Form-A as the default authoring surface` entry. - Delete: 156 `examples/.ail.json` files (the non-carve-outs; 99 fresh-rendered fixtures + 57 fixtures that already had `.ail` siblings pre-iter). The eight surviving `.ail.json` files at iter close: seven §C4 (a) subject-matter carve-outs + one §C4 (b) compile-time-embed carve-out (`prelude.ail.json`). Verified by Task 12 acceptance check #1. --- ### Task 1: Add new T1/T4 tests (additive, cover existing 58 `.ail`) **Files:** - Modify: `crates/ailang-surface/tests/round_trip.rs` (add new test after line 277) - Modify: `crates/ail/tests/roundtrip_cli.rs` (add new test after line 146) - Create: `crates/ailang-core/tests/carve_out_inventory.rs` - [ ] **Step 1: Add `parse_is_deterministic_over_every_ail_fixture` in round_trip.rs** Append after the existing `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture` test (around line 278): ```rust /// Parse-determinism (post-form-a-default-authoring §C3): for every /// well-formed `.ail` text `t`, `parse(t)` produces the same canonical /// bytes every invocation. Loader is a pure function of input — no /// randomness, no time dependence, no environment leak. #[test] fn parse_is_deterministic_over_every_ail_fixture() { let mut checked = 0usize; for ail_path in list_ail_fixtures() { let text = std::fs::read_to_string(&ail_path) .unwrap_or_else(|e| panic!("read {ail_path:?}: {e}")); let m1 = ailang_surface::parse(&text) .unwrap_or_else(|e| panic!("parse {ail_path:?}: {e:?}")); let m2 = ailang_surface::parse(&text) .unwrap_or_else(|e| panic!("parse {ail_path:?} (2nd): {e:?}")); let b1 = ailang_core::canonical::to_bytes(&m1); let b2 = ailang_core::canonical::to_bytes(&m2); assert_eq!( b1, b2, "parse non-determinism on {ail_path:?}", ); checked += 1; } assert!(checked > 0, "no .ail fixtures found"); } ``` - [ ] **Step 2: Run the new test in isolation** Run: ``` cargo test -p ailang-surface --test round_trip parse_is_deterministic_over_every_ail_fixture ``` Expected: PASS, `checked > 0`. The current `.ail` corpus is 58 files (prelude + 57 paired); the test must pass on all of them. - [ ] **Step 3: Add `cli_parse_then_render_then_parse_is_idempotent` in roundtrip_cli.rs** Append after the existing `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture` test (around line 147): ```rust /// CLI-pipeline idempotency (post-form-a-default-authoring §C3): for /// every `.ail` fixture, the user-facing pipeline /// `ail parse | ail render | ail parse` is byte-identical /// to direct `ail parse `. Pins drift internal tests cannot see. #[test] fn cli_parse_then_render_then_parse_is_idempotent() { let mut checked = 0usize; for ail_path in list_ail_fixtures() { // Direct parse → JSON tempfile. let json1 = tempfile::NamedTempFile::new().unwrap(); ail() .args(["parse", ail_path.to_str().unwrap(), "-o", json1.path().to_str().unwrap()]) .assert() .success(); // Render that JSON → .ail tempfile. let ail_round = tempfile::NamedTempFile::new().unwrap(); let rendered = std::process::Command::new(ail_bin()) .args(["render", json1.path().to_str().unwrap()]) .output() .expect("ail render"); assert!(rendered.status.success(), "ail render failed on {ail_path:?}"); std::fs::write(ail_round.path(), &rendered.stdout).unwrap(); // Re-parse → second JSON tempfile. let json2 = tempfile::NamedTempFile::new().unwrap(); ail() .args(["parse", ail_round.path().to_str().unwrap(), "-o", json2.path().to_str().unwrap()]) .assert() .success(); // Assert the two canonical JSONs are byte-equal. let b1 = std::fs::read(json1.path()).unwrap(); let b2 = std::fs::read(json2.path()).unwrap(); assert_eq!( b1, b2, "CLI parse|render|parse non-idempotent on {ail_path:?}", ); checked += 1; } assert!(checked > 0, "no .ail fixtures found"); } ``` Note: the test uses the existing `list_ail_fixtures()` helper at `roundtrip_cli.rs:31-46` if present, or define an analogue if the file currently only has `list_json_fixtures()`. Recon should have flagged this — verify before writing the body; if `list_ail_fixtures` is missing in roundtrip_cli.rs, add a parallel helper at lines 47+. - [ ] **Step 4: Run the new test in isolation** Run: ``` cargo test -p ail --test roundtrip_cli cli_parse_then_render_then_parse_is_idempotent ``` Expected: PASS, `checked > 0`. Same 58-file corpus as Step 2. - [ ] **Step 5: Create `carve_out_inventory.rs` with the eight-file list** Write `crates/ailang-core/tests/carve_out_inventory.rs`: ```rust //! Carve-out inventory check — §T4 of the form-a-default-authoring //! spec. Asserts that exactly the eight named carve-out files exist //! under `examples/*.ail.json` after milestone close. The list is //! hardcoded; any change is a deliberate, brainstorm-level decision. //! //! Eight carve-outs at iter form-a.1 close: //! - §C4 (a) subject-matter: 7 fixtures (canonical-form rejection / unbound / class-def rejection). //! - §C4 (b) compile-time-embed: 1 fixture (prelude.ail.json). //! //! When the prelude-embed-refactor milestone retires §C4 (b), this //! test's pass-count drops to seven and `EXPECTED` is re-published. use std::collections::BTreeSet; const EXPECTED: &[&str] = &[ // §C4 (a) — subject-matter "broken_unbound.ail.json", "test_22b2_invalid_superclass_param.ail.json", "test_22b2_kind_mismatch.ail.json", "test_22b2_unbound_constraint_var.ail.json", "test_ct1_bad_qualifier.ail.json", "test_ct1_bare_xmod_rejected.ail.json", "test_ct1_qualified_class_rejected.ail.json", // §C4 (b) — compile-time-embed "prelude.ail.json", ]; fn examples_dir() -> std::path::PathBuf { let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); crate_dir.parent().unwrap().parent().unwrap().join("examples") } #[test] fn examples_ail_json_inventory_matches_carve_outs() { let expected: BTreeSet<&str> = EXPECTED.iter().copied().collect(); let mut actual: BTreeSet = BTreeSet::new(); for entry in std::fs::read_dir(examples_dir()).expect("read examples/") { let entry = entry.unwrap(); let name = entry.file_name().into_string().unwrap(); if name.ends_with(".ail.json") { actual.insert(name); } } let actual_refs: BTreeSet<&str> = actual.iter().map(String::as_str).collect(); assert_eq!( actual_refs, expected, "examples/*.ail.json inventory drifted from carve-out list", ); } ``` - [ ] **Step 6: Run the carve-out inventory test — expect RED** Run: ``` cargo test -p ailang-core --test carve_out_inventory ``` Expected: FAIL. The current `examples/*.ail.json` count is ~164 — the test asserts 8. This is the RED state for the iter; the GREEN side lands after Task 8 (deletion). NOTE: this RED test is deliberate; Task 1 leaves the workspace in a state where this single test fails. Other tasks should run `cargo test --workspace --exclude carve_out_inventory_test_target` or equivalent until Task 8 closes the loop. Alternatively, gate the test body with `#[ignore]` (remove the ignore in Task 8). Use `#[ignore]` for cleanliness; the implementer phase verifies cargo-test-green per-task. - [ ] **Step 7: Apply `#[ignore]` to the carve-out test** Wrap the test: ```rust #[test] #[ignore = "GREEN-ratified by Task 8 (.ail.json deletion); RED until then"] fn examples_ail_json_inventory_matches_carve_outs() { // ...body unchanged... } ``` Final cargo test for Task 1: ``` cargo test --workspace ``` Expected: PASS, 561 tests (was 558; +3 from this task: parse-determinism + CLI-pipeline-idempotency + carve-out-inventory-but-ignored). The ignored test does not run; the other two cover their 58-fixture corpus. --- ### Task 2: Render the 99 missing `.ail` files **Files:** - Create: 99 `examples/.ail` files (one render per non-pre-existing fixture in the non-carve-out cohort). - [ ] **Step 1: Confirm the render target list** Run from the workspace root: ``` for f in examples/*.ail.json; do base=$(basename "$f" .ail.json) if [ ! -f "examples/${base}.ail" ]; then case "$base" in test_ct1_bare_xmod_rejected|test_ct1_qualified_class_rejected|test_ct1_bad_qualifier|broken_unbound|test_22b2_invalid_superclass_param|test_22b2_kind_mismatch|test_22b2_unbound_constraint_var|prelude) : ;; *) echo "$base" ;; esac fi done | wc -l ``` Expected: `99`. If the count differs, the corpus drifted between recon (2026-05-13 morning) and execution; investigate before continuing. - [ ] **Step 2: Render all 99 missing `.ail` files** Run from the workspace root: ``` for f in examples/*.ail.json; do base=$(basename "$f" .ail.json) if [ ! -f "examples/${base}.ail" ]; then case "$base" in test_ct1_bare_xmod_rejected|test_ct1_qualified_class_rejected|test_ct1_bad_qualifier|broken_unbound|test_22b2_invalid_superclass_param|test_22b2_kind_mismatch|test_22b2_unbound_constraint_var|prelude) : ;; *) cargo run -q --bin ail -- render "$f" > "examples/${base}.ail" \ || { echo "RENDER FAILED on $base" >&2; exit 1; } ;; esac fi done ``` Expected: exit 0. The seven §C4 (a) carve-outs and `prelude` are explicitly skipped (prelude.ail already exists; the seven carve-outs have no Form-A counterpart and must not be rendered per §C4). - [ ] **Step 3: Sanity-check the count and run the new T1 tests** Run: ``` ls examples/*.ail | wc -l ``` Expected: `157` (58 pre-iter + 99 newly rendered). Run: ``` cargo test -p ailang-surface --test round_trip parse_is_deterministic_over_every_ail_fixture parse_then_print_then_parse_is_idempotent_on_every_ail_fixture cargo test -p ail --test roundtrip_cli cli_parse_then_render_then_parse_is_idempotent ``` Expected: both PASS. Each test's `checked` count is now 157 (was 58 in Task 1). If any test fails, a specific render is broken; halt the iter and bug-fix render/parse. - [ ] **Step 4: Final cargo test for Task 2** Run: ``` cargo test --workspace ``` Expected: PASS, 561 tests green. The migration is additive at this point (no `.ail.json` deletion yet), so consumer tests still find their existing `.ail.json` and pass. --- ### Task 3: Group A test files — `load_workspace` consumers switch to `.ail` **Files (14 files):** - Modify: `crates/ailang-check/tests/env_construction_pin.rs:128` - Modify: `crates/ailang-check/tests/method_collision_pin.rs:37,72` - Modify: `crates/ailang-check/tests/workspace.rs:42,55,75,677` - Modify: `crates/ailang-check/tests/show_dispatch_pin.rs:25,52` - Modify: `crates/ailang-core/tests/ctt2_registry_rekey.rs:30` - Modify: `crates/ail/tests/eq_float_noinstance.rs:22` - Modify: `crates/ail/tests/mono_recursive_fn.rs:43` - Modify: `crates/ail/tests/mono_unification.rs:22,65,96,126,147,173` - Modify: `crates/ail/tests/mono_xmod_ctor_pattern.rs:66` - Modify: `crates/ail/tests/mono_xmod_qualified_ref.rs:60` - Modify: `crates/ail/tests/codegen_import_map_fallback_pin.rs:28` - Modify: `crates/ail/tests/polyfn_dot_qualified_branch_pin.rs:25` - Modify: `crates/ail/tests/show_no_instance_e2e.rs:21` - Modify: `crates/ail/tests/typeclass_22b2.rs:28,62,147,167,189,210,233,256,280,310,332,355` - Modify: `crates/ail/tests/typeclass_22b3.rs:23,32,66,123,379,419,483,573,627,644,677` - [ ] **Step 1: Switch import + path-suffix pattern across all 14 files** For every Group A file, change every line that constructs an `examples/.ail.json` path AND uses `ailang_core::load_workspace`. Two coupled edits per call site: 1. Path string: `".ail.json"` → `".ail"`. 2. Loader call: `ailang_core::load_workspace(&path)` → `ailang_surface::load_workspace(&path)`. The two loaders return compatible `Workspace` values (`ailang_surface::load_workspace` is the extension-dispatching superset; see `crates/ailang-surface/src/loader.rs:37-60`). For each carve-out reference (`test_22b2_kind_mismatch`, `test_22b2_invalid_superclass_param`, `test_22b2_unbound_constraint_var`, `test_ct1_*`, `broken_unbound`): **do NOT change** — those tests keep `.ail.json` paths and continue using `ailang_core::load_workspace`. The recon flagged `typeclass_22b2.rs` and `typeclass_22b3.rs` as touching multiple fixtures including carve-outs; preserve carve-out lines unchanged. Concretely, for `crates/ailang-check/tests/method_collision_pin.rs:37`: ```rust // before let entry = examples_dir().join("test_22b2_method_name_collision_a.ail.json"); let ws = ailang_core::load_workspace(&entry).expect("load"); // after let entry = examples_dir().join("test_22b2_method_name_collision_a.ail"); let ws = ailang_surface::load_workspace(&entry).expect("load"); ``` Apply this pattern to every non-carve-out call site enumerated above. The exact diff per file is mechanical; verify by line number before saving. - [ ] **Step 2: Ensure `ailang_surface` is a dev-dependency of each Group A crate** Check the `[dev-dependencies]` section of each `Cargo.toml`: ``` grep -l 'ailang-surface' crates/ailang-check/Cargo.toml crates/ailang-core/Cargo.toml crates/ail/Cargo.toml ``` Expected: all three present. If any is missing, add `ailang-surface = { path = "../ailang-surface" }` under `[dev-dependencies]`. - [ ] **Step 3: Run the Group A test files** Run: ``` cargo test -p ailang-check --test env_construction_pin cargo test -p ailang-check --test method_collision_pin cargo test -p ailang-check --test workspace cargo test -p ailang-check --test show_dispatch_pin cargo test -p ailang-core --test ctt2_registry_rekey cargo test -p ail --test eq_float_noinstance cargo test -p ail --test mono_recursive_fn cargo test -p ail --test mono_unification cargo test -p ail --test mono_xmod_ctor_pattern cargo test -p ail --test mono_xmod_qualified_ref cargo test -p ail --test codegen_import_map_fallback_pin cargo test -p ail --test polyfn_dot_qualified_branch_pin cargo test -p ail --test show_no_instance_e2e cargo test -p ail --test typeclass_22b2 cargo test -p ail --test typeclass_22b3 ``` Expected: each PASS. If a test fails, the migration pattern was misapplied — re-inspect the diff for that file. - [ ] **Step 4: Final cargo test for Task 3** Run: ``` cargo test --workspace ``` Expected: PASS, 561 tests green. --- ### Task 4: Group B test files — subprocess consumers switch suffix **Files (12 files):** - Modify: `crates/ail/tests/e2e.rs:81-689` (85 invocations of `build_and_run`/`build_and_run_with_alloc`/`build_and_run_with_rc_stats`) - Modify: `crates/ail/tests/eq_ord_e2e.rs:62,73` - Modify: `crates/ail/tests/typeclass_22c.rs:56` - Modify: `crates/ail/tests/show_print_e2e.rs:58,70` - Modify: `crates/ail/tests/floats_e2e.rs:23` - Modify: `crates/ail/tests/ir_snapshot.rs:137,142,147,152,161` - Modify: `crates/ail/tests/ct1_check_cli.rs:169` (the `ordering_match.ail.json:169` non-carve-out; carve-outs at :40, :68, :101, :140 preserve) - Modify: `crates/ail/tests/mq3_multi_class_e2e.rs:47,79,95` - Modify: `crates/ail/tests/mono_hash_stability.rs:16,23` - Modify: `crates/ail/tests/prelude_free_fns.rs:41,50,59,68,77` - Modify: `crates/ail/tests/print_mono_body_shape.rs:27` - Modify: `crates/ail/tests/show_mono_synthesis.rs:17` - [ ] **Step 1: Switch the fixture-name string suffix across all 12 files** The CLI accepts both `.ail` and `.ail.json` since iter ext-cli.1; the test helpers prepend `examples/` and pass the rest verbatim to `ail` subprocess. Change is suffix-only at call sites. For every non-carve-out call site enumerated above, change: ```rust build_and_run(".ail.json") // to: build_and_run(".ail") ``` For carve-out references in `ct1_check_cli.rs:40,68,101,140` (`test_ct1_bare_xmod_rejected`, `test_ct1_qualified_class_rejected`, `test_ct1_bad_qualifier`, plus the second-time `test_ct1_qualified_class_rejected` at :140): **do NOT change** — they continue passing `.ail.json` paths. For `eq_ord_e2e.rs`, recon noted line :82 is also a Group A call (`load_workspace`) — Task 3 handled that. Lines :62, :73 are the Group B `build_and_run` calls in this file. - [ ] **Step 2: Run each Group B test file** Run: ``` cargo test -p ail --test e2e cargo test -p ail --test eq_ord_e2e cargo test -p ail --test typeclass_22c cargo test -p ail --test show_print_e2e cargo test -p ail --test floats_e2e cargo test -p ail --test ir_snapshot cargo test -p ail --test ct1_check_cli cargo test -p ail --test mq3_multi_class_e2e cargo test -p ail --test mono_hash_stability cargo test -p ail --test prelude_free_fns cargo test -p ail --test print_mono_body_shape cargo test -p ail --test show_mono_synthesis ``` Expected: each PASS. The `e2e.rs` synthesised-diff test at lines 695-735 is NOT yet repaired in Task 4 — if it fails because of the renamed fixture suffix, Task 7 repairs it. Mark the failing test with `#[ignore = "Task 7 reauthors"]` if needed to keep Task 4 green; remove the ignore in Task 7. - [ ] **Step 3: Final cargo test for Task 4** Run: ``` cargo test --workspace ``` Expected: PASS, 561 tests green (minus the ignored e2e.rs synthesised-diff test if marked). --- ### Task 5: Relocate production-code `#[cfg(test)] mod tests` to integration tests **Files:** - Create: `crates/ailang-core/tests/hash_pin.rs` - Create: `crates/ailang-core/tests/workspace_pin.rs` - Create: `crates/ailang-codegen/tests/eq_primitives_pin.rs` - Modify: `crates/ailang-prose/tests/snapshot.rs:28` — switch to `.ail` + `ailang_surface::load_module` - Delete (test-bodies in production source): `crates/ailang-core/src/hash.rs:110-346` - Delete (non-carve-out test-bodies): per-line ranges in `crates/ailang-core/src/workspace.rs:1620,1641,1743,1761,1777,1801,1833,1858,1895,1926,2059` - Delete (test-bodies): `crates/ailang-codegen/src/lib.rs:3723-3782` - [ ] **Step 1: Relocate `hash.rs` unit tests** The hash.rs `#[cfg(test)] mod tests` block (lines 110-346, 12 fixture-loading references) uses only public APIs (`ailang_core::load_module`, `ailang_core::canonical::to_bytes`, `ailang_core::hash::hash_module` — verify each is `pub` before relocation). Create `crates/ailang-core/tests/hash_pin.rs` and move the test bodies there verbatim, with two edits per fixture-loading site: 1. Path string: `".ail.json"` → `".ail"`. 2. Loader: `ailang_core::load_module(&path)` → `ailang_surface::load_module(&path)`. Add `ailang-surface = { path = "../ailang-surface" }` to `crates/ailang-core/Cargo.toml` `[dev-dependencies]` if absent (verify with `grep ailang-surface crates/ailang-core/Cargo.toml`). Then delete lines 110-346 from `crates/ailang-core/src/hash.rs`. The production module body above line 110 is unchanged. - [ ] **Step 2: Relocate non-carve-out `workspace.rs` unit tests** The `crates/ailang-core/src/workspace.rs` `#[cfg(test)] mod tests` block contains both carve-out tests (preserve in-place) and non-carve-out tests (relocate). Recon enumerated these as in-place-preserved (carve-out): - `class_param_in_applied_position_fires_canonical_form_rejection` (line ~1945, loads `test_22b2_kind_mismatch.ail.json`) - `superclass_with_wrong_param_fires_invalid_superclass_param` (line ~1968, loads `test_22b2_invalid_superclass_param.ail.json`) - `unbound_constraint_var_in_class_def_fires` (line ~2024, loads `test_22b2_unbound_constraint_var.ail.json`) - the three `test_ct1_*` tests at :2681, :2697, :2720 And these as non-carve-out (relocate): - All test functions at lines :1620, :1641, :1743, :1761, :1777, :1801, :1833, :1858, :1895, :1926, :2059, :1998 (the `test_22b2_overriding_nonexistent.ail.json` consumer — Boss-decided to relocate since it uses only `load_workspace` public API), and any others in the non-carve-out cohort. Create `crates/ailang-core/tests/workspace_pin.rs` and move the non-carve-out test bodies verbatim, with the two-edit pattern (path suffix + `ailang_surface::load_workspace`). Preserve the carve-out tests in-place (they still use `ailang_core::load_workspace` and `.ail.json` paths). If any moved test references a `pub(crate)` or private item, expose it as `pub` in workspace.rs or via a `pub use` re-export to keep the integration test compilable. Recon believed all moved tests use public APIs; verify per test before saving. - [ ] **Step 3: Relocate `codegen/src/lib.rs` unit tests** The `crates/ailang-codegen/src/lib.rs` `#[cfg(test)] mod tests` block at lines 3723-3782 (3 references to `eq_primitives_smoke.ail.json`) uses presumably public APIs. Create `crates/ailang-codegen/tests/eq_primitives_pin.rs` and move the bodies, with the two-edit pattern (path suffix + `ailang_surface::load_workspace` or `ailang_surface::load_module` as appropriate). Add `ailang-surface = { path = "../ailang-surface" }` to `crates/ailang-codegen/Cargo.toml` `[dev-dependencies]` if absent. Delete lines 3723-3782 from `crates/ailang-codegen/src/lib.rs`. - [ ] **Step 4: Migrate `ailang-prose/tests/snapshot.rs:28`** Edit the path-construction and loader call: ```rust // before (line 28 area) let json_path = dir.join(format!("{stem}.ail.json")); let m = ailang_core::load_module(&json_path).expect("load"); // after let ail_path = dir.join(format!("{stem}.ail")); let m = ailang_surface::load_module(&ail_path).expect("load"); ``` Update the doc-comment at `crates/ailang-prose/tests/snapshot.rs:9` similarly (`.ail.json` → `.ail`). - [ ] **Step 5: Run the relocated test crates** Run: ``` cargo test -p ailang-core --test hash_pin cargo test -p ailang-core --test workspace_pin cargo test -p ailang-codegen --test eq_primitives_pin cargo test -p ailang-prose --test snapshot ``` Expected: each PASS. If any fail because of a missing public-API export, expose the needed item and re-run. - [ ] **Step 6: Final cargo test for Task 5** Run: ``` cargo test --workspace ``` Expected: PASS, 561 tests green (Task 5 removed N tests from `hash.rs` / `codegen/src/lib.rs` / `workspace.rs` and added the same N tests under `tests/*.rs`; net = 0 test-count change). --- ### Task 6: Bench-driver path-suffix updates **Files:** - Modify: `bench/compile_check.py:43,101` - Modify: `bench/cross_lang.py:48,80` - Modify: `bench/mono_dispatch.py:10,112` - Modify: `bench/run.sh:68,182,183` - [ ] **Step 1: Suffix change in `bench/compile_check.py`** Edit two lines: ```python # line 43 area — comment update # Walks examples/*.ail and runs `ail check` on each. # was: examples/*.ail.json # line 101 area — path construction src = EXAMPLES / f"{fixture}.ail" # was: f"{fixture}.ail.json" ``` The `fixture` list at lines 44-58 is stems-only; no change there. - [ ] **Step 2: Suffix change in `bench/cross_lang.py`** Edit two lines analogously: ```python # line 48 area — comment update # line 80 area src = EXAMPLES / f"{stem}.ail" # was: f"{stem}.ail.json" ``` - [ ] **Step 3: Suffix change in `bench/mono_dispatch.py`** ```python # line 10 area — comment update # line 112 area str(EXAMPLES / "bench_mono_dispatch.ail") # was: bench_mono_dispatch.ail.json ``` - [ ] **Step 4: Suffix change in `bench/run.sh`** Edit three lines: ```bash # line 68 area src="$ROOT/examples/$f.ail" # was: $f.ail.json # line 182 area LAT_IMPL_SRC="$ROOT/examples/bench_latency_implicit.ail" # line 183 area LAT_EXPL_SRC="$ROOT/examples/bench_latency_explicit.ail" ``` - [ ] **Step 5: Run the bench drivers against the migrated corpus** Note: this step temporarily expects the `.ail` files to exist alongside their `.ail.json` counterparts (we have not deleted JSON yet). The CLI's extension-dispatch (ext-cli.1) accepts `.ail` paths. Run: ``` python3 bench/compile_check.py python3 bench/cross_lang.py ``` Expected: both exit 0 (no regression in compile-time or cross-language ratios). If exit non-zero, the bench driver may have a comment-line edit mistake (broken Python) or the fixture coverage shifted. - [ ] **Step 6: Final cargo test for Task 6** Run: ``` cargo test --workspace ``` Expected: PASS, 561 tests green. --- ### Task 7: Re-author e2e.rs raw-JSON-inspect tests (5 tests) **Files:** - Modify: `crates/ail/tests/e2e.rs` — 5 tests total per iter form-a.1 (T1-5) journal Known-debt section The iter form-a.1 (T1-5) journal surfaced that 5 tests in `crates/ail/tests/e2e.rs` raw-read `.ail.json` bytes for substring asserts / mutation / byte-roundtrip comparison. They pass today (`.ail.json` files still present pre-T8) but break at T8 when the non-carve-out `.ail.json` siblings are deleted. The five tests are: 1. `diff_detects_changed_def` — loads `sum.ail.json`, mutates the JSON to produce `sum_v2.ail.json`, runs `ail diff` on the pair. (This was T7's originally-named test.) 2. `borrow_own_demo_modes_are_metadata_only` — raw-reads `borrow_own_demo.ail.json` for substring assertions about mode annotations. 3. `reuse_as_demo_under_rc_uses_inplace_rewrite` — raw-reads `reuse_as_demo.ail.json` for substring assertions about reuse-as encoding. 4. `render_parse_round_trip_canonical` — byte-roundtrips a chosen `.ail.json` through `ail render | ail parse` for byte-equal assertion. (Note: `cli_parse_then_render_then_parse_is_idempotent` from T1 already pins this property across the whole `.ail` corpus; this test may simply be retired.) 5. `ail_run_accepts_ail_source_with_same_stdout_as_ail_json` — dual-form smoke: runs a fixture as both `.ail` and `.ail.json` and asserts identical stdout. Structurally requires BOTH forms to exist; post-T8 only the eight carve-outs have `.ail.json`. The seven §C4(a) carve-outs are negative-test fixtures (parse-rejected or load-rejected), so they cannot serve as the dual-form smoke fixture either. `prelude.ail.json` is dual-form post-iter (per §C4 (b)), but prelude is not user-runnable directly. - [ ] **Step 1: Inspect the five test bodies** Read the five tests in `crates/ail/tests/e2e.rs`. For each, note: - the exact `.ail.json` filename read. - the assertion shape (substring / byte-equal / stdout-match). - whether the test's subject is JSON-AST byte mechanics (which require the JSON form) or program behaviour (which is form-agnostic). - [ ] **Step 2: Re-author `diff_detects_changed_def` (test 1)** This is the test originally named in the plan. The subject is JSON-byte mutation for `ail diff`. Replace the read-from-disk with a parse-derive: ```rust // before — reads JSON bytes directly: let json = std::fs::read(workspace.join("examples").join("sum.ail.json")) .expect("read sum.ail.json"); let sum_v1 = String::from_utf8(json).unwrap(); // after — derive the canonical JSON from sum.ail via `ail parse`: let tmpdir = tempfile::tempdir().unwrap(); let v1_path = tmpdir.path().join("sum_v1.ail.json"); std::process::Command::new(ail_bin()) .args([ "parse", workspace.join("examples").join("sum.ail").to_str().unwrap(), "-o", v1_path.to_str().unwrap(), ]) .status() .expect("ail parse") .success(); let sum_v1 = std::fs::read_to_string(&v1_path).expect("read v1"); ``` The downstream mutation logic (creating `sum_v2.ail.json` with byte mutation) and `ail diff` invocation continue against tempfile paths. - [ ] **Step 3: Re-author `borrow_own_demo_modes_are_metadata_only` (test 2) and `reuse_as_demo_under_rc_uses_inplace_rewrite` (test 3)** Both raw-read `.ail.json` for substring assertions about mode/reuse-as encoding. The substring is in the JSON bytes; we derive the JSON via `ail parse` on the `.ail` source, then assert against the temp-file content. Same pattern as Step 2. For each test, find the `std::fs::read*("examples/.ail.json")` line and replace with a `ail parse examples/.ail -o ` + read of the tempfile. - [ ] **Step 4: Retire `render_parse_round_trip_canonical` (test 4)** This test's property (CLI render→parse byte-roundtrip) is already pinned across the entire `.ail` corpus by `cli_parse_then_render_then_parse_is_idempotent` (added in T1). Delete the redundant test body — the new test is strictly stronger (whole-corpus vs. one-fixture). Verify by inspection that the new T1 test covers the same property; if so, `git rm`-equivalent inline delete (remove the `#[test] fn render_parse_round_trip_canonical(...)` block from `e2e.rs`). - [ ] **Step 5: Re-author `ail_run_accepts_ail_source_with_same_stdout_as_ail_json` (test 5)** This test originally proved that the CLI accepts both extensions for a single program. Post-iter form-a.1, only carve-outs have `.ail.json` form, and carve-outs are negative-test fixtures (load/parse rejection — they don't have stdout to compare). Re-author into a structurally equivalent test that proves the same property without needing a dual-form fixture: render an existing `.ail` to a tempdir `.ail.json` via `ail parse`, run `ail run` against both the original `.ail` and the tempdir `.ail.json`, assert identical stdout. ```rust // derive a parallel .ail.json in a tempdir from the existing .ail let tmpdir = tempfile::tempdir().unwrap(); let json_path = tmpdir.path().join("dual_form.ail.json"); std::process::Command::new(ail_bin()) .args([ "parse", workspace.join("examples").join("hello.ail").to_str().unwrap(), "-o", json_path.to_str().unwrap(), ]) .status() .expect("ail parse") .success(); // then run both forms and compare stdout let ail_stdout = run_ail_run(workspace.join("examples").join("hello.ail")); let json_stdout = run_ail_run(&json_path); assert_eq!(ail_stdout, json_stdout, "dual-form stdout mismatch"); ``` Pick `hello.ail` (small, no I/O, predictable stdout) as the fixture. If the test originally used a different fixture, pick whichever has a stable single-line stdout. - [ ] **Step 6: Run e2e.rs** Run: ``` cargo test -p ail --test e2e ``` Expected: PASS. If a re-authored test still references a deleted carve-out path or a renamed helper, the implementer-phase repairs. - [ ] **Step 7: Final cargo test for Task 7** Run: ``` cargo test --workspace ``` Expected: PASS, 559 tests green. Task 7 net change: −1 test (`render_parse_round_trip_canonical` retired in Step 4). 560 − 1 = 559. - [ ] **Step 1: Inspect the current test body** Read the test (around lines 695-735 of `crates/ail/tests/e2e.rs`) and identify: - Where it reads `examples/sum.ail.json`. - Where it mutates the JSON. - Where it invokes `ail diff`. - [ ] **Step 2: Replace the `.ail.json` read with a parse-from-`.ail`** Change: ```rust // before — reads the JSON bytes directly let json = std::fs::read(workspace.join("examples").join("sum.ail.json")) .expect("read sum.ail.json"); let sum_v1 = String::from_utf8(json).unwrap(); // after — derive the canonical JSON from sum.ail via `ail parse` let tmpdir = tempfile::tempdir().unwrap(); let v1_path = tmpdir.path().join("sum_v1.ail.json"); ail() .args([ "parse", workspace.join("examples").join("sum.ail").to_str().unwrap(), "-o", v1_path.to_str().unwrap(), ]) .assert() .success(); let sum_v1 = std::fs::read_to_string(&v1_path).expect("read v1"); ``` Subsequent mutation logic (creating `sum_v2.ail.json` with the byte mutation) stays in the tempdir. The `ail diff` invocation receives both tempfile paths. - [ ] **Step 3: Run the e2e test** Run: ``` cargo test -p ail --test e2e ``` Expected: PASS. If marked `#[ignore]` in Task 4, remove the ignore in this step. - [ ] **Step 4: Final cargo test for Task 7** Run: ``` cargo test --workspace ``` Expected: PASS, 561 tests green. --- ### Task 8: Delete the 156 non-carve-out `.ail.json` files **Files:** - Delete: 156 `examples/*.ail.json` files (every fixture except the eight §C4 carve-outs). - [ ] **Step 1: Delete every non-carve-out `.ail.json`** Run from the workspace root: ``` for f in examples/*.ail.json; do base=$(basename "$f" .ail.json) case "$base" in test_ct1_bare_xmod_rejected|test_ct1_qualified_class_rejected|test_ct1_bad_qualifier|broken_unbound|test_22b2_invalid_superclass_param|test_22b2_kind_mismatch|test_22b2_unbound_constraint_var|prelude) : ;; *) rm "$f" ;; esac done ``` Expected: exit 0. The eight carve-outs are skipped; all other `.ail.json` are removed. - [ ] **Step 2: Verify the inventory** Run: ``` ls examples/*.ail.json | wc -l ls examples/*.ail | wc -l ``` Expected: `8` (carve-outs) and `158` (157 non-carve-out + prelude.ail-from-form-a.0). Wait — recount: pre-iter `.ail` = 58 (57 paired + prelude.ail-from-form-a.0). Task 2 added 99 more. Total = 157. Plus `prelude.ail` is the 58th from form-a.0, so 58 + 99 = 157 `.ail` files post-Task 8. The `prelude.ail` and `prelude.ail.json` coexist (dual-form per §C4 (b)). Expected `ls examples/*.ail | wc -l`: `157`. - [ ] **Step 3: Remove the `#[ignore]` on the carve-out inventory test** Edit `crates/ailang-core/tests/carve_out_inventory.rs`: ```rust // before: #[test] #[ignore = "GREEN-ratified by Task 8 (.ail.json deletion); RED until then"] fn examples_ail_json_inventory_matches_carve_outs() { ... } // after: #[test] fn examples_ail_json_inventory_matches_carve_outs() { ... } ``` - [ ] **Step 4: Run the carve-out inventory test** Run: ``` cargo test -p ailang-core --test carve_out_inventory ``` Expected: PASS. The eight files on disk exactly match the hardcoded `EXPECTED` list. - [ ] **Step 5: Final cargo test for Task 8** Run: ``` cargo test --workspace ``` Expected: PASS, 561 tests green. All consumers that pointed at `.ail.json` were migrated in Tasks 3-7; the only remaining `.ail.json`-loaders are the carve-out tests (which still find their carve-out fixtures). --- ### Task 9: Retire obsolete roundtrip tests + flip schema_coverage corpus **Files:** - Modify: `crates/ailang-surface/tests/round_trip.rs` (retire `print_then_parse_round_trips_every_fixture` lines 57-101; retire `every_ail_fixture_matches_its_json_counterpart` lines 120-204; retire `list_json_fixtures` if no other test uses it) - Modify: `crates/ail/tests/roundtrip_cli.rs` (retire `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture` lines 116-146; retire `list_json_fixtures` if no other test uses it) - Modify: `crates/ailang-core/tests/schema_coverage.rs:324-410` (flip corpus walker) - [ ] **Step 1: Retire `every_ail_fixture_matches_its_json_counterpart`** Delete lines 120-204 from `crates/ailang-surface/tests/round_trip.rs`. The property it pinned (no drift between two hand-authored forms) is no longer expressible: only one form is hand-authored post-iter. - [ ] **Step 2: Retire `print_then_parse_round_trips_every_fixture`** Delete lines 57-101 from `crates/ailang-surface/tests/round_trip.rs`. Its corpus shrinks to just the eight carve-outs, redundant with the new `parse_is_deterministic_over_every_ail_fixture` plus the corpus reduction. - [ ] **Step 3: Retire `list_json_fixtures` if no consumer remains** Check whether `list_json_fixtures` is called elsewhere in `round_trip.rs`: ``` grep -n "list_json_fixtures" crates/ailang-surface/tests/round_trip.rs ``` If only the deleted tests called it: delete lines 35-55 (`list_json_fixtures` helper). - [ ] **Step 4: Retire `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`** Delete lines 116-146 from `crates/ail/tests/roundtrip_cli.rs`. The new `cli_parse_then_render_then_parse_is_idempotent` (added in Task 1) replaces it with the corpus-flipped property. Check whether `list_json_fixtures` in roundtrip_cli.rs is still consumed: ``` grep -n "list_json_fixtures" crates/ail/tests/roundtrip_cli.rs ``` If unused, delete lines 31-46. - [ ] **Step 5: Flip schema_coverage.rs corpus from `.ail.json` to `.ail`** Edit `crates/ailang-core/tests/schema_coverage.rs:338` (the filter): ```rust // before: .filter(|n| n.ends_with(".ail.json")) // after: .filter(|n| n.ends_with(".ail")) ``` Edit the loader call in the test body (around line 359 per recon): ```rust // before: let m = ailang_core::load_module(&entry.path()).expect("load"); // after: let m = ailang_surface::load_module(&entry.path()).expect("load"); ``` Add `ailang-surface = { path = "../ailang-surface" }` to `crates/ailang-core/Cargo.toml` `[dev-dependencies]` if not already added by Task 5. - [ ] **Step 6: Run the surviving roundtrip tests** Run: ``` cargo test -p ailang-surface --test round_trip cargo test -p ail --test roundtrip_cli cargo test -p ailang-core --test schema_coverage cargo test -p ailang-core --test carve_out_inventory ``` Expected: each PASS. The surviving named tests at iter close: - `parse_is_deterministic_over_every_ail_fixture` (round_trip.rs) - `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture` (round_trip.rs, unchanged) - `cli_parse_then_render_then_parse_is_idempotent` (roundtrip_cli.rs) - `every_ast_variant_is_observed_in_the_fixture_corpus` (schema_coverage.rs, now over `.ail`) - `examples_ail_json_inventory_matches_carve_outs` (carve_out_inventory.rs) Five tests = five §C3 enforcement bullets after Task 10 rewrites DESIGN.md. - [ ] **Step 7: Final cargo test for Task 9** Run: ``` cargo test --workspace ``` Expected: PASS, 559 tests green. Task 9 retired 3 tests (`print_then_parse_*`, `every_ail_fixture_matches_*`, `cli_render_then_parse_*`); 561 - 3 = 558. Plus Task 1 added 3 (parse-determinism, CLI-pipeline-idempotency, carve-out-inventory). Plus Task 5 had net-zero count change. So expected ≈ 558 + 3 (T1) - 3 (T9) = 558 net. Adjust as actually observed. --- ### Task 10: §C3 DESIGN.md §"Roundtrip Invariant" restatement **Files:** - Modify: `docs/DESIGN.md:2027-2109` - [ ] **Step 1: Locate the section boundaries** The current §"Roundtrip Invariant" starts at line 2027 (`## Roundtrip Invariant`) and ends at line 2109 (just before `## Mangling scheme` at 2110). Sub-sections: - 2029-2048: Direction 1 / Direction 2 framing — replaced - 2050-2059: Float literals are inside the invariant — preserved - 2061-2094: Enforcement (the five named tests) — five-bullet list updated - 2096-2108: Why this is anchored at top level — preserved - [ ] **Step 2: Replace the Direction-framing prose (lines 2029-2048)** The current text frames the invariant as a two-way `.ail.json` ↔ `.ail` bijection. Replace with: ```markdown 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.** Eight `.ail.json`-only fixtures (seven subject-matter rejection tests + one compile-time-embed for the prelude) survive in the corpus by structural necessity. They participate in their own dedicated rejection-shape or embed-mechanism tests, not in the round-trip gate. 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. ``` - [ ] **Step 3: Update the §Enforcement bullets (lines 2061-2094)** Replace the five existing test-name bullets with the five surviving tests from Task 9 Step 6. Pattern per bullet (verbatim): ```markdown - `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 (corpus flipped from `.ail.json` to `.ail` at iter form-a.1). 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 eight named carve-out files exist under `examples/*.ail.json` at any commit. A new `.ail.json` or a missing carve-out fails the test. ``` - [ ] **Step 4: Preserve lines 2050-2059 (Floats) and 2096-2108 (Why top-level)** These two sub-sections remain valid post-migration. Float-bits-hex is form-agnostic; the top-level anchoring rationale is independent of the migration. Do not edit. - [ ] **Step 5: Verify the section is internally consistent** Read the rewritten §"Roundtrip Invariant" top-to-bottom. Confirm: - Numbered bullets in the prose (1-4) match the §Enforcement bullets (each property has a named test). - The "byte-isomorphism" claim is consistent with parse-determinism (it is — see §"Why"). - No reference to retired tests (`print_then_parse_round_trips_every_fixture`, `every_ail_fixture_matches_its_json_counterpart`, `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`) remains. - [ ] **Step 6: Final cargo test for Task 10** Run: ``` cargo test --workspace ``` Expected: PASS. (Doc edits should not affect tests, but confirm.) --- ### Task 11: §A4 doctrine edits — CLAUDE.md + DESIGN.md §"What this Decision..." **Files:** - Modify: `CLAUDE.md:5-6` - Modify: `docs/DESIGN.md:465-466` - [ ] **Step 1: Replace the CLAUDE.md doctrine sentence** Edit `CLAUDE.md` around line 5-6. The current sentence: ```markdown - **Machine readability over human readability.** Source of truth is structured data (`.ail.json`), not text. ``` Replace with (verbatim from spec §A4): ```markdown - **Machine readability over human readability.** The canonical, hashable, content-addressed form is structured data (`.ail.json`); the authoring projection is Form A (`.ail`). Authors write `.ail`; the build derives the JSON-AST in-process via `ailang_surface::parse`, gated by the round-trip invariant. The two forms are byte-isomorphic — picking either does not change the identity of the module. ``` - [ ] **Step 2: Replace the DESIGN.md doctrine sentence** Edit `docs/DESIGN.md` around line 465-466. The current text: ```markdown - It does not promote form (A) to the source of truth. Form (A) is one projection; the JSON-AST stays canonical. ``` Replace the first sentence; preserve the second. Final form (verbatim from spec §A4): ```markdown - It does not change which form is canonical: the JSON-AST remains the hashable, content-addressed representation, and all hashing, content-addressing, and cross-module references flow through it unchanged. What did change in 2026-05-13 (this milestone) is which form is authored: authors now write Form A; the JSON-AST is materialised in-process by callers that need it. The two forms are byte-isomorphic by the round-trip invariant. Form (A) is one projection; the JSON-AST stays canonical. ``` - [ ] **Step 3: Final cargo test for Task 11** Run: ``` cargo test --workspace ``` Expected: PASS. Doc edits do not affect tests. --- ### Task 12: Milestone close — WhatsNew + roadmap strike + final verifications **Files:** - Modify: `docs/WhatsNew.md` (append) - Modify: `docs/roadmap.md` (strike `[milestone] Form-A as the default authoring surface` entry) - [ ] **Step 1: Append the WhatsNew milestone-close entry** Per `/boss` "Done-state notifications: WhatsNew.md": user-facing language, no internals, lead with the change. Append at the end of `docs/WhatsNew.md`: ```markdown ## 2026-05-13 — Form A is now the authoring surface `.ail` is now the form you read, edit, and check in. The JSON-AST counterpart is still the canonical hashable representation, but it is no longer something you look at or maintain by hand — the tools derive it from `.ail` whenever they need it. About a hundred example programs that previously existed as JSON-AST were converted to `.ail` and the JSON files were deleted; only eight stay as `.ail.json` (seven negative-test fixtures whose subject matter cannot be expressed in `.ail`, plus the prelude, which is loaded at compile time and depends on internal plumbing that lands in a later milestone). If you were used to opening a `.ail.json` file to see what a program looks like, you now open the `.ail` sibling instead. ``` (Wording per `/boss` editorial rules: no crates, no iter codes, no agent names, lead with the change, factual tone.) - [ ] **Step 2: Strike the `[milestone] Form-A as the default authoring surface` roadmap entry** Edit `docs/roadmap.md`. The entry spans approximately lines 96-145 (post-recent-edits; the prelude todo was already struck in the spec-amendment commit). Change the leading checkbox from `- [ ]` to `- [x]` and prepend `(Closed 2026-05-13 by iter form-a.1.)` to the description. The entry stays in the file at this stage; it gets removed in the next roadmap cleanup pass per the CLAUDE.md convention "finished items get checked off, then removed". ```markdown - [x] **\[milestone\]** Form-A as the default authoring surface for examples and docs. (Closed 2026-05-13 by iter form-a.1.) Render every `examples/*.ail.json` to its `.ail` sibling via `ail render`, ... ``` - [ ] **Step 3: Final `cargo test --workspace`** Run: ``` cargo test --workspace ``` Expected: PASS. Test count is ~558 (Task 9 final tally). Confirm zero failures. - [ ] **Step 4: Final bench verification** Run: ``` python3 bench/compile_check.py python3 bench/cross_lang.py ``` Expected: both exit 0 (per spec §Acceptance criteria #5). If either exits non-zero, inspect for a migration-induced regression vs. the noise envelope (the audit-cma onward 9-audit lineage); a new regression class triggers brainstorm-level escalation per §Acceptance #6. - [ ] **Step 5: Final inventory check** Run: ``` ls examples/*.ail.json | wc -l ls examples/*.ail | wc -l ``` Expected: `8` and `157` respectively. Verify the eight `.ail.json` match the carve-out list: ``` ls examples/*.ail.json ``` Expected output (in alphabetical order): ``` examples/broken_unbound.ail.json examples/prelude.ail.json examples/test_22b2_invalid_superclass_param.ail.json examples/test_22b2_kind_mismatch.ail.json examples/test_22b2_unbound_constraint_var.ail.json examples/test_ct1_bad_qualifier.ail.json examples/test_ct1_bare_xmod_rejected.ail.json examples/test_ct1_qualified_class_rejected.ail.json ``` If any deviation: investigate before declaring iter close. - [ ] **Step 6: Per-iter journal entry** The implement skill's orchestrator-agent will write the per-iter journal. The Boss appends the INDEX line at commit time, per the standard form-a.0 pattern. --- **Iter form-a.1 closes structurally when Task 12 Step 5 passes.** The milestone `[Form-A as the default authoring surface]` is then done; `audit-form-a` runs as the next dispatch per the milestone- close pipeline. The carve-out (b) `prelude.ail.json` motivates the follow-up milestone `[Prelude embed: Form-A as compile-time source]` queued in `docs/roadmap.md`.