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

> **Parent spec:** `docs/specs/2026-05-15-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 1–4; 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:658–736`) — 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:908–916`) 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:402–408` (`Term::App.tail`),
  `:442–447` (`Term::Do.tail`), `:908–916` (`is_false` doc only),
  `:974–1006` (serde test ctors dropping `tail:`)
- `crates/ailang-surface/src/parse.rs:40–55,1203,1207,1224–1227,
  1274–1317,1368–1395` (EBNF, dispatch, keyword list,
  parse_tail_app/parse_tail_do, parse_app_body/parse_do_body `tail`
  param)
- `crates/ailang-surface/src/print.rs:417–422,464–466`
- `crates/ailang-prose/src/lib.rs:573–597,655–658,782–784,
  1203–1229` (+ delete tests `:1486–1494,2050,2177`)
- `crates/ailang-check/src/lib.rs:558–562` (`TailCallNotInTailPosition`
  variant), `:781` (`code()` arm), `:2706–2817`
  (`verify_tail_positions` tail-app arms `:2709–2727`; KEEP Loop
  `:2803–2808`/Recur `:2815` + `verify_loop_body:2829–2869`),
  `:3026–3037` (grandfather `if !*tail` `:3027`), `:3197–3199`
  (no-ADT-candidate skip)
- `crates/ailang-codegen/src/lib.rs:2120,2397–2432,2445–2513,
  2615–2654` (musttail in lower_app/emit_call/emit_indirect_call/
  lower_effect_op), `crates/ailang-codegen/src/match_lower.rs:658–736`
  (18g.1 husk-dec — delete)
- `docs/DESIGN.md`: Decision 8 `:709–747+` (→ tombstone), EBNF
  `:552–565` (`tail-app-term`/`tail-do-term`), §Data-model
  `:2357–2359,2373–2375,2452–2467` (tail jsonc + transitional
  paragraph). **Decision 3 `:163–176` UNTOUCHED** (it.2 payload).
- `crates/ailang-core/specs/form_a.md:268,270,295–298,378–381,
  405–407,508–539` (tail grammar + §4 tail-rec exhibit)
- `crates/ailang-core/tests/design_schema_drift.rs:53–60,87–94`
  (drop `tail:`), `spec_drift.rs:30–37,64–69` + delete
  `spec_mentions_tail_variants:331–337`, `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
(`:3197–3199 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` `:1486–1494 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`):

```rust
#[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:3026–3037`, the `if !*tail {` guard at `:3027`):
collect the recursive call **regardless of `tail`**. Delete the
guard; the comment blocks `:2919–2927,3007–3014,3260–3262`
referencing the grandfather are removed/re-scoped.

- [ ] **Step 4.3: Delete the no-ADT-candidate skip.**

Recon `lib.rs:3197–3199`: delete
`if cand.is_empty() && group.members.is_empty() { return Ok(()) }`
and its rationale comment `:3183–3196`. 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` (`:406–407`) + `Term::Do.tail`
(`:445–446`) + their `#[serde]` attrs. **Keep `is_false`**; only
delete its two `tail` uses; re-scope its doc-comment
(`:908–912`) to mention only `WorkspaceDef.drop_iterative`. Fix
serde-roundtrip test ctors (`:974–1006`) 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:658–736`: the `arm_body_is_tail_call =
matches!(&arm.body, Term::App{tail:true,..}|Term::Do{tail:true,..})`
gate (`:697–704`) 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 + `:2427–2431` ret-block,
`emit_indirect_call` `:2508–2512`, `lower_effect_op` `:2627`
`"tail call"` + `:2650–2653`, `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:674–677` cited):
Run: `bash bench/run.sh 2>&1 | tail -20` (or the specific RC-RSS
harness; identify it from `match_lower.rs:674–677`'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:1979–1996`)
/ 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 (`:709–747+`): 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/2026-05-15-iteration-discipline.md`." EBNF block
`:552–565`: remove `tail-app-term`/`tail-do-term` productions.
§"Data model" `:2357–2359` (`"app"` tail jsonc + comment),
`:2373–2375` (`"do"`), `:2452–2467` (the it.1/it.2 transitional
paragraph's tail-app/tail-do + grandfather/retire sentences —
reword to "retired in it.3"). **Decision 3 `:163–176` 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 `:295–298,378–381,
405–407`; the §4 "Tail-recursive sum" exhibit `:508–539` (incl.
`:527 (tail-app sum_acc …)`) — replace §4 with a `(loop …)`/`recur`
worked example (mirror an it.1 loop fixture). Keep loop/recur
grammar `:287–288,327–341`.

- [ ] **Step 6.3: Drift/spec/coverage lockstep.**

`design_schema_drift.rs:53–60,87–94`: drop `tail:false` from the
App/Do ctors. `spec_drift.rs:30–37,64–69`: drop `tail:false`;
**delete `spec_mentions_tail_variants` (`:331–337`)** (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`
(`:1486–1494`), `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).
