Files
Sylpheed/tools/port/audit-kinds
MechaCat02 a23c321831 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00

320 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
"""What does each `kind` label in `authored/` actually REST on?
Every authored entry carries a `kind` -- `measured`, `authored`, `name match,
not measured` -- and a `why`. The label is the load-bearing part: `measured`
means the port is repeating something observed off the running game, and a
reader downstream will treat it as fact.
Nothing has ever checked them. That is the point: **a discipline that has never
visibly failed is the one nothing directs attention at.** The Decoder reached
this from the input side -- Ⓐ and Ⓑ were delivery-confirmed because they had
once broken, so the d-pad never was -- and on the same day a `measured` label of
mine turned out to rest on a single entry that may have been measuring history.
So this checks what is checkable about a label, and is explicit that the rest is
not:
citations resolvable references in the `why` -- a `docs/` path that exists on
some ref, a commit sha that resolves, a capture filename
BARE a label whose `why` cites nothing a reader could go and open
DANGLING a citation that does not resolve anywhere in the repository
🔴 What it CANNOT do is read the cited page and confirm it says what the `why`
claims. A label with three resolvable citations can still be wrong. This narrows
"which labels rest on nothing" from unknown to a list; it does not audit meaning.
"""
import json, glob, os, re, subprocess, sys
REFS = None
def known_paths():
"""Every path in the repo, across ALL refs -- docs/re/ lives on a branch.
Checked against the working tree as well: a file added this iteration is not
in any ref yet, and reporting a citation to it as unresolvable would make the
audit fail every time it is itself referenced.
"""
global REFS
if REFS is None:
out = subprocess.run(["git", "rev-list", "--all", "--objects"],
capture_output=True, text=True).stdout
REFS = {l.split(" ", 1)[1] for l in out.splitlines() if " " in l}
return REFS
HANDOFF_TEXT = None
def handoff():
"""The live HANDOFF, so a cited Q number is checked against the real table."""
global HANDOFF_TEXT
if HANDOFF_TEXT is None:
sha = subprocess.run(["git", "log", "--all", "--format=%h", "--",
"docs/port/HANDOFF.md"], capture_output=True,
text=True).stdout.split()[0]
HANDOFF_TEXT = subprocess.run(["git", "show", f"{sha}:docs/port/HANDOFF.md"],
capture_output=True, text=True).stdout
return HANDOFF_TEXT
def sha_ok(s):
r = subprocess.run(["git", "cat-file", "-e", s + "^{commit}"], capture_output=True)
return r.returncode == 0
def text_of(why):
if isinstance(why, str):
return why
if isinstance(why, list):
return " ".join(str(x) for x in why)
return ""
def citations(t):
"""References a reader could actually follow."""
out = []
for p in re.findall(r"\b(?:docs|crates|port|tools|authored)/[\w./-]+\w", t):
out.append(("path", p.rstrip(".,")))
for sha in re.findall(r"\b([0-9a-f]{7,40})\b", t):
# 🔴 A PURE-DECIMAL RUN IS NOT A SHA. `1118268` and `1171516` are byte
# counts in `voice/presentation_why`, and this reported them as
# unresolvable commits -- a DANGLING verdict on a why that cites
# nothing of the kind. A sha in this corpus always carries at least one
# of a-f; requiring that removes the whole class without a length rule.
if any(c in "abcdef" for c in sha):
out.append(("sha", sha))
for p in re.findall(r"\b([\w-]+\.(?:png|txt|tsv|wav))\b", t):
out.append(("file", p))
# The corpus cites two things that are not paths and are still followable:
# a HANDOFF question number, and a MISSION section. Leaving these out made
# the first run report four labels as resting on nothing when they rest on
# the two documents the mission names -- an audit inventing defects is worse
# than no audit, because its false positives are indistinguishable from its
# true ones until each is opened.
for q in re.findall(r"HANDOFF Q(\d+)", t):
out.append(("handoff", "Q" + q))
for m in re.findall(r"(PORT-MISSION|MISSION)[ ]section[ ](\d+)", t):
out.append(("mission", m[1]))
for r in re.findall(r"MODDING rule (\d+)", t):
out.append(("modding", r))
# 🔴 A CAPTURE FILENAME IS A CITATION and this could not see one. Five of the
# sixteen `why` fields I reported as uncited name `live-extras.png` or an
# equivalent -- openable, in `docs/re/captures/`, and exactly the evidence a
# reader wants. My published "17 uncited" was inflated by a third by my own
# extractor, which is the invents-defects failure aimed at my own backlog.
for cap in re.findall(r"\b([\w-]+\.(?:png|txt|wav|tsv))\b", t):
out.append(("capture", cap))
# ⚠️ A bare `HANDOFF` names the document and not the section. Counted, and
# counted SEPARATELY, because "the contract says so" is a weaker pointer than
# "Q5 says so" -- it sends a reader to 4 000 lines.
if re.search(r"\bHANDOFF\b", t) and not re.search(r"HANDOFF Q\d+", t):
out.append(("handoff-vague", "HANDOFF"))
return out
def walk(o, f, path, out):
if isinstance(o, dict):
for k, v in o.items():
if (k == "kind" or k.endswith("_kind")) and isinstance(v, str):
stem = "" if k == "kind" else k[: -len("_kind")]
own = o.get((stem + "_why") if stem else "why")
# 🔴 An earlier version fell back to the parent's `why` when a
# label had none of its own, and reported the result as `ok`.
# That credits a label with evidence for a DIFFERENT claim:
# every `goto_name_kind` scored on a sibling `why` about the
# DESTINATION, while the label is about where the NAME came
# from. Borrowed evidence is now its own outcome, because a
# label resting on a neighbour's argument is exactly the case
# this audit exists to surface.
out.append((f, path + "/" + k, v, text_of(own),
own is None and bool(text_of(o.get("why")))))
walk(v, f, path + "/" + k, out)
elif isinstance(o, list):
for x in o:
walk(x, f, path, out)
def selftest():
"""Does this audit notice a label that rests on nothing?
🔴 THE GAP: `audit-kinds` has always reported what it found and never been
asked whether it can find anything. A walk that matched no labels, a citation
extractor that accepted everything, or a `main` that returned 0 regardless
would all have produced the same clean run -- and clean runs from this tool
are cited in `DECISIONS.md` as evidence that fifteen labels are grounded.
Three synthetic rows are pushed through the REAL classifier, and its verdict
is read rather than reasoned about:
a `why` citing nothing -> must be BARE
a `why` citing a path that exists -> must be ok
a `why` citing a path that does not -> must be DANGLING
Exit codes follow the convention the Decoder and I converged on: 0 all good,
1 a real audit failure, **2 the harness is broken** and no clean run from it
means anything.
"""
paths = known_paths()
# The liveness case belongs in the self-test too, driven as a subprocess so
# its real exit code is read rather than reasoned about.
empty = os.path.join(os.environ.get("TMPDIR", "/tmp"), "audit-kinds-liveness")
os.makedirs(empty, exist_ok=True)
got = subprocess.run([sys.executable, os.path.abspath(__file__)], cwd=empty,
capture_output=True).returncode
print(f" harness: an empty tree -> exit {got} (want 2) "
f"{'' if got == 2 else '🔴 examined nothing and reported clean'}")
live_ok = got == 2
cases = [
("bare", "no citation of any kind here, just prose", "BARE"),
("ok", "see tools/port/audit-kinds for the method", "ok"),
("dangling", "see docs/port/NO-SUCH-FILE-XYZ.md", "DANGLING"),
]
bad = 0
for name, why, want in cases:
cites = citations(why)
if not cites:
got = "BARE"
else:
unresolved = [c for t, c in cites
if t == "path" and c not in paths and not os.path.exists(c)]
got = "DANGLING" if unresolved else "ok"
mark = "" if got == want else "🔴"
print(f" harness: a why that is {name:<9} -> {got:<8} (want {want:<8}) {mark}")
if got != want:
bad += 1
print()
if not live_ok:
bad += 1
if bad:
print("🔴 the classifier cannot tell grounded labels from ungrounded ones,")
print(" or it reports clean on an empty tree.")
print(" Exit 2: nothing this tool has reported clean is trustworthy.")
return 2
print("the classifier separates bare, dangling and grounded citations")
return 0
def coverage(files):
"""How much of the authored corpus this audit can even see.
🔴 IT SEES 15 OF 70. Every `kind` label is checked for a citation, and a
clean run has been quoted in `DECISIONS.md` as evidence that the authored
data is grounded -- but a `why` with NO `kind` beside it is invisible to this
walk entirely, and there are 55 of those against 15 labels.
Found by reading the data rather than the tool: `audio.json`'s three SE cues
carry measured provenance from HANDOFF Q8 and no `kind` field, so the audit
that exists to check provenance never looked at them.
⚠️ NOT every `why` should have a `kind`. Section prose and `_` blocks explain
a group rather than assert one value's provenance, and forcing a label there
would invite mislabelling to satisfy a counter. So this REPORTS the ratio
rather than demanding it be 1 -- a clean run must not read as full coverage.
"""
labelled = orphan = 0
for f in files:
def walk(o):
nonlocal labelled, orphan
if isinstance(o, dict):
for k, v in o.items():
if k.endswith("_why") or k == "why":
stem = k[:-4] if k.endswith("_why") else ""
kk = (stem + "_kind") if stem else "kind"
if kk in o:
labelled += 1
else:
orphan += 1
walk(v)
elif isinstance(o, list):
for x in o:
walk(x)
walk(json.load(open(f, encoding="utf-8")))
return labelled, orphan
def main():
if "--selftest" in sys.argv:
return selftest()
rows = []
for f in sorted(glob.glob("authored/*.json")):
walk(json.load(open(f)), f, "", rows)
# 🔴 LIVENESS. Run against a tree with no `authored/*.json` this printed
# "0 kind label(s)" and exited 0 -- examined nothing, reported clean. The
# Decoder's generalisation of my empty-band case, which is more general than
# either instance: **a control that only compares two things cannot tell you
# the comparison is happening.** An empty input makes a checker AGREEABLE
# rather than wrong, and agreeable is indistinguishable from correct in a
# log.
if not rows:
print("🔴 no `kind` labels found at all -- this audit examined NOTHING.")
print(" Exit 2: the harness is broken (wrong directory, renamed files),")
print(" not the corpus.")
return 2
paths = known_paths()
bare = dangling = 0
kinds = {}
print(f" {len(rows)} kind label(s) in authored/\n")
for f, where, kind, why, borrowed in rows:
kinds.setdefault(kind, 0)
kinds[kind] += 1
cites = citations(why)
bad = []
for typ, c in cites:
if typ == "capture":
if c not in paths and not os.path.exists(c) \
and not any(p.endswith("/" + c) for p in paths):
bad.append(c)
elif typ == "handoff":
if not re.search(rf"\|\s*{c}\s*\|", handoff()):
bad.append(f"HANDOFF {c} (no such row)")
elif typ == "path" and c not in paths and not os.path.exists(c):
bad.append(c)
elif typ == "sha" and not sha_ok(c):
bad.append(c)
mark = "ok "
if not cites and borrowed:
mark, bare = "🔴 BORROW", bare + 1
elif not cites:
mark, bare = "🔴 BARE", bare + 1
elif bad:
mark, dangling = "🔴 DANGL", dangling + 1
print(f" {mark} {kind:<24} {f.split('/')[-1]}{where}")
if not cites and borrowed:
print(" no `why` of its own; a sibling `why` argues a"
" DIFFERENT claim")
elif not cites:
print(f" cites nothing openable -- {len(why)} chars of prose")
elif bad:
print(f" unresolvable: {', '.join(sorted(set(bad))[:4])}")
else:
print(f" {len(cites)} citation(s), all resolve")
print()
# Casing is checked because a consumer comparing == "measured" silently
# misses "MEASURED", and a label that fails to match reads as absent.
variants = [k for k in kinds if k.lower() == "measured"]
if len(variants) > 1:
print(f" ⚠️ {len(variants)} spellings of the same label: {variants}")
print(" A consumer comparing == 'measured' misses the others, and a")
print(" label that fails to match reads as ABSENT, not as wrong.\n")
lab, orph = coverage(sorted(glob.glob("authored/*.json")))
print(f" COVERAGE: {lab} `why` field(s) carry a `kind` and were audited above;")
print(f" {orph} carry NO `kind` and are INVISIBLE to this audit. A clean run")
print(f" below is a statement about {lab} of {lab + orph} authored justifications.")
print(" ⚠️ The denominator is not a target. Of the unlabelled ones, the great")
print(" majority are SECTION PROSE -- `_` blocks and group explanations that")
print(" assert no single value's provenance, where a label would be")
print(" mislabelling to satisfy a counter. What was audited on 2026-09-01 is")
print(" the other kind: a `why` sitting beside an actual VALUE. Thirteen of")
print(" those existed unlabelled; all thirteen now carry a kind, and two of")
print(" them failed the citation check the moment they became visible.")
print()
print(f" {bare} bare or borrowed, {dangling} dangling, {len(rows) - bare - dangling} with resolving citations")
print(" 🔴 A resolving citation is not a verified label. Nothing here reads")
print(" the cited page to confirm it says what the `why` claims.")
return 1 if (bare or dangling) else 0
sys.exit(main())