Files
AILang/docs/plans/0060-iter-form-a.1.md
T
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

52 KiB
Raw Blame History

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):

/// 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):

/// 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:

//! 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>.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:

// 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:

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:

// 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:

  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:

// 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.

// 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:

// 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:

// 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):

// 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 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:

## 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".

- [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.