handoff: split Q5's bundled 'measured' -- a label is as strong as its weakest cell

The port's authored/flow.json stamped title/on_cancel_why = 'MEASURED, HANDOFF
Q5' for a clause whose evidence cell in the source table reads 'none'. It did not
invent that: HANDOFF's Q5 row opened with one **measured** covering six clauses of
different strength, and HANDOFF is the document it authors against.

Split per clause. Measured: initial focus varies; up/down move one item per press
and wrap both ends; left/right do nothing; B on a submenu restores focus 4/4; B on
the main menu goes to the title in <= 0.4 s with no loading screen. NOT measured,
evidence cell empty: 'no auto-repeat at the durations tried', and 'B on the title
-> nothing'. Both marked do-not-stamp.

Also splits the source table's own up/down row, which bundled 'one item per press'
(indirectly but soundly evidenced by the 4-press wrap count) with 'no auto-repeat'
(nothing behind it, and the hedge was carrying the claim).

METHOD entry: the failure is in summarising, not at either endpoint. The source
table was honest and the consumer cited its source; flattening six claims into one
adjective created a provenance nothing supports. A strength label is not
distributive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-30 11:11:08 +00:00
parent 86a8ce7990
commit b56dfcba13
4 changed files with 170 additions and 2 deletions

View File

@@ -883,7 +883,7 @@ discovered it after re-exporting. ❔ The sweeps may simply be a small term, and
| Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, and ✅ **pak entries 0/1 and 12/15 are the LOADING screen** — two variants, plain and dressed, decoded from their `pgloading_*` element names (2026-08-29). ⚠️ **Read that in ENTRY space.** This row said "0/1 and 10/11" until 2026-08-29; that is true only of `screen list`'s ordinals, where 10→entry 12 and 11→entry 15. In entry space 10/11 are the **publisher and developer splashes** (`palogo_sqex`, `palogo_gamearts`). The port caught it; see [METHOD](../re/METHOD.md#mechanics-that-have-bitten). 🟡 which of the two is `LOADING` vs `LOADING2` is undecided; 🟡 the English member of a pair is the one in the first half of the data segment — [`ui-title-build-map.md`](../re/ui-title-build-map.md) |
| Q3 | paint order for the six screens | ✅ answered, ❔ tie-break | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md). ⚠️ **The key does not fully order a screen**: elements sharing a key are tied, and the tie-break is ❔ **undecodable from the bundle** — declaration table, `T8aD` header (exhaustive: every offset 0x000x7f at u8/u16/u32, both directions, **0** fields match the measured order against **64** for the control) and the RATC child order all give the same order the game does *not* use. ✅ **Your exposure is now measured at ZERO PIXELS (2026-08-29).** The 2 overlapping tied pairs on `EXTRAS` are `ptframe3`×`ptframe4` (the other is a `loop*` you never draw), and rendering the screen with that pair swapped changes **0 px** — because the two sprites put ink on ~3 600 pixels each and **share none of them**; the 102×132 "overlap" was a bounding-box artefact. Same on the main menu's `ptframe1`×`ptframe2`. This is blend-independent: layers that never touch the same pixel cannot be ordered wrongly. **Nothing about the tie-break can change a pixel on any of your five screens** — [`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md) |
| Q4 | button → GamePart | ✅ answered | **measured** which screen all **5** buttons open, by pressing each one and reading the screen's own title off the framebuffer. ✅ **In the form you need it: exactly ONE main-menu button opens a `GP_TITLE` entry.** `EXTRAS`**entry 6** (EN) / **9** (JP). The other four leave the archive: `NEW GAME``DIFFICULTY``SELECT DATA`; `LOAD GAME` → the save-slot list; `TUTORIAL` → the lesson list; `OPTIONS` → GAME/CONTROL/SOUND/SCREEN SETTINGS. None of those four is a `GP_TITLE` build — so a menu→submenu→back cycle inside this archive is `main menu ↔ EXTRAS` and nothing else. The **GamePart id is still a name match**, not a measurement, and 🔴 the "cheap way to measure it" this page used to point at is a dead route (the guest words are monotonic counters, not a screen id) — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
| Q5 | navigation semantics | ✅ answered | **measured**: initial focus varies boot to boot (2× `TUTORIAL`, 2× `NEW GAME`); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
| Q5 | navigation semantics | ✅ answered, ⚠️ **per clause** | 🔴 **This row used to open with a single `**measured**` covering six clauses of different strength, and the port's `authored/flow.json` copied that word into a `MEASURED` provenance stamp for a clause whose evidence cell reads `none`. A bundled label is exactly as strong as its weakest cell.** Split: ✅ **measured** — initial focus varies boot to boot (2× `TUTORIAL`, 2× `NEW GAME`); ⬆⬇ move **one item per press** (indirect: the 4-press wrap count only works if each press moves one) and **wrap both ends**; ⬅➡ do nothing; Ⓑ on a submenu returns to the parent **with focus restored** (4/4); Ⓑ on the main menu → **title**, ≤ 0.4 s, no loading screen (2026-08-30). 🟡 **NOT measured, evidence cell empty** — *"no auto-repeat at the durations tried"* (nothing records a **held** direction) and *"Ⓑ on the title → nothing"* (the one run that pressed there did so **during the build-in**, so its result is confounded). **Do not stamp either as MEASURED** — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
| Q6 | boot sequence + what drives it | ✅ answered | sequence **measured** end to end; the driver is **code, not data** — four search spaces closed, so the port **authors** the sequence — [`boot-config-and-gamepart-registry.md`](../re/boot-config-and-gamepart-registry.md) |
| Q7 | transitions | ✅ answered | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. Fade-in ramp is **decoded** from its keyframes; the ~0.4 s fade-out is **measured** (not in the file) — [`screen-transitions.md`](../re/screen-transitions.md) |
| Q8 | menu audio bindings | ✅ answered | cue vocabulary + bank **decoded**; event binding is a **name match** (the authors' own event names). ✅ **You CAN have the SE audio** — ⚠️ an earlier version of this row said it was "undecodable from the disc"; that was **retracted** and the row was stale. Three cues are located in `Static.slb` and **decode to PCM**: d-pad move `0x1ec0` (4 packets), Ⓑ back `0x0ec0` (2), Ⓐ confirm `0x5d6c0` (6), all mono 48 kHz. The bank is a packed run of XMA waves with no delimiter, so a wave is only (offset, packet count) — and ⚠️ the file order is **not** cue-id order, so the index cannot be counted out — [`menu-audio-cues.md`](../re/menu-audio-cues.md) |

View File

@@ -1532,3 +1532,26 @@ control is what said so, which is the only reason the weaker number never got us
📌 Ask, before building a matcher: **does the artefact already state the answer?**
Screens have titles, files have magic, logs have names. A derived statistic is for
when nothing states it.
## A bundled label is exactly as strong as its weakest cell
`HANDOFF.md`'s Q5 row opened with a single **`measured`** and then listed **six**
clauses of very different strength — two of which had an evidence cell reading
`none` in the source table. The port agent's `authored/flow.json` then stamped
`title/on_cancel_why = "MEASURED, HANDOFF Q5"` for one of those two.
It did not invent that. **It read the label the summary offered**, and the summary
was the document it is told to author against.
⚠️ **The failure is in summarising, not in either endpoint.** The source table was
honest — it had the empty cells. The consumer was honest — it cited its source.
Flattening six claims into one adjective is what created a provenance that nothing
supports, and provenance is the part that gets believed later, long after anyone
would re-derive the value.
**So a summary row carries a label per clause, or it carries the weakest one.**
Never the strongest, and never one label over a list. If that makes the row ugly,
the row is telling you it is doing too much.
📌 The general shape: a strength label is **not distributive**. "These six things are
measured" is a claim about the conjunction, and a reader takes it about each element.

View File

@@ -18,7 +18,8 @@ with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py).
|---|---|---|
| **initial focus, main menu** | **`TUTORIAL`** — the *middle* item, not the top | 2/2 boots, the first frame after the menu appears |
| **initial focus, `EXTRAS`** | `MISSION SELECT` — the top item | [`extras-wrap.png`](captures/menu-nav/extras-wrap.png) |
| **up / down** | one item per press, no auto-repeat at the durations tried | |
| **⬆⬇ — one item per press** | one item per press | ✅ *indirect but sound*: the wrap montage's **4 presses from `EXTRAS` landing on `OPTIONS`** only counts out if each press moves exactly one |
| **⬆⬇ — no auto-repeat** | 🟡 *"no auto-repeat at the durations tried"* | **none** — and the hedge is carrying the claim. Nothing here records a *held* direction |
| **wrap at the top** | ⬆ from the first item goes to the **last** | [`wrap-montage.png`](captures/menu-nav/wrap-montage.png), panels 1→2 |
| **wrap at the bottom** | ⬇ from the last item goes to the **first** | same, panels 3→4, and 4 presses from `EXTRAS` landing on `OPTIONS` — i.e. wrapping — is what makes the count come out |
| **left / right** | **nothing**, on the main menu | cursor unmoved across one ⬅ and one ➡ |

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Two empty evidence cells in one run: d-pad auto-repeat, and Ⓑ on a SETTLED title.
Both asked for by the port agent, and both are rows in
`menu-navigation-semantics.md` with nothing in the evidence column:
* **"no auto-repeat at the durations tried"** — the hedge is doing the work. Hold
⬇ for 2 s and count cursor moves.
* **"Ⓑ on the title → nothing"** — the previous run's second Ⓑ landed *during* the
title's build-in, so what followed was the build-in finishing. This one waits for
the plate pulse, which is the title's own settled signature
(`plate-pulse-measured.md`), before pressing.
⚠️ **The move counter is controlled before it is used**: a single 0.12 s tap must
produce exactly ONE frame-to-frame spike. If the control does not give 1, the hold
result means nothing and is not reported.
nav_repeat_and_b.py LOG OUTDIR [wait_s]
"""
import os
import re
import subprocess
import sys
import time
import numpy as np
from PIL import Image
LOG, OUT = sys.argv[1], sys.argv[2]
WAIT = float(sys.argv[3]) if len(sys.argv) > 3 else 520
W, H = 1280, 720
NEED, CEIL, HOLD_N = 500, 2500, 12
MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
SPIKE = 0.004 # fraction of pixels that must change to count as a cursor move
def deliveries(vk):
pat = re.compile((r"vk=%s flags=0001" % vk).encode())
try:
return len(pat.findall(open(LOG, "rb").read()))
except FileNotFoundError:
return 0
def _open():
return subprocess.Popen(
["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0",
"-video_size", f"{W}x{H}", "-i", ":98", "-r", "4",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
stdout=subprocess.PIPE, bufsize=W * H * 3 * 2)
def grab(p, n):
buf = p.stdout.read(n)
if len(buf) < n:
return None
return np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(float)
def glyph(a):
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def count_moves(p, n, secs, label):
"""Frame-to-frame spikes over `secs`. Each cursor move repaints the highlight."""
prev, spikes, t0, series = None, 0, time.time(), []
while time.time() - t0 < secs:
a = grab(p, n)
if a is None:
continue
if prev is not None:
d = float((np.abs(a - prev).max(axis=2) > 12).mean())
series.append(round(d, 5))
if d > SPIKE:
spikes += 1
prev = a
print(f" {label}: {spikes} spike(s) over {secs:.1f}s diffs={series}", flush=True)
return spikes
T0 = time.time()
p, n, seg = _open(), W * H * 3, time.time()
phase, streak, mark, base = "wait", 0, None, None
res = open(f"{OUT}/result.txt", "w")
while True:
el = time.time() - T0
if el > WAIT:
print(f"TIMEOUT in {phase}", flush=True); break
if time.time() - seg > 30:
p.kill(); p = _open(); seg = time.time()
a = grab(p, n)
if a is None:
p.kill(); p = _open(); seg = time.time(); continue
c = glyph(a)
if phase == "wait":
streak = streak + 1 if NEED <= c <= CEIL else 0
if streak >= HOLD_N:
print(f"[{el:7.1f}s] TITLE", flush=True)
subprocess.run([sys.executable, PAD, "tap", "A", "0.5"], check=False)
phase, streak = "tomenu", 0
elif phase == "tomenu":
streak = streak + 1 if MENU_LO <= c <= MENU_HI else 0
if streak >= MENU_HOLD:
print(f"[{el:7.1f}s] MENU (glyph {c})", flush=True); time.sleep(2)
print(" CONTROL: one 0.12 s DOWN tap — must give exactly 1 spike", flush=True)
subprocess.run([sys.executable, PAD, "tap", "DOWN", "0.12"], check=False)
ctrl = count_moves(p, n, 3.0, "control")
time.sleep(1.5)
print(" TEST: hold DOWN for 2.0 s", flush=True)
subprocess.Popen([sys.executable, PAD, "hold", "press=DOWN", "2.0"])
test = count_moves(p, n, 4.0, "hold-2s")
res.write(f"control_tap_spikes\t{ctrl}\nhold_2s_spikes\t{test}\n")
res.flush()
print(f" => control {ctrl}, hold {test}"
f"{'CONTROL FAILED, hold result void' if ctrl != 1 else ('AUTO-REPEAT' if test > 1 else 'NO AUTO-REPEAT')}",
flush=True)
time.sleep(1.5)
subprocess.run([sys.executable, PAD, "tap", "B", "0.5"], check=False)
print(f"[{time.time()-T0:7.1f}s] B pressed on the menu — waiting for the title to SETTLE", flush=True)
phase, streak = "resettle", 0
elif phase == "resettle":
streak = streak + 1 if NEED <= c <= CEIL else 0
if streak >= HOLD_N:
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/title-settled.png")
print(f"[{el:7.1f}s] TITLE SETTLED (plate pulse, glyph {c}) — pressing B", flush=True)
base = a.copy()
before = deliveries("5801")
subprocess.run([sys.executable, PAD, "tap", "B", "0.5"], check=False)
for _ in range(20):
time.sleep(0.25)
if deliveries("5801") > before:
print(" B delivered", flush=True); break
mark = time.time(); phase = "afterB"
elif phase == "afterB":
if time.time() - mark > 20:
Image.fromarray(a.astype(np.uint8)).save(f"{OUT}/after-b-on-settled-title.png")
d = float((np.abs(a - base).max(axis=2) > 12).mean())
print(f"[{el:7.1f}s] 20 s after Ⓑ on the settled title: {100*d:.1f}% of pixels differ "
f"from the moment of the press, glyph {c}", flush=True)
res.write(f"b_on_settled_title_diff_pct\t{100*d:.2f}\nb_on_settled_title_glyph\t{c}\n")
break
p.kill(); res.close()