From 1d3081bca4762d936ddfaa8a0c780876bc59fcf5 Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:02:36 -0600 Subject: [PATCH] fix(gate): refuse to commit a COLLAPSED config file, restore it from HEAD instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate's dirty-tree committer swept an empty config/overlays.mk into commit:2863 and took the whole fleet down with it. R42 says commit a dirty tree rather than revert — true for src/, where a per-binary gate leaves PROVEN banks uncommitted and reverting destroys them. A config file is the opposite case: it holds no proven state that exists only in the worktree, and a collapsed one is never intended. config_sane() runs at all three commit sites: if config/overlays.mk or config/dedup.us.yaml has fewer than 80% of HEAD's lines, it is restored from HEAD, NOT committed, and the refusal is logged loudly. Controls both ways — positive (min_ratio=1.5 makes the healthy registry trip the same branch: detected, restore path runs, file intact) and negative (normal threshold: silent, returns True). P28's registry died this way too (H5); now it is enforced rather than remembered. --- tools/ox_campaign.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tools/ox_campaign.py b/tools/ox_campaign.py index 3a6d96ee0..6a8335323 100644 --- a/tools/ox_campaign.py +++ b/tools/ox_campaign.py @@ -505,6 +505,44 @@ def gate_main_batch(tag, mains): return len(banked), banked +def config_sane(min_ratio=0.8): + """Refuse to commit a config file that COLLAPSED, and restore it from HEAD. + + P31 S60, the most expensive defect of the campaign: config/overlays.mk — the 5,077-line + registry defining all 141 overlay binaries — was committed as a ZERO-LINE file by + commit:2863 ("ox wave dk overlays — 2 banked"). Two failures, neither sufficient alone: + some writer rewrites that file in place with no tmp+rename while lanes edit it + concurrently (the version before the deletion already carried a stray partial line), and + THIS committer swept the wreckage in, because "the tree is dirty at gate entry" cannot + tell a truncated config from an intended edit. + + While it was empty: main could not build at all (its object glob prunes siblings via + $(_ASM_DIR), so every overlay's nonmatchings/*.s fell into MAIN's OBJS and was + assembled standalone), the main lane correctly refused to gate against a RED baseline + with 1,288 stubs behind it, and overlay gates collapsed — GATE do banked 0 of 236 gated. + + R42 says commit a dirty tree rather than revert it, and that stands for src/: a per-binary + gate leaves PROVEN banks uncommitted and reverting destroys them. A config file is the + opposite case — it holds no proven state that exists only in the worktree, and a collapsed + one is never intended. P28's registry died the same way (H5: never silently drop content + on a rewrite); this enforces the rule instead of remembering it. + """ + bad = [] + for path in ("config/overlays.mk", "config/dedup.us.yaml"): + full = os.path.join(REPO, path) + if not os.path.exists(full): + continue + with open(full, errors="replace") as fh: + now = sum(1 for _ in fh) + was = sh(f"git show HEAD:{path}").stdout.count("\n") + if was and now < was * min_ratio: + sh(f"git checkout HEAD -- {path}") + bad.append(f"{path}: {now} lines vs {was} at HEAD — RESTORED, not committed") + for b in bad: + log(f" *** CONFIG COLLAPSE REFUSED — {b}") + return not bad + + def gate(tag, keep, jobs, run_id=None): # Any main drafts that slipped through (an older wave's cards) are PARKED for the periodic main # batch rather than gated inline — see draw_wave's note on why main is off the critical path. @@ -559,6 +597,7 @@ def gate(tag, keep, jobs, run_id=None): # (main_lane); this lane leaves main's TUs alone in both directions. log(f" leaving dirty main TU(s) alone — gate_main/main_lane own them: {main_dirty}") log(f" tree dirty at gate entry ({n_files} files) — committing it rather than reverting") + config_sane() sh("git add -A src/ config/") unstage_main_tus() r = sh('git commit -q -m "chore(decomp): commit in-tree banked work before the next gate\n\n' @@ -584,6 +623,7 @@ def gate(tag, keep, jobs, run_id=None): # Nothing was lost permanently (the drafts survive in .run/wave_/) but the gate cycle was. # Uncommitted banked work is fragile; commit it the moment it exists. if banked: + config_sane() sh("git add -A src/ config/") unstage_main_tus() sh(f'git commit -q -m "feat(decomp): ox wave {tag} overlays — {len(banked)} banked\n\n' @@ -606,6 +646,7 @@ def gate(tag, keep, jobs, run_id=None): def commit(tag, n, banked): if not sh("git status --porcelain -- src/ config/").stdout.strip(): return None + config_sane() sh("git add -A src/ config/") unstage_main_tus() msg = (f"feat(decomp): ox wave {tag} — {n} banked\\n\\n"