diff --git a/authored/flow.json b/authored/flow.json index ef7d675f..ce3f3693 100644 --- a/authored/flow.json +++ b/authored/flow.json @@ -137,7 +137,7 @@ }, "main_menu": { "initial_focus": "ptbtn01", - "initial_focus_kind": "authored", + "initial_focus_kind": "measured", "focus_persists": true, "focus_persists_kind": "measured", "focus_persists_why": [ @@ -180,19 +180,40 @@ "non-persistence half until it returns." ], "initial_focus_why": [ - "AUTHORED, standing in for HANDOFF Q5, which measured that initial focus is NOT STABLE: four boots of the same harness opened on TUTORIAL, TUTORIAL, NEW GAME, NEW GAME. A port has to open on something. ptbtn01 (NEW GAME) is picked because it is one of the two states actually observed and it is the top item, so a reader can predict it. It is a CHOICE. Delete this the day the RE agent finds what selects it. CORROBORATED 2026-08-29, and still not decoded: the committed capture live-main-menu.png has NEW GAME focused. Identified by rendering all five focus states and taking the minimum difference -- 531 differing pixels against 6080-7094 for the others, an 11.5x margin -- with the method controlled on live-main-menu-options-focused.png, whose answer is in its filename and which it picks by 4.7x. That means the port's choice matches the state of one committed frame. It does NOT make focus stable: Q5's four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME, and this identifies one frame rather than a rule. Delete this entry the day something says what SELECTS it. TIGHTENED 2026-08-29: Q5 now has SIX boots, and the shape is sharper than 'unstable' -- TUTORIAL x3, NEW GAME x3, and NO OTHER ITEM EVER OBSERVED. So it is not uniform over five buttons; whatever selects it has to explain a two-way split. That does not change this choice (NEW GAME remains one of exactly two observed states, and it is the state of the committed capture) but it does change what would REFUTE it: a boot opening on LOAD GAME, OPTIONS or EXTRAS would break the two-way shape, and a rule that predicts the split would delete this entry outright.", + "MEASURED 2026-08-30 (later) -- `NEW GAME` on a fresh boot, 2/2 fresh boots,", + "both the FIRST menu entry. Decoder, HANDOFF `bf9e07f`, section \"correcting", + "today's focus delivery\"; ring row y=225.5 against a measured 79.25 px step,", + "data in docs/re/data/menu-focus-reader-offset.txt.", "", - "✅ CONSISTENT WITH THE ONE CAPTURE, measured 2026-08-30. Rendering each of the", - "five buttons focused against `live-main-menu.png` gives 0.0705 % for ptbtn01", - "and 0.72-0.84 % for the other four -- a 10x discrimination. So that capture", - "shows NEW GAME focused, and the authored choice matches it.", + "🔴 THIS FIELD WAS `authored` UNTIL NOW AND THE UPGRADE IS NOT BECAUSE IT", + "AGREES WITH ME. The value did not change; its standing did. The confirmation", + "is a direct reading of a fresh boot's first menu entry, independent of the", + "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.", "", - "⚠️ THIS DOES NOT OVERTURN Q5. Q5 measured initial focus as UNSTABLE across", - "four boots; one capture showing ptbtn01 is consistent with that and does not", - "contradict it. What the measurement establishes is narrower and still worth", - "having: the port's focus rendering is distinctive enough that a capture", - "identifies which button is focused, and this authored value is not at odds", - "with the only frame we can check it against. It stays AUTHORED." + "⚠️ 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", + "invalidated the earlier TUTORIAL/NEW GAME disagreement, and this measurement", + "is the one that is immune to it.", + "", + "The superseded reasoning is kept below, because it is what made the wait cheap:", + "the field existed and was labelled honestly, so arriving at a measurement was a", + "label change and not an archaeology problem.", + "", + " (was) AUTHORED, standing in for HANDOFF Q5, which measured that initial focus is NOT STABLE: four boots of the same harness opened on TUTORIAL, TUTORIAL, NEW GAME, NEW GAME. A port has to open on something. ptbtn01 (NEW GAME) is picked because it is one of the two states actually observed and it is the top item, so a reader can predict it. It is a CHOICE. Delete this the day the RE agent finds what selects it. CORROBORATED 2026-08-29, and still not decoded: the committed capture live-main-menu.png has NEW GAME focused. Identified by rendering all five focus states and taking the minimum difference -- 531 differing pixels against 6080-7094 for the others, an 11.5x margin -- with the method controlled on live-main-menu-options-focused.png, whose answer is in its filename and which it picks by 4.7x. That means the port's choice matches the state of one committed frame. It does NOT make focus stable: Q5's four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME, and this identifies one frame rather than a rule. Delete this entry the day something says what SELECTS it. TIGHTENED 2026-08-29: Q5 now has SIX boots, and the shape is sharper than 'unstable' -- TUTORIAL x3, NEW GAME x3, and NO OTHER ITEM EVER OBSERVED. So it is not uniform over five buttons; whatever selects it has to explain a two-way split. That does not change this choice (NEW GAME remains one of exactly two observed states, and it is the state of the committed capture) but it does change what would REFUTE it: a boot opening on LOAD GAME, OPTIONS or EXTRAS would break the two-way shape, and a rule that predicts the split would delete this entry outright.", + " (was) ", + " (was) ✅ CONSISTENT WITH THE ONE CAPTURE, measured 2026-08-30. Rendering each of the", + " (was) five buttons focused against `live-main-menu.png` gives 0.0705 % for ptbtn01", + " (was) and 0.72-0.84 % for the other four -- a 10x discrimination. So that capture", + " (was) shows NEW GAME focused, and the authored choice matches it.", + " (was) ", + " (was) ⚠️ THIS DOES NOT OVERTURN Q5. Q5 measured initial focus as UNSTABLE across", + " (was) four boots; one capture showing ptbtn01 is consistent with that and does not", + " (was) contradict it. What the measurement establishes is narrower and still worth", + " (was) having: the port's focus rendering is distinctive enough that a capture", + " (was) identifies which button is focused, and this authored value is not at odds", + " (was) with the only frame we can check it against. It stays AUTHORED." ], "on_cancel": { "goto": "title", diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index e1f3428f..11739707 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -151,7 +151,7 @@ HANDOFF. | Milestone | Needs | HANDOFF | State | |---|---|---|---| -| P0–P7 — Q2's map of `GP_TITLE` | **list entries 10, 11, 13 and 14 in the Q2 row** | `12c9f04` | 🟡 **the count is right and the enumeration is short; not blocking.** Q2 says *"`GP_TITLE` is 8 screens shipped twice, EN/JP"* and lists `2/3`, `4/7`, `5/8`, `6/9`, `0/1`, `12/15` — **six states of the eight**. The two boot splashes, `publisher_logo` (10/13) and `developer_logos` (11/14), appear nowhere in it. ✅ The headline is confirmed by my export's entry map: 4 UI states + 2 loading variants + **2 splashes** = 8, shipped twice = the 16 entries the archive holds. 🔴 A reader counting Q2 gets twelve and has no slot for the splashes — **and this is the row already corrected once for an ordinal-versus-entry error**, which is the mistake four unlisted entries feed. The port is unaffected: both splashes are exported, named, and verified against captures at RMSE 2.17 and 3.05. | +| ~~P0–P7 — Q2's map of `GP_TITLE`~~ | ~~list entries 10, 11, 13 and 14 in the Q2 row~~ | `12c9f04` | ✅ **FIXED SAME DAY, and the row was worse than I reported.** All eight states are enumerated now and the per-entry names are committed as reference data, so the next reader checks rather than counts. ⚠️ My report said the splashes were *missing*; they were also **mis-paired as 10/11**, which is one half each of two different pairs — 10/13 is `palogo_sqex`, 11/14 is `gamearts`/`seta`/`anima`, matching my export's entry map exactly. Original text: 🟡 **the count is right and the enumeration is short; not blocking.** Q2 says *"`GP_TITLE` is 8 screens shipped twice, EN/JP"* and lists `2/3`, `4/7`, `5/8`, `6/9`, `0/1`, `12/15` — **six states of the eight**. The two boot splashes, `publisher_logo` (10/13) and `developer_logos` (11/14), appear nowhere in it. ✅ The headline is confirmed by my export's entry map: 4 UI states + 2 loading variants + **2 splashes** = 8, shipped twice = the 16 entries the archive holds. 🔴 A reader counting Q2 gets twelve and has no slot for the splashes — **and this is the row already corrected once for an ordinal-versus-entry error**, which is the mistake four unlisted entries feed. The port is unaffected: both splashes are exported, named, and verified against captures at RMSE 2.17 and 3.05. | ## Ask for the HUMAN, 2026-08-30 — derived from HANDOFF `27938aa` diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 68686288..8da1c9cb 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -244 sections. Search this before re-deriving anything. +245 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) @@ -255,6 +255,7 @@ dies, which is what this file is for. * [🔴 Correction, same day: I encoded an absence of measurement as a finding](#correction-same-day-i-encoded-an-absence-of-measurement-as-a-finding) * [The `kind` sweep I said I owed: 15 labels, and 7 rested on a neighbour's argument](#the-kind-sweep-i-said-i-owed-15-labels-and-7-rested-on-a-neighbours-argument) * [A refutation attempt on Q2's map of `GP_TITLE` — the count is right, the list is short](#a-refutation-attempt-on-q2s-map-of-gp_title--the-count-is-right-the-list-is-short) +* [An authored value became a measured one, and a difference-only check got an origin](#an-authored-value-became-a-measured-one-and-a-difference-only-check-got-an-origin) ## P0 — the exporter, 2026-08-28 @@ -12706,3 +12707,63 @@ off-by-three to hide. The splashes are not obscure to the contract either; the Not a defect in the port — `publisher_logo` and `developer_logos` are exported, named and verified against captures at RMSE 2.17 and 3.05. Reported because the map is what the next reader will trust. + +## An authored value became a measured one, and a difference-only check got an origin + +The Decoder corrected their own focus delivery today: the item names in the +persistence run were **two positions out** — reported `TUTORIAL → EXTRAS → +EXTRAS`, actually `NEW GAME → TUTORIAL → TUTORIAL` — from a reader using +design-space rows against captures carrying Xenia's chrome and a 1.060 scale. + +### ✅ `initial_focus` is no longer authored + +**`NEW GAME` on a fresh boot, 2/2 fresh boots, both the first menu entry.** +`initial_focus_kind` moves from `authored` to `measured`. + +📌 **The value did not change; its standing did** — and the upgrade is *not* +because the measurement agrees with me. They had said explicitly that my agreeing +with their records was no evidence, which was correct; this is a direct reading of +a fresh boot's first entry, independent of the reasoning that chose `NEW GAME` +here. **"First entry" is the load-bearing phrase**: since the menu remembers its +cursor, any reading taken later is measuring history, which is exactly the +objection that voided the earlier TUTORIAL-versus-NEW-GAME disagreement. + +The superseded reasoning is kept under `(was)` lines rather than deleted. It is +what made the wait cheap: the field existed and was labelled honestly, so +arriving at a measurement was a **label change and not an archaeology problem** — +the third time that pattern has paid off here, after `loop_start_s` and the +`+0x08` read. + +### 🔴 My anchor survived a correction it should not have been able to detect + +`check_focus_persists` anchors on the **heading** — the conclusion — not on the +item names, so the correction did not break it. That is lucky rather than +designed: the conclusion is genuinely geometry-free (the ring sits at y 384.0 +before the round trip and 385.5 after, an *equality* immune to a constant +offset), while the names were not. **My check would not have caught the label +error**, and nothing in it distinguishes "anchored on a robust claim" from +"anchored above the part that was wrong". + +### The generalisation, and where it bit me + +Their statement of it: **a control that only checks differences is blind to the +origin.** Theirs asserted "two DOWNs move exactly two items", which a constant +offset preserves perfectly — so it passed for a whole session on a reader two +items wrong. Ground truth caught it; the control could not. + +🔴 **`check_splash_dwell` is that shape.** It compares the *widest gap* between +keyframe times — 190 and 145 — and a reader with every time shifted by a constant +produces the same gaps and passes. Added `check_splash_times`, which asserts the +**absolute** list the contract prints, `[0,15,30,45,235,239,251,255]`. Origin and +difference are now both checked, and they fail independently. + +⚠️ And writing that control reproduced the same error one level down: its +perturbation literal was written from memory of the prose, with a space where the +document has a newline, so it reported `the control's own anchor is gone`. The +check's `\s*` had spanned the line break; the control's literal did not. **A +control written from a memory of the source rather than from the source is the +class of error these checks exist to catch.** Thirteen controls now, all firing. + +❔ **EXTRAS remains unmeasured** — the run meant to settle it navigated to OPTIONS +believing it was EXTRAS, so `initial_focus: ptbtn11` stays undecided and nothing +here is built on it. diff --git a/tools/port/contract-check b/tools/port/contract-check index 36bb8817..0788b4f3 100755 --- a/tools/port/contract-check +++ b/tools/port/contract-check @@ -158,6 +158,41 @@ def check_splash_dwell(h): 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") + + @@ -303,6 +338,14 @@ CONTROLS = [ (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 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_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 @@ -324,7 +367,8 @@ def main(): 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_wrap, check_focus_persists, guard_focus_scope, + check_splash_times, check_initial_focus): fn(h) print() print(" A passing run means the port agrees with the contract ON THESE VALUES.")