From 639bdf1a2504025569bbe57e5eaae17566cad369 Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Thu, 3 Sep 2026 20:57:25 +0000 Subject: [PATCH] port: check that no linked record is dropped -- and two bugs in the check itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/port/check-all | 2 + tools/port/check-linked-records | 101 ++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100755 tools/port/check-linked-records diff --git a/tools/port/check-all b/tools/port/check-all index 8a4155f7..2ee63246 100755 --- a/tools/port/check-all +++ b/tools/port/check-all @@ -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 diff --git a/tools/port/check-linked-records b/tools/port/check-linked-records new file mode 100755 index 00000000..42f60f05 --- /dev/null +++ b/tools/port/check-linked-records @@ -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())