From aaaa08b1641ff2e04071f40fba44466fa8b4b64f Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Sat, 29 Aug 2026 02:34:16 +0000 Subject: [PATCH] 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. --- docs/re/METHOD.md | 13 +++++ docs/re/data/doc-link-audit.txt | 10 ++++ docs/re/doc-link-audit.md | 50 ++++++++++++++++++ docs/re/entities-live-roster.md | 2 +- docs/re/mission-freeze-resume-spin.md | 4 +- docs/re/script-runtime-probe.md | 2 +- .../structures/isl-message-dialogue-link.md | 2 +- .../structures/mission-objective-counter.md | 4 +- docs/re/structures/stage-settings-table.md | 2 +- docs/re/structures/ui-rat-layout.md | 4 +- docs/re/structures/xbg7-mesh.md | 2 +- tools/re-capture/doc_link_check.py | 52 +++++++++++++++++++ 12 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 docs/re/data/doc-link-audit.txt create mode 100644 docs/re/doc-link-audit.md create mode 100755 tools/re-capture/doc_link_check.py diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 78489757..eb07535d 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -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. diff --git a/docs/re/data/doc-link-audit.txt b/docs/re/data/doc-link-audit.txt new file mode 100644 index 00000000..5c8c3055 --- /dev/null +++ b/docs/re/data/doc-link-audit.txt @@ -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 diff --git a/docs/re/doc-link-audit.md b/docs/re/doc-link-audit.md new file mode 100644 index 00000000..26d8162b --- /dev/null +++ b/docs/re/doc-link-audit.md @@ -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. diff --git a/docs/re/entities-live-roster.md b/docs/re/entities-live-roster.md index a6401b50..c068c258 100644 --- a/docs/re/entities-live-roster.md +++ b/docs/re/entities-live-roster.md @@ -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.3–1.5 % in flight), so no distance readout was available to compare against. diff --git a/docs/re/mission-freeze-resume-spin.md b/docs/re/mission-freeze-resume-spin.md index 5a51557e..65f1a1c6 100644 --- a/docs/re/mission-freeze-resume-spin.md +++ b/docs/re/mission-freeze-resume-spin.md @@ -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 diff --git a/docs/re/script-runtime-probe.md b/docs/re/script-runtime-probe.md index 28550800..8e709ee4 100644 --- a/docs/re/script-runtime-probe.md +++ b/docs/re/script-runtime-probe.md @@ -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`: ``` diff --git a/docs/re/structures/isl-message-dialogue-link.md b/docs/re/structures/isl-message-dialogue-link.md index a0cfe5cd..87168c49 100644 --- a/docs/re/structures/isl-message-dialogue-link.md +++ b/docs/re/structures/isl-message-dialogue-link.md @@ -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 diff --git a/docs/re/structures/mission-objective-counter.md b/docs/re/structures/mission-objective-counter.md index 631a3fc9..3bc9d3ff 100644 --- a/docs/re/structures/mission-objective-counter.md +++ b/docs/re/structures/mission-objective-counter.md @@ -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 diff --git a/docs/re/structures/stage-settings-table.md b/docs/re/structures/stage-settings-table.md index 242cf7e0..48166730 100644 --- a/docs/re/structures/stage-settings-table.md +++ b/docs/re/structures/stage-settings-table.md @@ -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. diff --git a/docs/re/structures/ui-rat-layout.md b/docs/re/structures/ui-rat-layout.md index ed256cc6..8a607eb3 100644 --- a/docs/re/structures/ui-rat-layout.md +++ b/docs/re/structures/ui-rat-layout.md @@ -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 diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index 90ca236f..9beaf2af 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -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. diff --git a/tools/re-capture/doc_link_check.py b/tools/re-capture/doc_link_check.py new file mode 100755 index 00000000..2b9ab2bc --- /dev/null +++ b/tools/re-capture/doc_link_check.py @@ -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)