Files
BFM-decomp/tools/build_wave.py
T
Drew T 540d2cefaa feat(phase-31): wave C banked 32 (R22 213/213) + tools/build_wave.py; 3 instrument defects fixed
- wave C: 35 cards (11 tell + 24 weak) -> 35/35 standalone (re-verified independently, R14)
  -> 32 banked / 3 near, 91% gate, 0 symbol failures (Law 4 prevention worked)
- weak lane proven for the first time: 24/24 on haiku; 890 candidates remain
- reach measured: 32 exemplars, 8 with sharers, x2 each => ~1.25x effective (the x134
  era ended in P25/29/30) -> throughput, not leverage, is now the lever
- tools/build_wave.py (pool=adapt|weak, corpus-derived open-stub filter, R35 gate guard)
- 3 self-inflicted instrument defects found+fixed+NC'd (P9, recorded not buried):
  pgrep self-match via shell=True; corpus.stubs() is addr->Stub not names (nearly
  declared both card pools spent); a wave fired on hand-typed placeholder cards (stopped)
- STRATEGIC: card lanes are ~0.23% of open ins/wave; the Atlas's head-crack bucket is
  1,276 groups / 186k ins with high-reach groups up to 265 instances -> retarget waves
  at atlas groups next
2026-08-14 23:44:03 -06:00

98 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Build the next campaign wave from a card pool (adapt | weak), in the proven wave-B shape.
Selection: still an open stub (derived from the corpus oracle, R32/R33 — never a stale label),
not already spent in a prior wave, ranked by fleet leverage (reach desc, then similarity desc).
Model routed by size per the subagent ladder (haiku <=50 ins, sonnet 51-120); larger is left to a
targeted lane, not a bulk wave.
MUST be run with NO gate in flight — gate_lane transiently substitutes drafts into src/, so a
concurrent read of corpus.stubs() reports substituted functions as already-banked (R35).
Usage: build_wave.py <pool: adapt|weak> <out.json> [N]
"""
import json, sys, collections, subprocess
sys.path.insert(0, 'tools')
import corpus
pool = sys.argv[1]
out = sys.argv[2]
n = int(sys.argv[3]) if len(sys.argv) > 3 else 48
# NB: invoke pgrep WITHOUT a shell. With shell=True the wrapping `sh -c` carries the pattern in
# its own command line and pgrep -f matches it — the self-match false positive (cookbook §174
# ops note, same class as the grinder pgrep). pgrep excludes only its own PID, not its parent.
_busy = subprocess.run(['pgrep', '-f', 'tools/(gate_stage|dedup_propagate|gate_lane)'],
capture_output=True, text=True)
if _busy.returncode == 0 and _busy.stdout.strip():
sys.exit(f"REFUSING: a gate is in flight (pids {_busy.stdout.split()}) — corpus.stubs() would "
f"misreport substituted drafts as banked (R35). Wait for it.")
PRIORS = ['.run/wave_p31a_cards.json', '.run/wave_p31b_cards.json', '.run/wave_p31c_cards.json',
'.run/wave_p31d_cards.json', '.run/wave_p31e_cards.json', '.run/wave_p31f_cards.json',
'.run/wave_p31g_cards.json', '.run/wave_p31h_cards.json']
taken = set()
for p in PRIORS:
try:
taken |= {c.get('fn') or c.get('name') for c in json.load(open(p))}
except (FileNotFoundError, json.JSONDecodeError):
pass
src = {'adapt': '.run/adapt_cards.json', 'weak': '.run/weak_cards.json'}[pool]
cards = json.load(open(src))
_open = {}
def is_open(binary, fn):
if binary not in _open:
# corpus.stubs() is addr -> Stub; the NAME lives on the record (R35: I read the
# container, not the contract, and every card looked 'already-banked')
_open[binary] = {st.symbol for st in corpus.stubs(binary).values()}
return fn in _open[binary]
def model_for(nins):
if not nins: return 'haiku'
if nins <= 50: return 'haiku'
if nins <= 120: return 'sonnet'
return None
sel, skipped = [], collections.Counter()
for c in cards:
fn = c.get('fn') or c.get('name')
binary = c['binary']
if pool == 'adapt' and c.get('klass') != 'SMALL-EDIT':
skipped['not-small-edit'] += 1; continue
if fn in taken:
skipped['already-waved'] += 1; continue
m = model_for(c.get('nins'))
if not m:
skipped['too-big-for-wave'] += 1; continue
if not is_open(binary, fn):
skipped['already-banked'] += 1; continue
rec = {'fn': fn, 'binary': binary, 'lane': pool, 'model': m,
'nins': c.get('nins'), 'sim': c.get('sim'), 'reach': c.get('reach'),
'sub': c.get('sub'), 'addr': c.get('addr')}
if pool == 'adapt':
rec.update(klass=c.get('klass'),
seed_path=(c.get('seed') or {}).get('path'),
seed_kind=(c.get('seed') or {}).get('kind'),
seed_name=(c.get('seed') or {}).get('name'),
sites=[{'at': d.get('member_at'), 'kind': d.get('kind'),
'member': d.get('member_disasm', []), 'seed': d.get('seed_disasm', [])}
for d in (c.get('diff') or [])])
else:
rec.update(seed=c.get('seed'), note=c.get('note'), unit_ins=c.get('unit_ins'))
sel.append(rec)
sel.sort(key=lambda c: (-(c.get('reach') or 0), -(c.get('sim') or 0)))
wave = sel[:n]
json.dump(wave, open(out, 'w'), indent=1)
print(f"pool={pool} total={len(cards)} candidates={len(sel)} skipped={dict(skipped)} -> wave {len(wave)} -> {out}")
if wave:
print("models:", dict(collections.Counter(c['model'] for c in wave)))
ni = [c['nins'] for c in wave if c.get('nins')]
if ni: print(f"nins {min(ni)}-{max(ni)}")
print("binaries:", len({c['binary'] for c in wave}))
json.dump([{'fn': c['fn'], 'binary': c['binary'], 'lane': c['lane'], 'model': c['model'],
'sim': c.get('sim'), 'seed_path': c.get('seed_path'), 'seed_name': c.get('seed_name')}
for c in wave], open(out.replace('.json', '_index.json'), 'w'))