docs: the UI decode's own evidence images were unreachable -- 11 links repaired

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.
This commit is contained in:
Sylpheed RE agent
2026-08-29 02:34:16 +00:00
parent 3eba09aae0
commit 6c8ee2ee6a
12 changed files with 136 additions and 11 deletions

View File

@@ -585,3 +585,16 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
with the capture". Each step is a filter that costs one query and changes the
number by more than an order of magnitude in total. A raw count is almost never
the number a consumer needs.
* **Nothing was checking that the docs' cited evidence exists.** A sweep of every
relative link under `docs/` found **16 broken**, and two of them were the
figures backing the UI layout decode's headline claim — the port's foundation,
unreachable from its own page, because a path in `structures/` was written one
directory too shallow. Eleven were wrong relative depth with the target present;
five name files that do not exist. Evidence that cannot be opened is not
evidence, and a link is exactly the kind of thing no one re-reads.
`tools/re-capture/doc_link_check.py` now does it, and also flags targets that
resolve to a **zero-byte** file — which looks correct in every listing.
* **Repair in bulk only when the counts pair.** The fixer rewrote 11 links; the
checker went from 1 038 resolving / 16 missing to 1 049 / 5. +11 and 11
against 11 edits is the confirmation that the pass did what it said and touched
nothing else. A bulk edit without that arithmetic is a hope.

View File

@@ -0,0 +1,10 @@
# tools/re-capture/doc_link_check.py -- 2026-08-29, after the repair pass
1050 link(s) resolve
5 MISSING target(s):
docs/re/autopilot-knowledge-sources.md -> ../../MEMORY.md
docs/re/challenge-mission-gate.md -> ../../../xenia-canary-native/src/xenia/hid/file/file_input_driver.h
docs/re/entities-live-roster.md -> ../../MEMORY.md
docs/re/mission-freeze-resume-spin.md -> canary-build-verified-env-confound.md
docs/re/stage-drift-is-navigation-not-save.md -> structures/weapon-datasheet-runtime.md

50
docs/re/doc-link-audit.md Normal file
View File

@@ -0,0 +1,50 @@
# ✅ Do the docs' cited artifacts exist? — 11 broken links repaired
**Status:** ✅ checked mechanically and fixed, with the count verified both ways.
The brief's rule is *"commit the reference data beside the finding, so the port
can be built without a disc in the loop"*. An answer whose evidence is not
reachable cannot be used. Nothing had ever checked that.
[`tools/re-capture/doc_link_check.py`](../../tools/re-capture/doc_link_check.py)
walks every markdown file under `docs/`, resolves each relative link, and reports
targets that do not exist — and, separately, targets that exist but are **empty**,
which is the sneakier failure since a zero-byte file looks fine in any listing.
| | before | after |
|---|---|---|
| links resolving | 1 038 | **1 049** |
| missing targets | **16** | 5 |
| empty targets | 0 | 0 |
**+11 resolving, 11 missing — the two numbers pair exactly**, which is the check
that the repair did what it claimed and nothing else.
## 🔴 Two of them were the evidence for the UI decode itself
`structures/ui-rat-layout.md` is the layout decode the port is built on. Its two
figures — the ones 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 was unreachable from its own
document.**
## What was wrong, and what still is
Eleven links had the **wrong relative depth** while their targets existed — a
missing or surplus `../`, or a missing `structures/`. Those are repaired; each
was rewritten only when exactly one candidate path resolved, so nothing was
guessed.
**Five remain genuinely absent** and are left alone rather than invented:
| doc | target |
|---|---|
| `autopilot-knowledge-sources.md`, `entities-live-roster.md` | `../../MEMORY.md` — outside the repo |
| `challenge-mission-gate.md` | a header in the separate `xenia-canary-native` tree |
| `stage-drift-is-navigation-not-save.md` | `structures/weapon-datasheet-runtime.md` — never written |
| `mission-freeze-resume-spin.md` | `canary-build-verified-env-confound.md` — never written |
None is port-relevant: they are mission, entity and emulator-side documents. Two
name documents that do not exist, which is a different problem from a bad path
and is not something to paper over with a link fix.

View File

@@ -79,7 +79,7 @@ looks stuck on the main menu while it is in fact three screens further on.
## Still open
* The **world-unit measurement** ([collisionset](collisionset.md)) — positions
* The **world-unit measurement** ([collisionset](structures/collisionset.md)) — positions
in world units against the HUD's own distance readout. This run reached the
mission but the flight HUD was not yet up (green 0.03 %, vs 1.31.5 % in
flight), so no distance readout was available to compare against.

View File

@@ -264,7 +264,7 @@ and the boot never happens.
A Stage 02 run froze after ~4 minutes of flight (screen still `flight`, not GAME
OVER), and `gdb_bt.sh` took backtraces of all **79** threads
([`captures/stage02-freeze-gdb-backtraces.txt`](../captures/stage02-freeze-gdb-backtraces.txt)).
([`captures/stage02-freeze-gdb-backtraces.txt`](captures/stage02-freeze-gdb-backtraces.txt)).
**Every single one is in a wait.** Not one thread is executing guest code or
sitting in a xenia loop:
@@ -394,7 +394,7 @@ and the control are all in place; what is missing is one frozen sample.
The fourth run froze **9 seconds** into the watcher's window, in flight
(`freeze_watch.sh` confirmed the HUD was still on screen), and the probe built
for exactly this moment reported **the healthy-run baseline and nothing else**
([`captures/stage02-freeze-stuck-wait-probe.txt`](../captures/stage02-freeze-stuck-wait-probe.txt)):
([`captures/stage02-freeze-stuck-wait-probe.txt`](captures/stage02-freeze-stuck-wait-probe.txt)):
```
FROZEN IN FLIGHT at 9s

View File

@@ -146,7 +146,7 @@ the encoding: **1 = not yet deployed, 2 = active, 4 = destroyed**.
### ✅ The mission-over branch, observed exactly as disassembled
The phase ended at 694.9 s, but **the ordinal did not advance** — and the reason
is the branch [mission-phase-advance](../mission-phase-advance.md) read out of
is the branch [mission-phase-advance](mission-phase-advance.md) read out of
`sub_82260710`:
```

View File

@@ -82,7 +82,7 @@ names had no text to resolve *to*, and the 100 % figure was unreachable.
* **Which recording plays.** The caption is the *text*; the voice bank binding
is a separate and still-unresolved question — see
[voice-bank-leading-region.md](voice-bank-leading-region.md) and the
[voice-bank-leading-region.md](../voice-bank-leading-region.md) and the
known case of a generic line playing against a specific subtitle.
* **Which page a `MSG_DEMO_*` id belongs to.** The `DEMO` family is not called
from the stage scripts at all — 78 of its ids are multi-page, so something

View File

@@ -270,7 +270,7 @@ result of the two.
## ✅ 2026-08-23 (third pass) — the counter is at `0xbdb59668` again, and the refutation above is *refined*, not reversed
Run 4, with the hunt automated end to end
([`ob_hunt.py`](../../tools/re-capture/ob_hunt.py) + the HUD reader), produced
([`ob_hunt.py`](../../../tools/re-capture/ob_hunt.py) + the HUD reader), produced
**exactly one** surviving address:
```
@@ -322,7 +322,7 @@ With the per-entity searches refuted at both word and bit level
counter. It sits at `0xbdb59668`, inside the entity-heap window, so the answer is
readable directly: sample ±0x200 around it across one transition and keep the
words that move **with** it
([`ob_neighbourhood.py`](../../tools/re-capture/ob_neighbourhood.py),
([`ob_neighbourhood.py`](../../../tools/re-capture/ob_neighbourhood.py),
[`captures/ob-counter-neighbourhood-stage02.json`](../captures/ob-counter-neighbourhood-stage02.json)).
Control first: over an 8-second interval while the counter sat still, **0 of the

View File

@@ -40,7 +40,7 @@ resolves all twenty-four:
| `stage\StageParameter_Test.tbl` | 1 — the developer stage |
**That is exactly the 24, and it explains 24 against the 29 stage records**
([`challenge-mission-gate.md`](challenge-mission-gate.md) counts 29:
([`challenge-mission-gate.md`](../challenge-mission-gate.md) counts 29:
16 story + 6 tutorial + 6 challenge + `Test`): the six tutorials do not get one
table each, they **share a single `_Tutorial` table**, and `_Test` accounts for
the last. 16 + 6 + 1 + 1 = 24.

View File

@@ -4,7 +4,7 @@
**reassembled from the disc**: the tutorial PAUSE menu rebuilds pixel-accurately from its
sprites placed at the coordinates in their `.rat` records — no fitting, no manual nudging.
![real vs rebuilt](captures/ui-layout/pause-tutorial-real-vs-rebuilt.png)
![real vs rebuilt](../captures/ui-layout/pause-tutorial-real-vs-rebuilt.png)
*Left: the running game (Canary screenshot). Right: rebuilt from `GP_PAUSE_MENU.pak` alone.
The remaining differences are the animated frame/glow sprites (`*eff*`) that were not
@@ -157,7 +157,7 @@ game, twice, in that order — the records were never fitted to the picture.
The same method run against `GP_TITLE.pak` reproduces the **main menu**, which is a
different screen with a different item count and a different pitch:
![main menu real vs rebuilt](captures/ui-layout/title-mainmenu-real-vs-rebuilt.png)
![main menu real vs rebuilt](../captures/ui-layout/title-mainmenu-real-vs-rebuilt.png)
`ptbtn01..05.rat` give X = 542 for all five and Y = 162 / 242 / 322 / 402 / 482 — an
**80 px** pitch, where the pause menu used 70. Measured against the screenshot, the sprite

View File

@@ -507,7 +507,7 @@ run scan that builds the candidate list — which does not emit a start for thes
resources' real vertex buffers. That is where the remaining 51 live.
The ignored test
[`mesh_consistency_disc.rs`](../../crates/sylpheed-formats/tests/mesh_consistency_disc.rs)
[`mesh_consistency_disc.rs`](../../../crates/sylpheed-formats/tests/mesh_consistency_disc.rs)
still asserts the target state and now records 63 rather than 125; the remaining
cases are where the neighbourhood is itself wrong or absent.

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Do the files the docs cite actually exist in the repo?
An answer whose evidence is not committed cannot be used by anyone without a
disc and an emulator, which is the whole point of the reference data. This walks
every markdown file under docs/ and resolves each relative link, reporting the
ones that point at nothing.
Skips external links (http, mailto) and pure anchors. Reports missing targets
and, separately, committed-but-EMPTY files, which are the sneakier failure --
a link that resolves to a zero-byte file looks fine in every listing.
doc_link_check.py [docs-root]
"""
import os, re, sys
ROOT = sys.argv[1] if len(sys.argv) > 1 else "docs"
LINK = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
missing, empty, ok = [], [], 0
for dirpath, _dirs, files in os.walk(ROOT):
for f in files:
if not f.endswith(".md"):
continue
src = os.path.join(dirpath, f)
try:
body = open(src, encoding="utf-8").read()
except Exception:
continue
for target in LINK.findall(body):
if target.startswith(("http://", "https://", "mailto:", "#")):
continue
path = os.path.normpath(os.path.join(dirpath, target.split("#")[0]))
if not path:
continue
if not os.path.exists(path):
missing.append((src, target))
elif os.path.isfile(path) and os.path.getsize(path) == 0:
empty.append((src, target))
else:
ok += 1
print(f"{ok} link(s) resolve")
if missing:
print(f"\n{len(missing)} MISSING target(s):")
for s, t in sorted(missing):
print(f" {s} -> {t}")
if empty:
print(f"\n{len(empty)} link(s) resolve to an EMPTY file:")
for s, t in sorted(empty):
print(f" {s} -> {t}")
sys.exit(1 if (missing or empty) else 0)