diff --git a/docs/plans/0127-cma-revival.1.md b/docs/plans/0127-cma-revival.1.md new file mode 100644 index 0000000..e9183ca --- /dev/null +++ b/docs/plans/0127-cma-revival.1.md @@ -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\``).