# 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\``).
