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:
@@ -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.
|
||||
|
||||
10
docs/re/data/doc-link-audit.txt
Normal file
10
docs/re/data/doc-link-audit.txt
Normal 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
50
docs/re/doc-link-audit.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`:
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
*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:
|
||||
|
||||

|
||||

|
||||
|
||||
`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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
52
tools/re-capture/doc_link_check.py
Executable file
52
tools/re-capture/doc_link_check.py
Executable 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)
|
||||
Reference in New Issue
Block a user