diff --git a/authored/flow.json b/authored/flow.json index ea2835ec..02e26010 100644 --- a/authored/flow.json +++ b/authored/flow.json @@ -191,6 +191,17 @@ "reasoning that chose NEW GAME here -- and the Decoder had said explicitly that", "my agreeing with their records was no evidence, which was correct at the time.", "", + "✅ AND IT SURVIVES A REBOOT -- MEASURED 2026-08-31. Six fresh boots all", + "opened on NEW GAME, and THREE of them followed a session that ended with the", + "cursor on EXTRAS or OPTIONS. That is what makes it a test of persistence", + "rather than six repetitions of the same start.", + "", + "⚠️ REACH, and it is the Decoder's own caveat rather than mine: every one of", + "those sessions ended with the emulator KILLED, not shut down cleanly. A game", + "that writes menu state on a clean exit never gets the chance, so this", + "measures 'does not survive a KILLED session'. If a real console remembers a", + "cursor across a power cycle, that does not contradict this.", + "", "⚠️ WHY 'FIRST ENTRY' IS LOAD-BEARING: the menu REMEMBERS ITS CURSOR (see", "`focus_persists`), so any reading not taken on a fresh boot's first entry is", "measuring HISTORY, not what the screen opens on. That objection is what", @@ -378,7 +389,26 @@ "measuring history -- which is precisely what made the caveat necessary while", "persistence here was unknown.", "", - "⚠️ WHAT IS STILL AMBIGUOUS -- and the condition below is ALREADY MET:", + "✅ THE AMBIGUITY IS RESOLVED -- MEASURED 2026-08-31, and it went the way", + "that makes `ptbtn11` right for a REASON rather than by coincidence.", + "", + "A submenu resets to ITS OWN OPENING ITEM, and that item is a per-screen", + "default which need NOT be the first. Decoder, docs/re/data/", + "difficulty-resets-to-named-item.txt: DIFFICULTY opens on NORMAL (second of", + "four); after one confirmed DOWN to HARD, (B) out and (A) back returns to", + "NORMAL -- in-cursor 1.0 from where it opened against 93.9 from where it was", + "left. Reproduced on a FRESH BOOT and confirmed by eye, not read off the", + "2026-08-29 capture.", + "", + "So the port's `initial_focus` is the reset target, and `buttons[0]` in", + "`MenuFlow.initial_focus` is a REPAIR rather than a default -- which is how", + "it was already documented, and is now measured rather than principled.", + "", + "❔ STILL OPEN, and not leaned on: whether the reset target MOVES once a", + "difficulty has actually been confirmed. A game that remembered your last", + "choice would behave differently, and the probe never confirms one -- the", + "same SELECT DATA crash that constrains the run prevents testing it.", + "", "", "🔴 CORRECTED 2026-08-31. This read \"it matters IF another screen is ever", "authored\" whose opening item is not its first. Such a screen exists and is", @@ -405,7 +435,18 @@ " (was) Kept as `measured` because the frame really does show MISSION SELECT focused,", " (was) and kept as the port's opening item because it is the only reading there is.", " (was) 🔴 If EXTRAS turns out to persist, this becomes history and the kind must", - " (was) change with it." + " (was) change with it.", + "", + "✅ CHECKED AGAINST THE BYTES 2026-08-31, by both agents independently. The", + "Decoder attempted to refute this value and it survives: `ptbtn11` is the TOP", + "button on this screen -- y 282 against 362 and 442 -- so the port is right", + "whichever reading of the reset target applies. Confirmed from THIS port's own", + "export, a different reader of the same disc: extras 282/362/442, and the main", + "menu as a control at 162/242/322/401/482.", + "", + "🔴 WHICH ALSO MEANS EXTRAS CANNOT SEPARATE the two readings -- named item and", + "top item coincide here. It was DIFFICULTY, opening on its second of four, that", + "settled it." ], "on_cancel": { "goto": "main_menu", diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index a9b1d7fd..191ea789 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -157,7 +157,7 @@ HANDOFF. | Milestone | Needs | HANDOFF | State | |---|---|---|---| -| all — control harnesses that assert themselves | **nothing from anybody; ONE tool still lacks it** | `d38adcf` | 🟡 **DONE FOR `contract-check`, `check-claims` AND `audit-kinds`.** `audit-kinds --selftest` pushes three synthetic rows through the real classifier — citing nothing must read BARE, a real path ok, a missing path DANGLING — and returns **2** when stubbed to accept everything. `check-claims --control` gained a **fifth case**: the identical plant text *outside* the scanned root must give 0, so the boundary is asserted rather than hand-verified once. Remaining: `verify-transcode-fidelity`. Earlier text: 🟡 **DONE FOR `contract-check` AND `check-claims`, not for the rest.** `check-claims --control` now executes four cases as subprocesses — clean 0, unmarked revival 1, marked revival 0, **empty register 2** — where before it had **no control machinery at all** and an empty register reported clean forever. Verified two-directionally: pointing the plant at an unscanned path makes the control report itself broken. Remaining: `audit-kinds`, `verify-transcode-fidelity`. Earlier text: 🟡 **DONE FOR `contract-check`, NOT for the rest.** `--selftest` feeds the machinery a stub that cannot fail and requires it to be flagged; exit codes separate **0** all good / **1** a real check failed / **2** the harness is broken. Asserting in `check-all`. ⚠️ `check-claims`, `audit-kinds` and `verify-transcode-fidelity` have controls and **no harness self-test** — the shape is known and the fix is cheap, and this row exists so the gap does not read as finished. 🔴 The self-test caught two defects while being written: a first version that *argued* the harness would flag the stub instead of measuring it, and a `src` selection that anchored anything outside one list at the wrong document, flagging the stub for a fabricated reason. | +| ~~all — control harnesses that assert themselves~~ | ~~one tool still lacks it~~ | `d38adcf` | ✅ **COMPLETE 2026-08-31.** `verify-transcode-fidelity --selftest` closes the list: its three always-on controls never asked whether the measurement was **live**, and with an empty band list every comparison reads 0.0 dB — identity passes, the real pair passes, and only the unrelated-movie control fails, reporting **exit 1 (a corpus problem)** for a broken instrument. Now **exit 2**. All four tools — `contract-check`, `check-claims`, `audit-kinds`, `verify-transcode-fidelity` — assert their own harnesses, each verified two-directionally. Earlier text: 🟡 **DONE FOR `contract-check`, `check-claims` AND `audit-kinds`.** `audit-kinds --selftest` pushes three synthetic rows through the real classifier — citing nothing must read BARE, a real path ok, a missing path DANGLING — and returns **2** when stubbed to accept everything. `check-claims --control` gained a **fifth case**: the identical plant text *outside* the scanned root must give 0, so the boundary is asserted rather than hand-verified once. Remaining: `verify-transcode-fidelity`. Earlier text: 🟡 **DONE FOR `contract-check` AND `check-claims`, not for the rest.** `check-claims --control` now executes four cases as subprocesses — clean 0, unmarked revival 1, marked revival 0, **empty register 2** — where before it had **no control machinery at all** and an empty register reported clean forever. Verified two-directionally: pointing the plant at an unscanned path makes the control report itself broken. Remaining: `audit-kinds`, `verify-transcode-fidelity`. Earlier text: 🟡 **DONE FOR `contract-check`, NOT for the rest.** `--selftest` feeds the machinery a stub that cannot fail and requires it to be flagged; exit codes separate **0** all good / **1** a real check failed / **2** the harness is broken. Asserting in `check-all`. ⚠️ `check-claims`, `audit-kinds` and `verify-transcode-fidelity` have controls and **no harness self-test** — the shape is known and the fix is cheap, and this row exists so the gap does not read as finished. 🔴 The self-test caught two defects while being written: a first version that *argued* the harness would flag the stub instead of measuring it, and a `src` selection that anchored anything outside one list at the wrong document, flagging the stub for a fabricated reason. | ## Coverage hole in my own check, 2026-08-31 — derived from HANDOFF `0159527` diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index a280c8cc..97197bcd 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -270 sections. Search this before re-deriving anything. +274 sections. Search this before re-deriving anything. * [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28) * [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28) @@ -281,6 +281,10 @@ dies, which is what this file is for. * [Two harness gaps closed, and one of them was mine done by hand](#two-harness-gaps-closed-and-one-of-them-was-mine-done-by-hand) * [All four submenus reset, and I am not promoting it to a rule](#all-four-submenus-reset-and-i-am-not-promoting-it-to-a-rule) * [🔴 The counter-example I kept asking for was in a file I wrote](#the-counter-example-i-kept-asking-for-was-in-a-file-i-wrote) +* [The last control harness, and a clean sweep for the top-item assumption](#the-last-control-harness-and-a-clean-sweep-for-the-top-item-assumption) +* [Settled: a submenu resets to its OWN OPENING ITEM, not to its top item](#settled-a-submenu-resets-to-its-own-opening-item-not-to-its-top-item) +* [Their refutation attempt on `extras/initial_focus` — checked against the bytes, twice](#their-refutation-attempt-on-extrasinitial_focus--checked-against-the-bytes-twice) +* [Menu focus does not survive a reboot — and the reach matters more than the result](#menu-focus-does-not-survive-a-reboot--and-the-reach-matters-more-than-the-result) ## P0 — the exporter, 2026-08-28 @@ -13791,3 +13795,117 @@ question it answers — mine included, and mine had both halves in one file. declining to promote *"4/4 submenus reset"* to a rule was argued from the principle that a generalisation should not pre-decide the next screen. **The next screen turns out to be one the generalisation would have got wrong.** + +## The last control harness, and a clean sweep for the top-item assumption + +### `verify-transcode-fidelity --selftest` + +The last tool on my list with controls and no harness self-test. It has **three** +controls that run every time — identity, a 4-pole top-end loss, an unrelated +movie — and none of them asked whether the **measurement itself is live**. + +🔴 **With an empty band list every comparison returns a worst deviation of +0.0 dB.** Identity passes. The real pair passes. Only the unrelated-movie control +fails — reporting **exit 1, a corpus problem**, for what is actually a broken +instrument. Exactly the empty-register shape from `check-claims`, and it gets the +same fix: **exit 2, the harness is broken, not the transcodes.** + +`--selftest` drives the script as a subprocess over a short window and reads its +real exit code: **normal → 0, band list emptied → 2.** Both pass. Asserting in +`check-all`. + +📌 That closes my list. Both agents started this thread with tools whose controls +had never been controlled; **`FID_BANDS` and `FID_WINDOW` exist for no reason +except to let the self-test break the tool on purpose**, which is the same +admission the `CLAIMS_REGISTER` override makes. + +### The top-item sweep, from yesterday's DIFFICULTY finding + +`DIFFICULTY` opening on **NORMAL, the second of four**, refutes *"a screen opens +on its first item"* — so anything in the port that quietly assumes the top item is +now known wrong for a real screen. Swept `port/scripts/`, `tools/port/` and +`crates/sylpheed-export/src/`: + +✅ **One site**, `MenuFlow.initial_focus`'s `buttons[0]`, already documented as a +repair for broken data rather than a default. Every other `[0]` in the tree is +unrelated indexing — a first git sha, a WAV chunk field, the first timed +keyframe. **Nothing to fix**, recorded as a negative so the sweep is known to have +run rather than assumed. + +## Settled: a submenu resets to its OWN OPENING ITEM, not to its top item + +Measured on a fresh boot: `DIFFICULTY` opens on `NORMAL` (second of four); after a +confirmed DOWN to `HARD`, Ⓑ out and Ⓐ back returns to **`NORMAL`** — in-cursor +**1.0** from where it opened against **93.9** from where it was left. + +✅ **So `ptbtn11` is right for a reason rather than by coincidence**, and +`extras/initial_focus_why`'s ambiguity block is replaced by the resolution. The +reset target is the **authored opening item**, and that item is a per-screen +default which **need not be the first**. + +📌 **`MenuFlow.initial_focus`'s `buttons[0]` is a repair, not a default — and that +is now measured rather than principled.** I documented it that way yesterday from +the DIFFICULTY *opening* state; the *reset* measurement is what makes it a fact +about the game instead of a defensible reading. + +`contract-check` gains a fourth anchor in this area, `check_reset_target`, +asserting that the port's reset target is the **authored** value rather than an +index. ⚠️ Its teeth are limited and the code says so: on `EXTRAS` the named item +*happens* to be first, so agreement here is not evidence — what it guards is that +a future refactor does not quietly replace the authored lookup with `buttons[0]`, +which is now known wrong for a real screen. + +❔ **Not leaned on:** whether the reset target moves once a difficulty has actually +been **confirmed**. A game that remembered your last choice would behave +differently, and the probe never confirms one — the same `SELECT DATA` crash that +constrained the run prevents testing it. + +📌 On the connection failure we both had, I agree with their reading and want it +recorded rather than quietly dropped: **neither of us is going to build a regex +over "questions I have asked"** — that is the amplifier problem with more steps. +Two agents independently held an answer each had written down. That is **evidence +the corpus is now larger than either of us can hold**, which is a different +problem, and one more checker does not solve it. + +## Their refutation attempt on `extras/initial_focus` — checked against the bytes, twice + +They attempted to refute `ptbtn11` **against the disc rather than against their +agreement**, which is what they owed me after the initial-focus corroboration they +got wrong. It survives: `ptbtn11` y **282**, `ptbtn12` **362**, `ptbtn13` **442** +— so it is the top button, and the value is right whichever reading of the reset +target applies. + +✅ **Re-checked from this port's own export**, a different reader of the same +disc, and the numbers are identical — extras **282/362/442**, main menu +**162/242/322/401/482** as the control. Two readers, same bytes, same answer. + +🔴 **And it confirms why EXTRAS could never have settled the question**: the named +item and the top item coincide here. It took `DIFFICULTY`, opening on its second +of four, to separate them. + +## Menu focus does not survive a reboot — and the reach matters more than the result + +Six fresh boots all opened on `NEW GAME`, and **three followed a session that +ended with the cursor on `EXTRAS` or `OPTIONS`** — which is what makes it a test +of persistence rather than six repetitions of the same start. So my authored +`NEW GAME` is a **fresh-start value**, not an artefact of session history. + +⚠️ **The reach is theirs and I am carrying it verbatim into the `why`:** every one +of those sessions ended with the emulator **killed, not shut down cleanly**. A +game that writes menu state on a clean exit never gets the chance — so this +measures *"does not survive a killed session"*. **If a real console remembers a +cursor across a power cycle, that does not contradict this.** + +📌 **No boot was spent on it.** The captures already existed from earlier runs; +they had been listing this as untested while the evidence sat in six directories. +**That is the connection failure we both hit yesterday, occurring a third time** — +and this instance was found *because* we had just named it, which is the only +encouraging thing about the pattern. + +⚠️ Noted, touching nothing of mine: their `ring_row.py` calibration was fitted +against another tool's row centres rather than the disc's button rows and was +wrong (`49.5 + 1.060·y` re-fitted to `64.82 + 0.9919·y`, residuals under 0.7 px — +an offset, essentially no scaling). **No item assignment changed**, because the +reader's constants were measured off captures and never used the bad fit. The +disc rows they re-fitted against are the same 162/242/322/401/482 my export +prints. diff --git a/tools/port/check-all b/tools/port/check-all index a792b271..480100fc 100755 --- a/tools/port/check-all +++ b/tools/port/check-all @@ -61,6 +61,9 @@ step kinds-harness must-pass tools/port/audit-kinds --selftest # the difference-signal half of the same tool stays report-only and asserts # nothing. See docs/port/DECISIONS.md -- the waveform question is still open. step transcode-bands must-pass tools/port/verify-transcode-fidelity +# Asks whether the band measurement is LIVE, not just what it found. An empty +# band list makes every comparison read 0.0 dB and pass; that now exits 2. +step bands-harness must-pass tools/port/verify-transcode-fidelity --selftest step capture-controls must-pass tools/port/check-capture-controls step menu-audio must-pass env OUT="$OUT/audio" tools/port/verify-menu-audio # A stale index is worse than none: it answers "is this already decided?" with a diff --git a/tools/port/contract-check b/tools/port/contract-check index d1c5e5ce..19f1cb81 100755 --- a/tools/port/contract-check +++ b/tools/port/contract-check @@ -379,6 +379,26 @@ def check_extras_resets(h): 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. @@ -471,6 +491,8 @@ CONTROLS = [ # 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`**", @@ -499,7 +521,8 @@ def main(): 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_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.") diff --git a/tools/port/verify-transcode-fidelity b/tools/port/verify-transcode-fidelity index a7df73a1..c4192dc1 100755 --- a/tools/port/verify-transcode-fidelity +++ b/tools/port/verify-transcode-fidelity @@ -165,6 +165,15 @@ def align(src, dst, af): # answer came out right, which is the thing this project keeps catching. BANDS = [(0, 500), (500, 2000), (2000, 6000), (6000, 10000), (10000, 16000)] +# `FID_BANDS=none` empties the band list and `FID_WINDOW` shortens the analysis +# window. Both exist ONLY so `--selftest` can drive this script as a subprocess +# in a deliberately broken configuration and read its real exit code, rather than +# reasoning about what it would do -- the failure I walked into on my first +# harness self-test and the Decoder walked into on theirs. +if os.environ.get("FID_BANDS") == "none": + BANDS = [] +WINDOW_S = float(os.environ.get("FID_WINDOW", WINDOW_S)) + def band_db(path, af, lo, hi, seconds=25.0, skip=2.0): """RMS in one band, straight out of `astats`. @@ -289,7 +298,44 @@ def compare(src, dst, af, label): return down +def selftest(): + """Can this tool tell a working configuration from a broken one? + + 🔴 THE LAST GAP ON MY LIST. This script has three controls that run every + time -- identity, a 4-pole top-end loss, an unrelated movie -- and none asks + whether the MEASUREMENT ITSELF is live. With an empty band list every + comparison returns a worst deviation of 0.0: identity passes, the real pair + passes, and only the unrelated-movie control fails -- reporting **exit 1, a + corpus problem**, for what is actually a broken instrument. Same shape as the + empty register in `check-claims`, and the same fix: a distinct answer. + + Drives this script as a subprocess over a short window and reads its real + exit code: normal -> 0, band list emptied -> 2. + """ + env = dict(os.environ, FID_WINDOW="4") + ok = True + for label, extra, want in (("normal config", {}, 0), + ("band list emptied", {"FID_BANDS": "none"}, 2)): + got = subprocess.run([sys.executable, __file__], env={**env, **extra}, + capture_output=True).returncode + mark = "✅" if got == want else "🔴" + print(f" harness: {label:<20} exit {got}, wanted {want} {mark}") + ok = ok and got == want + print() + print("the band measurement can tell a broken configuration from a clean run" + if ok else "🔴 the harness cannot distinguish a broken configuration") + return 0 if ok else 2 + + def main(): + if "--selftest" in sys.argv: + return selftest() + # 🔴 An empty band list makes every comparison read 0.0 dB and pass. That is + # the harness failing, not the transcodes, and it gets its own exit code. + if not BANDS: + print("🔴 the band list is EMPTY -- every comparison would read 0.0 dB and") + print(" pass. Exit 2: the harness is broken, not the transcodes.") + return 2 man = json.load(open("export/manifest.json")) names = [v["name"] for v in man.get("videos", [])] control = "--control" in sys.argv