fix(wave): cards keyed by (binary,fn) — t5a received 48/48 ANOTHER binary's card (R48); t5_cards.py builds the target's OWN fuel (tu_ref 88% / decl_prior 98%) via wave_card_fuel.py extracted from build_wave_atlas (R33); t5_bank.sh recovers un-banked drafts via fix_tu_ret_decls (2/2 on t5a); distill workflow generalized (P31 S63 T5)

This commit is contained in:
Drew T
2026-08-26 20:19:41 -06:00
parent 827b0b156f
commit 41b259e528
8 changed files with 291 additions and 112 deletions
+1 -1
View File
@@ -737,7 +737,7 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo
| | `tools/interleave_check.py` | **(Phase 31 S62 T2, cookbook §302)** Order ⇔ yaml consistency for an overlay's jtbl carve: the `_JTBL_INTERLEAVE` order must equal the yaml subseg sequence position by position. `--fix` regenerates the order from the yaml (never hand-edit it). Run after ANY restore of overlays.mk or a splat yaml (two of the five S61 reds and four of the six clean-sweep failures were exactly this). |
| | `tools/pads_audit.py` | **(Phase 31 S62 T2, cookbook §302)** DERIVES each object's `JTBL_PADS` from the bytes: compiles the TU to count its tables, walks the retail words inside the yaml carve with those sizes (pad 4 ⇔ one zero word), reports SPEC-DRIFT / CARVE-DRIFT / ok with the derived spec. Reproduced 15/15 correct specs as its negative control; replaced S61's blind value searches. |
| | `tools/claude_wave_packs.py` · `tools/workflows/claude_wave_draft.js` · `tools/wave_judge.py` | **(Phase 31 S62 T4)** The Claude-subagent wave harness that ran the model-ladder probe: identical packs from `api_agent` (SYS + user_msg + law-1c warm start + gate feedback) → a Workflow of drafting agents per (arm, fn) with CLI equivalents of grep/read_file/match_one/submit, writing only under `.run/<wave>/<arm>/` → `wave_judge.py` gates each arm with `sweep_parallel` (banked = removed INCLUDE_ASM lines in `git diff`; tree reset between arms; `--union` leaves the cheapest winning drafts in the tree). `tools/workflows/claude_wave_distill.js` = the R16 distill (extract per byte-proven transcript → verify vs the cookbook). Routing rule from T4: ≤50 Sonnet, 51–120 Sonnet, >120 Opus, Opus on residue; haiku retired; DeepSeek optional at ~$0.03/fn. |
| | `tools/t5_targets.py` · `tools/t5_bank.sh` | **(Phase 31 S63 T5)** The wave *draw* and the wave *bank* halves around the T4 harness: `t5_targets.py --wave .run/t5x --n 48 [--residue .run/t5w] [--main-only]` draws a routed slate (open per `corpus.stubs`, ledger `.run/t5/drawn.json` keyed (binary,fn), ≤120 ins → sonnet / >120 → opus, residue → opus once, stratified by band, ≤4 per binary, one fn NAME per wave — the harness keys packs/drafts by bare name, so `claude_wave_packs`/`wave_judge` now REFUSE duplicates, R43/R48); then `claude_wave_packs.py` → Workflow `tools/workflows/claude_wave_draft.js` `args={wave, targets}` (one agent per target, `model=target.arm`) → `tools/t5_bank.sh .run/t5x sonnet opus` (refuses if a lane/gate runs or src/ config/ is dirty; `wave_judge --union`; R22 clean fleet sweep parsed `N passed, 0 failed of N`; commit `— N banked`). |
| | `tools/t5_targets.py` · `tools/t5_cards.py` · `tools/wave_card_fuel.py` · `tools/t5_bank.sh` | **(Phase 31 S63 T5)** The wave *draw* and the wave *bank* halves around the T4 harness: `t5_targets.py --wave .run/t5x --n 48 [--residue .run/t5w] [--main-only]` draws a routed slate (open per `corpus.stubs`, ledger `.run/t5/drawn.json` keyed (binary,fn), ≤120 ins → sonnet / >120 → opus, residue → opus once, stratified by band, ≤4 per binary, one fn NAME per wave — the harness keys packs/drafts by bare name, so `claude_wave_packs`/`wave_judge` now REFUSE duplicates, R43/R48); then **`t5_cards.py --wave .run/t5x`** BUILDS the target's OWN card fuel (`tu_ref` = in-TU banked neighbours §194-E, `decl_prior` = fleet signatures §196) from `wave_card_fuel.py` — those helpers extracted VERBATIM out of `build_wave_atlas.py` (which parses argv at import and so was unusable as a library) so both consumers share ONE oracle (R33); measured 88% tu_ref / 98% decl_prior on a fresh K-class slate, against 0% correct fuel before the fix. Then `claude_wave_packs.py --cards <wave>/cards.json` → Workflow `tools/workflows/claude_wave_draft.js` `args={wave, targets}` (one agent per target, `model=target.arm`) → `tools/t5_bank.sh .run/t5x sonnet opus` (refuses if a lane/gate runs or src/ config/ is dirty; `wave_judge --union`; then a RECOVERY pass — every un-banked draft through `fix_tu_ret_decls`, which retypes a destination TU's `extern void` decl on a value-returning function and re-gates (wave t5a: 2/2 banked, both byte-exact bodies refused for that decl alone); then the R22 clean fleet sweep parsed `N passed, 0 failed of N`; commit `— N banked`). |
| | `tools/jtbl_rodata_pads.py --derive` | **(Phase 31 S62 T3a, cookbook §303)** Module path of the §8e pads filter: the Makefile runs `--derive $(BINARY) --tu <tu>` for every `md_*` object — jump-table pads derived at build time from the retail island + the emission stream (trailing pads `0t1`, table-aware, const data passes through). No stored spec; an anchor miss fails the build with the offset. |
| | `tools/blocker_probe.py` | **(Phase 29 SESSION-16, cookbook §65)** WHY a byte-correct draft fails the whole-binary gate. Read-only; **two oracles** (R34): STATIC (`cdecl.parse` + `cdecl.compatible` — cc1's own acceptance question, never text equality) beside the REAL cc1 (via `rtu_match`), leading with the DISAGREEMENT table. Classes `self_decl_hdr`/`self_decl_tu`/`callee_decl`/`data_decl`/`local_type`, each mapped to a blast-radius tier (T0 draft-only / T1 binary-local / T2 fleet-shared). Blockers STACK, so a function's tier is the MAX over them. 36 drafts in ~9 s. Replaced+deleted `.run/diag_plumbing.py`. |
| | `tools/symcheck.py` | **(Phase 29 SESSION-18, cookbook §67a)** The pre-gate SYMBOL-SET guard: diffs the symbols a draft's object references (reloc records) against the target `.s`'s `%hi`/`%lo`/`jal` set. Catches the class **every masked oracle is structurally blind to** — `match_one`/`masked_diff` compare relocation-MASKED words (object-vs-`.s` mode is symbol-agnostic by construction) and `rtu_match` **compiles without linking**, so a draft that invents an extern no symbol table defines reads MATCH in both and can never bank (the SESSION-17 `func_801463A0` `_s`-alias trap). Negative-control-proven: on a draft with one renamed data extern, `match_one` reports the SAME 14 mismatched as the correct draft while `symcheck` exits 1 naming both the MISSING and the INVENTED symbol. `--c` (compiles via `match_one`, so the triple can never drift) or `--obj`. A cheap necessary condition, NOT a match oracle — still finish on the byte-gate (G3/P9). |
+4 -92
View File
@@ -293,20 +293,10 @@ if not PRIORS:
levers = set(a.levers.split(','))
atlas = json.load(open(a.atlas))
_open = {}
def _stubmap(binary):
if binary not in _open:
# corpus.stubs() is addr -> Stub; the NAME lives on the record
_open[binary] = {st.symbol: st for st in corpus.stubs(binary).values()}
return _open[binary]
def is_open(binary, fn):
return fn in _stubmap(binary)
def home_tu(binary, fn):
"""The stub's home .c — this is the GATE GROUP KEY (gate_lane groups by (binary, src))."""
st = _stubmap(binary).get(fn)
return st.path if st else None
# CARD FUEL LIVES IN ONE PLACE (R33, P31 S63): _stubmap/is_open/home_tu/_tu_bodies/tu_neighbours
# moved to tools/wave_card_fuel.py so the Claude wave packs (tools/t5_cards.py) build IDENTICAL
# fuel. They were unreachable as a library here: this module parses argv at import.
from wave_card_fuel import _stubmap, is_open, home_tu, _tu_bodies, tu_neighbours # noqa: F401
def model_for(nins):
if nins <= 50: return 'haiku'
@@ -315,84 +305,6 @@ def model_for(nins):
_DPIDX = DP.load() # symbol -> fleet declaration evidence (§196)
knn = atlas.get('knn') or {} # exemplar key -> neighbours; 'M:' entries are BANKED (§193-A)
_TU_BODIES = {}
def _tu_bodies(tu_path):
"""{fn: body_text} for every function DEFINED in a TU (banked C only -- stubs are INCLUDE_ASM).
Brace-matched from each definition so a symbol is attributed to the function that uses it, not
to the file. Memoized per TU: a wave draws many cards from one .c."""
if tu_path in _TU_BODIES:
return _TU_BODIES[tu_path]
out = {}
try:
txt = open(tu_path, errors='replace').read()
except (OSError, TypeError):
_TU_BODIES[tu_path] = out
return out
import re as _re
for m in _re.finditer(r'^[A-Za-z_][\w \t\*]*?\b(\w+)\s*\([^;{]*\)\s*\{', txt, _re.M):
i, depth = m.end() - 1, 0
while i < len(txt):
if txt[i] == '{': depth += 1
elif txt[i] == '}':
depth -= 1
if depth == 0: break
i += 1
out[m.group(1)] = txt[m.start():i + 1]
_TU_BODIES[tu_path] = out
return out
_SYM = None
def tu_neighbours(binary, fn, tu_path, asm_file, topn=2):
"""Banked functions in the card's OWN TU, ranked by symbols shared with the target's asm.
The symbols are read from the TARGET's .s (its relocation operands), so this is evidence about
the function being drafted, not about the file. See §194-E for why the card needs this at all."""
global _SYM
if not tu_path or not asm_file:
return []
import re as _re
if _SYM is None:
# OPERANDS ONLY. A splat .s carries the encoded WORD in a comment column, so a naive
# "[A-Z]\w{3,}" reads `D8FFBD27` and `CC00228E` as symbol names and the overlap score
# becomes noise (measured: 34 "symbols" for one function, 31 of them hex words).
# Symbols reach the .s in exactly three shapes: a jal target, and %hi()/%lo() operands.
_SYM = _re.compile(r'\b(?:jal\s+(\w+)|%[hl][io]\(([\w+]+)\))')
try:
raw = _SYM.findall(open(asm_file, errors='replace').read())
except OSError:
return []
want = {(a or b).split('+')[0] for a, b in raw if (a or b)}
want.discard(fn)
if not want:
return []
scored = []
for other, body in _tu_bodies(tu_path).items():
if other == fn:
continue
shared = [s for s in want if s in body]
if len(shared) >= 2:
scored.append((len(shared), other, sorted(shared)[:6]))
scored.sort(key=lambda x: -x[0])
if not scored:
# FALL BACK TO THE BEST SINGLE SHARED SYMBOL rather than emitting nothing. One shared
# callee is weak evidence, but the card reports the count so the agent can weigh it, and a
# weak same-TU lead still beats the zero-locality state §194-E measured.
weak = sorted(((len([s for s in want if s in body]), other)
for other, body in _tu_bodies(tu_path).items() if other != fn), reverse=True)
if weak and weak[0][0] == 1:
o = weak[0][1]
body = _tu_bodies(tu_path)[o]
return [{'fn': o, 'shared': 1, 'symbols': [s for s in sorted(want) if s in body][:6]}]
return [{'fn': o, 'shared': n, 'symbols': syms} for n, o, syms in scored[:topn]]
def _o0_unbankable(spath):
"""True iff this target is an -O0 function sitting in a subseg the build compiles -O2.
+25 -8
View File
@@ -6,6 +6,12 @@ prior_draft (law-1c filtered: symbols must overlap the target .s) + gate_feedbac
written once. Identical packs are what made the T4 arms comparable; keep it that way.
Usage: claude_wave_packs.py <targets.json> <out_dir> [--cards cards.json]
CARDS ARE KEYED BY (binary, fn), NEVER BY BARE NAME (R48; byte-measured P31 S63). Overlays share
function NAMES at equal addresses, and a K-class target has never been carded, so a name-keyed
lookup over `.run/wave_*_cards.json` returns SOME OTHER overlay's same-named function every time:
in wave t5a it mis-carded 48 of 48 targets, and nine agents independently reported "the pack's
warm-start/twin/decl hints were a different function". A card whose binary does not match the
target is DROPPED and counted, never silently substituted (R32/R43).
targets.json: [{name, binary, nins, sub, asm, tu}, ...] (api_agent's target shape; see
tools/main_queue_rebuild.py / .run/t4/targets.json for how to derive one from corpus)
out_dir/SYS.md + out_dir/packs/<fn>.md are produced; the workflow script
@@ -29,22 +35,28 @@ def main():
dup = [n for n, c in __import__('collections').Counter(t['name'] for t in targets).items() if c > 1]
if dup: # packs are keyed by bare fn name; two binaries' same-named fns would overwrite (R43/R48)
sys.exit('REFUSED: %d duplicate fn name(s) in one wave (packs are name-keyed): %s' % (len(dup), ' '.join(dup)))
cards = {}
cards, wrong_bin = {}, 0
def add(e):
nonlocal wrong_bin
if not (isinstance(e, dict) and e.get('fn')):
return
b = e.get('binary') or e.get('b')
if not b: # a card that cannot prove its binary cannot be trusted to a target
wrong_bin += 1; return
cards[(b, e['fn'])] = e
if '--cards' in sys.argv:
for c in json.load(open(sys.argv[sys.argv.index('--cards') + 1])):
if c.get('fn'): cards[c['fn']] = c
else: # newest per-fn card across every wave card file
for c in json.load(open(sys.argv[sys.argv.index('--cards') + 1])): add(c)
else: # newest per-(binary,fn) card across every wave card file
for f in sorted(glob.glob('.run/wave_*_cards.json'), key=os.path.getmtime):
try:
for e in json.load(open(f)):
if isinstance(e, dict) and e.get('fn'): cards[e['fn']] = e
for e in json.load(open(f)): add(e)
except Exception:
pass
os.makedirs(os.path.join(out, 'packs'), exist_ok=True)
open(os.path.join(out, 'SYS.md'), 'w').write(A.SYS)
n = 0
for t in targets:
um = A.user_msg(t, cards.get(t['name']))
um = A.user_msg(t, cards.get((t['binary'], t['name'])))
pd, where = A.prior_draft(t)
if pd:
um += ("\n\nA PREVIOUS ATTEMPT on this exact function left this body behind (%s). The whole-binary "
@@ -55,7 +67,12 @@ def main():
if gf:
um += '\n\n' + gf + '\n'
open(os.path.join(out, 'packs', t['name'] + '.md'), 'w').write(um); n += 1
print('packs: %d written to %s (cards matched %d/%d)' % (n, out, sum(1 for t in targets if t['name'] in cards), len(targets)))
hit = sum(1 for t in targets if (t['binary'], t['name']) in cards)
byname = sum(1 for t in targets if any(k[1] == t['name'] for k in cards))
print('packs: %d written to %s' % (n, out))
print('cards: %d/%d matched on (binary, fn); %d target(s) have a same-named card in ANOTHER binary '
'(DROPPED, not substituted — R48); %d card(s) skipped for carrying no binary'
% (hit, len(targets), byname - hit, wrong_bin))
if __name__ == '__main__':
main()
+38 -3
View File
@@ -4,9 +4,14 @@
# src/ or config/ is already dirty (R42: never blind-revert; someone's banks may be uncommitted).
# 2. WAVE=<wave> tools/wave_judge.py <arms> --union : whole-binary gate per arm (git-diff bank
# detection, tree reset between arms), then the union gated once and LEFT IN THE TREE.
# 3. R22 clean fleet sweep (make clean -> extract-all -> check-all); the pass line must read
# 3. RECOVER the un-banked: a draft whose BODY is byte-correct but whose destination TU declares
# the function `extern void ...` when it returns a value is refused at closeness 0 with no
# codegen fault at all. `fix_tu_ret_decls` retypes those decls, re-gates, and restores every
# edit the gate does not pay for. Measured on wave t5a: 2 of 2 such refusals banked, both
# self-reported MATCH and both confirmed byte-exact by match_one in isolation.
# 4. R22 clean fleet sweep (make clean -> extract-all -> check-all); the pass line must read
# "N passed, 0 failed of N" (exit codes checked, R53) or nothing is committed.
# 4. commit the union; the subject carries "— N banked" (the maintenance lane's exemplar counter).
# 5. commit the union; the subject carries "— N banked" (the maintenance lane's exemplar counter).
# Drafting must be FINISHED before this runs: the judge rewrites src/ and the sweep deletes asm/.
cd /home/musashi/bfm-decomp || exit 2
WAVE=${1:?usage: t5_bank.sh <wave_dir> [arms...]}; shift
@@ -23,8 +28,38 @@ WAVE="$WAVE" .venv/bin/python tools/wave_judge.py $ARMS --union 2>&1 | tee "$WAV
[ "${PIPESTATUS[0]}" = 0 ] || { say "judge exited non-zero — stopping (tree left as is)"; exit 3; }
N=$(python3 -c "import json;j=json.load(open('$WAVE/judge.json'));print(len(j.get('union',{}).get('banked',[])))")
SUMMARY=$(python3 -c "import json;j=json.load(open('$WAVE/judge.json'));print(', '.join('%s %d/%d'%(a,v['n_banked'],v['drafts']) for a,v in j['arms'].items()))")
# ---- recovery pass over everything the union did not bank (see step 3) --------------------------
UNB=$(.venv/bin/python - "$WAVE" <<'PY'
import json, os, shutil, sys
w = sys.argv[1]
tg = json.load(open(w + '/targets.json')); j = json.load(open(w + '/judge.json'))
banked = set(j.get('union', {}).get('banked', []))
for v in j.get('arms', {}).values(): banked |= set(v.get('banked', []))
best = j.get('union', {}).get('picked', {})
arms = list(j.get('arms', {}))
rec = w + '/recover'; shutil.rmtree(rec, ignore_errors=True)
pairs = []
for t in tg:
if t['name'] in banked: continue
for arm in [best.get(t['name'])] + arms:
src = w + '/%s/%s.c' % (arm, t['name']) if arm else ''
if src and os.path.exists(src) and os.path.getsize(src):
os.makedirs('%s/%s' % (rec, t['binary']), exist_ok=True)
shutil.copy(src, '%s/%s/%s.c' % (rec, t['binary'], t['name']))
pairs.append([t['binary'], t['name']]); break
json.dump(pairs, open(w + '/ret_pairs.json', 'w'))
print(len(pairs))
PY
)
if [ "${UNB:-0}" != 0 ]; then
say "recovery: $UNB un-banked draft(s) -> fix_tu_ret_decls (TU 'extern void' on a value-returning fn)"
flock .run/auto/draw.lock .venv/bin/python tools/fix_tu_ret_decls.py --pairs-file "$WAVE/ret_pairs.json" \
--drafts "$WAVE/recover" -j 4 2>&1 | tee "$WAVE/recover.log" | tail -3
N=$(git diff -U0 -- src/ | grep -cE '^-INCLUDE_ASM' || true)
SUMMARY="$SUMMARY, +recovery"
fi
if [ "$N" = 0 ]; then say "0 banked ($SUMMARY) — nothing to commit"; git status --short -- src/ config/ | head; exit 0; fi
say "$N banked in the union ($SUMMARY) — R22 clean fleet sweep"
say "$N banked total ($SUMMARY) — R22 clean fleet sweep"
( make clean && make extract-all JOBS=32 && make check-all JOBS=32 ) > "$WAVE/sweep.log" 2>&1
RC=$?
PASS=$(grep -E '^check-all: ' "$WAVE/sweep.log" | tail -1)
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""tools/t5_cards.py — BUILD the card fuel for a wave's own targets (P31 S63 T5).
Usage: t5_cards.py --wave .run/t5b [--out <wave>/cards.json]
WHY (byte-measured, P31 S63 wave t5a). `claude_wave_packs.py` looked cards up by BARE FUNCTION NAME
across `.run/wave_*_cards.json`; overlays share names at equal addresses, so 48 of 48 t5a targets
(and 15 of T4's 20) got ANOTHER binary's card — wrong banked twin, wrong TU neighbours, wrong
declarations. Nine agents independently reported discarding it. Keying by (binary, fn) fixes the
mis-attribution but leaves K-class ("never touched") targets with NO fuel at all, and `_fuel()`
then emits nothing: no `tu_ref`, no `decl_prior`. Yet t5a's agents cracked function after function
by finding an already-banked sibling in their own TU BY HAND ("in-TU twin ~150 lines earlier",
"structural twin in the SAME TU") — which is exactly what `tu_ref` is (§194-E, 62% of targets).
So this builds the two computable fields for the target ITSELF, from the same oracle
`build_wave_atlas` uses (`tools/wave_card_fuel.py`, R33):
tu_ref already-banked functions in the target's OWN TU, ranked by symbols shared with the
target's .s relocations
decl_prior the fleet's consensus signature for every symbol the .s references (§196) — kills
the two-arity A/B compile §195-A proved the asm cannot answer
`seed_ref` (the cross-TU banked twin) needs the atlas knn and is NOT built here: a card with no
seed_ref makes `_fuel` say "no banked twin — derive from the .s", which is TRUE, where the old
name-keyed card pointed confidently at an unrelated function. Coverage is printed (R32/R41)."""
import argparse, json, os, sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))); os.chdir(REPO)
sys.path.insert(0, 'tools')
import wave_card_fuel as F
import decl_prior as DP
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--wave', required=True)
ap.add_argument('--out', default='')
a = ap.parse_args()
targets = json.load(open(os.path.join(a.wave, 'targets.json')))
idx = DP.load()
cards, n_tu, n_dp = [], 0, 0
for t in targets:
tu = t.get('tu') or F.home_tu(t['binary'], t['name'])
tu_ref = F.tu_neighbours(t['binary'], t['name'], tu, t['asm'])
try:
dp = DP.for_asm(t['asm'], tu, idx=idx, binary=t['binary'])
except Exception as e:
print(' decl_prior failed for %s/%s: %s' % (t['binary'], t['name'], str(e)[:80])); dp = []
n_tu += bool(tu_ref); n_dp += bool(dp)
cards.append({'fn': t['name'], 'binary': t['binary'], 'addr': t.get('addr'), 'nins': t['nins'],
'sub': t['sub'], 'tu_ref': tu_ref, 'decl_prior': dp})
p = a.out or os.path.join(a.wave, 'cards.json')
json.dump(cards, open(p, 'w'), indent=1)
print('cards: %d built for %s (tu_ref on %d/%d = %.0f%%; decl_prior on %d/%d = %.0f%%; seed_ref not built — '
'the pack will honestly say "no banked twin")'
% (len(cards), a.wave, n_tu, len(targets), 100.0 * n_tu / max(1, len(targets)),
n_dp, len(targets), 100.0 * n_dp / max(1, len(targets))))
print('wrote %s' % p)
if __name__ == '__main__':
main()
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""tools/t5_distill_args.py — build the R16 distill slate for a Claude wave (P31 S63 T5).
Usage: t5_distill_args.py --wave .run/t5a --wfdir <workflow transcript dir> [--label 'P31 S63 t5a'] [--all-arms]
Only BYTE-PROVEN transcripts are distilled (the distiller's contract: "the whole-binary byte-gate
ACCEPTED the final draft, so the final body is ground truth"), so the slate = the wave's banked set
from `<wave>/judge.json`, joined to the drafting agents' transcripts via the workflow's
`journal.jsonl` (result rows carry fn/arm/note; the transcript is `agent-<agentId>.jsonl`).
By default only the arm that BANKED each fn is distilled (--all-arms keeps every arm's attempt).
Coverage is asserted (R32): a banked fn with no transcript is reported and counted, never skipped
silently. Writes <wave>/distill_args.json for
Workflow(scriptPath=tools/workflows/claude_wave_distill.js, args=<that file's content>)."""
import argparse, json, os, sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))); os.chdir(REPO)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--wave', required=True)
ap.add_argument('--wfdir', required=True, help="the drafting run's transcript dir (holds journal.jsonl + agent-*.jsonl)")
ap.add_argument('--label', default='', help="provenance string for the cookbook header, e.g. 'P31 S63 t5a'")
ap.add_argument('--all-arms', action='store_true')
ap.add_argument('--out', default='')
a = ap.parse_args()
jd = json.load(open(os.path.join(a.wave, 'judge.json')))
banked_by_arm = {arm: set(v.get('banked', [])) for arm, v in jd.get('arms', {}).items()}
banked = set().union(*banked_by_arm.values()) if banked_by_arm else set()
union = set(jd.get('union', {}).get('banked', []))
picked = jd.get('union', {}).get('picked', {})
jp = os.path.join(a.wfdir, 'journal.jsonl')
if not os.path.exists(jp): sys.exit('no journal.jsonl in %s' % a.wfdir)
rows = [json.loads(l) for l in open(jp) if l.strip()]
tr = {}
for r in rows:
res = r.get('result') or {}
if isinstance(res, dict) and res.get('fn'):
tr[(res['fn'], res.get('arm'))] = (os.path.join(a.wfdir, 'agent-%s.jsonl' % r['agentId']), res.get('note', ''))
out, missing = [], []
for fn in sorted(banked):
arms = sorted(banked_by_arm) if a.all_arms else [picked.get(fn) or next(x for x in banked_by_arm if fn in banked_by_arm[x])]
for arm in arms:
if fn not in banked_by_arm.get(arm, ()): continue
hit = tr.get((fn, arm))
if not hit or not os.path.exists(hit[0]): missing.append('%s/%s' % (arm, fn)); continue
out.append({'fn': fn, 'arm': arm, 'transcript': hit[0], 'note': hit[1]})
print('banked %d (union-gated %d); transcripts found %d; label %r' % (len(banked), len(union), len(out), a.label))
if missing:
print('NO TRANSCRIPT for %d banked draft(s) — NOT distilled (R32, reported not skipped): %s'
% (len(missing), ' '.join(missing[:20])))
p = a.out or os.path.join(a.wave, 'distill_args.json')
json.dump({'targets': out, 'label': a.label or os.path.basename(a.wave.rstrip('/'))}, open(p, 'w'), indent=1)
print('wrote %s' % p)
if __name__ == '__main__':
main()
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""tools/wave_card_fuel.py — the per-target CARD FUEL an agent pack carries, as ONE oracle (R33).
These helpers were defined inside `build_wave_atlas.py`, which parses argv at import and so cannot
be reused as a library. Extracted VERBATIM (P31 S63) so `build_wave_atlas` and the Claude wave's
`tools/t5_cards.py` compute the SAME fuel rather than two copies that drift:
home_tu(binary, fn) the stub's home .c (the gate group key)
_tu_bodies(tu_path) {fn: body} for every function DEFINED (banked) in a TU
tu_neighbours(...) already-banked functions in the target's OWN TU, ranked by symbols
shared with the target's .s relocations (§194-E, the highest-yield
source measured; wave t5a's agents rediscovered it by hand and said so)
Why it exists (byte-measured, P31 S63): `claude_wave_packs` looked cards up by BARE FUNCTION NAME,
and overlays share names at equal addresses — so ALL 48 of wave t5a's targets (and 15 of T4's 20)
received ANOTHER binary's card: wrong twin, wrong TU neighbours, wrong declarations. Nine agents
reported it independently. The fix is a (binary, fn) key — which, for never-carded K-class work,
means there is no card at all. So the fuel has to be BUILT for the target itself, from here."""
import os, sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, 'tools'))
import corpus
_open = {}
def _stubmap(binary):
if binary not in _open:
# corpus.stubs() is addr -> Stub; the NAME lives on the record
_open[binary] = {st.symbol: st for st in corpus.stubs(binary).values()}
return _open[binary]
def is_open(binary, fn):
return fn in _stubmap(binary)
def home_tu(binary, fn):
"""The stub's home .c — this is the GATE GROUP KEY (gate_lane groups by (binary, src))."""
st = _stubmap(binary).get(fn)
return st.path if st else None
_TU_BODIES = {}
def _tu_bodies(tu_path):
"""{fn: body_text} for every function DEFINED in a TU (banked C only -- stubs are INCLUDE_ASM).
Brace-matched from each definition so a symbol is attributed to the function that uses it, not
to the file. Memoized per TU: a wave draws many cards from one .c."""
if tu_path in _TU_BODIES:
return _TU_BODIES[tu_path]
out = {}
try:
txt = open(tu_path, errors='replace').read()
except (OSError, TypeError):
_TU_BODIES[tu_path] = out
return out
import re as _re
for m in _re.finditer(r'^[A-Za-z_][\w \t\*]*?\b(\w+)\s*\([^;{]*\)\s*\{', txt, _re.M):
i, depth = m.end() - 1, 0
while i < len(txt):
if txt[i] == '{': depth += 1
elif txt[i] == '}':
depth -= 1
if depth == 0: break
i += 1
out[m.group(1)] = txt[m.start():i + 1]
_TU_BODIES[tu_path] = out
return out
_SYM = None
def tu_neighbours(binary, fn, tu_path, asm_file, topn=2):
"""Banked functions in the card's OWN TU, ranked by symbols shared with the target's asm.
The symbols are read from the TARGET's .s (its relocation operands), so this is evidence about
the function being drafted, not about the file. See §194-E for why the card needs this at all."""
global _SYM
if not tu_path or not asm_file:
return []
import re as _re
if _SYM is None:
# OPERANDS ONLY. A splat .s carries the encoded WORD in a comment column, so a naive
# "[A-Z]\w{3,}" reads `D8FFBD27` and `CC00228E` as symbol names and the overlap score
# becomes noise (measured: 34 "symbols" for one function, 31 of them hex words).
# Symbols reach the .s in exactly three shapes: a jal target, and %hi()/%lo() operands.
_SYM = _re.compile(r'\b(?:jal\s+(\w+)|%[hl][io]\(([\w+]+)\))')
try:
raw = _SYM.findall(open(asm_file, errors='replace').read())
except OSError:
return []
want = {(a or b).split('+')[0] for a, b in raw if (a or b)}
want.discard(fn)
if not want:
return []
scored = []
for other, body in _tu_bodies(tu_path).items():
if other == fn:
continue
shared = [s for s in want if s in body]
if len(shared) >= 2:
scored.append((len(shared), other, sorted(shared)[:6]))
scored.sort(key=lambda x: -x[0])
if not scored:
# FALL BACK TO THE BEST SINGLE SHARED SYMBOL rather than emitting nothing. One shared
# callee is weak evidence, but the card reports the count so the agent can weigh it, and a
# weak same-TU lead still beats the zero-locality state §194-E measured.
weak = sorted(((len([s for s in want if s in body]), other)
for other, body in _tu_bodies(tu_path).items() if other != fn), reverse=True)
if weak and weak[0][0] == 1:
o = weak[0][1]
body = _tu_bodies(tu_path)[o]
return [{'fn': o, 'shared': 1, 'symbols': [s for s in sorted(want) if s in body][:6]}]
return [{'fn': o, 'shared': n, 'symbols': syms} for n, o, syms in scored[:topn]]
+9 -8
View File
@@ -1,13 +1,14 @@
export const meta = {
name: 't4-idiom-distill',
description: 'R16 flywheel: distill the unlocking lesson from each byte-proven T4 transcript, verify novelty against the cookbook, return §-formatted candidates',
name: 'claude-wave-idiom-distill',
description: 'R16 flywheel: distill the unlocking lesson from each byte-proven wave transcript, verify novelty against the cookbook, return §-formatted candidates',
phases: [
{ title: 'Extract', detail: '31 transcripts -> one lesson each' },
{ title: 'Extract', detail: 'one distiller per byte-proven transcript -> one lesson each' },
{ title: 'Verify', detail: 'novel claims grepped against docs/matching-cookbook.md' },
],
}
// args: { targets: [{fn, arm, transcript, note}, ...], label: 'P31 S63 t5a' } — from tools/t5_distill_args.py
const REPO = '/home/musashi/bfm-decomp'
const WFDIR = '/home/musashi/.claude/projects/-home-musashi-bfm-decomp/b0026d2d-2724-4aed-813a-3deef224d210/subagents/workflows/wf_c691ef44-320/'
const LABEL = args.label || 'P31 S63'
const LESSON = {
type: 'object',
properties: {
@@ -38,7 +39,7 @@ const lessons = await pipeline(
args.targets,
t => agent(
`You are distilling ONE byte-proven decompilation transcript for the project's matching cookbook (docs/matching-cookbook.md, gcc-2.7.2 MIPS idioms). Repo: ${REPO}.
Transcript (JSONL, an agent's full tool trace): ${t.transcript.replace("WF/", WFDIR)}
Transcript (JSONL, an agent's full tool trace): ${t.transcript}
Function: ${t.fn} (arm ${t.arm}); the whole-binary byte-gate ACCEPTED the final draft, so the final body is ground truth. The agent's own closing note was: "${t.note}"
Read the transcript. Find the moment(s) the masked diff (match_one closeness) dropped to 0 and what C change caused it. Report ONE lesson — the most generalizable one — as:
@@ -64,8 +65,8 @@ Read only; write nothing.`,
refs the agent used: ${JSON.stringify(lesson.cookbook_refs_used)}
Check docs/matching-cookbook.md (grep for the mechanism, the instruction pattern, the C construct; docs/cookbook-index.md is symptom-keyed) and decide:
COVERED — an existing § already states this law (name it in covered_by)
ADDENDUM — an existing § is close but this adds a byte-proven refinement (name it; entry_markdown = a 3-8 line addendum paragraph starting "**Addendum (P31 S62 T4, ${t.fn}):**")
NEW — no § states it; entry_markdown = a full section in the book's style, header "## §NNN — <TITLE IN CAPS> (P31 S62 T4; byte-proven ${t.fn})" with NNN left literally as NNN, then the tell, the mechanism, the C shape, the evidence line
ADDENDUM — an existing § is close but this adds a byte-proven refinement (name it; entry_markdown = a 3-8 line addendum paragraph starting "**Addendum (${LABEL}, ${t.fn}):**")
NEW — no § states it; entry_markdown = a full section in the book's style, header "## §NNN — <TITLE IN CAPS> (${LABEL}; byte-proven ${t.fn})" with NNN left literally as NNN, then the tell, the mechanism, the C shape, the evidence line
REFUTED — the claim contradicts a byte-proven § or the evidence does not support it (say why)
Quote the § numbers you checked in why. Read only; write nothing.`,
{ label: `verify:${t.fn}`, phase: 'Verify', model: 'opus', schema: VERDICT }).then(v => ({ ...lesson, ...(v || { verdict: 'UNVERIFIED', why: 'verifier returned null' }) }))
@@ -75,4 +76,4 @@ const out = lessons.filter(Boolean)
const tally = {}
for (const l of out) tally[l.verdict] = (tally[l.verdict] || 0) + 1
log('verdicts: ' + JSON.stringify(tally))
return { lessons: out }
return { label: LABEL, lessons: out }