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