mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-28 06:49:47 -04:00
dcba5d0f4f
- family_cousins.py --adapt-cards: per seeded-unit member, drift classified vs the seed (LI-ONLY 27 / SMALL-EDIT 491 / MIXED 310 excluded); cards carry the seed C location + the aligned diff blocks with the member's raw words + disasm (the new constant is readable in the card). 518 cards / 1,101 instances / 23,820 ins; 514 haiku-band. - tools/wave/adapt_wave.js: the EDIT-contract wave (crack_wave contracts preserved: per-agent dirs, sha1-last, UNVERIFIED != refuted); symbol surface from the TARGET .s; haiku<=60/sonnet. - regen chain absorbed the 48 lane-A banks (A-prop open ins -6,475 == the report's instr delta exactly — two independent derivations agree); pilot slate .run/wave7a_pilot.json (30). - R37: pilot before scaling to the 518-card pool.
153 lines
8.8 KiB
JavaScript
153 lines
8.8 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'},
|
|
},
|
|
}
|
|
|
|
function diffBlock(c) {
|
|
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')
|
|
}
|
|
|
|
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})),
|
|
}
|