port: --focus was ignored on the menu path; focus rendering now verified against the oracle

live-main-menu-options-focused.png -- the only capture of a known focus state --
was untestable because --focus= parsed, was stored, and was overwritten by the
authored initial focus on every _menu_enter. Every run logged focus ptbtn01
whatever was asked for. Now pushed into the menu model so navigation continues
from where it was forced.

With it working, each capture picks out exactly one button: ptbtn04 at 0.1355%
against 0.70-0.82% for the others on the OPTIONS capture, and ptbtn01 at 0.0705%
against 0.72-0.84% on the plain one. 5x and 10x discrimination. First time the
port's focus rendering has been checked against the game at all -- the existing
main_menu row uses an authored focus and could never have caught a focus error.

Records in flow.json that live-main-menu.png shows NEW GAME focused, so the
authored initial_focus matches the one frame it can be checked against -- and
that this does NOT overturn Q5's measured instability. It stays authored.

Adds main_menu_options to verify-capture at 0.13%.

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 01:45:47 +00:00
parent 69e9043b96
commit a758f9b247
4 changed files with 100 additions and 3 deletions

View File

@@ -120,7 +120,21 @@
"main_menu": {
"initial_focus": "ptbtn01",
"initial_focus_kind": "authored",
"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.",
"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.",
"",
"✅ 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 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."
],
"on_cancel": {
"goto": "title",
"goto_name": "TITLE_SCREEN",

View File

@@ -6054,3 +6054,53 @@ row, which makes it the most sensitive regression detector in the harness.
places it, found by sweeping. Choosing which frame to compare against is what
every row here does, but the 0.00093 % is a floor for *that pose*, not a general
statement of accuracy.
## `--focus=` did nothing on the menu path, and the corpus had an untested focus capture
Two unused live captures were sitting in `docs/re/captures/title-builds/`.
`live-main-menu-options-focused.png` is the menu with **OPTIONS** focused — the
only capture of a *known* focus state — and it was untestable, because
`--focus=` **silently did nothing on the `--menu` path**.
The flag parsed, was stored in `_force_focus`, and was applied to
`view.focused_id` at startup — and then `_menu_enter` overwrote it with the
authored initial focus on every entry. Every run logged `focus ptbtn01` whatever
was asked for, and all five buttons produced the same frame. It is now pushed
into the **menu model**, not just the view, so navigation continues from where it
was forced rather than jumping back on the first press.
### The port's focus rendering is right, measured against the oracle
Rendering each of the five buttons focused, against each capture:
| focused | vs `live-main-menu-options-focused` | vs `live-main-menu` |
|---|---|---|
| `ptbtn01` NEW GAME | 0.7352 % | **0.0705 %** |
| `ptbtn02` LOAD GAME | 0.8204 % | 0.8378 % |
| `ptbtn03` TUTORIAL | 0.7220 % | 0.7365 % |
| **`ptbtn04` OPTIONS** | **0.1355 %** | 0.7449 % |
| `ptbtn05` EXTRAS | 0.7029 % | 0.7236 % |
Each capture picks out exactly one button, by **5×** and **10×**. This is the
first time the port's focus rendering has been checked against the game at all —
the harness's own `main_menu` row uses an *authored* focus, so it could never
have caught a focus error.
### What that settles, and what it does not
✅ The port draws focus on the right button, distinguishably.
`live-main-menu.png` shows **NEW GAME** focused, so the authored
`initial_focus: ptbtn01` matches the one frame it can be checked against.
⚠️ **It does not overturn HANDOFF Q5**, which measured initial focus as *unstable
boot to boot* across four runs. One capture showing `ptbtn01` is consistent with
instability, not evidence against it. The value stays **authored**, with the
agreement recorded beside it. Reading this as "initial focus is settled" would be
exactly the over-generalisation from a single observation that this corpus keeps
having to withdraw.
`main_menu_options` joins `verify-capture` at 0.13 %.
⚠️ Still unused: `live-attract-title-press-a-band.png`, a 1279×**120** strip
rather than a full frame. It needs a banded comparison the harness does not do,
so it is named here rather than quietly left out.

View File

@@ -646,6 +646,27 @@ func _menu_enter(name: String, fresh: bool) -> void:
return
if fresh:
_menu.enter(name, view.screen.get("buttons", []))
# `--focus=` wins over the authored initial focus, and it did NOT before.
#
# 🔴 The flag parsed, was stored, and was applied to `view.focused_id` at
# startup -- and then this line overwrote it on every `_menu_enter`. So on
# the `--menu` path `--focus=` did nothing at all, silently: the run logged
# `focus ptbtn01` whatever was asked for.
#
# That mattered because it made an oracle capture untestable.
# `live-main-menu-options-focused.png` is the menu with OPTIONS focused --
# the only capture in the corpus of a MEASURED focus state, where the
# harness's own `main_menu` row uses an AUTHORED initial focus standing in
# for a measurement that says initial focus is unstable (HANDOFF Q5). There
# was no way to ask the port for the state the capture shows.
#
# It is pushed into the MENU MODEL, not just the view, so that navigation
# continues from where it was forced rather than jumping back on the first
# press.
if _force_focus != "" and not _menu.stack.is_empty():
var buttons: Array = view.screen.get("buttons", [])
if buttons.has(_force_focus):
_menu.stack[_menu.stack.size() - 1]["focus"] = _force_focus
view.focused_id = _menu.focus()
view.queue_redraw()
# AUTHORED, and the weakest thing in P6: HANDOFF Q10 says nothing on the disc

View File

@@ -52,6 +52,14 @@ CAPS=docs/re/captures/title-builds
MAP=(
"main_menu:$CAPS/live-main-menu.png:menu"
"extras:$CAPS/live-extras.png:menu"
# The only capture of a MEASURED focus state, and it was unusable until
# `--focus=` was made to work on the `--menu` path (it parsed, was stored, and
# was overwritten by the authored initial focus on every `_menu_enter`).
#
# It discriminates: rendering each of the five buttons focused against this
# capture gives 0.1355 % for `ptbtn04` and 0.70-0.82 % for the other four. The
# port's focus rendering identifies the right button by a factor of five.
"main_menu_options:$CAPS/live-main-menu-options-focused.png:focus:ptbtn04"
# ⚠️ THE TITLE IS POSED AT t=357.7 UNITS, NOT AT ITS SETTLE, and the time is
# MEASURED rather than chosen. The two `ptloop` sweeps are a continuous
# animation whose leaf group ends at t=600 with the quads parked off-screen at
@@ -104,10 +112,14 @@ echo "REGION -- a missing or misplaced element is a large connected blob."
echo
printf '%-17s %-9s %-7s %-22s %s\n' screen raw-rmse diff region note
for row in "${MAP[@]}"; do
IFS=: read -r name cap pose <<<"$row"
IFS=: read -r name cap pose forced <<<"$row"
if [ ${#want[@]} -gt 0 ] && ! printf '%s\n' "${want[@]}" | grep -qx "$name"; then continue; fi
[ -f "$cap" ] || { printf '%-17s %s\n' "$name" "no capture"; continue; }
if [ "$pose" = menu ]; then
if [ "$pose" = focus ]; then
godot --path port --resolution 1280x720 -- "--menu=main_menu" "--focus=$forced" \
--script=wait "--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true
[ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png"
elif [ "$pose" = menu ]; then
godot --path port --resolution 1280x720 -- "--menu=$name" --script=wait \
"--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true
[ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png"