Files
BFM-decomp/tools/ghidra_roster.py
T
Drew T 936d7d741c feat(phase-33): B5 Ghidra regenerability PROVEN — the RE work as text (config/ghidra/*.jsonl + ROSTER.md), six programs rebuilt from disc + symbol files + that file with PROOF PASS
- ImportAnnotations.java: the S86 OSGi-bundle blocker was 3 javac errors (Long->int unboxing x2, a nonexistent
  LocalVariableImpl ctor -> VariableStorage); "/undefined" resolves to DataType.DEFAULT (it lives in neither type
  manager — main's first proof passed the cmp with failed=13 because the plate-comment rows had set the same function
  comments); ghidra_rebuild.sh now dies unless the import printed failed=0 (R49), writes .proof markers
- ghidra_annotations_delta.py: analysis drift measured and encoded as three counted classes — Error/Analysis bookmarks;
  auto-named DEFAULT functions the rebuild did not create (29 in main's LINKED regions); auto-named rows lagging the curated
  symbol file (10 sep8 + 5 aug31, R15). Result: main 38 hand-authored rows (13 annotated fns incl. 3 the ELF does not
  define, 22 comments, 3 labels); resident/overlays/protos container rows only; the DB holds no hand-authored types
- controls (R39): mutated block row -> PROOF FAIL; synthetic comment/bookmark/label/signature round-trip -> PROOF PASS
  twice (idempotent); the filter keeps the synthetic rows and a hand-renamed name-only diff; fake failed=2 refused,
  resident re-proven; roster --check controls both ways
- proofs, all PASS failed=0: resident 65s, ov_SC01_077 169s, ov_SC06_018 173s, SLUS_007.26 210s, sep8 202s, aug31 206s
- tools/ghidra_roster.py -> config/ghidra/ROSTER.md (--check in tools-health, ignores the per-machine proof column)
- .claude/settings.json hooks $CLAUDE_PROJECT_DIR-relative; ghidra_mcp_start.sh is a silent exit 0 without Ghidra or
  the project (both controlled); SETUP P33 B5 section + 5 inventory rows + §2.8 (R21); CURRENT_PHASE log + checkpoint
2026-09-06 20:32:35 -06:00

124 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""ghidra_roster.py — config/ghidra/ROSTER.md from the committed Ghidra annotation files (P33 B5).
tools/ghidra_roster.py # writes config/ghidra/ROSTER.md
tools/ghidra_roster.py --check # exit 1 if ROSTER.md is stale (for tools-health)
One row per config/ghidra/<program>.jsonl: the program's kind (PS-X EXE via the PSX loader, or a raw
blob at its vram), its payload and vram as the build registers them (`make -s print-EXE / print-VRAM_BASE`
— the same source tools/ghidra_rebuild.sh imports from, R33), the memory blocks the file pins, the
hand-authored row census (everything that is not a `program`/`block`/`archive` container row), and the
last `--proof` verdict recorded by tools/ghidra_rebuild.sh in .run/ghidra_rebuild/<program>.proof
("unproven here" when that scratch file is absent — a fresh clone has not run the proof yet).
The 123 other programs of the live project carry no hand-authored rows and are regenerated on demand
(tools/prefetch_fleet.py); they are not in the roster on purpose.
"""
import collections
import datetime as dt
import json
import pathlib
import subprocess
import sys
REPO = pathlib.Path(__file__).resolve().parent.parent
CONF = REPO / "config" / "ghidra"
SCR = REPO / ".run" / "ghidra_rebuild"
CONTAINER = {"program", "block", "archive"}
# The PS-X EXE programs (the PSX loader, no vram argument) — mirrors the case table in ghidra_rebuild.sh.
EXES = {
"SLUS_007.26": ("main", "extracted/retail/SLUS_007.26"),
"sep8_SLUS_007.26": ("proto-sep8", "extracted/proto/sep8_SLUS_007.26 (prototype disc)"),
"aug31_USA_DEMO.EXE": ("proto-demo", "extracted/proto/aug31_USA_DEMO.EXE (prototype disc)"),
}
def make_print(var, alias):
r = subprocess.run(["make", "-s", f"print-{var}", f"BINARY={alias}"], cwd=REPO, capture_output=True, text=True)
return r.stdout.strip() if r.returncode == 0 else "?"
def row_for(path):
prog = path.stem
kinds = collections.Counter()
blocks = []
image_base = fmt = "?"
for ln in path.read_text(encoding="utf-8").splitlines():
if not ln.strip():
continue
o = json.loads(ln)
kinds[o["k"]] += 1
if o["k"] == "program":
image_base, fmt = o.get("image_base", "?"), o.get("format", "?")
elif o["k"] == "block":
blocks.append(o)
if prog in EXES:
kind, payload = "PS-X EXE", EXES[prog][1]
vram = "header"
else:
kind, payload, vram = "raw blob", make_print("EXE", prog), make_print("VRAM_BASE", prog)
hand = {k: v for k, v in kinds.items() if k not in CONTAINER}
census = ", ".join(f"{k} {v}" for k, v in sorted(hand.items())) or "none (container rows only)"
proof = SCR / f"{prog}.proof"
verdict = proof.read_text(encoding="utf-8").strip() if proof.exists() else "unproven here"
# the loaded (initialized) blocks by name+range; the loader's hardware/uninitialized blocks only as a count
loaded = [f"{b['name']} {b['start']}–{b['end']}" for b in blocks if b.get("init")]
other = len(blocks) - len(loaded)
blk = "; ".join(loaded) + (f" (+{other} uninit/hardware)" if other else "")
return f"| `{prog}` | {kind} ({fmt}) | `{payload}` | {vram} | {blk} | {sum(hand.values())} — {census} | {verdict} |"
def render():
files = sorted(CONF.glob("*.jsonl"))
lines = [
"# config/ghidra/ROSTER.md — the Ghidra programs whose hand-authored RE work is tracked as text",
"",
"> Generated by `tools/ghidra_roster.py` — never edit by hand. One row per `config/ghidra/<program>.jsonl`",
"> (the delta format of `tools/ghidra_scripts/ExportAnnotations.java`). Rebuild any row's program from the",
"> disc + this file with `tools/ghidra_rebuild.sh <program>`; prove it with `--proof` (P33 B5). The binary",
"> Ghidra project embeds the game's bytes and left git at P33; these files are what R20 backs up instead.",
"",
"| program | kind | payload | vram | blocks | hand-authored rows | last proof (this machine) |",
"|---|---|---|---|---|---|---|",
]
lines += [row_for(p) for p in files]
lines += [
"",
f"{len(files)} programs tracked. The live project's other programs (the location overlays and `md_*` modules,",
"129 on disk at P33) hold no hand-authored rows and are regenerated on demand by `tools/prefetch_fleet.py`.",
"Proof verdicts come from `.run/ghidra_rebuild/<program>.proof` (scratch; absent on a fresh clone).",
"",
f"_Generated {dt.date.today().isoformat()}._",
"",
]
return "\n".join(lines)
def main(argv):
out = CONF / "ROSTER.md"
text = render()
if "--check" in argv:
cur = out.read_text(encoding="utf-8") if out.exists() else ""
# the proof column and the date are per-machine; --check compares everything else (a fresh clone
# has no .run/ghidra_rebuild/*.proof and must not read as stale)
def strip(t):
out = []
for l in t.splitlines():
if l.startswith("_Generated"):
continue
if l.startswith("| `"):
l = l.rsplit("|", 2)[0]
out.append(l)
return "\n".join(out)
if strip(cur) != strip(text):
print(f"ghidra_roster: {out} is STALE — run tools/ghidra_roster.py", file=sys.stderr)
return 1
print(f"ghidra_roster: OK ({len(list(CONF.glob('*.jsonl')))} programs)")
return 0
out.write_text(text, encoding="utf-8")
print(f"ghidra_roster: wrote {out} ({len(list(CONF.glob('*.jsonl')))} programs)")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))