port: check that no linked record is dropped -- and two bugs in the check itself

The other agent got a value wrong by resolving a record BY NAME and stopping at
the first hit; their census puts 1467 of 15493 elements (9.5%) behind a second
link, and they asked how exposed this exporter is.

Measured: NOT AT ALL. 135 links resolve -- 116 through a focus block's record, 19
to a top-level element on the same screen -- and 0 reach nothing. Now a standing
check, because this port is adding archives and a link into a record the exporter
does not emit would be invisible: the element still draws, it just silently loses
whatever that record animates.

🔴 THE CHECK'S FIRST VERSION REPORTED 116 FALSE FAILURES, including
ptbtn00 -> ptbtn00f.rat, which I had verified by hand an hour earlier. It compared
the link against the focus ELEMENT's  (a .t32) instead of the focus
BLOCK's  (a .rat). Caught only because one row was already known to be
right -- a new check producing a confident wrong answer is exactly what its own
subject matter is about.

🔴 And its selftest printed 'passed=False -> ok' in one line: the message
restated the condition as og==1 while the verdict tested og==2. The same stale
restatement bug hit fit-trajectory earlier today, so the message is now DERIVED
from the verdict rather than written twice.

Selftest covers both resolution routes, including the .rat/.t32 mismatch that
broke v1. In check-all.
This commit is contained in:
Sylpheed port agent
2026-09-03 20:57:25 +00:00
parent edbacd44f5
commit 639bdf1a25
2 changed files with 103 additions and 0 deletions

View File

@@ -156,6 +156,8 @@ step decisions-index must-pass tools/port/index-decisions --check
# is merely on a peer's unmerged branch is reported, because the fix is a merge
# and nobody in this container can make it.
step trajectory-fit must-pass tools/port/fit-trajectory --selftest
step linked-records must-pass tools/port/check-linked-records
step linked-rec-ctl must-pass tools/port/check-linked-records --selftest
step authored-declared must-pass tools/port/check-authored-vs-declared
step authored-decl-ctl must-pass tools/port/check-authored-vs-declared --selftest
step doc-citations must-pass tools/port/check-citations

101
tools/port/check-linked-records Executable file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Every linked record must be reachable somewhere in its screen.
tools/port/check-linked-records [--selftest]
🔴 WHY. The other agent got a value wrong by resolving a record BY NAME and
stopping at the first hit: `ptbtn00` carries two children, and its pulse lives in
the second. Their census puts 1467 of 15493 elements -- 9.5%, across 815 builds --
behind that second link, and they asked how exposed this exporter is.
Measured answer at the time of writing: **not at all**. 116 links resolve through
an exported `focus` block's `record`, 19 resolve to a top-level element on the
same screen, and **0 reach nothing**. But that is a property of today's tree, and this port is
adding archives -- `GP_OPTIONS` landed today and `GP_SAVE_LOAD`, `GP_TUTORIAL`
and `GP_DIALOG` are each one line away. A link into a record the exporter does not
emit would be invisible: the element still draws, it simply loses an animation
nobody knows to look for.
⚠️ `opt_link` is NOT "the focused state of a button", whatever the parser's field
name says. On this export it also chains `pgloading_loop1 -> loop3 -> loop4` (three
loop animations) and `ptloop01 -> ptloop02` (the two title sweeps), and on
`main_menu` it runs sweep -> sweep -> button -> button-variant. It links records;
focus is one use of it. This check therefore accepts EITHER resolution and does
not care which, because caring would be believing the name.
"""
import glob
import json
import os
import sys
def scan(paths):
ok, missing = 0, []
for p in paths:
d = json.load(open(p))
screen = os.path.basename(p)[:-5]
top = {e.get("declared") for e in d["elements"]}
for e in d["elements"]:
link = e.get("opt_link")
if not link:
continue
# 🔴 THE BLOCK'S `record`, NOT THE ELEMENT'S `declared`. The link
# names a `.rat`; the focus element's `declared` is a `.t32`. The
# first version of this compared against the `.t32` and reported 116
# dangling links -- including `ptbtn00 -> ptbtn00f.rat`, which had
# been verified by hand an hour earlier. A confident wrong answer,
# caught only because one row was already known.
focus_record = e.get("focus", {}).get("record")
if link == focus_record or link in top:
ok += 1
else:
missing.append((screen, e.get("id"), link))
return ok, missing
def main():
if "--selftest" in sys.argv:
# 🔴 A CHECK THAT CANNOT FAIL IS NOT A CHECK. Plant a link that resolves
# nowhere and require it to be caught; plant one that resolves via a
# top-level element and require it to pass -- both directions, because a
# checker that flagged everything would also "catch" the first.
tmp = os.path.join(os.environ.get("TMPDIR", "/tmp"), "check-linked-records")
os.makedirs(tmp, exist_ok=True)
bad = {"elements": [{"id": "a", "declared": "a.rat", "opt_link": "ghost.rat"}]}
good = {"elements": [
{"id": "a", "declared": "a.rat", "opt_link": "b.rat"},
{"id": "b", "declared": "b.rat"},
# the focus-block route, with the .rat/.t32 mismatch that broke v1
{"id": "c", "declared": "c.rat", "opt_link": "cf.rat",
"focus": {"record": "cf.rat", "elements": [{"declared": "cf.t32"}]}}]}
for name, doc in (("bad", bad), ("good", good)):
json.dump(doc, open(f"{tmp}/{name}.json", "w"))
_, mb = scan([f"{tmp}/bad.json"])
og, mg = scan([f"{tmp}/good.json"])
ok = len(mb) == 1 and len(mg) == 0 and og == 2
# 🔴 DERIVE THE MESSAGE FROM THE VERDICT, never restate the condition.
# The first version printed `og == 1` while `ok` tested `og == 2`, so it
# said "passed=False -> ok" in one line. The same stale-restatement bug
# hit `fit-trajectory` earlier the same day; a selftest whose output
# contradicts its own verdict is worse than one that only returns a code.
caught, resolved = len(mb) == 1, (len(mg) == 0 and og == 2)
print("selftest: dangling link caught=%s, both resolution routes passed=%s -> %s"
% (caught, resolved, "ok" if (caught and resolved) else "🔴 BROKEN"))
ok = caught and resolved
return 0 if ok else 2
ok, missing = scan(sorted(glob.glob("export/screens/*/*.json")))
print("linked records: %d resolve (focus block or top-level element)" % ok)
if missing:
print(" 🔴 resolve to NOTHING in their screen: %d" % len(missing))
for r in missing:
print(" %-22s %-18s -> %s" % r)
print("\n🔴 an element links a record this export does not emit. The element still"
"\n draws; it silently loses whatever that record animates.")
return 1
print(" 🔴 resolve to nothing: 0")
return 0
if __name__ == "__main__":
raise SystemExit(main())