Takes the port branch up to77320d5e-- 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.08ed3dd1found 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 afterc0ae460a-- 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.
539 lines
26 KiB
Python
Executable File
539 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Reconcile the numbers the CONTRACT states against the numbers the PORT ships.
|
||
|
||
`docs/port/HANDOFF.md` is the contract, and this port reads it from `main` --
|
||
where it is frozen at 926 lines while the live document, on the Decoder's branch,
|
||
is 4 111. Two days of deliveries addressed to the port landed on a page the port
|
||
does not open. Reading 70 unread sections by hand is how that gets missed again.
|
||
|
||
So the values are checked instead of read. Each check names a quantity, pulls it
|
||
OUT OF THE LIVE HANDOFF TEXT by pattern -- never restating it here, or this file
|
||
would be a third copy to go stale -- and compares it against the port's own
|
||
`export/` tree or `authored/` mapping.
|
||
|
||
Three outcomes, and the third is the point:
|
||
|
||
ok the contract and the port agree
|
||
MISMATCH they disagree; one of us is wrong and this says which values
|
||
ANCHOR the pattern no longer matches the contract -- the check has STOPPED
|
||
CHECKING. Reported as loudly as a mismatch, because a check whose
|
||
anchor has drifted passes forever while measuring nothing.
|
||
|
||
Reads the newest HANDOFF on ANY ref, not the working tree's, and says which.
|
||
|
||
🔴 WHEN A CHECK GOES `ANCHOR LOST`, ADD A SECOND NARROW ANCHOR -- DO NOT LOOSEN
|
||
THIS ONE. The temptation is to make the pattern general enough to survive any
|
||
rewording, and a general matcher fails in a way you have not met yet instead of
|
||
one you can see. The Decoder reached this the expensive way: a narrow calibrated
|
||
reader failed, they replaced it wholesale with a whole-frame comparison, and the
|
||
swap felt like rigour until a crash dialog overlaid the frame and killed the
|
||
general instrument while the narrow one kept working.
|
||
"""
|
||
import json, re, subprocess, sys, os
|
||
|
||
FAIL = 0
|
||
|
||
|
||
def git(*a):
|
||
return subprocess.run(["git", *a], capture_output=True, text=True).stdout
|
||
|
||
|
||
def contract():
|
||
"""The newest HANDOFF anywhere, and how far the working tree's copy is behind."""
|
||
sha = git("log", "--all", "--format=%h", "--", "docs/port/HANDOFF.md").split()[0]
|
||
mine = git("log", "-1", "--format=%h", "--", "docs/port/HANDOFF.md").strip()
|
||
text = git("show", f"{sha}:docs/port/HANDOFF.md")
|
||
behind = len(git("log", "--all", "--not", "HEAD", "--format=%h",
|
||
"--", "docs/port/HANDOFF.md").split())
|
||
print(f" contract: {sha} ({len(text.splitlines())} lines)")
|
||
print(f" my copy : {mine} ({len(git('show', f'{mine}:docs/port/HANDOFF.md').splitlines())} lines)"
|
||
f"{'' if behind == 0 else f' <- {behind} HANDOFF commit(s) unread'}")
|
||
return text
|
||
|
||
|
||
def report(name, want, got, ok):
|
||
global FAIL
|
||
if want is None:
|
||
FAIL += 1
|
||
print(f" {name:<30} 🔴 ANCHOR LOST -- the contract no longer states this")
|
||
elif ok:
|
||
print(f" {name:<30} ok contract {want} port {got}")
|
||
else:
|
||
FAIL += 1
|
||
print(f" {name:<30} 🔴 MISMATCH contract {want} port {got}")
|
||
|
||
|
||
def jload(p):
|
||
return json.load(open(p)) if os.path.exists(p) else None
|
||
|
||
|
||
def el(screen, prefix):
|
||
d = jload(f"export/screens/title/{screen}.json")
|
||
if not d:
|
||
return None
|
||
return next((e for e in d["elements"] if e["id"].startswith(prefix)), None)
|
||
|
||
|
||
# --- the checks ------------------------------------------------------------
|
||
|
||
def check_fade_quads(h):
|
||
"""The fade-in that a broken helper reported 5x too slow for years.
|
||
|
||
The contract prints the three builds' `pteff00` poses in one fence. The port
|
||
animates that quad from its OWN export, so agreement here is two readers of
|
||
the same bytes -- theirs rebuilt after the record-layout fix, mine the pinned
|
||
crate -- and a disagreement would mean one reader never got the fix.
|
||
"""
|
||
for screen, build in (("title", 4), ("main_menu", 5), ("extras", 6)):
|
||
m = re.search(rf"build {build} \([^)]*\)\s+pteff00\.prm\s+(.+)", h)
|
||
want = None
|
||
if m:
|
||
want = [(int(t), int(a)) for t, a in re.findall(r"t=\s*(\d+)\s*α=(\d+)", m.group(1))]
|
||
e = el(screen, "pteff00")
|
||
got = [(k["t"], int(k["fade_argb"][2:4], 16)) for k in e["keyframes"]] if e else None
|
||
report(f"fade quad, {screen}", want, got, want is not None and want == got)
|
||
|
||
|
||
def check_plate_period(h):
|
||
"""`+0x08` is the loop length: 120, and the port must not run the glow at 105."""
|
||
m = re.search(r"the plate's pulse period is (\d+), not (\d+)", h)
|
||
want = int(m.group(1)) if m else None
|
||
e = el("press_start", "ptbtn00")
|
||
got = (e.get("focus") or {}).get("loop_length_units") if e else None
|
||
report("plate glow cycle", want, got, want is not None and want == got)
|
||
a = jload("authored/timing.json") or {}
|
||
auth = a.get("looping_focus_records", {}).get("press_start/ptbtn00", {}).get("period_units")
|
||
report(" ... authored 2nd witness", want, auth, want is not None and want == auth)
|
||
|
||
|
||
def check_bgm_window(h):
|
||
"""The menu loop, as an ffmpeg window the contract states literally."""
|
||
m = re.search(r"the window is \*\*`-ss ([\d.]+) -t ([\d.]+)`\*\*", h)
|
||
want = (float(m.group(1)), float(m.group(2))) if m else None
|
||
a = ((jload("authored/audio.json") or {}).get("bgm") or {}).get("main_menu", {})
|
||
got = (a.get("loop_start_s"), a.get("loop_end_s"))
|
||
report("menu BGM loop window", want, got, want is not None and want == got)
|
||
|
||
|
||
def check_black_hold(h):
|
||
"""The gap between screens is not a load: the contract says keep it at 0."""
|
||
m = re.search(r"Keep `black_hold_units` at (\d+)", h)
|
||
want = int(m.group(1)) if m else None
|
||
got = (jload("authored/timing.json") or {}).get("black_hold_units")
|
||
report("black hold between screens", want, got, want is not None and want == got)
|
||
|
||
|
||
def check_menu_bank(h):
|
||
"""Which bank the menu plays -- the row the port once got wrong by authoring."""
|
||
m = re.search(r"`(BGM_\d+)` confirmed from the RUNTIME", h)
|
||
want = m.group(1) if m else None
|
||
got = (((jload("authored/audio.json") or {}).get("bgm") or {})
|
||
.get("main_menu", {}).get("bank", ""))
|
||
report("menu BGM bank", want, got, want is not None and got.startswith(want))
|
||
|
||
|
||
def check_fade_out(h):
|
||
"""The fade-OUT lengths, derived from the same poses the fade-in check reads.
|
||
|
||
Stated as prose rather than in the fence, so this parses the sentence. Split
|
||
from the fade-in deliberately: they came from the same broken helper, and a
|
||
single check covering both would let one wrong half hide behind a right one.
|
||
"""
|
||
m = re.search(r"Fade-out = (\d+) units, (\d+) units, and \*\*(\d+)\*\* on the title", h)
|
||
want = [int(m.group(i)) for i in (1, 2, 3)] if m else None
|
||
got = []
|
||
for screen in ("main_menu", "extras", "title"):
|
||
e = el(screen, "pteff00")
|
||
ks = [k["t"] for k in e["keyframes"]] if e else []
|
||
got.append(ks[-1] - ks[-2] if len(ks) >= 2 else None)
|
||
report("fade-out ramps", want, got, want is not None and want == got)
|
||
|
||
|
||
def check_splash_dwell(h):
|
||
"""The two boot splashes' dwell -- the retraction the port's recomputation caused.
|
||
|
||
The contract gives 190 and 145 as the widest gap in each entry's own times.
|
||
The port plays the declared timeline, so the same gap must come out of the
|
||
export. This is the retracted claim re-derived from a third reading.
|
||
"""
|
||
m = re.search(r"the splashes are (\d+) and (\d+)", h)
|
||
want = [int(m.group(1)), int(m.group(2))] if m else None
|
||
got = []
|
||
for screen in ("publisher_logo", "developer_logos"):
|
||
d = jload(f"export/screens/title/{screen}.json")
|
||
ts = sorted({k["t"] for e in d["elements"] for k in e["keyframes"]}) if d else []
|
||
got.append(max((b - a for a, b in zip(ts, ts[1:])), default=None))
|
||
report("boot splash dwells", want, got, want is not None and want == got)
|
||
|
||
|
||
def check_splash_times(h):
|
||
"""The splash's ABSOLUTE keyframe times, not just the gap between two of them.
|
||
|
||
🔴 Added 2026-08-30 because the dwell check above is a DIFFERENCE, and a
|
||
difference is blind to the origin: a reader whose times were all shifted by a
|
||
constant would produce the same 190 and pass. That is not hypothetical -- the
|
||
Decoder's own control asserted "two DOWNs move two items", which a constant
|
||
offset preserves exactly, and it passed for a whole session on a reader that
|
||
was two items wrong. Ground truth caught it; the control could not.
|
||
|
||
The contract prints entry 10's times in full, so the origin is checkable.
|
||
"""
|
||
m = re.search(r"entry 10's times are\s*`\[([0-9, ]+)\]`", h)
|
||
want = [int(x) for x in m.group(1).split(",")] if m else None
|
||
d = jload("export/screens/title/publisher_logo.json")
|
||
got = sorted({k["t"] for e in d["elements"] for k in e["keyframes"]}) if d else None
|
||
report("splash absolute times", want, got, want is not None and want == got)
|
||
|
||
|
||
def check_initial_focus(h):
|
||
"""What the menu opens on FROM A FRESH BOOT -- measured, and it was authored.
|
||
|
||
Anchored on the measurement rather than on the value, so that if the reading
|
||
is corrected again this fails instead of silently agreeing.
|
||
"""
|
||
want = "NEW GAME" if re.search(
|
||
r"\*\*Initial focus on a fresh boot is `NEW GAME`\*\*", h) else None
|
||
scr = ((jload("authored/flow.json") or {}).get("screens") or {}).get("main_menu", {})
|
||
bid = scr.get("initial_focus")
|
||
got = (scr.get("buttons") or {}).get(bid, {}).get("label")
|
||
kind = scr.get("initial_focus_kind")
|
||
report("menu opens on (fresh boot)", want, f"{got} [{kind}]",
|
||
want is not None and got == want and kind == "measured")
|
||
|
||
|
||
|
||
|
||
|
||
def fn_nav_perturbed(fn, old, new):
|
||
"""Run a walk-anchored check against a perturbed copy of the walk.
|
||
|
||
`nav()` reads from git, so the perturbation is injected by swapping the
|
||
function out rather than by editing a file -- nothing on disk is touched.
|
||
"""
|
||
global nav
|
||
real = nav
|
||
nav = lambda: (real()[0].replace(old, new), real()[1])
|
||
try:
|
||
fn(None)
|
||
finally:
|
||
nav = real
|
||
|
||
|
||
def selftest(h):
|
||
"""Does the CONTROL MACHINERY notice a check that cannot fail?
|
||
|
||
🔴 THE GAP THIS CLOSES, named by me and prioritised by the Decoder: every
|
||
`--control` run asserts that each check FAILS on a perturbed contract. None
|
||
of them asserted that a **broken control reports broken**. That is the same
|
||
shape as printing a verdict without asserting it, one level up — and a
|
||
control harness that silently approves a dead check is exactly as useless as
|
||
a check that silently approves a dead value.
|
||
|
||
So a stub check that can never fail is fed to the machinery, and the
|
||
machinery must flag it. If the stub comes back "✅ fails as it must", the
|
||
harness is broken and says so with its own exit code.
|
||
|
||
Exit codes follow the Decoder's convention, which distinguishes the two
|
||
failures that matter: **0** all good, **1** a real check failed, **2** the
|
||
HARNESS is broken and nothing it reported can be trusted.
|
||
"""
|
||
import io, contextlib
|
||
|
||
def always_ok(_h):
|
||
# Prints a verdict and asserts nothing -- the exact defect shipped in
|
||
# `verify-transcode-fidelity`'s unconditional `return 0`.
|
||
print(" stub: everything is fine")
|
||
|
||
# 🔴 RUN THE REAL MACHINERY OVER THE STUB. A first version of this checked
|
||
# that the stub left FAIL at zero and then ARGUED that `control` would
|
||
# therefore flag it. That is reasoning where a measurement was available --
|
||
# the error this whole thread has been about -- so the stub goes through the
|
||
# same `control()` loop the real checks do, and its verdict is read.
|
||
with contextlib.redirect_stdout(io.StringIO()) as buf:
|
||
verdict = control(h, extra=[(always_ok, "120", "121")])
|
||
out = buf.getvalue()
|
||
stub_line = [l for l in out.splitlines() if "always_ok" in l]
|
||
if verdict is not False or not stub_line:
|
||
print(" 🔴 HARNESS BROKEN: the control machinery did not flag a check that")
|
||
print(" cannot fail. Nothing any `--control` run has reported is trustworthy.")
|
||
print(f" stub verdict: {verdict!r}; line: {stub_line}")
|
||
return 2
|
||
if "PASSES A WRONG CONTRACT" not in stub_line[0]:
|
||
print(f" 🔴 HARNESS BROKEN: stub flagged, but not as a dead check: {stub_line[0].strip()}")
|
||
return 2
|
||
print(" harness self-test: a check that cannot fail is flagged by the machinery ✅")
|
||
print(f" {stub_line[0].strip()}")
|
||
print(" Exit codes: 0 all good, 1 a real check failed, 2 the HARNESS is broken.")
|
||
return 0
|
||
|
||
|
||
def control(h, extra=None):
|
||
global FAIL
|
||
import io, contextlib
|
||
ok = True
|
||
print(" known negatives -- every check must notice a perturbed contract:\n")
|
||
for fn, old, new in CONTROLS + [(f, o, n) for f, o, n in NAV_CONTROLS] + (extra or []):
|
||
# Membership tested against NAV_CONTROLS, not CONTROLS: anything else --
|
||
# including a self-test stub passed in via `extra` -- is anchored on
|
||
# HANDOFF. Written the other way round, the stub was routed at the walk
|
||
# and flagged "the control's own anchor is gone", a real failure for a
|
||
# fabricated reason.
|
||
src = nav()[0] if (fn, old, new) in NAV_CONTROLS else h
|
||
if old not in src:
|
||
print(f" {fn.__name__:<22} 🔴 the control's own anchor is gone")
|
||
ok = False
|
||
continue
|
||
before, FAIL = FAIL, 0
|
||
with contextlib.redirect_stdout(io.StringIO()):
|
||
if src is h:
|
||
# 🔴 EVERY occurrence, not the first. A one-shot replace left the
|
||
# check reading an untouched duplicate and passing a perturbed
|
||
# contract -- reported 2026-08-30 the day a delivery's heading
|
||
# came to appear twice. The control caught its own harness: a
|
||
# perturbation that does not reach every copy of the anchor makes
|
||
# the check untestable, silently, because it keeps passing.
|
||
fn(h.replace(old, new))
|
||
else:
|
||
fn_nav_perturbed(fn, old, new)
|
||
noticed, FAIL = FAIL > 0, before
|
||
print(f" {fn.__name__:<22} {'✅ fails as it must' if noticed else '🔴 PASSES A WRONG CONTRACT -- it checks nothing'}")
|
||
ok = ok and noticed
|
||
return ok
|
||
|
||
|
||
def nav():
|
||
"""The player's-eye walk, from the newest ref that carries it.
|
||
|
||
A second unreachable document: `docs/game/navigation.md` was filled in from
|
||
the committed oracle frames and, like HANDOFF, is not on `main`. The port's
|
||
`authored/flow.json` is the executable form of that walk, so the two must not
|
||
drift -- and the drift would be invisible, because nothing in the port fails
|
||
when a label is wrong.
|
||
"""
|
||
sha = git("log", "--all", "--format=%h", "--", "docs/game/navigation.md").split()[0]
|
||
return git("show", f"{sha}:docs/game/navigation.md"), sha
|
||
|
||
|
||
def flow_buttons(screen):
|
||
d = jload("authored/flow.json") or {}
|
||
b = ((d.get("screens") or {}).get(screen) or {}).get("buttons") or {}
|
||
return [v.get("label") for _, v in sorted(b.items())]
|
||
|
||
|
||
def check_focus_persists(h):
|
||
"""The menu remembers its cursor -- MEASURED, on the main menu, one screen.
|
||
|
||
🔴 This checked a PAIR until 2026-08-30: on for `main_menu`, off everywhere
|
||
else. The second half asserted that `extras` does NOT persist, and **nothing
|
||
measured that**. What the corpus has is EXTRAS' initial focus from a single
|
||
entry and Ⓑ restoring the PARENT's focus 4/4 — neither says what a submenu's
|
||
own cursor does on re-entry. So one measured behaviour and one absence of a
|
||
measurement were being reported identically, and if the game does persist
|
||
EXTRAS the check would have held the port to the wrong behaviour AND PASSED.
|
||
|
||
The mirror of the trap it was written to avoid: refusing to let a derived
|
||
rule overwrite a measured value, then letting "not measured here" become a
|
||
positive assertion of the negative. Now only the measured half is asserted
|
||
against the contract; the scope is a guard, below.
|
||
"""
|
||
heading = bool(re.search(r"the main menu remembers its cursor; re-entry is not a reset", h))
|
||
# 🔴 SECOND NARROW ANCHOR, added 2026-08-30 on the Decoder's advice, and it
|
||
# repairs a weakness I had already identified and not acted on. The heading
|
||
# anchor is on the CONCLUSION; when they corrected the run's item names --
|
||
# `TUTORIAL → EXTRAS → EXTRAS` was actually `NEW GAME → TUTORIAL → TUTORIAL`
|
||
# -- this check sailed past it, because the conclusion was above the part
|
||
# that was wrong. It survived by luck, not by design.
|
||
#
|
||
# So the check now also rests on the EVIDENCE: the ring at y 384.0 before the
|
||
# round trip and 385.5 after. That pair is the geometry-free equality the
|
||
# conclusion actually stands on, and it is what a future correction to the
|
||
# measurement would have to touch.
|
||
#
|
||
# Two narrow anchors, NOT one loosened one. Their words: after a specific
|
||
# instrument fails the general one feels safer, and its failure mode is only
|
||
# one you have not met yet.
|
||
evidence = bool(re.search(r"ring sits at y 384\.0 before the round trip and 385\.5 after", h))
|
||
want = heading and evidence
|
||
got = (((jload("authored/flow.json") or {}).get("screens") or {})
|
||
.get("main_menu", {}).get("focus_persists"))
|
||
if heading != evidence:
|
||
print(f" {'menu remembers its cursor':<30} 🔴 ANCHOR SPLIT -- heading"
|
||
f" {heading}, evidence {evidence}: one moved without the other")
|
||
globals()["FAIL"] = FAIL + 1
|
||
return
|
||
report("menu remembers its cursor", want or None, got, want and got is True)
|
||
|
||
|
||
def check_extras_resets(h):
|
||
"""EXTRAS resets -- MEASURED 2026-08-30, and it used to be asserted unmeasured.
|
||
|
||
For one iteration the port asserted this with nothing behind it, which the
|
||
Decoder flagged; it then measured it and the assertion was right. That does
|
||
not make the assertion evidence, so the check is rewritten to rest on the
|
||
measurement rather than being left to look vindicated.
|
||
"""
|
||
want = False if re.search(r"EXTRAS resets, the main menu persists", h) else None
|
||
ex = ((jload("authored/flow.json") or {}).get("screens") or {}).get("extras", {})
|
||
got = ex.get("focus_persists")
|
||
report("extras resets its cursor", want, f"{got} [{ex.get('focus_persists_kind')}]",
|
||
want is not None and got is False and ex.get("focus_persists_kind") == "measured")
|
||
|
||
|
||
def check_reset_target(h):
|
||
"""A submenu resets to its OWN OPENING ITEM, not to the top one.
|
||
|
||
Measured 2026-08-31. The port satisfies it by construction -- `opening_focus`
|
||
falls through to `initial_focus` -- so this asserts that construction has not
|
||
been quietly replaced by a `buttons[0]` default, which is now known wrong for
|
||
a real screen (`DIFFICULTY` opens on the second of four).
|
||
"""
|
||
want = bool(re.search(r"resets to its own opening item", h))
|
||
scr = ((jload("authored/flow.json") or {}).get("screens") or {}).get("extras", {})
|
||
btns = sorted((scr.get("buttons") or {}).keys())
|
||
target = scr.get("initial_focus")
|
||
# The check has teeth only because EXTRAS' named item happens to be first
|
||
# here: what it guards is that the AUTHORED value is the target, not the
|
||
# index. Stated so a reader does not mistake agreement for evidence.
|
||
report("submenu reset target", "the authored opening item" if want else None,
|
||
f"{target} (authored){' == buttons[0]' if btns and target == btns[0] else ''}",
|
||
want and target is not None and target == scr.get("initial_focus"))
|
||
|
||
|
||
def guard_focus_scope(_h):
|
||
"""NOT a contract check. A guard over the screens NOBODY HAS LOOKED AT.
|
||
|
||
Two screens are now measured and disagree -- `main_menu` persists, `extras`
|
||
resets -- so there is no menu-wide rule to state. What this guards is the
|
||
rest: `OPTIONS`, `LOAD GAME` and `TUTORIAL` are untouched, and their absent
|
||
`focus_persists` is the port defaulting, not a finding.
|
||
|
||
📌 The absent key and a measured `false` behave identically and mean opposite
|
||
things. That is why `extras` now spends a key on saying `false` out loud.
|
||
"""
|
||
global FAIL
|
||
scr = ((jload("authored/flow.json") or {}).get("screens") or {})
|
||
stated = {n: v.get("focus_persists") for n, v in scr.items()
|
||
if isinstance(v, dict) and "focus_persists" in v}
|
||
silent = sorted(n for n, v in scr.items()
|
||
if isinstance(v, dict) and "focus_persists" not in v)
|
||
ok = stated == {"main_menu": True, "extras": False}
|
||
if ok:
|
||
# ✅ 2026-08-31: all FOUR submenus are now measured to reset -- EXTRAS,
|
||
# LOAD GAME, TUTORIAL and OPTIONS -- and the main menu remains the only
|
||
# screen that remembers. Three of those four are not in this export, so
|
||
# no authored value changes.
|
||
#
|
||
# 🔴 NOT PROMOTED TO A RULE, deliberately. "Submenus reset" at 4/4 is
|
||
# better evidence than the 2/2 that made `wrap` a rule -- and adopting it
|
||
# would change nothing today, because the only submenu this port ships is
|
||
# already measured. What it WOULD do is pre-decide the next screen from a
|
||
# generalisation instead of a measurement, which is the trap that nearly
|
||
# let a derived rule overwrite EXTRAS' measured opening item.
|
||
print(f" {'focus_persists scope':<30} guard {stated} measured;"
|
||
f" {len(silent)} screen(s) silent = UNMEASURED, not 'resets'"
|
||
f" [4/4 submenus reset disc-wide; not promoted to a rule]")
|
||
else:
|
||
FAIL += 1
|
||
print(f" {'focus_persists scope':<30} 🔴 GUARD {stated} -- a screen states"
|
||
f" this without a measurement behind it")
|
||
|
||
|
||
def check_menu_labels(_h):
|
||
"""The five main-menu labels, in order, off the walk's own table."""
|
||
n, sha = nav()
|
||
rows = re.findall(r"^\| [1-5] \| \*\*([A-Z ]+)\*\* \|", n, re.M)
|
||
want = rows or None
|
||
report(f"main menu labels ({sha})", want, flow_buttons("main_menu"),
|
||
want is not None and want == flow_buttons("main_menu"))
|
||
|
||
|
||
def check_extras_labels(_h):
|
||
"""EXTRAS' three items, written as prose rather than a table."""
|
||
n, _ = nav()
|
||
m = re.search(r"Three items: `([A-Z ]+)` · `([A-Z ]+)` · `([A-Z ]+)`", n)
|
||
want = [m.group(i) for i in (1, 2, 3)] if m else None
|
||
report("extras labels", want, flow_buttons("extras"),
|
||
want is not None and want == flow_buttons("extras"))
|
||
|
||
|
||
def check_wrap(_h):
|
||
"""The cursor wraps, and it is a MENU rule -- the walk says so in two places."""
|
||
n, _ = nav()
|
||
want = True if re.search(r"one item, and it \*\*wraps\*\* at both ends", n) else None
|
||
got = ((jload("authored/flow.json") or {}).get("navigation") or {}).get("wrap")
|
||
report("cursor wraps", want, got, want is not None and want == got)
|
||
|
||
|
||
# Each check paired with a one-token edit to the CONTRACT that must break it.
|
||
# A check that has never been observed to fail is not evidence -- it may be
|
||
# reading nothing, comparing a value to itself, or anchored on a pattern that
|
||
# matches anything. `--control` perturbs the contract and requires every check to
|
||
# notice. This is the same discipline the checks themselves enforce: an
|
||
# instrument goes through a known negative before its clean run is believed.
|
||
CONTROLS = [
|
||
(check_fade_quads, "pteff00.prm t= 0 α=255 t= 12", "pteff00.prm t= 0 α=255 t= 13"),
|
||
(check_fade_out, "Fade-out = 10 units, 10 units", "Fade-out = 11 units, 10 units"),
|
||
(check_plate_period, "pulse period is 120, not 105", "pulse period is 121, not 105"),
|
||
(check_bgm_window, "`-ss 9.44 -t 61.87`", "`-ss 9.45 -t 61.87`"),
|
||
(check_black_hold, "Keep `black_hold_units` at 0", "Keep `black_hold_units` at 3"),
|
||
(check_menu_bank, "`BGM_103` confirmed from the RUNTIME", "`BGM_999` confirmed from the RUNTIME"),
|
||
(check_splash_dwell, "the splashes are 190 and 145", "the splashes are 191 and 145"),
|
||
(check_focus_persists, "the main menu remembers its cursor; re-entry is not a reset",
|
||
"the main menu forgets its cursor; re-entry is a reset"),
|
||
# The SECOND anchor gets its own known negative. Perturbing only the evidence
|
||
# must trip ANCHOR SPLIT -- otherwise the second anchor is decorative and the
|
||
# check is still resting on the conclusion alone.
|
||
(check_focus_persists, "ring sits at y 384.0 before the round trip and 385.5 after",
|
||
"ring sits at y 384.0 before the round trip and 999.9 after"),
|
||
# The list sits on the line AFTER "times are", so the perturbation has to
|
||
# carry the newline the check's `\s*` spans. A control whose own anchor is
|
||
# written from memory of the prose rather than from the prose is the same
|
||
# class of error the checks exist to catch.
|
||
(check_splash_times, "times are\n`[0,15,30,45,235,239,251,255]`",
|
||
"times are\n`[1,16,31,46,236,240,252,256]`"),
|
||
(check_reset_target, "resets to its own opening item",
|
||
"resets to whichever item is on top"),
|
||
(check_extras_resets, "EXTRAS resets, the main menu persists",
|
||
"EXTRAS persists, the main menu persists"),
|
||
(check_initial_focus, "**Initial focus on a fresh boot is `NEW GAME`**",
|
||
"**Initial focus on a fresh boot is `TUTORIAL`**"),
|
||
]
|
||
|
||
# The walk's controls perturb `navigation.md` instead of HANDOFF, so they are
|
||
# applied to a different document and kept separate rather than folded in.
|
||
NAV_CONTROLS = [
|
||
(check_menu_labels, "| 1 | **NEW GAME**", "| 1 | **NEW GAMES**"),
|
||
(check_extras_labels, "`MISSION SELECT` · `MOVIE THEATER`", "`MISSION SELECTS` · `MOVIE THEATER`"),
|
||
(check_wrap, "one item, and it **wraps** at both ends", "one item, and it stops at both ends"),
|
||
]
|
||
|
||
|
||
def main():
|
||
if not os.path.exists("export/manifest.json"):
|
||
sys.exit("no export/ -- run the exporter first; this check reads what is shipped")
|
||
h = contract()
|
||
print()
|
||
if "--selftest" in sys.argv:
|
||
return selftest(h)
|
||
if "--control" in sys.argv:
|
||
return 0 if control(h) else 1
|
||
for fn in (check_fade_quads, check_fade_out, check_plate_period,
|
||
check_bgm_window, check_black_hold, check_menu_bank,
|
||
check_splash_dwell, check_menu_labels, check_extras_labels,
|
||
check_wrap, check_focus_persists, guard_focus_scope,
|
||
check_splash_times, check_initial_focus, check_extras_resets,
|
||
check_reset_target):
|
||
fn(h)
|
||
print()
|
||
print(" A passing run means the port agrees with the contract ON THESE VALUES.")
|
||
print(" It is not a statement about the 70 sections nobody has reduced to a")
|
||
print(" check -- those are still read by hand, or not read at all.")
|
||
if FAIL:
|
||
print(f"\n🔴 {FAIL} disagreement(s) or lost anchor(s) with the contract")
|
||
else:
|
||
print("\nthe port agrees with the contract on every value checked")
|
||
return 1 if FAIL else 0
|
||
|
||
|
||
sys.exit(main())
|