Files
Drew T 79b7ff2cbf chore(phase-30 S49): wave 7b — adapt lane scaled, 44 banked (92% MATCH->bank); the TU-spread law
- thresholds relaxed to <=6 blocks/<=16 tokens UNION edit-fraction <=0.20: cards 518 -> 721,
  MIXED 310 -> 50 skeletons; the 753-ins func_8017BEBC (0.987 sim) became reachable.
- 59 cards -> 48 agent-MATCH (81%) -> 44 BANKED (92% MATCH->bank, 75% end-to-end), 6.9M tok.
- FINDING (the actionable one): 7b's bank rate crushed 7a's because it SPREAD 48 drafts over 35
  destination TUs; 7a's failures were per-TU declaration collisions between sibling drafts.
  Cookbook §169 updated with the spread law.
- R22 213/213 BYTE-IDENTICAL from clean; fleet 96.55% fn / 95.2% instr / 89.9% distinct;
  stubs 12,584 -> 12,535; dedup 2,035/0.
- incidents 3 & 4 recorded: an agent wrote a TRACKED header (guard caught it, prose is not
  enforcement); my own gate_lane filtered on the wrong key and printed 'gating 0 drafts' as a
  result (R32 silent skip) — fixed with a coverage assertion that refuses to report 0.
2026-08-13 11:50:04 -06:00

161 lines
9.3 KiB
JavaScript

export const meta = {
name: 'p30-s49-adapt-wave',
description: 'Micro-adapt wave: edit a proven matched body onto a near-identical cousin (LI-ONLY / SMALL-EDIT cards)',
phases: [
{ title: 'Adapt', detail: 'cheap-tier seeded edits — the diff names the <=6 tokens that change' },
{ title: 'Verify', detail: 'independent re-gate + existence proof, sha1 re-checked LAST' },
],
}
// ⚠ TEMPLATE-LITERAL MINEFIELD (same as crack_wave.js): prompts are backtick strings — a markdown
// backtick or a literal dollar-brace in prose TERMINATES the string and the workflow dies at parse
// time. Use "double quotes" for inline code in prose, escape any literal \` and \${.
//
// Cards come from: tools/family_cousins.py --adapt-cards -> .run/adapt_cards.json
// Each card: {name, binary, addr, nins, sub, reach, jr, klass, sim,
// seed:{name,binary,addr,nins,kind,path}, diff:[{kind,member_at,member_words,
// member_disasm,seed_at,seed_words,seed_disasm}], n_blocks, n_tokens}
// Invoke: Workflow({scriptPath:"tools/wave/adapt_wave.js", args:[{wave:"wave7a"}, ...cards]})
const REPO = '/home/musashi/bfm-decomp'
const ALL = typeof args === 'string' ? JSON.parse(args) : args
const CARDS = ALL.filter(c => c.name && c.seed)
const WAVE = (ALL.find(c => c.wave) || {}).wave || 'wave7a'
// The edit is small regardless of body size, but a long body copy still wants the mid tier.
const modelFor = c => (c.nins <= 60 ? 'haiku' : 'sonnet')
const ADAPT_SCHEMA = {
type: 'object',
required: ['fn', 'verdict', 'draft_path', 'notes'],
properties: {
fn: {type: 'string'},
verdict: {type: 'string', enum: ['MATCH', 'NEAR', 'FAIL']},
draft_path: {type: 'string'},
diff_ins: {type: 'integer'},
sha1: {type: 'string', description: 'sha1sum of the draft you are leaving on disk'},
notes: {type: 'string', description: 'the exact edit you made (constant/statement), the symbol renames, integration surface; exact residual if NEAR'},
},
}
const VERDICT_SCHEMA = {
type: 'object',
required: ['fn', 'confirmed', 'evidence'],
properties: {
fn: {type: 'string'},
confirmed: {type: 'boolean'},
file_exists: {type: 'boolean'},
sha1_after: {type: 'string', description: 'sha1sum re-run LAST, after the gate'},
evidence: {type: 'string'},
},
}
// The diff blocks can be inlined on the card (small slates) OR left on disk and read by the agent
// (large slates — keeps the orchestrator's args, and its context, small). `slate` + `idx` select
// the card; the agent prints it with one command.
function diffBlock(c) {
if (c.diff && c.diff.length) {
return c.diff.map((d, i) =>
` block ${i + 1} [${d.kind}] at member instr ${d.member_at} / seed instr ${d.seed_at}:
member words: ${d.member_words.join(' ')} (${d.member_disasm.join(' | ')})
seed words : ${d.seed_words.join(' ')} (${d.seed_disasm.join(' | ')})`).join('\n')
}
return ` (${c.n_blocks} block(s), ${c.n_tokens} token(s)) — PRINT YOUR CARD FIRST, it holds the
exact sites with both sides' words and disassembly:
cd ${REPO} && python3 -c "import json;c=json.load(open('${c.slate}'))[${c.idx}];print(json.dumps(c['diff'],indent=1))"`
}
function adaptPrompt(c) {
const seedWhere = c.seed.kind === 'macro'
? `${c.seed.path} — the body is the DEFINE_${c.seed.name}() macro (read it there; it is the byte-proven body)`
: `${c.seed.path} — the plain definition of ${c.seed.name}`
return `You are ADAPTING a proven byte-matched body onto a near-identical sibling function — Brave Fencer Musashi matching decomp (PS1, gcc-2.7.2 + maspsx). Repo: ${REPO}. This is an EDIT task, not a from-scratch crack.
TARGET: ${c.name} in ${c.binary} — ${c.nins} instructions${c.jr ? ' (mid-function jr / jump table)' : ''}. Asm truth: ${c.sub}/${c.name}.s
SEED (${(c.sim * 100).toFixed(1)}% skeleton-similar, already byte-matched as ${c.seed.name} in ${c.seed.binary}): ${seedWhere}
THE MEASURED DIFF — the target differs from the seed at exactly ${c.n_blocks} site(s), ${c.n_tokens} instruction(s) total (class ${c.klass}):
${diffBlock(c)}
METHOD:
1. Read the seed body. Copy it to ${REPO}/.run/${WAVE}/${c.name}/${c.name}.c as the definition of ${c.name}.
2. RENAME the symbol surface to the TARGET's own: every callee "func_XXXXXXXX", data "D_XXXXXXXX",
and jump-table label must come FROM THE TARGET .s (${c.sub}/${c.name}.s) — the .s is the symbol
ground truth. Do not keep the seed binary's symbols. Positional correspondence: the bodies align
1:1 outside the diff blocks above.
3. Apply the minimal SEMANTIC edit the diff implies. An insert/delete of lui/ori/addiu is almost
always a changed CONSTANT — decode the target's words above (lui hi16 + ori/addiu lo16) and use
the TARGET's value in the C literal. An inserted load/store or branch token usually means one
added or removed statement/condition. Make the smallest edit that explains the diff.
4. Iterate: cd ${REPO} && python3 tools/match_one.py ${c.name} --c .run/${WAVE}/${c.name}/${c.name}.c --asm-subdir ${c.sub}
5. On MATCH: sha1sum the draft, report it, STOP. If two honest attempts leave a residual, report
NEAR with the exact remaining diff — NEAR is a resumable state, not a failure.
OUTPUT DISCIPLINE (a previous wave lost 21 verified drafts to a shared directory):
- Your ONLY output dir is ${REPO}/.run/${WAVE}/${c.name}/ — create it, work there.
- Final draft MUST be exactly ${REPO}/.run/${WAVE}/${c.name}/${c.name}.c
- NEVER delete, move or modify ANY file outside your own directory. Sibling agents are working in
adjacent directories RIGHT NOW. Do NOT clean up at the end.
- Never write to src/, config/, include/ or any tracked file. No make. No git.
RULES: gcc-2.7.2 emits no "error:" prefix — read the raw compiler text. match_one MATCH is a
CANDIDATE (cookbook §52b) — state your integration surface in notes: which callees/globals you
declare and whether the host TU declares them differently (§161c). Match register allocation
exactly (G3). If the seed body is a DEFINE_ macro, expand it to a plain function definition in your
draft — do not emit a macro.`
}
function verifyPrompt(r, c) {
return `ADVERSARIAL verification, Brave Fencer Musashi decomp. Repo: ${REPO}.
${r.fn} (${c.binary}, ${c.nins} ins, ADAPTED from ${c.seed.name}) claimed MATCH at ${r.draft_path}; claimed sha1 ${r.sha1 || '(none)'}.
REFUTE it. Default confirmed=false. In this order:
1. "ls -l" and "sha1sum" the draft — it must EXIST.
2. Gate it yourself: cd ${REPO} && python3 tools/match_one.py ${r.fn} --c ${r.draft_path} --asm-subdir ${c.sub}
("FAIL: no C file" means the draft is MISSING — that is a refutation, never a pass.)
3. The draft must DEFINE ${r.fn} as a plain function (no INCLUDE_ASM, no macro emission, no inline
asm standing in for compiled code), and its symbol surface must come from the TARGET .s, not the
seed binary (spot-check two callee/data names against ${c.sub}/${r.fn}.s).
4. Re-run "sha1sum" LAST, after everything else, and report it as sha1_after. If the file vanished
or changed since step 1, say so loudly.
Do NOT edit or delete anything. Report failures; do not fix them.`
}
phase('Adapt')
const results = await pipeline(
CARDS,
c => agent(adaptPrompt(c), {label: `adapt:${c.name}`, phase: 'Adapt', model: modelFor(c), schema: ADAPT_SCHEMA})
.then(r => (r ? {...r, c} : null)),
r => {
if (!r || r.verdict !== 'MATCH' || !r.draft_path) return r
return agent(verifyPrompt(r, r.c), {label: `verify:${r.fn}`, phase: 'Verify', schema: VERDICT_SCHEMA})
.then(v => ({...r, verdict_check: v}))
}
)
const rows = results.filter(Boolean)
// A DEAD ORACLE IS NOT A NEGATIVE VERDICT (P30 S48): UNVERIFIED is its own outcome — re-verify,
// never bank, never discard.
const claimed = rows.filter(r => r.verdict === 'MATCH')
const unverified = claimed.filter(r => !r.verdict_check)
const confirmed = claimed.filter(r => r.verdict_check && r.verdict_check.confirmed
&& r.verdict_check.file_exists !== false)
const refuted = claimed.filter(r => r.verdict_check && !confirmed.includes(r))
const near = rows.filter(r => r.verdict === 'NEAR')
const fail = rows.filter(r => r.verdict === 'FAIL')
log(`adapt wave: confirmed ${confirmed.length} / refuted ${refuted.length} / UNVERIFIED ${unverified.length}`
+ ` / near ${near.length} / fail ${fail.length} of ${rows.length}`)
return {
counts: {cards: CARDS.length, returned: rows.length, confirmed: confirmed.length,
refuted: refuted.length, unverified: unverified.length,
near: near.length, fail: fail.length},
unverified: unverified.map(r => ({fn: r.fn, binary: r.c.binary, draft_path: r.draft_path,
sha1: r.sha1, note: 'VERIFIER NEVER RAN — re-verify, do not discard'})),
confirmed: confirmed.map(r => ({fn: r.fn, binary: r.c.binary, reach: r.c.reach, nins: r.c.nins,
klass: r.c.klass, seed: r.c.seed.name, model: modelFor(r.c),
draft_path: r.draft_path, sha1: r.sha1, notes: r.notes})),
refuted: refuted.map(r => ({fn: r.fn, draft_path: r.draft_path,
evidence: r.verdict_check && r.verdict_check.evidence})),
near: near.map(r => ({fn: r.fn, binary: r.c.binary, diff_ins: r.diff_ins, notes: r.notes,
draft_path: r.draft_path})),
fail: fail.map(r => ({fn: r.fn, binary: r.c.binary, notes: r.notes})),
}