port: an authored value becomes measured, and a difference-only check gets an origin

The Decoder corrected their own focus delivery: the persistence run's item names
were two positions out, from a reader using design-space rows against captures
carrying Xenia's chrome and a 1.060 scale. Two things follow.

initial_focus_kind moves from authored to measured. NEW GAME on a fresh boot, 2/2
fresh boots, both the first menu entry. The value did not change; its standing
did, and the upgrade is not because the measurement agrees with me -- they had
said my agreeing with their records was no evidence, which was correct, and this
is a direct reading independent of the reasoning that chose NEW GAME here. "First
entry" is load-bearing: since the menu remembers its cursor, a reading taken
later measures history, which is the objection that voided the earlier
TUTORIAL-versus-NEW-GAME disagreement. The superseded reasoning is kept under
(was) lines -- the field existing and being labelled honestly is what made
arriving at a measurement a label change rather than an archaeology problem, the
third time that has paid off after loop_start_s and the +0x08 read.

My check_focus_persists anchor survived a correction it should not have been able
to detect. It anchors on the heading, the conclusion, not on the item names. That
is lucky rather than designed: the conclusion is geometry-free -- ring at y 384.0
before the round trip and 385.5 after, an equality immune to a constant offset --
while the names were not. The 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.

Their generalisation: a control that only checks differences is blind to the
origin. check_splash_dwell is that shape -- it compares the widest gap between
keyframe times, and a reader with every time shifted by a constant passes. Added
check_splash_times, asserting the absolute list the contract prints. Origin and
difference now fail independently.

Writing that control reproduced the 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 its own anchor gone. 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, all firing.

Q2 closed: fixed same day, and the row was worse than I reported -- the splashes
were also mis-paired as 10/11, one half each of two different pairs.

EXTRAS remains unmeasured; the run meant to settle it navigated to OPTIONS
believing it was EXTRAS. Every asserting check passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
Sylpheed port agent
2026-08-30 22:03:58 +00:00
parent 90025a3e44
commit 5ff278a5ca
4 changed files with 141 additions and 15 deletions

View File

@@ -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",

View File

@@ -151,7 +151,7 @@ HANDOFF.
| Milestone | Needs | HANDOFF | State |
|---|---|---|---|
| P0P7 — 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. |
| ~~P0P7 — 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`

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
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)
<!-- /INDEX -->
## 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.

View File

@@ -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.")