diff --git a/docs/cookbook-index.md b/docs/cookbook-index.md
index 6d67ee6e4..c094962aa 100644
--- a/docs/cookbook-index.md
+++ b/docs/cookbook-index.md
@@ -2,7 +2,7 @@
> **Generated by `tools/cookbook_index.py` — do not hand-edit** (R33). Regenerate after adding a cookbook section.
>
-> `docs/matching-cookbook.md` is ~716 KB / 1081 sections. Grepping it blind is how three P30 wave-1 agents each "discovered" an idiom that was already written down. **Start here, then read the section.** A section appears under every symptom it addresses.
+> `docs/matching-cookbook.md` is ~716 KB / 1082 sections. Grepping it blind is how three P30 wave-1 agents each "discovered" an idiom that was already written down. **Start here, then read the section.** A section appears under every symptom it addresses.
**How to use:** name what you SEE in the diff (a stolen delay slot, an extra `la`, a swapped register pair, a `conflicting types` error), find that symptom below, read those sections first. If nothing fits, THEN grind — and add a section when you win.
@@ -1007,7 +1007,7 @@
- **§398** — ★★★ — `family_remap` CARRIES THE **SOURCE** TU's DECL ENVIRONMENT INTO A DESTINATION THAT ALREADY OWNS THOSE NAMES (P31 S69; measured 3 banked of 22) L32835
- **§3-D.** — INTEGRATION IS STILL THE BOTTLENECK, AND THE TU IS THE AUTHORITY L33140
-### build graph, splat & the harness (191)
+### build graph, splat & the harness (192)
- **§4** — Flag/toolchain gotchas L190
- **Build** — mechanism — per-file opt override (splat resegmentation) L307
@@ -1200,6 +1200,7 @@
- **§332b** — ★★★ — THE §332 "WALLS" ARE A PER-OBJECT ASSEMBLER MODE, NOT A C LIMIT — 6 CLOSE AS REAL C (P31 S69, Fable-3) L32702
- **§405** — ★★★ — THE S70 WAVE HARVEST: 130 agents, 113 MATCH, and the laws they brought back L33092
- **§3-A.** — THE ORACLE HAS A HOLE: match_one cannot see a jump table (§405-A) L33098
+- **§414** — ★★★ — `parallel_gate` ON `main` IS A FALSE PASS, AND THE RULE WAS ALREADY WRITTEN DOWN (P31 S71) L33510
### process, measurement & doctrine (132)
@@ -2747,6 +2748,7 @@
- **§411** — ★★★ — THE PACK MUST CARRY THAT FUNCTION'S OWN HISTORY (P31 S71; measured 38/39 vs 124/131) L33386
- **§412** — ★★★ — §323 CARVE BLOCKER 2 WAS A REGEX THAT COULD NOT SEE PAST `__attribute__` (P31 S71) L33424
- **§413** — ★★★ — DIFFICULTY IS THE RESIDUAL CLASS, NOT `nins` — ROUTE THE MODEL TIER OFF HISTORY (P31 S71, Drew) L33473
+- **§414** — ★★★ — `parallel_gate` ON `main` IS A FALSE PASS, AND THE RULE WAS ALREADY WRITTEN DOWN (P31 S71) L33510
---
@@ -3840,3 +3842,4 @@ Notes routinely quote that as a section id. This table resolves it. Grep bait: `
| L33386 | §411 | ★★★ — THE PACK MUST CARRY THAT FUNCTION'S OWN HISTORY (P31 S71; measured 38/39 vs 124/131) |
| L33424 | §412 | ★★★ — §323 CARVE BLOCKER 2 WAS A REGEX THAT COULD NOT SEE PAST `__attribute__` (P31 S71) |
| L33473 | §413 | ★★★ — DIFFICULTY IS THE RESIDUAL CLASS, NOT `nins` — ROUTE THE MODEL TIER OFF HISTORY (P31 |
+| L33510 | §414 | ★★★ — `parallel_gate` ON `main` IS A FALSE PASS, AND THE RULE WAS ALREADY WRITTEN DOWN (P3 |
diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md
index 0b8dc17a9..c79b76970 100644
--- a/docs/matching-cookbook.md
+++ b/docs/matching-cookbook.md
@@ -33506,3 +33506,40 @@ past-attempt fuel can serve one overlay's history to another's target, and any f
rows inherits it. `claude_wave_draft.js`'s `VERDICT` schema now requires `binary`, so new rows are
exact; the historical corpus stays name-keyed and should be read with that caveat.
`check-against-a-known-true-case`, again: the instrument passed because it measured nothing.
+
+## §414 ★★★ — `parallel_gate` ON `main` IS A FALSE PASS, AND THE RULE WAS ALREADY WRITTEN DOWN (P31 S71)
+
+**What happened.** S71 ran `main` through `parallel_gate`. It reported **11 banked**, the merge
+committed them, and the run looked like every other successful gate that session. The R22 clean-fleet
+verify then came back **212 / 213**: `main` did not compile from clean (`conflicting types for
+func_8004355C`, `conflicting types for func_80038FFC`), and once both declarations were reconciled it
+built and was **still not byte-identical**. Every one of the 11 was then re-gated the honest way —
+apply ONE function to a green `src/800.c`, `make extract`, `make build`, compare SHA1 — and
+**11 of 11 REJECTED**.
+
+**The rule already existed, three files away.** `ox_campaign.gate_main_batch`'s docstring:
+
+> *"main is gated by ONE CLEAN REBUILD of the whole EXE, never incrementally. gate_stage/
+> sweep_parallel build incrementally, and main's extract rewrites the linker script, so an
+> incremental main gate returns a FALSE DIFF. Measured P31 S58: wave `ab` drew 105 main cards and
+> banked 0 of them while its non-main cards banked 82% — 105 competent drafts thrown away."*
+
+`parallel_gate`'s worker **is** `gate_stage`, so it inherits that exactly — and S58 recorded the
+false-DIFF direction while this is the false-PASS one, which is strictly worse: a false diff wastes
+drafts, a false pass commits wrong bytes and reads green until the next clean fleet check.
+
+**Fixed as a refusal, not a note** (R43): `parallel_gate` now returns `REFUSED` for
+`binary == 'main'`, naming `tools/gate_main.py`.
+
+**Two instrument lessons from the recovery, both the same shape.**
+* The per-function re-gate's FIRST form lifted each function body WITHOUT the `extern` block above
+ it, and the first three came back REJECT with `cdReq_cdResult undeclared`. That measured the
+ extractor, not the body — while investigating a gate that had measured itself. Fixed by carrying
+ the declaration preamble; the verdicts after that are real (they compile and the SHA differs).
+* The failure is R53's exact signature: **a failed build leaves the previous object on disk, so a
+ SHA1 check downstream of it reads green.** R53 was written for a different tool and never applied
+ here.
+
+**The law.** *A tool that wraps another tool inherits its refusals.* Every constraint documented on
+`gate_stage` binds `parallel_gate`, on `harvest_verify`, and on anything else that shells it — and
+the place to put that knowledge is a refusal in the wrapper, not a paragraph in the callee.
diff --git a/tools/parallel_gate.py b/tools/parallel_gate.py
index 468b69891..3c914ec77 100644
--- a/tools/parallel_gate.py
+++ b/tools/parallel_gate.py
@@ -320,6 +320,24 @@ def gate_one(idx, pin, job):
# a clean success reporting a TRUE number about an EMPTY world. Measured: 35 binaries / 57 drafts
# "banked 0" in 1-2s each, while the SAME drafts gated in-tree banked 15/16 and 3/6.
drafts = drafts if os.path.isabs(drafts) else os.path.join(REPO, drafts)
+ # MAIN IS NOT GATEABLE HERE, AND THE RULE PREDATES THIS TOOL (R43, P31 S71).
+ # `ox_campaign.gate_main_batch` already states it: "main is gated by ONE CLEAN REBUILD of the
+ # whole EXE, never incrementally. gate_stage/sweep_parallel build incrementally, and main's
+ # extract rewrites the linker script, so an incremental main gate returns a FALSE DIFF."
+ # This tool's worker IS gate_stage, so it inherits that exactly — and the failure is not a
+ # false diff but a false PASS: S71 ran main through here, got "11 banked", committed it, and
+ # the R22 clean-fleet verify then came back 212/213. The tree did not compile from clean, and
+ # once the two declaration conflicts were reconciled it was still not byte-identical. Every one
+ # of the 11 was then re-gated the honest way (substitute -> extract -> build -> compare SHA1,
+ # one function at a time) and rejected. A tool must refuse an input it cannot handle rather
+ # than process it wrongly; use tools/gate_main.py.
+ if binary == 'main':
+ return {"binary": binary, "banked": [], "files": {}, "ovl": None,
+ "secs": round(time.time() - t0, 1), "missing_generated": [], "rc": None,
+ "error": "REFUSED — main cannot be gated incrementally (its extract rewrites the "
+ "linker script); gate_stage in a worktree reports a FALSE PASS. "
+ "Use tools/gate_main.py, which does one clean substitute -> extract -> "
+ "build -> SHA1 cycle for the whole batch."}
try:
# ...and assert the input actually exists before spending a worktree on it (R32/R43): a job
# whose drafts are unreadable is a DEFECT, not a zero-yield result.