From 254feb8ee4cf4b53f2361ef4a75be0bb3396495a Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:37:07 -0600 Subject: [PATCH] fix(gate_main): the uncommitted-work guard belonged OUTSIDE the bisection loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I added the guard to try_batch() an hour ago. try_batch runs REPEATEDLY during bisection, and its own first substitution makes main's TUs dirty -- so on iteration two the guard could not tell the operator's unsaved work from the gate's own in-flight edit, and aborted the run: M src/800c3.c gate_main: aborting with an UNVERIFIED substitution in main's TUs — reverting It failed safely (reverted, no bank lost, and said so), but it made the gate unusable for any batch larger than one. Hoisted to assert_main_tus_clean(), called ONCE from main() before any substitution. The lesson is worth the line it costs: A GUARD MUST BE ABLE TO DISTINGUISH THE STATE IT PROTECTS FROM THE STATE IT CREATES. Placed inside the loop it was checking its own footprints. Negative-controlled both directions: a genuinely dirty src/800c3.c is refused by name before anything is substituted, and a clean tree now proceeds into the bisection (currently running 21 drafts). --- tools/gate_main.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tools/gate_main.py b/tools/gate_main.py index de9292291..28ef7e14c 100644 --- a/tools/gate_main.py +++ b/tools/gate_main.py @@ -715,26 +715,30 @@ def _preserve_and_localize(entries, got): verdict, msg = MDL.classify(per, fn, ndiff) print(f" VERDICT {fn}: {verdict} — {msg}") -def try_batch(entries): - # REFUSE TO DESTROY UNCOMMITTED WORK (P31 S75). The `git checkout` below is correct for the - # normal flow -- restore the stubs, re-extract, substitute the drafts -- and it is CATASTROPHIC - # for anything uncommitted in main's TUs, because it reverts them without asking. - # - # Measured twice this session: (1) the SaveLoadRoutine decompile (1,179 ins, byte-identical) - # sat uncommitted in src/800_b.c while a gate ran, and only survived because it was committed - # first; (2) a §265 verbatim body converted to a stub is UNCOMMITTED BY CONSTRUCTION, so this - # line restored the __asm__ block NEXT TO the substituted C -- the TU then carried 9 jump - # tables instead of 5, `jtbl_rodata_pads --derive` refused, and the gate REJECTED a - # byte-identical bank. gate_main could not bank anything in the verbatim class, by construction. - # - # A destructive step that cannot be undone must ASK, not assume (R42: never blind-revert a dirty - # src/). Commit or stash first; `--allow-dirty` is the deliberate override. +def assert_main_tus_clean(): + """Refuse to START if main's TUs carry uncommitted work this gate would destroy. + + CALLED ONCE, BEFORE ANY SUBSTITUTION — never inside try_batch. `try_batch` runs repeatedly + during bisection, and its OWN first substitution makes the tree dirty, so a check placed there + cannot distinguish the operator's unsaved work from the gate's own in-flight edit: it aborts the + bisection on iteration two. (P31 S75 — I put it there first and did exactly that.) A guard must + be able to tell the state it PROTECTS from the state it CREATES. + + What it protects against, measured twice the same session: the SaveLoadRoutine decompile (1,179 + ins, byte-identical) sat uncommitted in src/800_b.c while a gate ran; and a §265 verbatim body + converted to a stub is UNCOMMITTED BY CONSTRUCTION, so the checkout restored the __asm__ block + NEXT TO the substituted C — 9 jump tables instead of 5, jtbl_rodata_pads refused, and the gate + REJECTED a byte-identical bank. A destructive step that cannot be undone must ASK (R42).""" dirty = run("git status --porcelain -- " + " ".join(main_tus())).stdout.strip() if dirty and not os.environ.get("GATE_MAIN_ALLOW_DIRTY"): - sys.exit("gate_main: main's TUs have UNCOMMITTED changes and this gate is about to `git\n" - "checkout` them, which would DESTROY that work:\n\n" + dirty + - "\n\nCommit it (R42 -- banked work is committed the moment it exists) or stash it.\n" + sys.exit("gate_main: main's TUs have UNCOMMITTED changes and this gate will `git checkout`\n" + "them before substituting, which would DESTROY that work:\n\n" + dirty + + "\n\nCommit it (R42 — banked work is committed the moment it exists) or stash it.\n" "Pass --allow-dirty / GATE_MAIN_ALLOW_DIRTY=1 only if you intend to discard it.") + + +def try_batch(entries): + # (uncommitted-work guard: assert_main_tus_clean(), called once from main()) run("git checkout -- " + " ".join(main_tus())) run("make extract BINARY=main") # regenerate .s for the reverted stubs (hazard 2) substitute(entries) @@ -757,6 +761,8 @@ def main(): a = ap.parse_args() if getattr(a, 'allow_dirty', False): os.environ["GATE_MAIN_ALLOW_DIRTY"] = "1" + if not a.assert_baseline: + assert_main_tus_clean() # ONCE, before any substitution if not a.assert_baseline and not a.slate: ap.error('a slate file is required unless --assert-baseline')