Files
Sylpheed/tools/re-capture/nav_repeat_and_b.py
sylph-decoder 7a63501bc1 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
2026-08-30 11:11:08 +00:00

145 lines
5.8 KiB
Python
Executable File

#!/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()