mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
79b7ff2cbf
- 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.
161 lines
9.3 KiB
JavaScript
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})),
|
|
}
|