docs: restore §462/§463, record S76 tooling in SETUP, add the gate-triage step to the playbook

Three gaps found by auditing instead of asserting.

§462 and §463 were MISSING from the cookbook although their commits are
ancestors of HEAD and added 37 and 34 lines. Same silent loss as §464, which
I caught only because I happened to re-check the three sections I had just
written. Both restored from their own commits; all of §460-§476 now verified
present one by one.

SETUP.md had no record of either new tool (R21). Added gate_main_parallel and
sync_tu_decls, plus the oracle corrections a reader needs in order to
re-judge older verdicts: the REORDER_TUS routing in match_one/rtu_match, the
draw_waves --main no-op, the verbatim-draft refusals at three points, and the
§179-C conversion guard.

The playbook had nothing on what to do when a gate banks far less than it
staged — which is exactly what happened this session. Added the triage step:
probe first (CC1-FAIL 16 / DIFF 18 / MATCH 6 on main's 40), sync declarations
for the plumbing class, hand self_decl_tu to cast_self_callers, and expect a
cascade because every bank changes the declaration environment for the drafts
that follow it.
This commit is contained in:
Drew T
2026-09-03 17:41:43 -06:00
parent 9bdd2e27f7
commit e0229af908
3 changed files with 114 additions and 0 deletions
+24
View File
@@ -973,6 +973,30 @@ fills fast). Nothing is leaking — but the host does not get the memory back on
| `tools/restage_matching.py` | rebuilds a gate plan from `recover_integration --probe-only` verdicts, keeping only drafts that compile-and-MATCH in their REAL TU | when a binary banks 0 and you suspect one bad draft is failing its siblings' shared build. **Caveat measured S71:** that probe compiles but never LINKS or CARVES, so its MATCH is not a bank prediction |
| `tools/weave_sweep.py` | the §406/§408 derived-selector sweep: scores each open stub's stored drafts, classifies the `sw $ra` disagreement from the residual, applies the clobber only to WEAVE-SUNK, `--lever-all` is the ablation control | as the template for "price a class by its RESIDUAL, not its SHAPE" — the sweep itself is a measured null (§408) |
### Tools added / changed 2026-09-03 (S76) — R21 record
| tool | what it does | when you need it |
|---|---|---|
| `tools/gate_main_parallel.py` **(NEW)** | runs the REAL `gate_main` inside N git worktrees to discover which drafts pass, then hands the union to ONE authoritative `gate_main` in the real tree. Workers discover; only the final serial pass banks | a main slate large enough that serial bisection hurts. **Measured: one gate cycle is 16 s**, so MAX_STEPS=24 is ~6.4 min serial and ~90 s across four workers. Run `--negative-control` once per environment first |
| `tools/sync_tu_decls.py` **(NEW)** | banks a draft the gate refuses by copying the TU's OWN `extern` line for whatever symbol the gate names, re-gating, and repeating | a draft that is byte-correct but rejected on a declaration conflict. Refuses `self_decl_tu` (use `cast_self_callers --sync-decls`) and refuses a NEAR up front, since syncing declarations makes a body COMPILE, never MATCH |
**Oracle corrections — re-read any verdict recorded before these:**
* `match_one` and `rtu_match` now route the Makefile's `REORDER_TUS` (`800c2 800c2_2 800c2_3 800c3`)
through `reorder_passthrough.py + as -O2`, the real build path. They previously modelled
`maspsx + as -O1` and manufactured a phantom §182/§188 epilogue wall for every function in those
four TUs. Same draft, `func_8005ECC0`: closeness **5 / 36 ins** before, **2 / 35** after.
* `oracle_reorder.py`'s docstring no longer says the shape is unreachable — for those four TUs the
second cell of its diagnostic IS the build path, so a 0 there means the draft will bank.
* `draw_waves --main` **drew ZERO main functions** until this session (`bins` came from `src/*`
DIRECTORIES and main has no `src/main/`), while printing a reassuring "refusing 49 LINKED subsegs".
Coverage is now asserted: `--main` yielding no main stubs exits 4. Also `--redraw-open`, because
the ledger records what was ATTEMPTED and a still-open stub is still work.
* A **verbatim-asm draft** is refused at three points — `gate_main`, `harvest_verify`, and
`api_agent.prior_draft` (which was OFFERING them as warm starts). 1,099 of the 704,375 `.c` files
in the draft store are §265 bodies under ordinary `<fn>.c` names.
* `verbatim_to_stub` refuses a **§179-C epilogue-less fragment** (no `jr $ra` of its own) — a stub
there is an unbankable target. Census: of main's verbatim blocks, 37 have `jr $ra` and 100 do not.
**Two gating rules that are now enforced in code, not remembered:**
* `parallel_gate` **REFUSES `main`** — main's extract rewrites the linker script, so an incremental
gate is a false PASS (§414). Use `tools/gate_main.py`: baseline assert → one clean rebuild per
+71
View File
@@ -35636,3 +35636,74 @@ as a whole-block schedule difference. Unpinning `o`/`col`/`sh23`/`abr` fixed the
*(Also: the 850 single-prim block written as an inline `addPrim` with block-local temps, and the
function defined with the TU's typed prototype `(Obj_80021D38*, u8*, DVec_80021D38*, u8*)` to avoid
the §41 declaration wall.)*
#### §462 — FOUR LEVERS FROM `main:func_80024054` (91 ins, 74/53/32 → 4)
**Source: the S76 agent on `func_80024054`.** Four independent unlocks, none previously recorded;
the last one is the kind of rule that silently costs a whole attempt.
**1. `array[var - K]` folds K into the symbol's LO16 / `lhu` displacement.** Naming an intermediate
`idx = var - K` does NOT stop it — the fold happens at the front end / in combine, before any
register assignment you could steer. The only thing that defeated it was a zero-byte opacity barrier
immediately after computing the index, one per use site:
`__asm__ __volatile__("" : "=r"(idx) : "0"(idx));`
Reach for this whenever the target loads from `sym+0` with a computed index and your build folds the
constant into the displacement instead.
**2. The fused `sll 16; sra 15` sign-extend-and-scale wants the index declared `s16`, not `s32`.**
This confirms §241's recipe on a fresh case — worth knowing it reproduces rather than being a
one-off of that function.
**3. A mask-then-compare LOCAL causes a cross-jump merge AND flips branch polarity.** Writing
`bits = val & 0xC000;` then `if (bits == …) else if …` merged two case tails into one shared block
and emitted `bne`-polarity branches where the target has `beq`. Dropping the local and switching on
the expression directly — `switch (val & 0xC000) { case 0x8000: … case 0xC000: … default: … }` —
fixed both at once and reproduced the target's forward-`beq` shape. **The temporary was the defect**;
this is the same family as §461's "laundering can be the defect", from the opposite direction.
**4. 🔴 A POINTER PARAMETER'S SIGNEDNESS DECIDES HOW `-1` IS MATERIALIZED.** Declaring `arg1` as
`s16 *` rather than `u16 *` flips the fail-path constant from `ori $x, 0xffff` to `addiu $x, -1`,
matching the target — because gcc-2.7.2 canonicalizes the RHS constant against the **lvalue's**
signedness before choosing the load-immediate opcode. Nothing about the store's *value* changes, so
this is invisible in the C and shows up only as a one-instruction opcode difference. If a residual is
a lone `ori 0xffff` vs `addiu -1`, check the signedness of the pointer being written through before
touching anything else.
**Left open:** one `DELAY-SLOT` residual — `addu $a3,$zero,$zero` is insn #0 in the target and lands
in the branch delay slot in every C variant. Two independent prior attempts hit the same wall;
five further variants (statement reorder, register pin, barriers either side of the load) each left
it unchanged or traded it for an equal residual elsewhere. Permuter-class, Law 3.
#### §463 — 🔴 SPILL SLOTS ARE 8 BYTES, AND THE §41b "LOAD ABOVE THE PROLOGUE" WALL IS REFUTED
**Source: the S76 agent on `main:func_8001FC08` (400 ins, 33 → 0 MATCH).** Three laws, and the
second one deletes a wall this file has been asserting.
**1. A 4-BYTE GAP IN AN OTHERWISE 4-PACKED FRAME IS A SPILL SLOT, NOT A PAD.** `sp+0xC8` / `sp+0xD0`
in this target are not struct members — they are spilled pseudos. reload's `alter_reg` calls
`assign_stack_local(mode, size, -1)`, and `align == -1` means `BIGGEST_ALIGNMENT` (8) with
`CEIL_ROUND`, so **every 4-byte spill slot occupies EIGHT bytes**. That is exactly why the target's
two slots sit 8 apart with `0xCC`/`0xD4` untouched. Reading those gaps as padding — or as fields of a
struct you then invent — is a wrong model of the frame. Worth 11 instructions here, and modelling
them as spills is also what evicts both values from local-alloc so reload picks `$t0`.
**2. §41b's "a global load cannot float above the RTL prologue" IS NOT A WALL — it is an `$a0`
ANTI-DEPENDENCE.** The parameter copy `addu $s0, $a0, $zero` *reads* `$a0`, which pins the load
below it. Get the value out of `$a0` and make the load the first statement, and it floats to idx 0
on its own. **Both moves are required and either alone is worthless** — statement-first by itself
measured 33 → **50** (worse); combined with law 1 (which is what frees the register) it went
22 → 4. Before treating a "load above the prologue" residual as unreachable, check what reads the
argument register.
**3. Which ARGUMENT POSITION a guard value is passed in decides its hard register.** Passing it as
arg 1 — `func_80021120(&L.cnt, L.lp)` — gives that pseudo a `qty_phys_copy_sugg` toward `$a1`, which
local-alloc's scan-from-`$v0` can never reach on its own. The sibling guards that do *not* pass it
stay in `$v0`, which is the control proving the mechanism rather than a coincidence.
**Banker caveat for this function:** its `INCLUDE_ASM` is at `src/800.c:11168`, but the TU's own
`MTX_80020248` typedef (`:11180`) and the `D_80074818`/`D_80075018` externs (`:11191-2`) are twelve
lines BELOW it. Hoist that block above `:11168` or drop the draft's copy, or the duplicate typedef is
a hard C89 error at bank time.
**Verified by hand (law 1c):** 26 `jal` targets and 16 HI16/LO16 relocs identical in name and order;
the four `D_1F800020` words are the scratchpad literal, byte-identical (`3c111f80` / `26310020`).
+19
View File
@@ -433,6 +433,25 @@ said; a sibling `scratch_<fn>/` with candidates but no final draft is worth scor
tier is exhausted, re-run its targets on the next tier ALONGSIDE the dying agents — never kill a
running workflow to relaunch it differently.
### 6-S76. WHAT TO DO WHEN A GATE BANKS FAR LESS THAN IT STAGED
Measured S76: the overlay gate banked **1 of 38** and main's first pass **0 of 49** — and none of it
was drafting quality. Classify before re-drafting anything:
1. **Run `recover_integration --probe-only --no-propagate`.** It compiles each stranded draft in its
ACTUAL TU and splits them three ways. On main's 40: **CC1-FAIL 16 / DIFF 18 / MATCH 6**. Only the
CC1-FAIL group is plumbing; the DIFF group is real residual and no declaration work will save it.
**A `CC1-FAIL` says the declaration blocked COMPILATION — never that the body underneath is right.**
2. **For a CC1-FAIL, run `tools/sync_tu_decls.py --binary main --fn F --draft D --apply`.** It copies
the TU's own `extern` for whichever symbol the gate names, re-gates, repeats. Banked
`func_8005EB28` in one round and `func_8005EC00` in two.
3. **If it refuses with `self_decl_tu`**, the TU declares the function being banked, so the call
SITES must change too — that is `cast_self_callers --sync-decls`, and the `--undo-journal --keep`
afterwards is mandatory.
4. **Expect a CASCADE.** Every bank gives its TU a real definition, which then contradicts the stale
`extern` that every later draft in that TU still carries. A draft that was compatible before a
bank can be incompatible after it — re-run the sync rather than concluding the draft went bad.
## 6. Gate — EVERYTHING PARALLEL. There is no serial lane.
> **P31 S74 — after a gate that CARVED, the binary's `asm/` is stale until a re-extract.** The