mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
1a603e0e22
Measured the top-candidate score distribution on wave Y's 67 gaps: min 0.009 / p50 0.08 / p90 0.26 / max 0.53. Hand-checking the low half showed generic co-occurrence -- 'delay slot' matching every section that mentions a delay slot -- not real answers. Emitting those is worse than emitting nothing: an empty list costs a reader nothing, a plausible-but-wrong section costs them a read. The tool now reports 14 confident candidates instead of 51 mostly-noise ones.
102 lines
5.1 KiB
Python
102 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""gap_triage.py — deterministically pre-filter a wave's index_gap reports against the cookbook.
|
|
|
|
WHY (P31 S55). Every harvest so far has spent most of its agents rediscovering that a gap was
|
|
already answered: 61 of 71 (wave T), 44/64 (U), 76/67 (V), 41/68 (W), 56/63 (X). That majority
|
|
verdict is the flywheel working, but PAYING an adversarial verifier to reach it is waste — the
|
|
check is a text search, and a text search does not need a model.
|
|
|
|
This scores each gap against the cookbook's section headers + bodies by distinctive-term overlap and
|
|
emits the top candidate sections. It DOES NOT decide anything: a high score means "a reader should
|
|
open these sections first", and the harvest prompt carries them so the reader starts from the
|
|
existing text instead of a blank page. R32: every gap appears in the output exactly once, asserted.
|
|
|
|
Deliberately dumb: term overlap, no embeddings, no model. It is a retrieval aid whose failure mode
|
|
is a bad suggestion the reader ignores, not a refusal that discards work (R39).
|
|
|
|
Usage:
|
|
gap_triage.py .run/wave_y_gaps.json [--top 3] [--out .run/wave_y_gap_triage.json]
|
|
"""
|
|
import argparse, collections, json, os, re, sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
CB = os.path.join(REPO, 'docs', 'matching-cookbook.md')
|
|
|
|
# Terms that carry meaning in this domain. Generic English is dropped by the IDF weighting below,
|
|
# but these patterns are what actually discriminate one residual class from another.
|
|
_TERM = re.compile(r'\b(?:'
|
|
r'[a-z_]+\.c:\d+' # sched.c:2395
|
|
r'|[a-z_]+\.(?:c|md|h)\b' # combine.c, mips.md
|
|
r'|§\d+[A-Za-z0-9\-]*' # §193-A
|
|
r'|\$[a-z0-9]+' # $v0, $sp
|
|
r'|(?:lhu|lh|lw|lb|lbu|sh|sw|sb|sll|sra|srl|addiu|addu|subu|ori|lui|jal|jr|beq'
|
|
r'|bne|beqz|bnez|slt|sltu|slti|sltiu|mflo|mult|div|nop|move)\b'
|
|
r'|(?:sched1|sched2|cse|combine|reload|regalloc|local_alloc|global_alloc|loop|jump2'
|
|
r'|cross_jump|reorg|expand|fold|giv|biv|preheader|allocno|pseudo|birthing|luid'
|
|
r'|delay.slot|epilogue|prologue|frame|stride|alias|clobber|volatile|typedef)\b'
|
|
r')', re.I)
|
|
|
|
|
|
def sections():
|
|
"""[(anchor, text)] for every cookbook section, split on its own headers."""
|
|
txt = open(CB, errors='replace').read()
|
|
out, cur, buf = [], 'preamble', []
|
|
for line in txt.splitlines():
|
|
m = re.match(r'^#{2,4}\s+(§\S+)', line)
|
|
if m:
|
|
if buf: out.append((cur, '\n'.join(buf)))
|
|
cur, buf = m.group(1), [line]
|
|
else:
|
|
buf.append(line)
|
|
if buf: out.append((cur, '\n'.join(buf)))
|
|
return out
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('gaps'); ap.add_argument('--top', type=int, default=3)
|
|
ap.add_argument('--out', default=None)
|
|
ap.add_argument('--min-score', type=float, default=0.15,
|
|
help='drop candidates below this. Measured on wave Y: the top-candidate score\n'
|
|
'distribution is min 0.009 / p50 0.08 / p90 0.26 / max 0.53, and hand-checking\n'
|
|
'the low half found generic co-occurrence ("delay slot" matching every section\n'
|
|
'that says "delay slot"), not real answers. Below ~0.15 this is noise dressed as\n'
|
|
'a suggestion, which is worse than an empty list -- it spends a readers attention.')
|
|
a = ap.parse_args()
|
|
|
|
secs = sections()
|
|
sec_terms = [(anc, collections.Counter(t.lower() for t in _TERM.findall(body))) for anc, body in secs]
|
|
df = collections.Counter()
|
|
for _, c in sec_terms:
|
|
for t in c: df[t] += 1
|
|
n = len(sec_terms) or 1
|
|
|
|
gaps = json.load(open(a.gaps))
|
|
rows = []
|
|
for g in gaps:
|
|
want = collections.Counter(t.lower() for t in _TERM.findall(g.get('gap') or ''))
|
|
scored = []
|
|
for anc, c in sec_terms:
|
|
# IDF-weighted overlap: a term in 300 sections says nothing, one in 2 says a lot.
|
|
s = sum(1.0 / (1 + df[t]) for t in want if t in c)
|
|
if s >= a.min_score: scored.append((round(s, 3), anc))
|
|
scored.sort(reverse=True)
|
|
rows.append({'fn': g['fn'], 'terms': sorted(want)[:8],
|
|
'candidates': [{'section': anc, 'score': s} for s, anc in scored[:a.top]]})
|
|
assert len(rows) == len(gaps), f"coverage (R32): {len(rows)} rows for {len(gaps)} gaps"
|
|
|
|
out = a.out or a.gaps.replace('.json', '_triage.json')
|
|
json.dump(rows, open(out, 'w'), indent=1)
|
|
hit = sum(1 for r in rows if r['candidates'])
|
|
print(f"gap_triage: {len(rows)} gaps vs {len(secs)} cookbook sections -> {hit} with a candidate "
|
|
f"section at score >= {a.min_score}, {len(rows)-hit} with none -> {out}")
|
|
print(" (a candidate is a STARTING POINT for the reader, never a verdict — the harvest still "
|
|
"runs its own grep and its own adversarial verifier)")
|
|
for r in rows[:5]:
|
|
c = ', '.join(f"{x['section']}({x['score']})" for x in r['candidates']) or '—'
|
|
print(f" {r['fn']:18s} {c}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|