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.
16 KiB
cma-revival.1 — Cross-model harness corpus revival — Implementation Plan
Parent spec:
docs/specs/0069-cma-corpus-revival.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill 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 exceptparam_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, soownis 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:
"type": {
"k": "fn",
"params": [],
"ret": { "k": "con", "name": "Int" },
"effects": []
},
to:
"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(existingborrow, addret_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
.ailand 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.ailand 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.ailand 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: aloopdeclares one or more typed binders with initial values; the body re-enters the loop viarecurwith new positional values for those binders, or falls through with a final value;recuris 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 typeTby callingT'snewdefinition; 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
VariantTagenum (:22-64)
After TermReuseAs, insert:
TermLoop,
TermRecur,
TermNew,
Remove the line ParamModeImplicit, (in the // ParamMode group).
- Step 2: Update
EXPECTED_VARIANTS(:70-105)
After VariantTag::TermReuseAs, insert:
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
Implicitarm invisit_param_mode(:298-310)
Delete:
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_termarms (after theReuseAsarm, :220-224)
Immediately before the closing } of the match t in visit_term, add:
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 passesail check+ail build+ run — give its fn types theparam_modes/ret_modefields per Task 1's rule. (Leavejson.t3_main_prints.1.contentas 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.*.contentstays failing to the turn limit (itsINFrow); 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``).