Files
AILang/docs/plans/0060-iter-form-a.1.md
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

1268 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.