This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/nav-guards.md
Sylpheed RE agent 549134c415 re: the guarded route now verifies end-to-end, and the menu order is confirmed
With the sliver stopped at both ends (290cbe3 producer, 30e53f5 consumer) a full
guarded run reached Stage 02 and asserted it: "guard: menu confirmed (1279x675)"
then "OK: UN_f101_TCAF_Acropolis is in the 14 definitions", exit 0.

Read the menu the guard confirmed rather than continuing to assume it:
NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS with the cursor on NEW GAME.
So dpad-down x1 = LOAD GAME is correct and dpad-down x2 = TUTORIAL, one press
further -- exactly consistent with the observed mis-selection when the guard was
being fed a sliver.  Capture committed as captures/main-menu-items.png.

Stated plainly: n=1 for "reproducible", and why the game window leaves the window
tree during a load was guarded against, not investigated.
2026-08-26 23:08:11 +00:00

4.8 KiB
Raw Blame History

Nav guards — one works, one was not enough, and the real cause was a sliver

Added and exercised 2026-08-26 against a live run.

✅ Guard 2 works, and earned its keep on the first run

tools/re-capture/assert_stage.py states the expected stage and checks it against the definition table (definitions are the stage's cast and are present from load; instances arrive in waves). Its first live run:

MISMATCH: UN_f101_TCAF_Acropolis is NOT in the 13 definitions
  UN_S01_Asteroid_cmesh_01a  ...  UN_e106_ADAN_Destroyer

The run had reached a live flight HUD and would have been recorded as a valid Stage 02 capture. It was the S01 tutorial. Without this check the wrong-stage roster looks exactly like a right one — which is how the earlier cross-run comparison was silently invalidated.

Use it as the precondition for any runtime measurement: ./assert_stage.py UN_f101_TCAF_Acropolis (exit 0 iff Stage 02 is loaded).

❌ Guard 1 was necessary but NOT sufficient — my explanation was wrong

require_menu in launch_mission.sh refuses to press until screen_id.py reads menu. The previous note said the tutorial was selected because the d-pad went out "from a state never confirmed to be the main menu". That explanation is refuted: this run confirmed the menu first, and still loaded the tutorial.

✅ The actual cause: the guard was shown a 10×710 sliver

sliver

The guard's own capture was 10 × 710 — not a frame at all. Every statistic in screen_id.py is an area fraction, so the sliver classified cleanly as menu (green 0.0000, white 0.0157). The guard passed on garbage, the fixed key sequence went out blind, and the selection landed on a tutorial entry.

🔑 bin/screenshot's own header already records this failure mode from 2026-08-18 — a second window of class xenia_canary meant grabs came back as slivers, and "a whole session's screen ids were noise". That fix hardened the capture side. It did not harden the consumer, so the same failure walked straight back in through a different path. Fixed now in screen_id.py: captures below 640×360 return none rather than a screen name (commit 30e53f5).

❔ Still open

  • Whether the menu guard is sufficient now that the classifier rejects slivers — not re-run. The sliver explains this run, but it does not prove the cursor/item-order assumption behind dpad down = LOAD GAME is correct.
  • Why the capture came back as a sliver at all when bin/screenshot is supposed to take the largest xenia_canary window by area. It succeeded for the other shots in the same run (1279×675), so it is intermittent, not systematic.

✅ Both ends fixed, and the route now verifies end-to-end

The sliver had to be stopped at both ends, because either alone leaves the failure reachable:

  • Producer (bin/screenshot, commit 290cbe3) — the app really does own two windows of class xenia_canary, listed in the tree right now as 10x10+10+10 and 1280x745+1+20. Largest-by-area picks the game window while both are present, but during a load the game window is briefly absent and the 10×10 helper wins by default; cropping 720 rows at its +10 gives exactly the 10x710 seen. Candidates narrower than 640 are now ignored, so nothing is selected and the existing fall-through hands back the raw root grab — itself a valid full frame. Verified on the selection logic directly: both windows listed → unchanged 1280x745+1+45; only the helper listed → old logic returned 10x10+10+10, new logic selects nothing.
  • Consumer (screen_id.py, commit 30e53f5) — captures below 640×360 return none instead of a screen name.

A full guarded run then went through cleanly:

guard: menu confirmed (1279x675)
loaded ... READYROOM ... armed ... >>> HUD
FLIGHT: flight green=0.0143
=== GUARD 2 ===
OK: UN_f101_TCAF_Acropolis is in the 14 definitions
exit=0

Stage 02, asserted rather than assumed.

✅ The menu order is confirmed by looking at it

main menu

NEW GAME · LOAD GAME · TUTORIAL · OPTIONS · EXTRAS, cursor on NEW GAME. So dpad down ×1 → LOAD GAME is right, and dpad down ×2 → TUTORIAL — one press further. That is exactly the observed failure: a single extra or mistimed press, issued while the guard was looking at a sliver, lands on the tutorial. (structures/ui-paint-order-key.md already recorded this item list; this confirms it visually and pins the cursor's start position.)

❔ Still open

  • n = 1. One clean run after the fix. The mechanism is understood and unit-tested, but "the route is now reproducible" rests on a single success.
  • Why the game window leaves the window tree during a load at all was not investigated — only guarded against.