Files
AILang/docs/plans/0073-iter-it.3.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

26 KiB
Raw Blame History

it.3 — retire tail-app/tail-do + corpus migration — Implementation Plan

Parent spec: docs/specs/0031-iteration-discipline.md (scope-corrected at 2018178 + it.3-recon-corrected at 10a0595)

For agentic workers: REQUIRED SUB-SKILL: use skills/implement to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Delete the marked-tail-call concept entirely (Term::App.tail/Term::Do.tail, both surface keywords, the musttail lowering, verify_tail_positions' tail-app role, TailCallNotInTailPosition, the 18g.1 husk-dec, the it.2 transitional grandfather + no-ADT-candidate skip, Decision 8 → tombstone) and migrate the full corpus non-structural-recursion set to (loop …)/recur (or plain structural calls), every fixture byte-identical in observable behaviour to before.

Architecture: Destructive + migration. The safe order is oracle → migrate → remove-exemptions → remove-machinery → docs/drift → accept. Migration before removal keeps the round-trip invariant green throughout (a migrated .ail has no (tail-app …) so it parses even while the field still exists). Removing the it.2 exemptions before removing the tail machinery is the correctness proof that migration was complete (if any fixture still needed the grandfather, that task goes RED — a migration bug, fixed in the migration task, never by re-adding the exemption). The one genuinely non-mechanical risk is the spec's §Codegen-rework contingency: deleting the 18g.1 husk-dec is RC-safe only if it.1 loop codegen drops superseded owned binders across recur; this is a RED-gated step with a named remediation (Task 5b).

Tech Stack: ailang-core (ast/serde), ailang-surface (parse/print), ailang-prose, ailang-check (verify_tail_positions, the it.2 exemptions, CheckError), ailang-codegen (musttail, 18g.1 husk-dec, loop-binder RC), DESIGN.md, crates/ailang-core/specs/form_a.md, the drift/spec/ schema-coverage test crates, ~25+ examples/*.ail fixtures.


Design decisions (Boss-resolved; not implementer judgement calls)

  • DD-1 — oracle = existing tests first, captured snapshots only for the gap. Many migration-set fixtures already have an e2e/round-trip/bench assertion (authoritative, already in cargo test --workspace). The byte-identical-stdout requirement is satisfied by: (a) those existing tests staying green across migration; (b) for a fixture with a main but no dedicated stdout test, a snapshot captured in Task 1 to bench/it3-oracle/<stem>.out and diffed post-migration; (c) for a check-only fixture (no main), the oracle is ail check exit status + diagnostic-code list, captured the same way. No fixture is migrated without an oracle.
  • DD-2 — migrate before remove. The Term::App.tail field stays present through Tasks 14; only Task 5 deletes it. This keeps parse/print/serde/round-trip compiling and green while fixtures are rewritten. The field is simply unused by migrated fixtures.
  • DD-3 — exemption-removal is the migration-completeness gate. Task 4 removes the it.2 tail==false grandfather AND the no-ADT-candidate skip. Its green cargo test --workspace is the proof that Task 3's migration left no fixture depending on either exemption. A RED here is a Task-3 migration defect to fix in Task 3, never by softening Task 4.
  • DD-4 — the 18g.1 / RC-RSS contingency (spec §Codegen-rework). Task 5 deletes the 18g.1 pre-tail-call husk-dec (match_lower.rs:658736) — field-forced, it cannot compile once Term::App.tail is gone. RC-safety is not assumed: Task 5 re-runs the RC-RSS bench pins (18f.2 + the RC-RSS suite) as a hard gate. If RED → Task 5b: add owned-loop-binder-drop-on-recur to it.1 loop codegen (the loop-side of the seam; in scope for it.3 per the corrected spec). Keeping 18g.1 is not an option (it cannot survive the field removal).
  • DD-5 — is_false survives. Recon: is_false (ast.rs:908916) is shared with WorkspaceDef.drop_iterative (ast.rs:168). Delete only the two tail-field skip_serializing_if="is_false" uses; keep the helper; re-scope its doc-comment to drop_iterative.

Recon line numbers (agent ad6bb077) are HEAD-at-recon. Drive every edit off grep/cargo build E0004/unused, not the literal numbers (memory: recon misindexing recurs; symbols authoritative).


Files this plan creates or modifies

Create:

  • bench/it3-oracle/MANIFEST.tsv + bench/it3-oracle/<stem>.out — pre-migration behavioural oracle (Task 1; deleted in Task 6 after it has served its purpose, or kept under bench/ if cheap — Boss call at commit)
  • Test: crates/ailang-check/tests/no_tail_grandfather_pin.rs — asserts a non-structural non-tail recursion now hard-fails non-structural-recursion (the exemption is gone)

Modify (HEAD anchors from recon ad6bb077; verify by symbol):

  • crates/ailang-core/src/ast.rs:402408 (Term::App.tail), :442447 (Term::Do.tail), :908916 (is_false doc only), :9741006 (serde test ctors dropping tail:)
  • crates/ailang-surface/src/parse.rs:4055,1203,1207,12241227, 12741317,13681395 (EBNF, dispatch, keyword list, parse_tail_app/parse_tail_do, parse_app_body/parse_do_body tail param)
  • crates/ailang-surface/src/print.rs:417422,464466
  • crates/ailang-prose/src/lib.rs:573597,655658,782784, 12031229 (+ delete tests :14861494,2050,2177)
  • crates/ailang-check/src/lib.rs:558562 (TailCallNotInTailPosition variant), :781 (code() arm), :27062817 (verify_tail_positions tail-app arms :27092727; KEEP Loop :28032808/Recur :2815 + verify_loop_body:28292869), :30263037 (grandfather if !*tail :3027), :31973199 (no-ADT-candidate skip)
  • crates/ailang-codegen/src/lib.rs:2120,23972432,24452513, 26152654 (musttail in lower_app/emit_call/emit_indirect_call/ lower_effect_op), crates/ailang-codegen/src/match_lower.rs:658736 (18g.1 husk-dec — delete)
  • docs/DESIGN.md: Decision 8 :709747+ (→ tombstone), EBNF :552565 (tail-app-term/tail-do-term), §Data-model :23572359,23732375,24522467 (tail jsonc + transitional paragraph). Decision 3 :163176 UNTOUCHED (it.2 payload).
  • crates/ailang-core/specs/form_a.md:268,270,295298,378381, 405407,508539 (tail grammar + §4 tail-rec exhibit)
  • crates/ailang-core/tests/design_schema_drift.rs:5360,8794 (drop tail:), spec_drift.rs:3037,6469 + delete spec_mentions_tail_variants:331337, schema_coverage.rs (no tail anchor — verify only)
  • examples/*.ail — the full migration set (enumerated in Task 1)
  • crates/ail/tests/e2e.rs + any IR-snapshot — migrated/retired per Task 1's fallout map

Task 1: Pre-migration oracle + migration-set enumeration

Files: bench/it3-oracle/* (create), no code change.

  • Step 1.1: Build the current ail once.

Run: cargo build -p ail 2>&1 | tail -1 Expected: clean build (HEAD = 10a0595, tail-app fully present).

  • Step 1.2: Class-(a) — every (tail-app|tail-do) fixture.

Run: grep -rl -E '\(tail-app|\(tail-do' examples/ | sort | tee /tmp/it3_class_a.txt | wc -l Expected: 23 files (recon-confirmed).

  • Step 1.3: Class-(b) — no-ADT-candidate counter recursions.

Read crates/ailang-check/src/lib.rs verify_structural_recursion

  • the grandfather (:3027 if !*tail) + the no-ADT-candidate skip (:31973199 if cand.is_empty() && group.members.is_empty()). These define exactly what it.2 currently spares. Then sweep every recursive .ail/.ail.json under examples/ NOT in /tmp/it3_class_a.txt:

Run:

for f in $(grep -rl -E '\(fn ' examples/ | grep -vFf /tmp/it3_class_a.txt); do
  cargo run -q -p ail -- check "$f" >/dev/null 2>&1 && echo "CLEAN $f" || echo "ERR   $f"
done | tee /tmp/it3_sweep.txt

Then, for each CLEAN fixture containing a self-recursive fn, classify by inspection against the two exemptions: a fixture that checks clean only because (cand is empty: recurses on a primitive/no-ADT param) OR (the rec call is tail-marked) is class-(b)/(a). Produce /tmp/it3_migration_set.tsv with columns: stem class(a|b|c) migration(loop|plaincall|already-loop) runnable(main|checkonly). Recon-verified seeds: bench_tree_walk (fn build_tree, class-b pure, no tail-app), bench_latency_explicit (build_tree class-b + tail-app sites class-a). Class-(c) = rc_pin_recurse_implicit, rc_let_alias_implicit_param (recon items 1c).

  • Step 1.4: Capture the behavioural oracle.
mkdir -p bench/it3-oracle
while IFS=$'\t' read stem class mig run; do
  if [ "$run" = main ]; then
    cargo run -q -p ail -- run "examples/$stem" > "bench/it3-oracle/$stem.out" 2>&1
  else
    cargo run -q -p ail -- check "examples/$stem" > "bench/it3-oracle/$stem.out" 2>&1
  fi
done < /tmp/it3_migration_set.tsv
cp /tmp/it3_migration_set.tsv bench/it3-oracle/MANIFEST.tsv
  • Step 1.5: Map existing-test fallout.

Run: grep -rln -E '$(paste -sd"|" <(cut -f1 /tmp/it3_migration_set.tsv))' crates/*/tests crates/*/src bench 2>/dev/null Record, per migrated fixture, which e2e/round-trip/bench/IR-snapshot test references it (these are the primary oracle per DD-1; they must stay green across migration). Also list the tail-app-specific tests recon named for deletion: prose :14861494 app_tail_renders_*, :2050 binop_with_tail_flag_*, :2177 not_with_tail_flag_*; spec_drift::spec_mentions_tail_variants; any tail_call_in_tail_position_is_accepted / iter14e_*musttail* e2e.

  • Step 1.6: Gate.

Expected: bench/it3-oracle/MANIFEST.tsv non-empty; every listed fixture has a .out; /tmp/it3_migration_set.tsv classifies every recursive corpus fixture. No code changed.


Task 2: Migrate the corpus (tail-app field still present)

Files: every examples/*.ail in MANIFEST.tsv with migration ∈ {loop, plaincall}.

  • Step 2.1: Migrate plaincall fixtures.

For each migration=plaincall stem (structurally-decreasing recursion that only carried a redundant tail-app marker, e.g. list_map_poly, sort, std_list, fieldtest/floats_2_average_int_list): replace (tail-app f …) with (app f …) (drop the marker only — the recursion is already structural; it.2's checker will verify it total once the grandfather is irrelevant). One stem at a time:

Run (per stem): cargo run -q -p ail -- run examples/<stem> | diff - bench/it3-oracle/<stem>.out (or ail check for check-only). Expected: empty diff (byte-identical).

  • Step 2.2: Migrate loop fixtures.

For each migration=loop stem (accumulator/counter recursion: bench_compute_collatz, bench_compute_intsum, bench_latency_explicit/_implicit, bench_list_sum*, rc_tail_sum_explicit_leak, mut_counter, mut_sum_floats, fieldtest/floats_1_newton_sqrt, fieldtest/forma_1_factorial, bench_closure_chain, bench_hof_pipeline, bench_mono_dispatch, ct_1_ordering_signum, rc_drop_iterative_long_list, test_mono_recursive_fn_bug, the class-b build_tree fixtures, class-c rc_pin_recurse_implicit/rc_let_alias_implicit_param): rewrite the recursive helper as a (loop ((var <p> <ty> <init>)…) <body-with-recur>). The accumulator/counter params become loop binders; the terminal return-value path becomes the non-recur exit; the recursive (tail-app f a b) / (app f a b) becomes (recur a b). Preserve the function's public signature and add !Diverge to its effect row (loop-bearing ⇒ Diverge, per it.2; mirror the it.1 loop_counter.ail shape + the it.2-migrated loop_needs_diverge.ail). One stem at a time, each immediately gated:

Run (per stem): cargo run -q -p ail -- check examples/<stem> → Expected: clean (the loop form passes it.2's checker; !Diverge declared). Run (per stem): cargo run -q -p ail -- run examples/<stem> | diff - bench/it3-oracle/<stem>.out → Expected: empty diff. (For RC fixtures the e2e RC==GC guard is the real oracle — see 2.4.)

  • Step 2.3: it.1 loop fixtures — confirm unaffected.

loop_counter.ail, loop_smoke.ail, loop_nested_in_lambda.ail, loop_in_lambda_e2e.ail have no tail-app; migration does not touch them. Run their e2e (loop_counter→55, loop_in_lambda→49) — Expected: still green (sanity).

  • Step 2.4: Workspace gate (field still present).

Run: cargo test --workspace 2>&1 | tail -3 Expected: green. Round-trip auto-rechecks every migrated .ail (globs examples/*.ail). The two RC fixtures' RC==GC e2e guards (e2e.rs rc_pin_recurse_implicit/rc_let_alias_implicit_param) stay green (the regression is in the unchanged pin/pin_aliased bodies; only the driver became a loop). If a bench fixture's e2e stdout pin moved, that is a migration error — fix the migration to restore byte-identical output, do NOT edit the pin.


Task 3: (folded into Task 2 gating — no separate task)

Migration correctness is proven by Task 2.4 (green with the exemptions still present) followed by Task 4 (green with the exemptions removed). No standalone task; this header is kept so the numbering matches the journal's task log.


Task 4: Remove the it.2 transitional exemptions (completeness gate)

Files: crates/ailang-check/src/lib.rs, crates/ailang-check/tests/no_tail_grandfather_pin.rs (create).

  • Step 4.1: RED — the exemption is gone.

Create crates/ailang-check/tests/no_tail_grandfather_pin.rs (harness copied verbatim from structural_recursion_pin.rs):

#[test]
fn tail_marked_non_structural_is_now_rejected() {
    // A tail-marked non-structural self-call must NO LONGER be
    // grandfathered once it.3 removes the exemption.
    assert!(
        check_fixture("test_non_structural_recursion.ail.json")
            .contains(&"non-structural-recursion".to_string()));
}

(Reuse the it.2 negative fixture; if it was tail-marked to exercise the grandfather, this pin asserts the grandfather is dead. If the it.2 fixture is not tail-marked, add a tail-marked sibling examples/test_tail_marked_non_structural.ail.json and inventory it in carve_out_inventory.rs EXPECTED 20→21.)

Run: cargo test --workspace -p ailang-check tail_marked_non_structural_is_now_rejected Expected: PASS already if fixture untail-marked; FAIL (still grandfathered) if tail-marked — driving the removal.

  • Step 4.2: Delete the tail==false grandfather.

In verify_structural_recursion's rec-call collection (recon lib.rs:30263037, the if !*tail { guard at :3027): collect the recursive call regardless of tail. Delete the guard; the comment blocks :29192927,30073014,32603262 referencing the grandfather are removed/re-scoped.

  • Step 4.3: Delete the no-ADT-candidate skip.

Recon lib.rs:31973199: delete if cand.is_empty() && group.members.is_empty() { return Ok(()) } and its rationale comment :31833196. A no-candidate counter recursion now reaches the guarded verdict and emits NonStructuralRecursion. The only legitimate early-OKs that remain are calls.is_empty() (:3215) and self_clear (:3247).

  • Step 4.4: GREEN — the completeness gate (DD-3).

Run: cargo test --workspace 2>&1 | tail -3 Expected: all green. This is the proof Task 2 migrated the entire set. If ANY fixture now fires non-structural-recursion, that fixture was not fully migrated in Task 2 — fix it in Task 2's migration (re-open 2.1/2.2 for that stem), NOT by restoring the exemption. Re-run until green with zero exemptions.


Task 5: Remove the tail-app/tail-do machinery + 18g.1 husk-dec

Files: ast.rs, parse.rs, print.rs, prose/lib.rs, check/lib.rs, codegen/lib.rs, codegen/match_lower.rs.

  • Step 5.1: Delete the AST fields (DD-5).

ast.rs: remove Term::App.tail (:406407) + Term::Do.tail (:445446) + their #[serde] attrs. Keep is_false; only delete its two tail uses; re-scope its doc-comment (:908912) to mention only WorkspaceDef.drop_iterative. Fix serde-roundtrip test ctors (:9741006) that pass tail:.

  • Step 5.2: Build-driven removal sweep.

Run: cargo build --workspace 2>&1 | grep -E "error|tail" | head -60 Every error is a tail read/construct site. Fix each by deleting the tail handling, driven off the compiler (authoritative over recon line numbers):

  • parse.rs: delete parse_tail_app/parse_tail_do, the "tail-app"/"tail-do" dispatch + keyword-list strings, the EBNF doc lines; collapse parse_app_body/parse_do_body's tail param.

  • print.rs: Term::App/Term::Do arms emit only (app /(do .

  • prose/lib.rs: as_binop/as_unary_not drop the tail guard; render App/Do drop the "tail " emit; subst App/Do drop tail: *tail. (Free-var counter arms use .. — no change.)

  • check/lib.rs: delete CheckError::TailCallNotInTailPosition + its code() arm (no ctx() arm exists — verify); in verify_tail_positions delete the tail-app arms (Term::App/Term::Do if *tail && !is_tail checks) but keep the function, the recursive walk skeleton, the Term::Loop/ Term::Recur arms, and verify_loop_body (they are entangled mutual recursion — verify_loop_body calls back into verify_tail_positions at 7 sites; only the tail-app arms go).

  • Step 5.3: Delete the 18g.1 husk-dec (DD-4).

match_lower.rs:658736: the arm_body_is_tail_call = matches!(&arm.body, Term::App{tail:true,..}|Term::Do{tail:true,..}) gate (:697704) no longer compiles. Delete the entire 18g.1 pre-tail-call shallow-dec block (recon: it is wholly tail-call specific, no recur analogue). Also delete the now-dead musttail lowering: emit_call tail param + :24272431 ret-block, emit_indirect_call :25082512, lower_effect_op :2627 "tail call" + :26502653, lower_app's tail param threading.

  • Step 5.4: Build + functional GREEN.

Run: cargo build --workspace → Expected: clean. Run: cargo test --workspace 2>&1 | tail -3 → Expected: all green.

  • Step 5.5: RC-RSS gate (DD-4 — the load-bearing risk).

Run the RC-RSS / 18f.2 tail-latency bench pins (the ones match_lower.rs:674677 cited): Run: bash bench/run.sh 2>&1 | tail -20 (or the specific RC-RSS harness; identify it from match_lower.rs:674677's comment + bench/'s RC-RSS script). Compare against bench/baseline*. Expected: RC-RSS within the established noise envelope (no RSS regression on the migrated accumulators — loops strand no husk).

  • Step 5b (CONDITIONAL — only if 5.5 RED): owned-binder drop-on-recur.

If 5.5 shows an RC-RSS regression, it.1 loop codegen is not dropping superseded owned binder values across the recur back-edge. Add that drop: in the Term::Recur lowering (codegen/lib.rs:19791996) / the loop-header phi machinery, before the back-edge br, emit an RC dec for each loop binder whose old value is owned and is being replaced by the recur arg (mirror the husk-dec shape 18g.1 used, but keyed on loop-binder ownership, not tail). RED test: the failing RC-RSS pin from 5.5. GREEN: 5.5 within envelope. (This is the loop-side of the seam the spec wrongly claimed it.1 fully shipped — in scope for it.3 per the corrected spec §Codegen-rework.)


Task 6: DESIGN.md + form_a.md + drift lockstep

Files: docs/DESIGN.md, crates/ailang-core/specs/form_a.md, design_schema_drift.rs, spec_drift.rs, schema_coverage.rs.

  • Step 6.1: DESIGN.md.

Decision 8 (:709747+): rewrite as a superseded tombstone — keep the heading, replace the body with "Superseded by the iteration-discipline milestone (2026-05-15): explicit tail calls were retired in favour of structural recursion (total by construction under Decision 10) + named loop/recur. See docs/specs/0031-iteration-discipline.md." EBNF block :552565: remove tail-app-term/tail-do-term productions. §"Data model" :23572359 ("app" tail jsonc + comment), :23732375 ("do"), :24522467 (the it.1/it.2 transitional paragraph's tail-app/tail-do + grandfather/retire sentences — reword to "retired in it.3"). Decision 3 :163176 is NOT touched (it.2 payload; the corrected spec struck the "remove Decision-3 Diverge line" clause).

  • Step 6.2: form_a.md.

Remove tail grammar :268,270; tail prose :295298,378381, 405407; the §4 "Tail-recursive sum" exhibit :508539 (incl. :527 (tail-app sum_acc …)) — replace §4 with a (loop …)/recur worked example (mirror an it.1 loop fixture). Keep loop/recur grammar :287288,327341.

  • Step 6.3: Drift/spec/coverage lockstep.

design_schema_drift.rs:5360,8794: drop tail:false from the App/Do ctors. spec_drift.rs:3037,6469: drop tail:false; delete spec_mentions_tail_variants (:331337) (it asserts the form_a spec contains tail-app/tail-do — now false by design). schema_coverage.rs: no tail tag exists; verify TermLoop/TermRecur anchors untouched.

Run: cargo test --workspace -p ailang-core design_schema_drift spec_drift schema_coverage Expected: green; loop/recur anchors present, zero tail anchors.

  • Step 6.4: Delete the tail-app-specific tests (Task 1.5 list).

Delete prose app_tail_renders_with_keyword (:14861494), binop_with_tail_flag_keeps_prefix_form (:2050), not_with_tail_flag_keeps_prefix_form (:2177); any tail_call_in_tail_position_is_accepted / tail_call_in_non_tail_position_is_rejected (the TailCallNotInTailPosition pins — the diagnostic is gone); the iter14e_*musttail* e2e. Each deletion is justified: the behaviour it pinned was deliberately removed.

Run: cargo test --workspace 2>&1 | tail -3 → Expected: all green.


Task 7: Acceptance gate

Files: none (verification only).

  • Step 7.1: No exemption remains (spec it.3 acceptance).

Run: grep -n "if !\*tail\|cand.is_empty() && group" crates/ailang-check/src/lib.rs || echo "NONE — exemptions gone" Expected: NONE. The tail==false grandfather and the no-ADT-candidate skip are gone.

  • Step 7.2: No tail-app concept remains.

Run: grep -rn "tail-app\|tail-do\|TailCallNotInTailPosition\|musttail\|\.tail\b" crates/ --include=*.rs | grep -v "block_terminated\|loop\|recur\|drop_iterative" | head Expected: empty (or only unrelated .tail like iterator methods — inspect each; zero AILang-tail-call references). Run: grep -rl -E '\(tail-app|\(tail-do' examples/ → Expected: empty.

  • Step 7.3: Behavioural identity.

For every MANIFEST.tsv stem: re-run and diff against bench/it3-oracle/<stem>.out. Expected: every diff empty (byte-identical observable behaviour pre/post migration).

  • Step 7.4: Full workspace + design-drift.

Run: cargo test --workspace 2>&1 | tail -3 → Expected: all green. Run: cargo build --workspace → Expected: clean (no dead-code warnings from half-removed tail machinery).

  • Step 7.5: Spec it.3 acceptance bullets, itemised.

Verify each: Term::App.tail/Term::Do.tail/both keywords/ musttail/verify_tail_positions tail-app role/ TailCallNotInTailPosition/Decision 8 all removed (7.2 + grep); design_schema_drift green with loop/recur anchors and no tail anchors (6.3); every migration-set fixture ail check clean

  • ail run byte-identical (7.3); no grandfather/no-candidate skip (7.1); cargo test --workspace green (7.4). Note for the milestone-close audit: bench may need re-ratification if loop-header codegen diverged from the retired musttail (5.5 result + any 5b remediation is the evidence the audit consumes).

Self-review (planner Step 5)

  1. Spec coverage. §Components it.3 §Remove → T5 + T6.1/6.2; §Codegen-rework (incl. the corrected 18g.1/RC-RSS contingency) → T5.3/5.5/5b; §Corpus-migration (full corrected set) → T1 (enumeration+oracle) + T2; the two exemptions → T4; Decision-8 tombstone + EBNF + §Data-model + form_a.md + drift → T6; §Acceptance it.3 (incl. "no grandfather/no-candidate skip remains") → T7. Decision 3 explicitly NOT touched (corrected spec). Every it.3 spec clause has a task.
  2. Placeholder scan. No "TBD/TODO/implement later/similar to/ add appropriate". The build-driven removal (T5.2) is an exact method (compiler enumerates sites), not a placeholder — same device the planner memory mandates over recon line numbers. Per memory plan-pseudo-vs-reality: no prose round-trip asserting AST-equality is scripted; no diagnostic.rs doc-list claim (T5.2 says delete the code() arm, explicitly notes no ctx() arm exists). Recon lines flagged drift-prone in the DD preamble.
  3. Type/name consistency. Term::App.tail/Term::Do.tail, is_false (kept), verify_tail_positions/verify_loop_body (kept, arms deleted), TailCallNotInTailPosition (deleted), 18g.1/arm_body_is_tail_call, bench/it3-oracle/, the exemption greps — consistent across tasks + files section.
  4. Step granularity. Each step is one action (one grep, one fixture migration with its gate, one symbol-class removal). The per-stem migration loops in T2 are explicitly "one stem at a time, each immediately gated".
  5. No commit steps. None. Working tree only; Boss commits the whole it.3 diff at iter end.

Recorded risk (named, decision rule — not a placeholder): the class-(b) enumeration (T1.3) is the one step that cannot be fully pre-scripted (the spec + it.2 journal §1 explicitly delegate it to a live sweep). The method is exact (read the two exemptions, sweep, classify by the shape they spare). If the sweep reveals a fixture shape that is neither cleanly loop-able nor structural (a genuine non-structural recursion with no natural loop form), that is a real finding → it surfaces as a Task-2 BLOCKED to the Boss with the fixture named, not a silent reshape (memory: don't adapt fixtures to dodge — but here the fixture SHOULD become a loop; a fixture that resists looping is a spec-signal worth surfacing).