Files
BFM-decomp/tools/dedup_extend.py
T
Drew T d54d0a899e feat(phase-30 S46-6): recovery ladder banks +6; wire gate_stage into dedup_extend
- RECOVERY PASS A (wave residue): gate_stage over the 3 big-3 draft dirs recovered
  6 sites the bare gate rejected — func_80168664 x3, func_80168F40 x2, func_8012B77C
  x1. That is 6 of 19 PLUMBING = ~32%, matching the 16-39% range P29 measured. Batch 1
  goes 27 -> 33 of 60. R22 213/213 + tools-health green.
- HONEST SIZING (correcting my own claim): ~32% is NOT "a one-time fix for a ~50% draft
  loss". It moves the batch loss from 55% to 45%. Real and free; not transformative.
- WIRED (task 9): dedup_extend now gates through gate_stage (the ladder:
  canon_resident_calls -> cast_call_sites -> sig_unify -> harvest_verify) instead of
  harvest_verify verbatim. Its 142 candidates failed 0/142 with reasons 118 PLUMBING /
  21 CC1-FAIL / 3 DIFF — ~1 in 50 a real byte divergence, the rest declaration conflicts
  in the TARGET TU, which is exactly what the ladder reconciles.
  GATE_NO_ARITY=1 is forced for the child: gate_stage's arity pre-pass writes the
  fleet-shared engine_core.h BEFORE the gate and a failing draft can leave that edit
  behind — the F1 defect that broke 141 of 213 binaries in S45. The ladder's other rungs
  are draft-local. --ladder can be disabled to restore the old path.
- DOCTRINE (step 3): gate every wave with gate_stage, not bare harvest_verify. Batch 1
  needed a second manual pass only because I used the bare gate first.
- LEVERAGE METRIC CORRECTED (R14/R35): the behemoth ranking must use LIVE reach
  (unmatched sharers), not total sharers. func_80144B9C reads 770 ins x 141 = 108,570
  by total, but 138 of those are already banked — its true weight is 770 x 3 = 2,310.
  Same x134 over-count the cookbook records in §25; build_wave_args --rank live exists
  precisely for this and I used the wrong ranking. Remaining >=400 ins: 57 distinct
  functions / 77 live instances / 38,968 ins, reach ~1.35 => ~0.3% instr-weighted.
2026-08-10 10:43:44 -06:00

293 lines
15 KiB
Python

#!/usr/bin/env python3
"""Phase-28 T4: EXTEND the existing dedup registry to newly-onboarded binaries.
THE GAP THIS FILLS (byte-measured, P28 T3-A). `tools/new_overlay.sh` produces a byte-clean binary
that is NOT a citizen of the shared-body ecosystem: its `.c` includes only `common.h` (never
`../shared/engine_core.h`), so not one shared engine body can reach it, and it appears in ZERO of
`config/dedup.us.yaml`'s groups. The 4 SC07 overlays the Phase-27 disc audit onboarded therefore sat
at ~80 matched / ~2,400 stubs while their siblings were ~2,150 matched — and 1,689 registry groups
still read "134 binaries", never 138.
WHY THE EXISTING TOOL CANNOT DO IT. `dedup_propagate` is built for CRACK -> AUTHOR MACRO ->
INSTANTIATE: `--auto-from` scans a source overlay's INLINE DEFS (it planned only 11 fns here,
skipping the ~1,600 whose bodies are ALREADY `DEFINE_func_*` macros in engine_core.h), and `--addr`
dies with "no source overlay has it matched" because no overlay holds an inline def — every one of
the 134 uses the macro. Extending an existing MACRO-BACKED group to a new binary is a different
operation, and nothing implemented it.
WHY IT IS SAFE (the correctness argument — this is a byte-gate feeder, so it must be explicit).
`h_exact` is the SHA1 of RAW INSTRUCTION BYTES (tools/sig_image.py). Two instances sharing an
h_exact are therefore identical *including* their `jal`/`lui`/`%lo` reloc immediates — same callee
addresses, same data addresses, same symbols. So the body that compiles byte-identically at one
member compiles byte-identically at the other, with no remap at all. (This is exactly why
dup_report calls h_exact "guaranteed byte-match" and h_norm "candidate-only".) The whole-binary
byte-gate remains the sole arbiter (G3/P9): a wrong instantiation changes the bytes and fails SHA1.
A bug here can make this tool FAIL TO BANK; it cannot make it falsely bank.
REUSE, DON'T REBUILD (R33). This owns only the SET COMPUTATION and the REGISTRY EDIT. The splice and
the gate are `tools/harvest_verify.py` verbatim — which already derives each stub's home TU from the
corpus oracle, chunks + bisects, and reverts on failure. h_exact members are byte-identical by
construction, so the happy path is ONE build per binary (not one per function).
tools/dedup_extend.py --binaries ov_SC07_006,ov_SC07_007,ov_SC07_010,ov_SC07_011 [--check-only]
[--chunk N] [--limit N]
"""
import argparse
import glob
import json
import os
import re
import subprocess
import sys
import yaml
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import corpus
import family_hseq # has_mid_jr — the ONE jr oracle (R33)
import family_remap as FR # stream_words/nins_of
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PY = sys.executable
REGISTRY = "config/dedup.us.yaml"
SHARED_INCLUDE = '#include "../shared/engine_core.h"'
EXTEND_DIR = ".run/extend"
def sig_hashes(binary):
"""{vram_int: h_exact} from the binary's sig — the ORIGINAL bytes, independent of splat."""
out = {}
p = os.path.join(REPO, f".run/sig.{binary}.jsonl")
for line in open(p):
d = json.loads(line)
out[int(d["addr"], 16)] = d["h_exact"]
return out
def load_groups():
doc = yaml.safe_load(open(os.path.join(REPO, REGISTRY)))
groups = doc if isinstance(doc, list) else doc.get("groups", doc)
if not isinstance(groups, list):
sys.exit(f"dedup_extend: unexpected {REGISTRY} shape: {type(groups).__name__}")
return doc, groups
def add_members_surgical(additions):
"""Append binaries to each group's `binaries: [...]` list by TEXT EDIT, in place.
NEVER `yaml.safe_dump` this file. The first cut of this tool round-tripped it through
safe_dump and silently destroyed BOTH of the things a human needs from it (H5 — "never
silently drop comments on a rewrite"):
* all 47 comment lines — including the curated Phase-11 header explaining WHY the share is
source-level (the linker cannot excise bytes interior to an object) — dumped to nothing;
* every `vram: 0x80162FF4` re-serialized as `vram: 2148937716` (PyYAML parses YAML-1.1 hex
to int, and dumps int as decimal), making 1,832 entries unreadable.
It was invisible to every gate: dedup-check passed 1840/0 and check-all stayed 140/140,
because `_addr()` accepts both forms — the data was fine and the DOCUMENT was ruined. A
formatting-destructive write that all your oracles call green is exactly the class this
project keeps re-learning: the gate measures bytes, not intent.
`additions` = {group_id: [binary, ...]}. Idempotent: a binary already listed is skipped.
"""
p = os.path.join(REPO, REGISTRY)
lines = open(p).read().splitlines(keepends=True)
cur, n = None, 0
for i, ln in enumerate(lines):
m = re.match(r"^\s*-?\s*id:\s*(\S+)\s*$", ln)
if m:
cur = m.group(1)
continue
if cur and cur in additions and re.match(r"^\s*binaries:\s*\[", ln):
add = [b for b in additions[cur] if re.search(rf"\b{re.escape(b)}\b", ln) is None]
if add:
lines[i] = ln.rstrip("\n").rstrip()[:-1].rstrip() + ", " + ", ".join(add) + "]\n"
n += len(add)
cur = None
open(p, "w").write("".join(lines))
return n
def _addr(v):
return int(v, 16) if isinstance(v, str) else int(v)
def plan_for(binary, groups):
"""[(group, vram, macro)] for every h_exact group this binary could join, plus the skip tally.
A group is extendable into `binary` iff: it is h_exact tier, the binary is not already a member,
the binary has a LIVE STUB at the group's vram, that stub's original bytes hash to the group's
recorded h_exact (the C1 equivalence the registry itself asserts), and it is NOT a jr/switch
function (see below).
THE jr GUARD (cookbook §53). A function with a mid-body `jr` on a non-$ra register dispatches
through a compiler-generated jump table the LINKER must place at the sibling's exact address — a
per-sibling `jtbl_carve` + interleave (`jtbl_family_bank.py`). This tool has no carve step and a
newly-onboarded overlay has no `_jr_*` split at all, so a jr body instantiated here would leave
its table unplaced and the gate would (correctly) reject it. The guard is PREVENTIVE: it skips
and REPORTS rather than feeding the gate drafts that cannot pass (R32 — a skip is only honest if
it is counted and named). On the SC07 set it currently skips ZERO: no jr function is in the
extendable set.
⚠️ R14 — WHAT THIS GUARD IS *NOT*. It does NOT explain the 12 DIFFs the first run produced
(`func_80162FF4/801630C4/80163194/8016325C`, the same 4 in every overlay, 12 of 6457 = 0.19%).
I first assumed those were the jr class because ov_SC01_077 hosts them in `_jr_8017A4AC.c` /
`_jr_80182268.c` — a natural read, and WRONG: `has_mid_jr` is **False** for all four (33-52 ins,
no jump table). They merely LIVE in a carved jr-REGION split (the carve region for
`func_8017A4AC` sweeps in every function in its address range), which says nothing about them.
Their DIFF cause is UNDIAGNOSED and logged for follow-up; they are correctly left as stubs by the
gate. Hosting file != function class — do not infer one from the other.
Detector reused verbatim from `family_hseq.has_mid_jr` (R33 — one oracle, not two).
"""
sig = sig_hashes(binary)
stubs = corpus.stubs(binary)
out, skips = [], {}
for g in groups:
if g.get("tier") != "h_exact":
continue
if "vram" not in g or "binaries" not in g:
skips["verbose-form"] = skips.get("verbose-form", 0) + 1
continue # not position-locked
if binary in g["binaries"]:
continue
vram = _addr(g["vram"])
if vram not in stubs:
continue # not a live stub here (matched already, or absent)
if sig.get(vram) != g["hash"]:
continue # DIFFERENT CODE at the same vram — the point of the check
words = FR.stream_words(binary, vram, FR.nins_of(binary, vram))
if words and family_hseq.has_mid_jr(words):
skips["jr-needs-carve"] = skips.get("jr-needs-carve", 0) + 1
continue # §53: route via jtbl_family_bank, not here
out.append((g, vram, g["func"]))
return out, skips
def ensure_include(binary, apply=True):
"""Add the shared-header include if absent. Byte-neutral: engine_core.h is only `#define`s, so
including it without instantiating emits no code (proven: ov_SC07_006 built 7ca772be with the
include + one macro). Returns True if the file was changed."""
p = os.path.join(REPO, f"src/{binary}/{binary}.c")
t = open(p).read()
if SHARED_INCLUDE in t:
return False
if apply:
t = t.replace('#include "common.h"', '#include "common.h"\n' + SHARED_INCLUDE, 1)
open(p, "w").write(t)
return True
def write_drafts(binary, plan):
d = os.path.join(REPO, EXTEND_DIR, binary)
subprocess.run(["rm", "-rf", d], check=False)
os.makedirs(d, exist_ok=True)
for g, vram, macro in plan:
fn = f"func_{vram:08X}"
body = f"{macro}() /* dedup: shared engine-core @0x{vram:08x} (src/shared) */\n"
open(os.path.join(d, fn + ".c"), "w").write(body)
return d
def gate(binary, drafts_dir, chunk, ladder=True):
"""The whole-binary byte-gate is the sole arbiter (G3/P9). Returns the banked fn set.
LADDER (S46): this called `harvest_verify` VERBATIM, which is the bare gate with no recovery.
Measured cost of that: 0 of 142 extendable groups banked, and the classified reasons were 118
PLUMBING / 21 CC1-FAIL / 3 DIFF — i.e. ~1 in 50 was a real byte divergence and everything else
was a declaration conflict in the TARGET TU (`conflicting types for memcpy / ApplyMatrixSV /
D_800AE620 / func_8014C568`). `gate_stage` wraps the same gate in exactly the reconcile ladder
those need (canon_resident_calls -> cast_call_sites -> sig_unify -> harvest_verify), and it
carries the §156 reconcile LEDGER so a failed candidate cannot leave a fleet-shared edit behind.
Measured on the same failure class in the S46 wave residue: 6 of 19 PLUMBING recovered (~32%).
GATE_NO_ARITY=1 is set for the child: gate_stage's arity pre-pass writes the fleet-shared
engine_core.h BEFORE the gate, and on a failing draft that edit can survive — the F1 defect that
broke 141 of 213 binaries in S45 and made every later gate report `near`. The ladder's other
rungs are draft-local and safe.
"""
good = open(os.path.join(REPO, f"config/check.{binary}.sha")).read().split()[0]
vout = f".run/extend_verified.{binary}.txt"
fout = f".run/extend_failed.{binary}.txt"
if ladder:
cmd = [PY, "tools/gate_stage.py", "--binary", binary,
"--drafts", os.path.relpath(drafts_dir, REPO), "--no-propagate",
"--verified-out", vout, "--failed-out", fout]
else:
cmd = [PY, "tools/harvest_verify.py", "--binary", binary,
"--out", f"build/{binary}/{binary}", "--good-sha", good,
"--drafts", os.path.relpath(drafts_dir, REPO), "--chunk", str(chunk),
"--verified-out", vout, "--failed-out", fout]
env = dict(os.environ, GATE_NO_ARITY="1")
subprocess.run(cmd, cwd=REPO, timeout=7200, env=env)
p = os.path.join(REPO, vout)
return set(open(p).read().split()) if os.path.exists(p) else set()
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--binaries", required=True, help="comma list of newly-onboarded binaries to wire in")
ap.add_argument("--chunk", type=int, default=512,
help="drafts per build (h_exact is byte-identical by construction, so the happy "
"path is ONE build; harvest_verify bisects on failure). default 512")
ap.add_argument("--limit", type=int, default=0, help="cap #groups per binary (0 = all)")
ap.add_argument("--check-only", action="store_true", help="print the plan, touch nothing")
a = ap.parse_args()
os.chdir(REPO)
doc, groups = load_groups()
targets = [b.strip() for b in a.binaries.split(",") if b.strip()]
dirty = subprocess.run("git status --porcelain -- config/ src/", shell=True,
capture_output=True, text=True).stdout.strip()
if dirty and not a.check_only:
sys.exit("dedup_extend: config/ or src/ is dirty — commit first so a failed gate reverts "
"cleanly.\n" + dirty[:400])
total_banked, total_planned = 0, 0
for b in targets:
plan, skips = plan_for(b, groups)
if a.limit:
plan = plan[:a.limit]
total_planned += len(plan)
print(f"[{b}] {len(plan)} extendable h_exact group(s); "
f"include {'ABSENT -> add' if ensure_include(b, apply=False) else 'present'}"
+ (f"; skipped {skips}" if skips else ""))
if a.check_only or not plan:
continue
added_include = ensure_include(b) # True IFF this run inserted the line
d = write_drafts(b, plan)
banked = gate(b, d, a.chunk)
print(f"[{b}] BANKED {len(banked)} / {len(plan)}")
total_banked += len(banked)
if not banked:
# ONLY undo what THIS run did (§61/§63). The revert is an INVERSE TRANSFORM, not a
# snapshot restore, so firing it on a binary that ALREADY had the include strips a
# load-bearing line: every `DEFINE_func_*()` in that overlay stops resolving. It was
# unconditional until Phase 29 SESSION-19, where a 0-banked run over 135 already-wired
# binaries removed the include from all 135 at once. Invisible to every byte-gate (R34)
# because the damage lands AFTER the last gate has run.
if added_include:
ensure_include_revert(b)
continue
adds = {g["id"]: [b] for g, vram, _ in plan # registry: only what the GATE accepted (P9)
if f"func_{vram:08X}" in banked}
n = add_members_surgical(adds) # TEXT edit — never safe_dump (see the docstring)
print(f"[{b}] registry: +{n} membership(s)")
print(f"\n=== dedup_extend: banked {total_banked} / {total_planned} planned "
f"across {len(targets)} binaries ===")
if a.check_only:
print("(--check-only: no files touched)")
def ensure_include_revert(binary):
p = os.path.join(REPO, f"src/{binary}/{binary}.c")
t = open(p).read().replace('#include "common.h"\n' + SHARED_INCLUDE, '#include "common.h"', 1)
open(p, "w").write(t)
if __name__ == "__main__":
main()