# 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/<stem>.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/<stem>.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> | ail render | ail parse` is byte-identical
/// to direct `ail parse <ail>`. 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<String> = 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/<stem>.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/<stem>.ail.json` path AND uses `ailang_core::load_workspace`. Two coupled edits per call site:

1. Path string: `"<stem>.ail.json"` → `"<stem>.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("<stem>.ail.json")
// to:
build_and_run("<stem>.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: `"<stem>.ail.json"` → `"<stem>.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 (`<fixture>.ail.json` → `<fixture>.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/<name>.ail.json")` line and replace with a `ail parse examples/<name>.ail -o <tempfile>` + 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`.
