Skip to content

Use prose examples the cartridge agrees with, and make the gate test prove work - #2074

Merged
andrewboudreau merged 2 commits into
mainfrom
tools/stable-prose-examples
Aug 31, 2026
Merged

Use prose examples the cartridge agrees with, and make the gate test prove work#2074
andrewboudreau merged 2 commits into
mainfrom
tools/stable-prose-examples

Conversation

@andrewboudreau

Copy link
Copy Markdown
Collaborator

Re-cut of #2070. Right diagnosis, wrong substitute — and today's red main proves the diagnosis.

#2070 has been sitting at the same head for nine hours with the ask unanswered, so I have re-cut it rather than let a correct idea rot. It should be closed in favour of this. Full credit for the finding is its; I only changed which header it points at, and added the assertion that makes the choice self-guarding.

The premise is correct, and it went off today

check_dead_references resolves any a/b-shaped token in a comment or docstring as a live repo-rooted path. So a class-specific path hard-coded in tool prose is a reference, and it dies when the class is renamed. That is exactly right.

It is also not hypothetical. main went red on dead references two hours ago (6d8f8e529, fixed in #2073) because a docstring in tools/test_tubuild.py named src/func_ov006_020f8224.c and #2071 promoted that file into a TU. Same mechanism, different trigger.

But the substitute has the same defect

#2070 replaces include/Amp.h with include/Camera.h. Both are coined names the cartridge contradicts. From tools/rtti_name_audit.py --class, which walks _ZTV-4 → _ZTI → _ZTS in the ROM images:

dActor_c   _ZTV8dActor_c @ 0x0208e3a4 (arm9)   ROM name 8dActor_c    AGREES
Camera     _ZTV6Camera   @ 0x02086f84 (arm9)   ROM name 9dCamera_c   DISAGREES
Amp        _ZTV3Amp      @ 0x02123278 (ov070)  ROM name 7daBrq_c     DISAGREES

Both sides of #2070's swap are in the 259-of-541 coined set, and both are scheduled for rename. The PR's own reason for existing applies to its own fix.

This one uses include/dActor_c.h and include/dScMgBase_c.h. Both agree with the cartridge, and dActor_c is TI[2] of most actor classes — nothing above it can re-key its name.

The part #2070 did not ask for, which matters more

test_a_real_tree_header_still_parses asserted only that the gate returns 0:

self.assertEqual(C.main(["include/Amp.h"]), 0)

A header the parser skipped satisfies that just as well as one it checked:

include/dActor_c.h  rc=0  33 commented fields, 0 mismatched, 0 unparsed, spans 0xd0
include/fBase_c.h   rc=0  skipped -- polymorphic C++ struct, implicit vptr not modelled

fBase_c also agrees with the cartridge, so it passes the naming criterion and is still useless as a test subject — an agreeing name is necessary, not sufficient. And "reported a pass and checked nothing" is the precise failure mode this test file's own module docstring says it exists to catch:

a gate that looks at the wrong (empty) set of files reports a pass exactly as convincingly as one that looks at the right set and finds nothing wrong.

The test had that hole itself. It now reads the gate's stdout and requires a positive checked-field count, so a future substitution into a skipped header fails loudly. I verified both directions: dActor_c.h passes, fBase_c.h would fail.

marker_evidence.py

#2070 rewrote the concrete struct sketch into abstract prose. Only the path could ever go dead, so this keeps the sketch and drops the path, plus a line saying why so it does not get restored:

    A byte-matching migrated file declared, in effect,
    `struct Amp : Actor { ModelAnim m0; /* 0xd4 */ ... }`, so both the offsets
    and the class names in it are pinned by the ROM's own relocations.

    Named by class, not by path, on purpose: check_dead_references resolves any
    `a/b`-shaped token in prose as a live repo-rooted reference, and the file
    this came from is a per-symbol one -- those die on rename AND on TU
    promotion, which folds them into src/actors/<Class>.cpp.

Amp stays as the class name in the sketch because that is the file it was actually read out of and a bare class name is invisible to the gate. Only the path token was ammunition.

Per-symbol files are worth calling out separately: src/_ZN3AmpD1Ev.cpp dies twice over — on rename, and on TU promotion. Promotion deletes those by the dozen (#2071 removed 23 in one go), so it is by far the more frequent killer.

Verified

check_dead_references    no new dead references / no broken markdown links
check_python_names       PASS (262 files, 0 unresolvable)
test_check_header_offsets  20 passed
marker_evidence            imports cleanly

Measured in a toolchain-wired worktree at 1090f44c5. Pre-push ran port-refcheck 405/405, duplicate-sources 11110 stems, src_tu 95/95 compiled.

…prove work

Re-cut of #2070. Its diagnosis was right -- a class-specific path hard-coded
in tool prose is a live reference that check_dead_references resolves, so it
goes red the moment the class is renamed. Its substitute had the same defect.

`tools/rtti_name_audit.py --class`, walking _ZTV-4 -> _ZTI -> _ZTS in the
cartridge:

  dActor_c   ROM 8dActor_c    AGREES
  Camera     ROM 9dCamera_c   DISAGREES -- #2070's replacement
  Amp        ROM 7daBrq_c     DISAGREES -- what #2070 replaced

Both sides of that swap are in the 259-of-541 coined set and both are
scheduled for rename, so #2070 bought nothing. dActor_c and dScMgBase_c
agree with the cartridge and nothing above them can re-key their names.

marker_evidence.py keeps its concrete struct sketch and loses only the
per-symbol path. Rewriting a specific docstring into abstract prose costs
explanatory value for nothing; the path was the only part that could die.
A per-symbol file dies twice over -- on rename, and on TU promotion folding
it into src/actors/<Class>.cpp.

test_a_real_tree_header_still_parses asserted only rc == 0, which a header
the parser SKIPPED satisfies just as well:

  include/dActor_c.h  rc=0  33 commented fields, 0 mismatched
  include/fBase_c.h   rc=0  skipped -- polymorphic, implicit vptr not modelled

The second is the exact "reported a pass and checked nothing" shape this
file exists to catch, and the old assertion passed on it. Now the test reads
the gate's own output and requires a positive checked-field count, so a
future substitution into a skipped header fails loudly instead of quietly
asserting nothing.

check_dead_references clean, check_python_names PASS, 20/20 tests pass.
@tangos-validator

tangos-validator Bot commented Aug 31, 2026

Copy link
Copy Markdown

✅ PR validation — Passed

noverify: no source/build-data changes in this PR

Each changed src/*.c|*.cpp is compiled and its relocated bytes compared to the binary data on a private build box. Passing requires every changed file to reproduce the ROM byte-for-byte with correct relocation targets — this catches WRONG-DEST relocations and non-reproducing near-misses that ledger-scoped linkcheck skips.

Both are pre-existing on main and both resolve today, so no gate is red --
but they are the per-symbol shape that dies twice over, on rename AND on TU
promotion, and one of them is live ammunition right now:

    src/_ZN8PoleLiftD1Ev.cpp   src_tu/actors/PoleLift.cpp already exists
    src/_ZN5Stage11RenderModelEv.cpp

A PoleLift promotion deletes the first one out from under this docstring and
takes main red on dead references, which is exactly how main broke earlier
today at 6d8f8e5 (fixed in #2073).

Named by mangled symbol instead. check_dead_references reads only a/b-shaped
path tokens, so `_ZN8PoleLiftD1Ev` cannot dangle, and the symbol survives both
rename and promotion. The concrete evidence in both comments -- the local
ModelComponents stand-in, and the 0x80/0x158/0x50 padding numbers -- is
unchanged; only the citation spelling moved.
@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Amended: two more doomed paths in marker_evidence.py, one of them live ammunition

While cross-checking this against the other open PR for the #2073 collision shape, I grepped the three touched files for prose paths and found two the original scope missed — both pre-existing on main, both resolving today, so no gate is red:

marker_evidence.py:663   src/_ZN5Stage11RenderModelEv.cpp
marker_evidence.py:903   src/_ZN8PoleLiftD1Ev.cpp

These are the per-symbol shape, which is strictly worse than the coined-name-header shape this PR started out fixing: a per-symbol file dies on rename and on TU promotion, and promotion is by far the more frequent event — #2071 deleted 23 of them in a single merge.

The PoleLift one is not hypothetical. src_tu/actors/PoleLift.cpp exists on main today (its inline-dtor flip landed in #2066; promotion is currently refused only because PoleLift is a coined name, which is a temporary state). The moment that TU is promoted, src/_ZN8PoleLiftD1Ev.cpp is deleted and dead references goes red on main — the same mechanism, on the same gate, as 6d8f8e529 earlier today.

Both are now cited by mangled symbol. _ZN8PoleLiftD1Ev has no a/b token in it, so the gate cannot resolve it and it cannot dangle; and unlike the path, it survives both rename and promotion. The concrete evidence in each comment is untouched — the local ModelComponents stand-in, and the 0x80 / 0x158 / 0x50 padding numbers that make the UPPER BOUND caveat mean something. Only the citation spelling moved.

Re-verified at f70ae701f:

check_dead_references      no new dead references / no broken markdown links
check_python_names         PASS (262 files, 0 unresolvable)
test_check_header_offsets  20 passed
marker_evidence            parses, imports, scan_casts intact

Pre-push: port-refcheck 405/405, duplicate-sources 11110 stems, src_tu 95/95 compiled. CRLF check 0.

One thing I am not bundling: check_dead_references reports 1 baselined reference(s) now resolve — a stale src/game/actors entry in config/dead-reference-baseline.json. It is a real (small) problem, because a baselined entry that resolves would mask a future genuine break at that path. It is unrelated tracked-config churn and belongs in its own PR, so it stays out of this one.

@andrewboudreau
andrewboudreau merged commit 6c7d35b into main Aug 31, 2026
7 checks passed
@andrewboudreau
andrewboudreau deleted the tools/stable-prose-examples branch August 31, 2026 09:56
andrewboudreau added a commit that referenced this pull request Aug 31, 2026
#2075)

Two assertions in tools/test_tubuild.py went stale at #2066 (467bde0),
which moved ~PoleLift() into the class body. One of them was not merely
stale -- it asserted the anomaly #2066 fixed:

    assert "1 ordinal pair(s) NOT in ROM order: [(0, 1)]" in out

An out-of-line destructor makes mwccarm emit D2/D0/D1 with D0 before D1,
opposite the ROM; the pilot report called that non-fixable. Defining the
destructor inline emits exactly the ROM's two variants in ROM order. So
the only way to make this assertion pass by "fixing the code" would be to
revert #2066.

Measured across 467bde0^ -> 467bde0, swapping the shadow source and
the class header together (the pre-#2066 source does not compile against
the post-#2066 header, which is why this surfaced as a failing assert
rather than a quietly wrong number):

  before  37 sections, 8 .text, D2 present, "1 ordinal pair(s) NOT in
          ROM order: [(0, 1)]", 12 unlicensed
  after   35 sections, 7 .text, no D2, "ALL MATCH, ROM order", 11
          unlicensed

Section arithmetic: each vague-linkage symbol that stops being emitted
costs two sections, itself plus its own .rela. That is 43->37 at #1555
(3 symbols) and 37->35 here (1).

Rather than bump the numbers, the stale expectations become regression
guards stated as absences, so reintroducing an out-of-line definition
fails loudly here:

  assert "NOT in ROM order" not in out
  assert "_ZN8PoleLiftD2Ev" not in out
  assert "UNLICENSED function symbols" not in out

The last is an absence because tubuild prints no function block at all
when the count is zero -- asserting "(0)" would never match.

Negative control: reverting both files to 467bde0^ fails the new
assertion at tools/test_tubuild.py:166. The guards have teeth.

No path naming PoleLift appears in the new prose. PoleLift is a coined
name the cartridge contradicts (ROM RTTI: 18daObjKm2_Ami_Bou_c) and the
rename is a prerequisite for promoting this very TU, so such a path is a
dead reference with a scheduled fuse -- see #2074.


Claude-Session: https://claude.ai/code/session_01WdCK1xgrJdiJzPCh3bAJfQ

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant