Two independent defects, both making the script report a correct checkout as wrong. Neither is new; both were invisible because nobody ran it here. 1. `export/` is the exporter's OUTPUT and is gitignored (`/export*/`). A checkout where nobody has run the exporter has no `export/` at all, so the four `DECISIONS.md`/`BLOCKED.md` citations of `export/manifest.json` and `export/screens/...` landed in "resolve NOWHERE" and the check exited 1 -- red for a state no edit can fix, which is the exact shape its own docstring says it exists to avoid. `check-capture-citations` learned this for `docs/re/captures/`; same rule now: absent BECAUSE UNBUILT is reported, absent while the tree IS built still fails. Verified both ways -- `mkdir export` and the same four go back to failing. 2. The selftest's peer-branch fixture cited `docs/re/f5-a-press-snaps-the-plate.md`, which the consolidation made an ordinary local file. The fixture stopped testing the scanner and started reporting it broken; `PEER_REFS` also still named `origin/auto/frame-blend-draw-path`, a branch that no longer exists. The selftest now FINDS a peer-only path at runtime, and where none exists -- the normal case on a clean checkout, measured: zero -- it says the class is empty here rather than claiming a failure. The class itself stays: the next topic branch that lands a finding recreates the condition exactly. Measured: check 123 citations, 119 resolve, 4 unbuilt, 0 nowhere, exit 0 (was exit 1). Selftest ok (was 🔴 BROKEN, on `main` too). The new generated-tree case was confirmed to FAIL against the unfixed function first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
230 lines
11 KiB
Python
Executable File
230 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Do the repo paths cited in `docs/port/*.md` actually resolve?
|
|
|
|
tools/port/check-citations # assert
|
|
tools/port/check-citations --selftest # can it fail?
|
|
|
|
`audit-kinds` checks citations in `authored/`. Nothing checked the PROSE, and
|
|
prose is where this port explains itself. A first run found **37 of 91**
|
|
non-resolving, 41 %, in two very different classes:
|
|
|
|
* **19 on the Decoder's topic branch** — real files, not merged here. Not
|
|
errors. A reader in this checkout still cannot follow them, which is worth
|
|
reporting and not worth failing on; the fix is a merge, not an edit.
|
|
* **7 that resolve NOWHERE** — `docs/BLOCKED.md`, `docs/FORMAT.md`,
|
|
`port/manifest.json`, `port/screens/title/*.json`. Left behind by the
|
|
monorepo move and the `export/` rename. Those are simply wrong: a reader
|
|
following one gets nothing, and nothing had ever told anyone.
|
|
|
|
So the two classes are separated and only the second fails. A check that failed
|
|
on the first would be red for a state nobody in this container can fix, which is
|
|
the shape the display guard exists to prevent.
|
|
|
|
⚠️ THE PEER-BRANCH CLASS IS THE OTHER AGENT'S POINT, TURNED ON MYSELF. They
|
|
observed that everything they hand over links into `docs/re/` files that live
|
|
only on their branch, so every link they send is dangling from here. The same is
|
|
true in reverse and neither of us was counting.
|
|
"""
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import glob
|
|
|
|
# A repo path with a file extension, optionally in backticks or a markdown link.
|
|
CITE = re.compile(
|
|
r"`?((?:docs|crates|port|tools|authored|export)/[\w./-]+"
|
|
r"\.(?:md|rs|gd|json|txt|py|tsv|csv))`?"
|
|
)
|
|
# ⚠️ `origin/auto/frame-blend-draw-path` used to head this list and NO LONGER
|
|
# EXISTS -- the consolidation merged it and the branch was removed. The class is
|
|
# kept because it is about the workflow, not about that one branch: the next
|
|
# topic branch that lands a finding recreates the condition exactly. What must
|
|
# not happen again is the selftest asserting the class against a fixture path
|
|
# that has since become an ordinary local file, which is how it came to print
|
|
# 🔴 BROKEN on a correct checkout. It now finds its own fixture, or says the
|
|
# condition does not exist here.
|
|
PEER_REFS = ("origin/main",)
|
|
|
|
|
|
def a_peer_only_path() -> str | None:
|
|
"""A path carried by a PEER_REF but absent from this working tree.
|
|
|
|
The selftest needs a REAL one: a hardcoded fixture silently stops testing
|
|
the moment that file lands locally, and then reports the scanner broken
|
|
instead of itself. On a clean, up-to-date checkout there is usually no such
|
|
path at all -- which is not a failure, it is the class being empty here.
|
|
"""
|
|
for ref in PEER_REFS:
|
|
out = subprocess.run(["git", "ls-tree", "-r", "--name-only", ref],
|
|
capture_output=True, text=True)
|
|
for f in out.stdout.splitlines():
|
|
if f.endswith((".md", ".rs", ".gd", ".json", ".txt", ".py",
|
|
".tsv", ".csv")) and not os.path.exists(f):
|
|
return f
|
|
return None
|
|
|
|
|
|
def gitignored(path: str) -> bool:
|
|
"""Is this path deliberately untracked? Pattern match -- existence not needed."""
|
|
return subprocess.run(["git", "check-ignore", "-q", path],
|
|
capture_output=True).returncode == 0
|
|
|
|
|
|
def unbuilt(path: str) -> bool:
|
|
"""A citation of GENERATED output whose tree has not been built here.
|
|
|
|
🔴 THE THIRD ABSENCE, AND IT IS NOT AN ERROR. `export/` is the exporter's
|
|
output and is gitignored (`.gitignore` `/export*/`). A checkout where nobody
|
|
has run the exporter has no `export/` at all, so the four `DECISIONS.md` and
|
|
`BLOCKED.md` citations of `export/manifest.json` and `export/screens/...`
|
|
were counted as "resolve NOWHERE" and this check was red on a clean tree --
|
|
for a state no edit can fix, which is precisely the shape its own docstring
|
|
says it exists to avoid.
|
|
|
|
`check-capture-citations` already learned this for `docs/re/captures/`.
|
|
Same rule here: absent BECAUSE UNBUILT is reported; absent while the tree
|
|
IS built is a real broken citation and still fails.
|
|
"""
|
|
if not gitignored(path):
|
|
return False
|
|
root = path.split("/")[0]
|
|
return not os.path.exists(root)
|
|
|
|
|
|
def on_a_ref(path: str) -> str | None:
|
|
"""The first ref that carries `path`, or None."""
|
|
for ref in PEER_REFS:
|
|
if subprocess.run(["git", "cat-file", "-e", f"{ref}:{path}"],
|
|
capture_output=True).returncode == 0:
|
|
return ref
|
|
return None
|
|
|
|
|
|
def scan(files):
|
|
resolves, peer, nowhere, ungenerated = 0, {}, {}, {}
|
|
for p in files:
|
|
try:
|
|
text = open(p, encoding="utf-8").read()
|
|
except OSError:
|
|
continue
|
|
for m in sorted(set(CITE.findall(text))):
|
|
if os.path.exists(m):
|
|
resolves += 1
|
|
elif unbuilt(m):
|
|
ungenerated.setdefault(m, p)
|
|
elif (ref := on_a_ref(m)):
|
|
peer.setdefault(m, (p, ref))
|
|
else:
|
|
nowhere.setdefault(m, p)
|
|
return resolves, peer, nowhere, ungenerated
|
|
|
|
|
|
def main() -> int:
|
|
if "--selftest" in sys.argv:
|
|
# 🔴 A CHECK THAT CANNOT FAIL IS NOT A CHECK. This plants a citation of a
|
|
# path that exists on no ref and requires the scanner to catch it, and a
|
|
# citation of a real file and requires it NOT to. Both directions,
|
|
# because a scanner that flagged everything would also "pass" the first.
|
|
tmp = os.path.join(os.environ.get("TMPDIR", "/tmp"), "check-citations-selftest")
|
|
os.makedirs(tmp, exist_ok=True)
|
|
bad = os.path.join(tmp, "bad.md")
|
|
open(bad, "w").write("see `docs/port/this-file-does-not-exist-anywhere.md`\n")
|
|
good = os.path.join(tmp, "good.md")
|
|
open(good, "w").write("see `docs/port/PORT-MISSION.md`\n")
|
|
# The THIRD class, which `--for-merge` turns into a failure. It has to be
|
|
# told apart from both others: a peer citation is not dangling (the file
|
|
# exists) and does not resolve here (the reader still gets nothing), and
|
|
# a scanner that collapsed it into either would make the flag meaningless
|
|
# while still passing the two checks above.
|
|
peer_probe = a_peer_only_path()
|
|
peerfile = os.path.join(tmp, "peer.md")
|
|
open(peerfile, "w").write("see `%s`\n" % (peer_probe or "docs/port/PORT-MISSION.md"))
|
|
# The FOURTH class: generated output whose tree is not built here.
|
|
# It has to be told apart from "resolves nowhere", which is the whole
|
|
# point -- a scanner that lumped them together is what made this check
|
|
# red on a clean checkout.
|
|
genfile = os.path.join(tmp, "gen.md")
|
|
open(genfile, "w").write("see `export/screens/title/main_menu.json`\n")
|
|
|
|
rp, pp, np_, gp = scan([peerfile])
|
|
|
|
_, _, nb, _ = scan([bad])
|
|
r, _, ng, _ = scan([good])
|
|
rg, pg, ng2, gg = scan([genfile])
|
|
caught = len(nb) == 1
|
|
passed = len(ng) == 0 and r == 1
|
|
# None == the class is empty in this checkout, not that it is broken.
|
|
peer_ok = (len(pp) == 1 and rp == 0 and len(np_) == 0 and len(gp) == 0) \
|
|
if peer_probe else None
|
|
# Only meaningful while `export/` is absent; if someone ran the exporter
|
|
# in this checkout the citation legitimately resolves instead.
|
|
gen_ok = (len(gg) == 1 and len(ng2) == 0 and len(pg) == 0) \
|
|
if not os.path.exists("export") else (rg == 1)
|
|
ok = caught and passed and gen_ok and peer_ok is not False
|
|
print("selftest: planted dangling caught=%s, real citation passed=%s, "
|
|
"peer-branch classed separately=%s, unbuilt-generated classed "
|
|
"separately=%s -> %s"
|
|
% (caught, passed,
|
|
"n/a (no peer-only path exists here)" if peer_ok is None
|
|
else peer_ok,
|
|
gen_ok, "ok" if ok else "🔴 BROKEN"))
|
|
if not gen_ok:
|
|
print(" 🔴 a citation of unbuilt generated output must NOT be "
|
|
"dangling; got resolves=%d peer=%d nowhere=%d ungenerated=%d"
|
|
% (rg, len(pg), len(ng2), len(gg)))
|
|
if peer_ok is False:
|
|
print(" 🔴 --for-merge cannot mean anything if the peer class is "
|
|
"not distinguished; got resolves=%d peer=%d nowhere=%d"
|
|
% (rp, len(pp), len(np_)))
|
|
return 0 if ok else 2
|
|
|
|
files = sorted(glob.glob("docs/port/*.md"))
|
|
resolves, peer, nowhere, ungenerated = scan(files)
|
|
total = resolves + len(peer) + len(nowhere) + len(ungenerated)
|
|
print("citations of repo paths in docs/port/*.md: %d" % total)
|
|
print(" resolve here : %d" % resolves)
|
|
# 🔴 --for-merge TURNS THE PEER CLASS INTO A FAILURE.
|
|
#
|
|
# Reporting-not-failing was right when it was written: a peer-branch
|
|
# citation was "a state nobody in this container can fix", so failing on it
|
|
# would have been red for something unactionable. Under the pull-request
|
|
# workflow that stopped being true -- a PR into `main` is EXACTLY where it
|
|
# becomes fixable, by opening the finding's PR first and depending on it.
|
|
# The citation is dead the moment this merges, so the merge is the last
|
|
# place the leniency can still be withdrawn.
|
|
#
|
|
# Left as a flag rather than made unconditional, because both readings are
|
|
# still live: mid-work on a topic branch the peer class really is unfixable
|
|
# noise. The difference the old code could not express is WHERE the code is
|
|
# going, and that is a condition the caller can state.
|
|
merging = "--for-merge" in sys.argv
|
|
label = "🔴 FAILS (--for-merge)" if merging else "reported, not failed"
|
|
print(" on a peer branch, not merged: %d (%s)" % (len(peer), label))
|
|
for m, (src, ref) in sorted(peer.items()):
|
|
print(" %-52s %s <- %s" % (m, ref.split("/")[-1], os.path.basename(src)))
|
|
if peer and merging:
|
|
print("\n🔴 %d citation(s) resolve only on a peer branch." % len(peer))
|
|
print(" After this merges they resolve NOWHERE -- the reader gets a dead")
|
|
print(" path. Land the finding first and make it a dependency of this PR.")
|
|
return 1
|
|
if ungenerated:
|
|
print(" not built in this checkout : %d (reported, not failed)"
|
|
% len(ungenerated))
|
|
for m, src in sorted(ungenerated.items()):
|
|
print(" %-52s <- %s" % (m, os.path.basename(src)))
|
|
print(" run the exporter and these resolve; a path still missing")
|
|
print(" afterwards IS dangling and fails below.")
|
|
if nowhere:
|
|
print(" 🔴 resolve NOWHERE : %d" % len(nowhere))
|
|
for m, src in sorted(nowhere.items()):
|
|
print(" %-52s <- %s" % (m, os.path.basename(src)))
|
|
print("\n🔴 a reader following those gets nothing. Fix the path or drop the citation.")
|
|
return 1
|
|
print(" 🔴 resolve nowhere : 0")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|