Files
benilla-octo/scripts/doc-links.py
T
SamandClaude Fable 5.1 3e12fa1d7f benilla is one repo: the working tree moves in, restructured
Until now this repo was an export of a private tree, pushed as squashed snapshots. From here on
it is the working tree itself, and the private repo is archived.

The root holds AGENTS.md and the Rust and GitHub files only. Everything an agent needs to orient
is in docs/: METHOD.md (the rules and how the project is built, 123 lines, project method only),
MAP.md (generated from the code at every land) and CONTRIBUTING.md (what gets in and how a
change is judged). One skill, `probe`, holds the procedure for running the client unattended
against the local server.

Brought over because the project needs it: reference/ (the 1.12 name catalogues 34 source
files read; the export failed its own tests without them), the gate chain and the pool
(scripts/), the hooks that enforce the install rule and the worktree rule, the full .gitignore,
the `cargo play` alias, and the original wording of eleven source files the export had reworded.

Left out: the decision records (archived with the private repo), the export tooling, the
operator's Discord and triage loop, the lab-laptop scripts and the instruments nothing
references, which live outside the repo now. What read the records is gone from the tooling:
no corpus lint, no records index, no `reserve` verb, no number-collision check.

Three hooks are gone: fmt-at-commit and the file-size nudge were redundant with the gates, and
the assets/ui guard is now a test (`assets_ui_does_not_grow`) that fails for everyone, not only
for a Claude Code session. The remaining pool hooks stand down on a machine without a pool.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 15:56:57 +07:00

127 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""doc-links — fail on a doc link whose target does not exist ANYWHERE in the workspace.
WHY THIS IS NARROW, AND WHY THE OBVIOUS VERSION IS WRONG (decision 1925). The docs are this
project's knowledge base, so a doc link that goes nowhere is real rot — decision 1903 found a
`UNIT_FLAG_STUNNED` comment still pointing at a `MovementState::stunned` that a later decision had
deleted, and nothing caught it because nothing runs rustdoc.
The obvious fix is `-D rustdoc::broken_intra_doc_links`. Measured, it does not survive contact with
this codebase: **941 warnings, 264 of them unambiguous `crate::`/`super::`/`self::` paths** — and
almost none of that is rot.
* **195** name a target that IS declared in its crate. They do not resolve because the target is
a *private* module named through its parent (`crate::net::apply`, where `mod apply;` is private
to `net`) — not a rustdoc quirk but Rust's own name resolution, so the link never worked.
Making them resolve means widening visibility for a documentation reason, trading encapsulation
for navigation.
* **~50** are one crate naming another's module with `crate::` — `benilla-world` writing
`crate::entities`, which is benilla-app's and which benilla-world cannot depend on. Those can
never resolve; the only "fix" is de-linking prose that reads fine as prose.
Both are conventions this codebase writes on purpose. A gate on them would be 249 edits of churn
that improve nothing, and would then have to stay green forever.
What IS unambiguous rot is the residue: a link whose **leaf is declared nowhere in the workspace**
— the item is gone, so no reading of the link can be right. That was 15 sites when this was
written (1925 fixed them); this script keeps it at zero.
scripts/doc-links.py # the gate: exit 1 on any dead-target link
scripts/doc-links.py --report # + the benign counts, for anyone re-opening the question
The leaf test is a declaration grep, deliberately loose: a name declared anywhere counts, because
the failure this catches is "the thing was deleted", not "the path is spelled right".
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def run_rustdoc() -> str:
out = subprocess.run(
["cargo", "doc", "--workspace", "--no-deps", "--document-private-items"],
cwd=ROOT,
capture_output=True,
text=True,
)
return out.stderr + out.stdout
def declared_names() -> set[str]:
"""Every name the workspace declares, in ONE pass.
A grep per link is what the first version did, and at ~900 links over a 19-crate tree it does
not finish inside a gate's patience. One scan, then set lookups.
"""
names: set[str] = set()
# `fn` gets its own pattern: a single alternation swallows the `const` of `const fn foo` and
# captures "fn" as the name, so `foo` is never indexed and the gate reports a live item as
# deleted. That bug shipped for one run of this script and is the reason the two passes exist.
pats = [
re.compile(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)"),
re.compile(r"\b(?:struct|enum|trait|type|const|static|union|mod)\s+([A-Za-z_][A-Za-z0-9_]*)"),
re.compile(r"\bmacro_rules!\s+([A-Za-z_][A-Za-z0-9_]*)"),
]
for rs in (ROOT / "crates").rglob("*.rs"):
try:
text = rs.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for pat in pats:
names.update(pat.findall(text))
names.discard("fn")
return names
def main() -> int:
report = "--report" in sys.argv
text = run_rustdoc()
found = re.findall(r"warning: unresolved link to `([^`]+)`\n\s*--> ([^\s:]+):(\d+)", text)
names = declared_names()
dead, benign = [], 0
for target, path, line in found:
# ONLY paths the author asserted are in THIS crate. A bare `[`SystemParam`]` or
# `[`KeyboardInput`]` names a bevy type that is simply not imported for linking — real,
# not rot — and there is no honest way to tell those from a deleted local name. A
# `crate::`/`super::`/`self::`/`Self::` path carries the assertion, so its absence is
# unambiguous.
if not re.match(r"^(crate|super|self|Self)::", target):
benign += 1
continue
leaf = target.split("::")[-1].split("(")[0].strip()
if not leaf or not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", leaf):
benign += 1
continue
if leaf in names:
benign += 1
else:
dead.append((target, path, line))
if report:
print(f"doc-links: {len(found)} unresolved-link warnings")
print(f"doc-links: {benign} name something that still exists (see this file's header)")
if not dead:
print(f"doc-links ok: no doc link points at a deleted item ({len(found)} benign)")
return 0
print(f"doc-links FAILED: {len(dead)} doc link(s) point at something that no longer exists:\n")
for target, path, line in dead:
print(f" {path}:{line}\n [`{target}`]")
print(
"\nEach names an item declared nowhere in the workspace. Repoint it at whatever replaced\n"
"the thing, or drop the brackets — a dangling link promises navigation that is not there."
)
return 1
if __name__ == "__main__":
raise SystemExit(main())