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.
52 KiB
Iter form-a.1 — Big-bang Migration + Milestone Close — Implementation Plan
Parent spec:
docs/specs/0025-form-a-default-authoring.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto 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>.ailfiles (rendered from their.ail.jsoncounterparts; recon-verified inventory). - Create:
crates/ailang-core/tests/hash_pin.rs— relocated#[cfg(test)] mod testsfromcrates/ailang-core/src/hash.rs:110-346(usesailang_surfacedev-dep). - Create:
crates/ailang-core/tests/workspace_pin.rs— relocated non-carve-out#[cfg(test)] mod testsfromcrates/ailang-core/src/workspace.rs(usesailang_surfacedev-dep). - Create:
crates/ailang-codegen/tests/eq_primitives_pin.rs— relocated#[cfg(test)] mod testsfromcrates/ailang-codegen/src/lib.rs:3723-3782(usesailang_surfacedev-dep). - Modify:
crates/ailang-surface/tests/round_trip.rs— addparse_is_deterministic_over_every_ail_fixture; later retireprint_then_parse_round_trips_every_fixtureandevery_ail_fixture_matches_its_json_counterpart. - Modify:
crates/ail/tests/roundtrip_cli.rs— addcli_parse_then_render_then_parse_is_idempotent; retirecli_render_then_parse_preserves_canonical_bytes_on_every_fixture. - Modify:
crates/ailang-core/tests/schema_coverage.rs:324-410— flip corpus walker from.ail.jsonto.ail. - Modify: 14 Group-A test files (
ailang_core::load_workspaceconsumers) — switch toailang_surface::load_workspace+.ailpaths. - 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 derivesum.ail.jsonon-the-fly viaail parse. - Delete (cargo-internal
#[cfg(test)] mod testsblock bodies, replaced by integration-test counterparts above):crates/ailang-core/src/hash.rs:110-346,crates/ailang-core/src/workspace.rsnon-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.jsonto.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 surfaceentry. - Delete: 156
examples/<stem>.ail.jsonfiles (the non-carve-outs; 99 fresh-rendered fixtures + 57 fixtures that already had.ailsiblings 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_fixturein round_trip.rs
Append after the existing parse_then_print_then_parse_is_idempotent_on_every_ail_fixture test (around line 278):
/// 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_idempotentin roundtrip_cli.rs
Append after the existing cli_render_then_parse_preserves_canonical_bytes_on_every_fixture test (around line 147):
/// 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.rswith the eight-file list
Write crates/ailang-core/tests/carve_out_inventory.rs:
//! 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:
#[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>.ailfiles (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
.ailfiles
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:
- Path string:
"<stem>.ail.json"→"<stem>.ail". - 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:
// 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_surfaceis 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 ofbuild_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(theordering_match.ail.json:169non-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:
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.rsunit 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:
- Path string:
"<stem>.ail.json"→"<stem>.ail". - 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.rsunit 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, loadstest_22b2_kind_mismatch.ail.json)superclass_with_wrong_param_fires_invalid_superclass_param(line ~1968, loadstest_22b2_invalid_superclass_param.ail.json)unbound_constraint_var_in_class_def_fires(line ~2024, loadstest_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.jsonconsumer — Boss-decided to relocate since it uses onlyload_workspacepublic 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.rsunit 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:
// 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:
# 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:
# 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
# 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:
# 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:
diff_detects_changed_def— loadssum.ail.json, mutates the JSON to producesum_v2.ail.json, runsail diffon the pair. (This was T7's originally-named test.)borrow_own_demo_modes_are_metadata_only— raw-readsborrow_own_demo.ail.jsonfor substring assertions about mode annotations.reuse_as_demo_under_rc_uses_inplace_rewrite— raw-readsreuse_as_demo.ail.jsonfor substring assertions about reuse-as encoding.render_parse_round_trip_canonical— byte-roundtrips a chosen.ail.jsonthroughail render | ail parsefor byte-equal assertion. (Note:cli_parse_then_render_then_parse_is_idempotentfrom T1 already pins this property across the whole.ailcorpus; this test may simply be retired.)ail_run_accepts_ail_source_with_same_stdout_as_ail_json— dual-form smoke: runs a fixture as both.ailand.ail.jsonand 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.jsonis 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.jsonfilename 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:
// 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) andreuse_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.
// 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.jsonread with a parse-from-.ail
Change:
// 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.jsonfiles (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:
// 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(retireprint_then_parse_round_trips_every_fixturelines 57-101; retireevery_ail_fixture_matches_its_json_counterpartlines 120-204; retirelist_json_fixturesif no other test uses it) -
Modify:
crates/ail/tests/roundtrip_cli.rs(retirecli_render_then_parse_preserves_canonical_bytes_on_every_fixturelines 116-146; retirelist_json_fixturesif 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_fixturesif 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.jsonto.ail
Edit crates/ailang-core/tests/schema_coverage.rs:338 (the filter):
// 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):
// 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:
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):
- `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:
- **Machine readability over human readability.** Source of truth is
structured data (`.ail.json`), not text.
Replace with (verbatim from spec §A4):
- **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:
- 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):
- 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 surfaceentry) -
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:
## 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 surfaceroadmap 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".
- [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.