Files
Drew T 827295e241 tools+docs(phase-33.5): task 13.5 — the tools audit + the two dictionaries: tools/tool_census.py (two agreeing enumerations of 327 tool files; docstring/SETUP row/consumers/class derived from the tree; the authored half in config/tool_dictionary.tsv — phase · portability · the NEED each tool answers · what · adapts · verdict — with coverage asserted both ways) → docs/tool-index.md (need-keyed, KEEP-GEN, Reference-index row, wiki + how-to pointers), the kit's tools/MANIFEST.md regenerated (header states live 293 + superseded 28 = 321 rows), and the two verbatim corpora in-tree (Drew, confirmed S91): decomp-architect/corpus/tools/<phase>/ (302 copies + 28 superseded pointers + INDEX) and corpus/cookbook/ (the cookbook, its symptom index, the codegen map, a front page stating what transfers per compiler) — sha1-equal to their sources by tool_census --check in tools-health, regenerated by make kit-corpus; kit_lint exempts the corpus dirs (verbatim evidence) but syntax-checks them; G66 (consult the tool dictionary first) + G67 (translate an inherited idiom through its pass) + two memory seeds (34 at install); SETUP Step 6 installs docs/knowledge-corpus.md and checks the manifest against its own stated total; the ops-setup dictionary rows; the intake's Phase 7 cites G66/G67 and Phase 10 + Part C name the raw-cast → declared-symbol step; templates/layout-contract.md (the five-tool probe, a draft for the split). The review under Drew's criterion: 93 no-consumer tools (one Opus agent's draft, verified: 0 defects, every successor live, 0 live consumers, 0 collisions; four one-off verdicts overturned to STILL-NEEDED) → 34 retired by git mv to tools/sunset/ (28 superseded, 6 one-offs; README review table; SETUP rows moved; Archive-index group). Run 4 (fresh throwaway, the final kit): stopped on my Step-6 check (321 vs the live 293) → both sides derived → resumed → PASS 10/10, manifest 56 == 56, 4 commits, guardrails held (the one foreign path was the timeline regenerated by the detached tools-health). tools-health OK; doc_links --strict rc 0; audit_public OK over 6,842 paths; the purge probe PASSED (Phase 34's gate open). decision-log "P33.5 S91" + accelerators "P33.5 S91" banked; log + checkpoint (NEXT = task 14, xHigh, fresh session)
2026-09-07 22:09:15 -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'))