Restamp mechanically-explained verified.tsv rows to the config spelling - #2039
Conversation
config/**/symbols.txt (what the build links against) and symbols/verified.tsv (the import mirror) disagree on hundreds of addresses, and a name-keyed check across them manufactured phantom crack targets out of matched, enrolled functions on 2026-08-30. The tool enumerates every divergence, classifies each one (class-rename wave, dtor/ctor variant, arg rename, real signature fix, canonical mangling, needs-a-human), answers --where for one address, and can plan or apply a restamp of the mechanically-explained mirror rows. The classifier refuses what it cannot prove: dtor pairs need a method row to corroborate a class rename (dtor bodies are byte-identical across classes), operators and malformed names are refused rather than guessed, and thunk fields, const-ness, and the full nested class path all have to agree. The tests pin the refusals next to the acceptances, and pin the allowlist of tools reading verified.tsv directly.
833 addresses where the records disagree today: 361 with a real name on both sides and no shared spelling, 5 kind mismatches, 382 where config still holds a placeholder, 85 with no config slot at all. Of the 361, the classifier explains 334 mechanically and leaves 27 for a human. Generated by tools/symrecords.py --report.
Names may only be looked up in the record the artifact in hand links against; joins between records happen on address, module-qualified for overlays. Also corrects the note's claim that verified.tsv is left stale on purpose: rows have been restamped as corrections land for a while now, and the divergence surface is banked in symbols/record_divergences.tsv.
✅ PR validation — Passednoverify: no source/build-data changes in this PR Each changed |
…elling Every row whose divergence tools/symrecords.py can fully explain (class-rename wave, dtor/ctor variant fix, argument rename, real signature fix, canonical mangling) now spells the name the way config/**/symbols.txt asserts it. The 27 rows it cannot explain are untouched, as are the placeholder and no-slot rows; the banked report shrinks accordingly. Generated with tools/symrecords.py --restamp --apply, report refreshed with --report. Import-time spellings remain in git history.
d5cb13c to
6390b4a
Compare
Gatekeeper review — MERGE AFTER FIXES (two applied by me, one is a merge-order instruction)The data is correct, complete and conservative. I could not find a data defect. The 334-row restamp is exactly what it claims: a mechanical re-spelling of the mirror to the name The decisive audit was demangling all 334 old and new names and looking for changes the body does not claim: zero unclaimed arity changes, zero unclaimed ctor/dtor variant changes. Every arity or C1/C2/D0/D1/D2 change in the diff is one of the 11 signature-fix or 13 variant rows you declare. An independent re-classification of all 334 rows reproduced your breakdown exactly (248/52/8/8/7/4/2/2/2/1 = 334); the four apparent disagreements were all artifacts of my tokenizer ( Two things I want to call out as genuinely good practice, because they are what made this reviewable:
And the class-rename corroboration rule is properly satisfied. For each of the 22 distinct old→new class pairs I counted restamped rows that are not ctors/dtors and whose demangled member name and argument list are identical across the rename — minimum 1, no pair had zero. That is the #774 lesson honoured: a compiler-generated destructor body is byte-identical across unrelated classes, so a lone diverging dtor pair is not evidence of a rename. Every dtor-based rename here is backed by ordinary method rows. No old class maps to two new ones ( Fixed — 1. The body's flagship signature-fix example did not existThe body cited The tree's only I have edited the body to use Fixed — 2. The provenance note goes stale the moment this merges
After this lands the mirror agrees with config at both, so the paragraph describes a divergence that no longer exists. The note is correct as of #2038 and wrong as of #2039, so the fix belongs here, not there — an agent is swapping in two of the 27 held-back DIVERGED rows, which stay true permanently. Instruction — 3. Merge #2038 with a merge commit, NOT a squashMeasured both ways:
A squash creates that file under a new blob identity while #2039 also adds it. The repo's history is merge commits so the default is already right; this only matters if someone reaches for the squash button. Your body's Two findings that are not yours to fix, but that I am recording
with OUT.open("w", encoding="utf-8") as f: # OUT = symbols/verified.tsv
for addr in sorted(theirs): ...
print(f"wrote {OUT.relative_to(REPO)} ...")
if not args.apply:
print("\n(dry run) re-run with --apply to write renames.")
returnSo No CI gate can see a One judgement call I am making knowinglyThe pre-restamp spellings survive only in git history — Three of the eleven signature fixes rest on judgement, not on the function's own bytesStating these plainly because the ROM contains no mangled names, so nothing in the cartridge can prove
None of these three decisions is made by this PR. All eleven already live in What I verifiedCross-overlay collision — no unflagged ambiguity. Exactly 3 of the 334 addresses are claimed by 2+ modules, and they are exactly the 3 you name. In each, the other module's entry is a machine-generated placeholder, and note the differing sizes and the function/data kind split — genuinely different code at an overlay-aliased address, which a flat mirror can only carry one of:
Building Nothing downstream breaks. Exactly two readers of Merge-result gating — no gate differs. Both trees exported and re- No Body fixed, note fix in flight. Once #2038 clears its own review this merges right behind it — retarget first, merge commit, no branch deletion. |
The paragraph cited 0x0203562c and 0x0200651c, both of which this branch restamps -- the examples would read as agreeing rows the moment it lands. Swap in two addresses the classifier holds back as needing a human, so they stay divergent: 0x02043444 (fBase_c vs ActorBase) for the plain case and 0x02023598 (BootScene D1 vs Scene D2) for class-and-variant at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
… map
The 21 tests this PR added never ran. .github/workflows/tool-tests.yml
enumerates its modules on purpose -- a glob would silently absorb a
self-skipping toolchain suite -- and test_symrecords was not on the list,
so run 33329528573 printed "Ran 312 tests", exactly the count on
unmodified main. The path filter fired; there was just nothing new to run.
Adds tools.test_symrecords to the run: list and to the trailing table, and
moves the header's own count. That count is documented as "the number to
compare against if this job's total ever moves", so it is measured, not
computed: the enumerated invocation now reports "Ran 336 tests ... OK".
The module contributes no skips (it reads only tracked text and imports
nothing outside the stdlib), so the expected-skip paragraph goes 306/312
-> 330/336. The module census in the same header goes 57 -> 58, stale
since this PR added the file.
class_rename_map() iterated every config entry at an address with no
module filter, while compare() refuses to explain a row where two modules
both assert a real name ("ambiguous-module"). An address the report
refuses to draw a conclusion FOR should not quietly supply evidence to
conclusions ABOUT other addresses -- that evidence is invisible to a
reviewer reading the report, because the row it came from is refused.
Same floor now applies in both places. On the live tree this changes no
conclusion: the class map is 109 pairs before and after, byte-identical,
and symbols/record_divergences.tsv regenerates with zero lines of diff.
Adds SharedOverlaySlotIsNotAutomaticallyAmbiguous, which pins the guard
in BOTH directions so the next reader does not "fix" it. The tempting
change is to widen ambiguous-module to "more than one module has an
entry" -- that would refuse three provably right restamps (0x020ada40,
0x020aed98, 0x020ee55c), where the colliding overlay's slot is still a
func_/data_ placeholder and the mirror's own old name identifies the
class. Those three are recorded in the docstring rather than asserted,
because #2039 restamps them and the rows then stop existing; a pin on the
addresses would be a test that deletes itself.
Two doc corrections from review: notes/symbol-name-provenance.md now
carries the caveat symrecords.py:14-17 states properly, that a mirror row
keeps its import-time spelling when no correction touches it -- the table
row read as though every row tracks config. And the allowlist test's
docstring narrows "nothing new reads verified.tsv at all" to "nothing new
in tools/", since its glob is tools/*.py non-recursively and cannot see
tools/archive/*.py or any reader outside tools/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ
Audited the 334-row restamp against the cartridge's own config. MERGE.A name-only restamp of The delta is mechanically clean
So: no address moved, no record was added or dropped, every new spelling is the one It also recomputes the mangled length prefixes correctly — What it actually is: coined → ROM normalization, 35 classesThe 12 rows where the class name is unchanged ( The one thing I want on the record: this swaps D1/D2, and that is correct41 of the 334 rows are destructors, and several swap the destructor variant: while others ( I checked each swapped address against config individually. Config already carries the The swap was settled on Blast radius
Stated gapI did not run One note for the reader, not a blockerThis PR adds Re-verified after main moved to |
Stacked on #2038 (the tool that generated it); review this one for the data, that one for the code. Merging: land #2038 first, then retarget or merge this; do not close it with a --delete-branch of the base.
What this is
The 334 mirror rows whose divergence from config the classifier fully explains, restamped to the spelling config asserts. Breakdown by verdict:
_ZN8CapEnemyD2Ev->_ZN11dCapEnemy_cD1Ev)Clipper::Func_020156DC()->Clipper::Func_020156DC(int, unsigned short, int, int)). Worth the closest read — three of the eleven rest on judgement rather than the function's own bytes: 0x020ada40's thirdFix12<int>is unused and argued from call sites, 0x020ae2b8'sFix12<int>->intis ABI-identical so the ROM cannot arbitrate, and 0x0203c6ac's signedness rests on abltin a different function plus family consistency. None of those three decisions is made by this PR — all eleven already live in config, backed by byte-matchingsrc/definitions; this PR only stops the mirror contradicting them.PKcPKcPKcivs the compiler'sPKcS1_S1_i)What it deliberately does not touch
The 27 rows the classifier cannot explain: the _ZThn80 Animation thunks, BootScene/FaderColor dtor identities, Spawn-vs-ctor naming debates, and every malformed or operator name. They stay divergent in the banked report for human eyes.
Three rows to eyeball
Cross-overlay addresses (flat mirror, two modules): 0x020ada40 (ov002/ov004, the KillByInvincibleChar find from the provenance note), 0x020aed98 (ov002/ov007), 0x020ee55c (ov002/ov007). In each case the one real config name at the address is what gets stamped.
No src/ files move, contributions.json is untouched, and dtor class pairs were never inferred from dtor rows alone (byte-identical bodies, the #774 lesson).