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())