Files
AILang/docs/plans/0127-cma-revival.1.md
T
Brummel 426ce88d2a 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.
2026-06-02 16:32:31 +02:00

395 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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\``).