The brief's rule is to commit reference data beside the finding so the
port can be built without a disc. Nothing had ever checked that the docs'
cited artifacts actually exist. doc_link_check.py walks every markdown
file under docs/, resolves each relative link, and reports targets that
are missing -- and separately targets that resolve to a ZERO-BYTE file,
which looks fine in any listing.
links resolving 1038 -> 1049
missing targets 16 -> 5
empty targets 0 -> 0
+11 resolving and -11 missing against 11 edits: the counts pair, which is
the confirmation the pass did what it claimed and touched nothing else.
Two of the sixteen were the evidence for the UI layout decode itself.
structures/ui-rat-layout.md is what the port is built on, and its two
figures -- backing "the tutorial PAUSE menu rebuilds pixel-accurately
from its sprites" and "the same method reproduces the main menu" -- were
written as captures/ui-layout/... from a file in structures/, one
directory too shallow. The headline evidence for the decode could not be
opened from its own document.
Eleven links had the wrong relative depth with the target present. Each
was rewritten only where exactly one candidate path resolved, so nothing
was guessed; the first pass left three alone because equivalent spellings
(captures/../captures/x) failed to collapse, and a second pass normalised
them.
Five remain genuinely absent and are left rather than invented: two point
at MEMORY.md outside the repo, one at a header in the separate
xenia-canary-native tree, and two name documents that were never written
(weapon-datasheet-runtime.md, canary-build-verified-env-confound.md).
None is port-relevant. A missing document is a different problem from a
bad path and is not something a link fix should paper over.
Ran this file's own transition filter on a guarded Stage 02 run (stage asserted),
reading the HUD from a crop taken at the same instant as each memory sample:
scan at HUD 004 -> 41537 candidates; filter at HUD 008 -> 8; verify across the
008 -> 012 transition, which was NOT selected on -> exactly ONE survivor.
That survivor, 0xbdb69668, tracked 4 -> 8 -> 12 against the HUD's 004 -> 008 ->
012. The other seven collapsed into noise at the first unselected transition,
which is precisely what that rule exists to catch.
The file's "try 0xbdb59668 first, re-scan when it reads 0" rule worked verbatim:
it read a hard 0 here, and the re-scan cost about the predicted five minutes.
The ...9668 page-offset pattern is REFINED, not reinstated: the three located
addresses (0xbdb49668, 0xbdb59668, 0xbdb69668) are three ADJACENT 64 KB pages at
one offset, and in this run exactly one of 8192 probed pages held 12 -- the
counter -- making it a one-step lookup. But the 2026-08-26 refutation stands as
measured (zero ...9668 VAs held the HUD value in that run), so this is a fast
heuristic to be HUD-checked, not a law.
The file proposed that the counter lives at a fixed offset inside an allocation
whose base moves by whole 64 KB pages, and stated the test itself: "a third scan
should again land on ...9668". Ran it on a fresh guarded Stage 02 run.
Probing all 8192 pages of the form 0x????9668 across 0xa0000000-0xbfffffff: with
the HUD at 004, exactly two VAs held 4 (0xbc3f9668, 0xbe3f9668); with the HUD at
012, ZERO held 12. Both candidates also failed the file's own transition rule --
over 252 s 0xbc3f9668 held a flat 4 and 0xbe3f9668 flickered 4/0 while the HUD
went 004 -> 012.
Both arms were sampled at the same instant (cropped HUD digits beside each memory
read), after a stale-screenshot comparison earlier in this session produced a
spurious 13-vs-004 mismatch.
Scope kept narrow: this refutes the page-offset prediction, not the confirmed
finding that 0xbdb59668 carries the counter in some runs. The counter's address
in THIS run remains unknown -- no transition filter was run.
The section I added last iteration claimed to be testing an untested 🟡 and
concluded "the address is run-dependent, always re-derive". Both halves were
wrong, and the evidence was already further down the SAME file:
- cross-run stability was not untested -- the body records 0xbdb59668 carrying
the counter in 3 of 5 measured runs and reading a hard 0 in the other two. The
stale claim was in the status line at the top, which I took at face value.
- "always re-derive" is worse advice than the rule the file already gives: try
0xbdb59668 first, check it against the HUD, re-scan only when it reads 0.
What my run genuinely adds is a SIXTH data point with a new failure mode: the
address read neither the counter nor 0 but 95748078, constant over four samples.
Status line rewritten to match the body (🟡 recurs but not universal), so the
document no longer contradicts itself.
The doc carried a 🟡 saying cross-run stability was untested. Tested now on a
fresh guarded Stage 02 run (stage asserted): the HUD reads "Remaining OB : 004"
while RAM at the documented 0xbdb59668 reads 95748078, constant over four samples
12s apart. Not 4, not near 4, not moving. So the address belongs to that run's
heap, as the corpus's own heap-reallocation warning predicts.
No constant-shift shortcut either: a BE u32 equal to 4 occurs 1654 times within
+-1 MB of the old address and 11202 times within +-16 MB, far too many to isolate
without the transition filter. The durable result is the METHOD (ob_scan.py:
scan at one value, filter against live memory at a DIFFERENT value), not the
number.
Also fixes a contradiction in INDEX.md, which said in one row that the address is
"still ❔" while another row linked the doc that had already CONFIRMED it.
With the per-entity searches refuted at word and bit level, the question became
which object owns the counter. Sampling +-0x200 around it across a 4->8
transition: the control interval moved 0 of 256 words, and the step moved nine -
the counter plus four words holding ASCII '4' -> '8' NUL-padded, and four
pointers into 0xbcad2xxx that swap with them. Read live at HUD 008, all four
character slots hold '8'.
So the neighbourhood is the HUD's rendered text for this counter, which reframes
the address: it is the HUD widget's value rather than "the mission's own
objective counter" as this file called it.
Recorded against that, because it is already measured: there is no separate
mission-side copy moving on the same step. ob_hunt scans all of guest memory and
requires a match across two transitions, and it left exactly one address.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
One filter to select (004 -> 008, 35897 -> 7), a second on a transition it was
NOT selected by (008 -> 012, 7 -> 1), leaving exactly one address; then three
live paired readings, RAM/screenshot/RAM, all agreeing with the HUD.
The address is the same one run 1 reported. That does NOT reverse yesterday's
refutation and the entry says so explicitly: runs 2 and 3 read a hard 0 there on
an allocated page while the HUD counted, so "it is there in every run" stays
refuted. What is withdrawn is the stronger claim that the number was meaningless
- it recurs exactly, in 2 of the 4 runs measured, and run 3's amber candidate
0xbdb49668 sits one 64 KB page below it at the identical page offset 0x9668. The
practical rule is therefore: try 0xbdb59668, check it against the HUD, re-scan
when it reads 0.
Why run 3 failed and run 4 did not is also recorded, because it is a method
lesson rather than luck: the evidence was always in the first four minutes of the
stage, and the earlier runs simply could not look often enough - every HUD
reading cost a human round trip, so the 008 step went by between two of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
With the launcher repaired the scan could be run the way the method asks: the HUD
was confirmed at 004 immediately before AND after the 0.9 s scan, and the filter
was taken at a confirmed 012. That leaves 17 candidates from 39596.
One of them is 0xbdb49668 - the run-1 address minus exactly 0x10000, at the same
page offset 0x9668. Checked rather than admired: of all 39596 words holding 4 at
scan time, exactly TWO sit at that page offset, so a survivor landing there by
chance is a ~5e-5 event. The reading it suggests is testable - the counter sits
at a fixed offset in an allocation whose base moves by whole 64 KB pages.
Recorded as amber, not green, and the reason is written down: the later sample
that killed 10 of the 17 was taken during the GAME OVER flash, with no HUD on
screen, so the seven survivors are unrefuted rather than confirmed. The mission
ended because the escorted ACROPOLIS was lost at ~13 min while pilot.py sat in
EVADE/RETIRE; REMAINING OB held at 012 for the ten minutes before that, so there
was no transition to verify against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
0xbdb59668 reads 0 in two independent Stage 02 runs while the HUD counts
004 -> 008 -> 012. Not an unmapped read: SEEK_DATA at that offset returns the
offset itself and the next hole is 5 MB later, so it is an allocated,
zero-filled word. The address was a per-run artefact, exactly as that file
already suspected it might be; the method is the durable result.
Re-finding it in the new run also failed, and both failures are recorded because
they are the instructive part. Two candidates were produced and both died on the
corpus's own rule -- verify across a transition you did not select on:
0xbc2377dc went 12 -> 18 while the HUD stayed 012 and read 3 two minutes later,
and 0xbd295b04 was plain noise.
One correction to the method note in that file: the scan is not slow. Over the
live /dev/shm image it takes 0.9 s. The real trap is that REMAINING OB climbs
004 -> 012 within about four minutes as waves spawn, so a scan is only valid if
the HUD is confirmed to hold the same value immediately before AND after it --
which is why the earlier 4-then-8 intersection came back empty.
What blocked finishing: with pilot.py retired at hull 340/1500 nothing was
killing objectives and the counter sat at 012 for five minutes, so there was no
later transition to filter on. What the counter counts, and whether an OB-badged
entity carries a flag, is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The mission's objective counter is a big-endian u32 at guest VA 0xbdb59668 in a
Stage 02 run on the upstream baseline. It was selected on an 18->24 transition
and then tracked 24->23->22 against the HUD on its own - four readings, two
changes it was not filtered on.
That last point is the whole discipline here, because the first attempt failed it.
An earlier differential over 19->18 also produced exactly one candidate,
0xbc22e83c, which matched the transition it was selected on and was still wrong:
read live it held 26 while the HUD showed 017. One matching transition is not
evidence.
A second trap is recorded too: a three-snapshot filter requiring 19 -> 19 -> 18
left ZERO survivors, because the value moves between the memory copy and the
screenshot that reads it. Filtering on the next DISTINCT value instead found the
counter on the first try.
ob_scan.py carries the method: scan one snapshot, then filter the candidate set
against live /dev/shm/xenia_memory_* at each new value, so only the first pass
needs a 4.8 GB copy.
Stated plainly as unsettled: the ADDRESS is from one run and cross-run stability
is untested, so the durable result is the method rather than the number. And what
the counter counts - whether every OB-badged entity is one of them, and whether
that badge is a flag in the entity object - is the follow-on the pilot actually
needs to CHOOSE targets rather than just know how many remain.