plan: cma-revival.1 cross-model harness corpus revival (refs #68)

Bite-sized plan for spec 0069. Six tasks: complete schema fields across the
13 examples + 4 references (own default, existing borrow preserved); rewrite
param_modes_all + spec.md section 4 (own Int / borrow boxed-ADT, value/boxed
rule); add loop_sum + new_rawbuf examples; spec_completeness variant coverage
+ Intrinsic allowlist; regenerate rendered + green render suite; migrate
mock_full_run fixture + green harness suite.

All verbatim .ail/.ail.json bodies parse-gate verified this session. The gate
caught a real language drift the spec had to absorb: borrow over an unboxed
value type (Int) is now a borrow-over-value error, so param_modes_all borrows
over a boxed ADT instead.
This commit is contained in:
2026-06-02 16:32:31 +02:00
parent 74849160b7
commit 426ce88d2a
+394
View File
@@ -0,0 +1,394 @@
# cma-revival.1 — Cross-model harness corpus revival — Implementation Plan
> **Parent spec:** `docs/specs/0069-cma-corpus-revival.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to
> run this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** Bring the schema-dead corpus of the cross-model authoring-form
harness (`experiments/2026-05-12-cross-model-authoring/`) up to today's
language and get both crates' `cargo test` suites green in mock mode — no
live IONOS call.
**Architecture:** The corpus `.ail.json` files lost validity when the
implicit-cutover made `param_modes`/`ret_mode` mandatory. Most files already
carry their `param_modes`; the repair completes the missing fields in place
(preserving deliberate `borrow` annotations), adds two new author-facing
examples (`loop`/`recur`, `new`) plus two `spec.md` sections, removes the
deleted `ParamMode::Implicit` from the completeness test while allowlisting
the non-authorable `Term::Intrinsic` out, and migrates the mock fixture's
embedded programs so the harness score assertions still hold.
**Tech Stack:** `experiments/2026-05-12-cross-model-authoring/` (render +
harness Cargo crates outside the root workspace, built via `--manifest-path`);
`ail check`/`ail parse` from `target/debug/ail` as the per-file oracle.
**Files this plan creates or modifies:**
- Modify: `experiments/.../master/examples/*.ail.json` (13) — complete schema fields
- Modify: `experiments/.../master/tasks/*.reference.ail.json` (4) — complete schema fields
- Modify: `experiments/.../master/examples/param_modes_all.ail.json` — rewrite (own/borrow only)
- Create: `experiments/.../master/examples/loop_sum.ail.json` — Loop/Recur example
- Create: `experiments/.../master/examples/new_rawbuf.ail.json` — New example
- Modify: `experiments/.../master/spec.md` — §4 rewrite + two new sections
- Modify: `experiments/.../render/tests/spec_completeness.rs` — variant coverage + Intrinsic allowlist
- Modify: `experiments/.../rendered/json.md`, `experiments/.../rendered/ail.md` — regenerated
- Modify: `experiments/.../harness/tests/fixtures/mock_full_run.json` — migrate embedded programs
Throughout, `AIL` = `target/debug/ail` (built, mtime 2026-06-02) and
`XP` = `experiments/2026-05-12-cross-model-authoring`.
---
### Task 1: Complete schema fields in the 13 examples + 4 reference solutions
**Files:**
- Modify: `$XP/master/examples/*.ail.json` (all 13 except `param_modes_all`, which is Task 2)
- Modify: `$XP/master/tasks/*.reference.ail.json` (all 4)
The transformation, applied to every `"type": { "k": "fn", … }` object in
each file: **preserve any existing `param_modes` verbatim**, then ensure both
mode fields are present:
- `param_modes`: if absent, set `[]` for a 0-parameter fn, else one `"own"`
per parameter (the natural consume default). If present, leave unchanged.
- `ret_mode`: if absent, set `"own"` (`borrow`-return is forbidden by contract
0015, so `own` is the only legal value).
Field placement: `param_modes` directly after `params`, `ret_mode` directly
after `ret`, matching the canonical key order in the shipped `examples/`
fixtures.
- [ ] **Step 1: Worked example A — `fn_returns_int.ail.json` (0-param)**
The `answer` fn's `type` block changes from:
```json
"type": {
"k": "fn",
"params": [],
"ret": { "k": "con", "name": "Int" },
"effects": []
},
```
to:
```json
"type": {
"k": "fn",
"params": [],
"param_modes": [],
"ret": { "k": "con", "name": "Int" },
"ret_mode": "own",
"effects": []
},
```
- [ ] **Step 2: Worked example B — `data_with_match.ail.json` (existing `borrow`, add `ret_mode`)**
Both fns already carry `"param_modes": ["borrow"]` over `List`. `List` is a
boxed ADT, so `borrow` is legal here — leave it untouched; add `"ret_mode":
"own"` directly after each `ret`. Do NOT rewrite `borrow` to `own`. (This is
the only legal `borrow` in the corpus; the illegal `(borrow Int)` lived only
in `param_modes_all`, handled in Task 2. `borrow` over an unboxed value type
is a `borrow-over-value` error — `ail check` in Step 4 catches any such slip.)
- [ ] **Step 3: Apply the transformation to all remaining corpus files**
Files needing only `ret_mode` (already have `param_modes`): `data_simple.ail.json`.
Files needing `param_modes` + `ret_mode`: `bool_str`, `fn_calls_prelude`,
`fn_returns_int`, `fn_with_do_seq`, `fn_with_lambda`, `forall_polymorphic`,
`match_literal_pattern`, and the 4 `tasks/*.reference.ail.json`. Files with no
fn defs (no change to a fn type, but still verify they load):
`class_def`, `floats`, `instance_def`. For each value-taking fn, default each
parameter to `"own"`.
- [ ] **Step 4: Verify every file checks green**
Run:
```
for f in $XP/master/examples/*.ail.json $XP/master/tasks/*.reference.ail.json; do
target/debug/ail check "$f" >/dev/null 2>&1 && echo "OK $f" || echo "FAIL $f"
done
```
Expected: every line `OK` (param_modes_all may FAIL here — it is rewritten in
Task 2; re-run after Task 2). A `FAIL` on a value-taking fn means a wrong mode
guess — flip that parameter `own``borrow` per the check error and re-run.
---
### Task 2: Rewrite `param_modes_all` and spec.md §4 (Implicit is deleted)
**Files:**
- Modify: `$XP/master/examples/param_modes_all.ail.json`
- Modify: `$XP/master/spec.md` (§4 "Mode annotations", lines ~134-193)
**Why not "two Int functions":** the typechecker rejects `(borrow Int)` with
`borrow-over-value` — unboxed value types (`Int`/`Bool`/`Float`/`Unit`) are
always `own`; `borrow` is legal only over a boxed type (an ADT). So the
example carries a small boxed ADT and borrows over *that*. The `.ail` below is
verified to parse + check green (parse-gate, 2026-06-02).
- [ ] **Step 1: Author the rewritten example as `.ail` and canonicalise**
Write `/tmp/param_modes_all.ail` (module name MUST be `param_modes_all` — the
checker enforces module-name = filename):
```
(module param_modes_all
(data List
(ctor Nil)
(ctor Cons (con Int) (con List)))
(fn f_own_value
(doc "(own Int) — unboxed value types (Int/Bool/Float/Unit) are always own; borrow over them is a borrow-over-value error.")
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params x)
(body x))
(fn f_borrow_boxed
(doc "(borrow List) — boxed types may be borrowed; the callee reads without consuming.")
(type (fn-type (params (borrow (con List))) (ret (own (con Int)))))
(params xs)
(body
(match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t) (app + 1 (app f_borrow_boxed t)))))))
```
Run: `target/debug/ail parse /tmp/param_modes_all.ail > $XP/master/examples/param_modes_all.ail.json`
- [ ] **Step 2: Verify it checks green**
Run: `target/debug/ail check $XP/master/examples/param_modes_all.ail.json`
Expected: exit 0. (Canonical output carries `f_own_value` param_modes `["own"]`,
`f_borrow_boxed` param_modes `["borrow"]`, both `ret_mode "own"`.)
- [ ] **Step 3: Rewrite spec.md §4 prose**
In `$XP/master/spec.md` §4 "Mode annotations", remove every mention of a
defaulted/omitted/`implicit` mode. Present the exhaustive rule: every
`Type::Fn` parameter carries exactly one of `own` (callee consumes) or
`borrow` (callee may not consume); `param_modes` is a list with one entry per
parameter; `ret_mode` is always `own`. **State the value/boxed rule:** `own`
is mandatory for unboxed value types (`Int`/`Bool`/`Float`/`Unit`) — `borrow`
over them is a `borrow-over-value` error; `borrow` is available only for boxed
types (ADTs), as the `f_borrow_boxed`/`List` example shows. Leave the
`{example: param_modes_all}` marker in place.
---
### Task 3: Add the two new author-facing examples + spec.md sections
**Files:**
- Create: `$XP/master/examples/loop_sum.ail.json`
- Create: `$XP/master/examples/new_rawbuf.ail.json`
- Modify: `$XP/master/spec.md` (two new sections)
The module name in each `.ail` MUST equal its target filename stem
(`loop_sum`, `new_rawbuf`) — the checker enforces module-name = filename. Both
`.ail` bodies below are verified parse + check green (parse-gate, 2026-06-02).
- [ ] **Step 1: Author `loop_sum.ail` and canonicalise**
Write `/tmp/loop_sum.ail`:
```
(module loop_sum
(fn sum_to
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params n)
(body
(loop (acc (con Int) 0) (i (con Int) 1)
(if (app gt i n)
acc
(recur (app + acc i) (app + i 1)))))))
```
Run: `target/debug/ail parse /tmp/loop_sum.ail > $XP/master/examples/loop_sum.ail.json`
Then: `target/debug/ail check $XP/master/examples/loop_sum.ail.json`
Expected: parse exit 0, check exit 0.
- [ ] **Step 2: Author `new_rawbuf.ail` and canonicalise**
Write `/tmp/new_rawbuf.ail`:
```
(module new_rawbuf
(fn main
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params)
(body
(let b (new RawBuf (con Int) 3)
(app print (app RawBuf.size b))))))
```
Run: `target/debug/ail parse /tmp/new_rawbuf.ail > $XP/master/examples/new_rawbuf.ail.json`
Then: `target/debug/ail check $XP/master/examples/new_rawbuf.ail.json`
Expected: parse exit 0, check exit 0.
- [ ] **Step 3: Add two spec.md sections with example markers**
In `$XP/master/spec.md`, after the existing function/ADT material and before
§"The prelude", add two sections:
- **§ Iteration (`loop` / `recur`)** — prose: a `loop` declares one or more
typed binders with initial values; the body re-enters the loop via `recur`
with new positional values for those binders, or falls through with a final
value; `recur` is valid only in tail position. Add a `{form-only: json}`
block, a `{form-only: ail}` block, and the marker `{example: loop_sum}`.
- **§ Kernel-extension types (`new`)** — prose: `(new T arg+)` constructs a
value of an extension type `T` by calling `T`'s `new` definition; args are
types or values per the def's signature. Add a `{form-only: json}` block, a
`{form-only: ail}` block, and the marker `{example: new_rawbuf}`.
Keep `{form-only: …}` token volume roughly balanced across the two cohorts
(the `token_balance` test allows ±5%).
---
### Task 4: Update `spec_completeness.rs` — variant coverage + Intrinsic allowlist
**Files:**
- Modify: `$XP/render/tests/spec_completeness.rs`
- [ ] **Step 1: Update the `VariantTag` enum (:22-64)**
After ` TermReuseAs,` insert:
```rust
TermLoop,
TermRecur,
TermNew,
```
Remove the line ` ParamModeImplicit,` (in the `// ParamMode` group).
- [ ] **Step 2: Update `EXPECTED_VARIANTS` (:70-105)**
After ` VariantTag::TermReuseAs,` insert:
```rust
VariantTag::TermLoop,
VariantTag::TermRecur,
VariantTag::TermNew,
```
Remove the line ` VariantTag::ParamModeImplicit,`. Do NOT add a tag for
`Intrinsic` — it is allowlisted out (Step 4).
- [ ] **Step 3: Remove the `Implicit` arm in `visit_param_mode` (:298-310)**
Delete:
```rust
ParamMode::Implicit => {
observed.insert(VariantTag::ParamModeImplicit);
}
```
The remaining `Own`/`Borrow` arms make the match exhaustive against the current
two-variant `ParamMode`.
- [ ] **Step 4: Add the new `visit_term` arms (after the `ReuseAs` arm, :220-224)**
Immediately before the closing `}` of the `match t` in `visit_term`, add:
```rust
Term::Loop { binders, body } => {
observed.insert(VariantTag::TermLoop);
for b in binders {
visit_type(&b.ty, observed);
visit_term(&b.init, observed);
}
visit_term(body, observed);
}
Term::Recur { args } => {
observed.insert(VariantTag::TermRecur);
for a in args {
visit_term(a, observed);
}
}
Term::New { type_name: _, args } => {
observed.insert(VariantTag::TermNew);
for a in args {
match a {
NewArg::Type(t) => visit_type(t, observed),
NewArg::Value(v) => visit_term(v, observed),
}
}
}
// `Intrinsic` is the compiler-supplied body marker — non-authorable
// (absent from the parser's term dispatch; typecheck enforces
// kernel-tier-only via IntrinsicOutsideKernelTier). A foreign-author
// mini-spec must not show it and no master example can legally
// contain it, so it is allowlisted OUT of completeness: no VariantTag,
// no EXPECTED entry, a no-op arm here so the match compiles without
// claiming coverage.
Term::Intrinsic => {}
```
Add `NewArg` to the `ailang_core::ast::{…}` import line at the top of the file
if it is not already imported (the build will name it if missing).
- [ ] **Step 5: Compile-gate the test crate**
Run: `cargo test --manifest-path $XP/render/Cargo.toml --no-run`
Expected: compiles with 0 errors (the `Implicit` unknown-variant error and the
non-exhaustive `visit_term` match are both resolved). If the build names a
wrong field (e.g. `LoopBinder.init`), correct it per the compiler message.
---
### Task 5: Regenerate `rendered/` and green the render suite
**Files:**
- Modify: `$XP/rendered/json.md`, `$XP/rendered/ail.md`
- [ ] **Step 1: Run the renderer**
Run:
```
cargo run --manifest-path $XP/render/Cargo.toml -- \
--master $XP/master --rendered $XP/rendered
```
Expected: exit 0, no load crash (it crashed before on the first example's
missing `param_modes`).
- [ ] **Step 2: Run the full render test suite**
Run: `cargo test --manifest-path $XP/render/Cargo.toml`
Expected: PASS — `example_roundtrip` (every example loads, prints to AIL,
reparses to the same canonical bytes), `spec_completeness` (every
`EXPECTED_VARIANTS` tag observed, including `TermLoop`/`TermRecur`/`TermNew`),
`token_balance` (±5%), `splitter_unit`. If `spec_completeness` reports a tag
not exercised, the corresponding new example or spec section is missing its
construct.
---
### Task 6: Migrate `mock_full_run.json` and green the harness suite
**Files:**
- Modify: `$XP/harness/tests/fixtures/mock_full_run.json`
The fixture maps `cohort → task → turn → {content, usage}`, where `content` is
a string-embedded `.ail.json` program (simulated model output). All are
legacy-schema and now fail `ail check`. Migrate the embedded programs to the
current schema, **preserving the `usage` fields byte-for-byte** (budget_abort
depends on them).
- [ ] **Step 1: Migrate the programs that must reach green**
The hard assertions in `mock_full_run.rs` are `json,t3_main_prints,false,2`
and `ail,t1_add_three,false,INF`. So:
- `json.t3_main_prints.2.content` (turn 2) MUST become a valid current-schema
program that passes `ail check` + `ail build` + run — give its fn types the
`param_modes`/`ret_mode` fields per Task 1's rule. (Leave
`json.t3_main_prints.1.content` as the intentional `"defs":[]` broken first
attempt.)
- Migrate the other green-intended embedded programs (the first-attempt-green
cohort×task cells) the same way, so the fixture is an honest simulation
rather than a uniform schema-fail.
- `ail.t1_add_three.*.content` stays failing to the turn limit (its `INF` row);
let its miss be content-driven, not merely a schema artefact.
- [ ] **Step 2: Verify each migrated embedded program checks green standalone**
For each `content` you migrated to be green, extract it to a temp file and run
`target/debug/ail check /tmp/x.ail.json` — expected exit 0. (The harness will
also `ail build` it; a green check that fails build means the program is
schema-valid but not runnable — fix the body.)
- [ ] **Step 3: Run the full harness test suite**
Run: `cargo test --manifest-path $XP/harness/Cargo.toml`
Expected: PASS — `mock_full_run` (8 rows; `json,t3_main_prints,false,2` and
`ail,t1_add_three,false,INF` both present; `RUN_STATUS=ok`), `budget_abort`
(`RUN_STATUS=budget_exceeded`, ≥1 `budget_abort` row), `verify_references`
(every `master/tasks/*.reference.ail.json` drives green through real ail+clang),
`strip_locations` (5 .stderr fixtures, including `missing field \`type\``).