Merge pull request 'fix(port): check-citations was red, and its selftest BROKEN, on a clean tree' (#66) from fix/citations-generated-tree into main
Reviewed-on: #66
This commit was merged in pull request #66.
This commit is contained in:
@@ -36,7 +36,60 @@ CITE = re.compile(
|
|||||||
r"`?((?:docs|crates|port|tools|authored|export)/[\w./-]+"
|
r"`?((?:docs|crates|port|tools|authored|export)/[\w./-]+"
|
||||||
r"\.(?:md|rs|gd|json|txt|py|tsv|csv))`?"
|
r"\.(?:md|rs|gd|json|txt|py|tsv|csv))`?"
|
||||||
)
|
)
|
||||||
PEER_REFS = ("origin/auto/frame-blend-draw-path", "origin/main")
|
# ⚠️ `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:
|
def on_a_ref(path: str) -> str | None:
|
||||||
@@ -49,7 +102,7 @@ def on_a_ref(path: str) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def scan(files):
|
def scan(files):
|
||||||
resolves, peer, nowhere = 0, {}, {}
|
resolves, peer, nowhere, ungenerated = 0, {}, {}, {}
|
||||||
for p in files:
|
for p in files:
|
||||||
try:
|
try:
|
||||||
text = open(p, encoding="utf-8").read()
|
text = open(p, encoding="utf-8").read()
|
||||||
@@ -58,11 +111,13 @@ def scan(files):
|
|||||||
for m in sorted(set(CITE.findall(text))):
|
for m in sorted(set(CITE.findall(text))):
|
||||||
if os.path.exists(m):
|
if os.path.exists(m):
|
||||||
resolves += 1
|
resolves += 1
|
||||||
|
elif unbuilt(m):
|
||||||
|
ungenerated.setdefault(m, p)
|
||||||
elif (ref := on_a_ref(m)):
|
elif (ref := on_a_ref(m)):
|
||||||
peer.setdefault(m, (p, ref))
|
peer.setdefault(m, (p, ref))
|
||||||
else:
|
else:
|
||||||
nowhere.setdefault(m, p)
|
nowhere.setdefault(m, p)
|
||||||
return resolves, peer, nowhere
|
return resolves, peer, nowhere, ungenerated
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -82,28 +137,51 @@ def main() -> int:
|
|||||||
# exists) and does not resolve here (the reader still gets nothing), and
|
# exists) and does not resolve here (the reader still gets nothing), and
|
||||||
# a scanner that collapsed it into either would make the flag meaningless
|
# a scanner that collapsed it into either would make the flag meaningless
|
||||||
# while still passing the two checks above.
|
# while still passing the two checks above.
|
||||||
|
peer_probe = a_peer_only_path()
|
||||||
peerfile = os.path.join(tmp, "peer.md")
|
peerfile = os.path.join(tmp, "peer.md")
|
||||||
open(peerfile, "w").write("see `docs/re/f5-a-press-snaps-the-plate.md`\n")
|
open(peerfile, "w").write("see `%s`\n" % (peer_probe or "docs/port/PORT-MISSION.md"))
|
||||||
rp, pp, np_ = scan([peerfile])
|
# 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")
|
||||||
|
|
||||||
_, _, nb = scan([bad])
|
rp, pp, np_, gp = scan([peerfile])
|
||||||
r, _, ng = scan([good])
|
|
||||||
|
_, _, nb, _ = scan([bad])
|
||||||
|
r, _, ng, _ = scan([good])
|
||||||
|
rg, pg, ng2, gg = scan([genfile])
|
||||||
caught = len(nb) == 1
|
caught = len(nb) == 1
|
||||||
passed = len(ng) == 0 and r == 1
|
passed = len(ng) == 0 and r == 1
|
||||||
peer_ok = len(pp) == 1 and rp == 0 and len(np_) == 0
|
# None == the class is empty in this checkout, not that it is broken.
|
||||||
ok = caught and passed and peer_ok
|
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, "
|
print("selftest: planted dangling caught=%s, real citation passed=%s, "
|
||||||
"peer-branch classed separately=%s -> %s"
|
"peer-branch classed separately=%s, unbuilt-generated classed "
|
||||||
% (caught, passed, peer_ok, "ok" if ok else "🔴 BROKEN"))
|
"separately=%s -> %s"
|
||||||
if not peer_ok:
|
% (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 "
|
print(" 🔴 --for-merge cannot mean anything if the peer class is "
|
||||||
"not distinguished; got resolves=%d peer=%d nowhere=%d"
|
"not distinguished; got resolves=%d peer=%d nowhere=%d"
|
||||||
% (rp, len(pp), len(np_)))
|
% (rp, len(pp), len(np_)))
|
||||||
return 0 if ok else 2
|
return 0 if ok else 2
|
||||||
|
|
||||||
files = sorted(glob.glob("docs/port/*.md"))
|
files = sorted(glob.glob("docs/port/*.md"))
|
||||||
resolves, peer, nowhere = scan(files)
|
resolves, peer, nowhere, ungenerated = scan(files)
|
||||||
total = resolves + len(peer) + len(nowhere)
|
total = resolves + len(peer) + len(nowhere) + len(ungenerated)
|
||||||
print("citations of repo paths in docs/port/*.md: %d" % total)
|
print("citations of repo paths in docs/port/*.md: %d" % total)
|
||||||
print(" resolve here : %d" % resolves)
|
print(" resolve here : %d" % resolves)
|
||||||
# 🔴 --for-merge TURNS THE PEER CLASS INTO A FAILURE.
|
# 🔴 --for-merge TURNS THE PEER CLASS INTO A FAILURE.
|
||||||
@@ -130,6 +208,13 @@ def main() -> int:
|
|||||||
print(" After this merges they resolve NOWHERE -- the reader gets a dead")
|
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.")
|
print(" path. Land the finding first and make it a dependency of this PR.")
|
||||||
return 1
|
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:
|
if nowhere:
|
||||||
print(" 🔴 resolve NOWHERE : %d" % len(nowhere))
|
print(" 🔴 resolve NOWHERE : %d" % len(nowhere))
|
||||||
for m, src in sorted(nowhere.items()):
|
for m, src in sorted(nowhere.items()):
|
||||||
|
|||||||
Reference in New Issue
Block a user