Commit Graph

375 Commits

Author SHA1 Message Date
sylph-decoder
7e717483e1 tools: walk to EXTRAS by cursor movement, and size-match against several builds
menu_blend_capture.sh counted two DOWNs to reach EXTRAS, which is wrong twice
over -- EXTRAS is the fifth item, and on 2026-08-31 four DOWNs landed on OPTIONS
because one press was dropped. It now presses until the cursor stops moving,
which needs no item count and no row calibration. Its title deadline follows the
same change as title_blend_capture.sh, 1200 s not 420.

ui_blend_map.py takes a comma-separated build list, because the live title is
TWO builds composited -- 4 draws the art, 2 draws the PRESS (A) plate -- and a
one-build size table cannot name the elements of a title capture.
frame_alpha_census takes its builds from argv for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:22:53 +00:00
sylph-decoder
63dce8daaf tools: capture the title's blend states, and replicate the menu in a second session
Completes the reach of ui-blend-mode-measured.md, which was explicitly two
screens and one session and explicitly not the title.

Three things it does differently from menu_blend_capture.sh, each one paid for
yesterday: a 1200 s title deadline instead of 420 (the 420 fired while the game
was minutes from the settled title); the title arm is gated on the plate pulse
because build 2's PRESS (A) plate is part of what is being measured; and on
timeout it leaves the emulator up and says to attach rather than reboot.

Committed before it is run, per METHOD.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:11:50 +00:00
sylph-decoder
5a16ce5aba method: a refusing instrument, and a boot that outlasts its capture script
Two traps this run paid for.

ring_row.py's ROW0/SPACING are x11grab constants. On a /sylph-home/re/shots/shot-0001.png grab of the
same live main menu the rows read 180.5/419.5/502.0 -- ROW0 is 45 px out, 0.57
of a step. The module refused rather than naming the wrong item, which is the
good failure, and is_main_menu() therefore returned False ON A REAL MAIN MENU. A
run gated on it would conclude 'not the menu' while sitting on the menu. Not
recalibrated: three rows from one session are not a calibration and other tools
share the constants; the module now says so where the numbers are.

menu_draw_capture.sh's 420 s title deadline fired, and the emulator left running
was at the settled title minutes later, took one A, and reached the menu first
try. A timeout is a measurement of the timeout. Leaving the emulator up after a
failed script rescued this run for one minute against a twenty-minute reboot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:08:41 +00:00
sylph-decoder
3a617ffafe re: the UI blend mode is MEASURED -- the frames are drawn ADDITIVE
Closes the one route t32-blend-mode-not-on-disc.md left open: the executable's
draw path. Canary's UI draw capture now logs RB_BLENDCONTROL0 per draw, and the
game was driven to the main menu and to EXTRAS with F10 at each.

The title-side UI uses two blend states and ONE pixel shader:

  0x07010701  src=ONE dst=1-SRC_ALPHA  alpha-over (premultiplied)
              ptbase, pteff05, the fade quad, ptmsg, ptmsg2, pttitle, buttons
  0x01010101  src=ONE dst=ONE          ADDITIVE
              ptframe1, ptframe2, ptframe3, pteff20, both rotated sweep strips

Two controls, both run before the result was read:

* the NDC->pixel conversion that identifies a draw by its quad size reproduces
  1134 and 1303 px for the two rotated sweep strips -- numbers measured by a
  different tool in a different session -- on BOTH screens. The tool prints
  PASS/FAIL and disclaims its own output on FAIL.
* pixel shader 0xE59B2B3DA4AA9008 is used with BOTH states, 12 draws additive
  and 18 alpha-over. ptframe1 and ptbase run the same shader; only the blend
  register differs. So this is a blend result, not a shader result.

This confirms the port's independent measurement -- it solved the composite per
pixel from two backgrounds and found additive halves alpha-over's error on both
frames -- by a route with nothing in common with it.

So the blend is no longer authored: 'any blend you choose is authored' was true
of the disc and is not true of the game. What is still unknown is which field
selects it; elements sharing a mode are batched into one draw call, so the
selection happens before the draw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:06:30 +00:00
sylph-decoder
b564f411c4 tools: map a Canary UI draw log's blend states onto named sprites
Identifies each draw by the pixel size of its quad -- NDC extents times the
1280x720 surface -- matched against sprite dimensions read off the disc, since
the log names no elements.

Carries its own control: build 5 draws two rotated sweep strips whose heights
were measured independently at 1134 and 1303 px in
docs/re/data/title-sweep-drawn-at-rest.txt. If the conversion does not reproduce
those, every size it prints is wrong and it says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:02:12 +00:00
sylph-decoder
619858104f tools: capture the BLEND STATE of every UI draw on the main menu
Canary's CaptureUiDrawForRE now logs RB_BLENDCONTROL0, RB_COLORCONTROL and
RB_COLOR_MASK per draw, raw and decoded, alongside the shader hashes and bound
texture it already logged. This script drives the game to the main menu and arms
it there.

Unlike menu_draw_capture.sh it does NOT pass --log_ui_draws: Canary's own source
records that arming is unconditional now and that launching with the flag
correlates with the title refusing (A), 0 of 7 runs against 4 of 5 without.

Committed before it is run, per METHOD.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 05:38:31 +00:00
sylph-decoder
d0eae043f4 tools: the ja DIFFICULTY run uses --reach-only, which is why the first attempt failed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 04:41:41 +00:00
sylph-decoder
0fb2c35da2 tools: add --reach-only, so a caller that just needs the menu does not run the round trip
The ja DIFFICULTY capture failed because focus_persistence.py's round trip --
menu, B to title, A back -- did not return, leaving the game off-menu, and the
sweep that followed timed out with nothing to work with. Arriving at the menu is
the cheap part; the round trip is that probe's own experiment and is not every
caller's.

--reach-only stops once the menu is reached, and the session script passes it
through REACH_ONLY.

Committed before running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 04:41:30 +00:00
sylph-decoder
41ad0ff50d tools: a ja capture of DIFFICULTY, to settle whether GP_DIALOG 2/3 are a language pair
The byte comparison showed 2/3 differ in 2.77 % while sharing every element name --
what a language pair looks like -- but that they are ENGLISH and JAPANESE rested on
the disc's convention rather than on this screen. This captures it in ja.

Reuses submenu_focus_sweep.py with SWEEP_TARGETS=0 rather than a new probe, so the
JP capture is taken by the same navigation that produced the English one. The locale
is restored on ANY exit including a crash, and the probe never presses A inside a
submenu, so it cannot reach SELECT DATA and the guest throw.

Committed before running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 04:11:47 +00:00
sylph-decoder
c2a8d8f4c6 tools: a layout control for structural claims -- and the obvious version does not work
sylpheed-port named a gap in their own rule: the fourth aside of mine to reach their
authored data was a STRUCTURE, not a decoration, and 'the unchecked things carry no
weight' did not cover it because a wrong field order looks like a fact. It carried
no weight only by luck. The fix belongs at my end, so this is the control that
should have existed when I published the layout.

The obvious form fails, and its failure is the useful part: checking that all
records are type-plausible passes on the SHIFTED alignments too, 69 of 70 in both
directions. A homogeneous repeated table has the same field types in sequence, so
any window starting on a field boundary type-checks and the interior carries no
information about phase.

Only the BOUNDARIES do. A shifted reading must consume a word from outside the table
at one end, and that word does not obey the field's type -- which is exactly how the
original error surfaced, record 0's handler reading as 0x10000000. Two-sided: the
published alignment survives at both edges and both shifts fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 03:46:52 +00:00
sylph-decoder
24dab177f4 tools: the register now prints its own scope, not just its verdict
sylpheed-port found audit-kinds auditing 16 of 71 authored justifications and never
saying so -- a checker that fails correctly while describing a sixth of the corpus.
Their line is the one that generalises: 'I checked and it was fine' and 'I checked
the part that declared itself' read identically in a log, and only one of them is
what gets quoted.

Measured here: of 86 refutation-shaped bullets in REFUTED.md, 83 are in the
registered form. 97 %, which is better than their 16/71 but was equally unstated.

The three gaps are deliberate, not a bug. They quote their claim in backticks and
are bare identifiers -- +0x29d0, position = instance - 0x12c -- so registering them
would match every live mention of the same offset and train the check to be
ignored. Reported rather than forced to 100 %, for the same reason they report the
ratio instead of demanding it: forcing a counter invites mislabelling, which is
worse than the gap.

Selftest and the real run both still exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 03:33:00 +00:00
sylph-decoder
50fd212d81 tools: the refuted-claim register never scanned code, and code is where a retraction fails to land
sylpheed-port found three live stale claims in their own source, each already
retracted in their log days earlier. Their framing is the one that matters: a
correction that does not reach the artifact a consumer reads has not been made, and
a comment sits beside the thing it describes.

check_refuted.py scanned docs/ only. Running it over tools/ and crates/ for the
first time found one here too: jp_title_session.sh justified its own existence with
'a free-running clock lands somewhere else on a fresh boot' -- a claim I refuted
myself the day before, when I measured the plate-pulse gate phase-locking the
shutter to 1.6 % of the sweep traverse. The script's stated rationale rested on a
premise I had already killed.

Fixed the file with the correction in place, including what it means for what that
script actually measures: a second capture through the same gate is a second sample
at nearly the same animation phase, so its RMSE 0.32 is a phase-locked lower bound.

Taught the register to scan code behind --code, excluding crates/sylpheed-viewer,
which is the human's tool. Controlled three ways: a planted code revival exits 1
with --code, exits 0 without it -- which is the proof the gap was real -- and 0
again once removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 03:22:55 +00:00
sylph-decoder
bef068479e tools: check_refuted now says when it judged a peer-owned file from a stale copy
It scans all of docs/, which includes files sylpheed-port authors, and my copies of
those come from main -- six of seven are days behind their branch head and one I do
not have at all. So a verdict here about one of their files is a verdict about my
stale copy.

The direction that matters is the false positive, and it is not hypothetical: on
2026-08-31 I did it by hand, telling them a BLOCKED.md row was wrong when it had
been struck for days, with their live file one git show away in a ref already
fetched here.

Reported, not excluded. Skipping their files silently would hide the exposure, and
being behind a peer's topic branch is the normal state -- making it an error would
be scenery within a day, which is sylpheed-port's call on their own peer-head tool
and the right one.

Zero hits land in those files today, so this is latent rather than active. Selftest
and the real run both still exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 02:30:17 +00:00
sylph-decoder
56ba2ea4f3 tools: ring_row gets a self-test, which found that a TITLE frame reads as NEW GAME
sylpheed-port closed their last harness gap on a control that never asked whether
the measurement was LIVE -- an empty band list made identity and the real pair both
pass. Applying that to ring_row.py, which underpins every focus finding I have made
and had no self-test at all.

It found a real defect immediately: main_menu_item(ring_row(f)) is not None was
being used as a main-menu test, and on a TITLE frame the gutter carries a bright
cluster at y=243, inside tolerance of row 0, so the title reads as NEW GAME. Glyph
714 against the menu's 327 separates them cleanly; the ring row alone does not.

It never misfired in the sweeps, because B from a submenu goes to the menu rather
than the title -- the test was simply weaker than it was being trusted to be. Added
is_main_menu(), which requires the row AND the glyph signature, and the sweep's two
menu tests now use it.

The self-test asserts the defect it guards, and includes a liveness case: a blanked
frame must return None rather than a number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 02:00:07 +00:00
sylph-decoder
d48781262e re: menu focus does not survive a reboot -- six fresh boots, three following a session that ended elsewhere
No new boot was spent: six runs had already captured the first menu entry of a
fresh boot, and all six read NEW GAME. Three of them follow a session that ended
with the cursor on EXTRAS or OPTIONS, which is what makes it a test of persistence
rather than a repeated observation.

Reach stated rather than implied: every session ends with the emulator KILLED, so a
game that writes menu state on a clean shutdown would never get the chance. This
measures 'does not survive a killed session'.

Refutation attempt on the port's extras/initial_focus: ptbtn11 -- it SURVIVES.
ptbtn11 is the top button on the EXTRAS build, with the main menu as a control
where ptbtn01 is top and is known to be NEW GAME.

Incidentally corrects ring_row.py's stated calibration. It cited capture_y = 49.5 +
1.060*design_y, fitted against menu_focus.py's row centres, which are NOT the
disc's button rows -- the disc says 162/242/322/401/482, spacing 80, and
menu_focus.py drifts up to 17 px against them. Re-fitted: 64.82 + 0.9919*design_y,
residuals under 0.7 px. No item assignment changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 01:51:03 +00:00
sylph-decoder
308d89c215 tools: let the submenu sweep target NEW GAME, for the DIFFICULTY reset question
DIFFICULTY opens on NORMAL, the second of four, so it is the screen that separates
'resets to the named item' from 'resets to the top item' -- the question
sylpheed-port has had open for several iterations.

The probe is safe on this path without modification: it presses A to enter, one
DOWN, then B to leave, and never presses A inside a submenu, so it cannot reach
SELECT DATA and the guest throw at PC 0x82307128. Noted in the source so nobody
adds one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 01:38:26 +00:00
sylph-decoder
b2a2c6a834 tools: assert the scan boundary in check_refuted's self-test
sylpheed-port's point: a control that runs where the tool does not look proves
nothing about the tool. Case 2 plants a revival INSIDE the scanned root and demands
exit 1; a new case plants the same text OUTSIDE it and demands exit 0. The pair
asserts the scan boundary is real rather than leaving it to be reasoned about --
which is how I had it, correctly but only in my head.

Verified first by hand: identical plant text gives exit 1 inside the root and 0
outside it. Five cases now, all passing, and the real corpus still runs clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 01:24:51 +00:00
sylph-decoder
571d7e87b0 tools: let the submenu sweep's self-test be checked without starting a run
Verifying it previously meant launching the script, which then waits ~150 s for a
main menu and opens x11grab captures -- so the question "did the self-test pass?"
could not be answered without disturbing whatever else was using the display. That
is how a check-the-tool step turned into competing load during a measurement once
already.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 01:14:53 +00:00
sylph-decoder
244648ae97 tools: check_refuted gets a harness self-test, which found it could not fail on an empty register
sylpheed-port closed this gap first: their controls asserted failure-on-perturbation
but nothing asserted that a BROKEN harness reports broken. Their stub is a check
that cannot fail; the equivalent here is a register that loaded no claims, which
reported clean forever.

The self-test drives the REAL machinery over synthetic corpora as subprocesses and
reads actual exit codes -- their first version reasoned about what the machinery
would do instead of running it, which is the error this whole thread is about
committed inside the tool built to prevent it.

Four cases, all passing: clean corpus 0, verbatim revival 1, marked revival 0, and
empty register 2. The fourth was a real hole, not a modelled one: the tool now
refuses instead of passing when it parses no claims. Exit convention is theirs --
0 fine, 1 a real check failed, 2 the harness is broken.

Also confirms the real corpus still runs clean at exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 01:12:09 +00:00
sylph-decoder
732907e5e1 tools: submenu sweep gets a cursor-region rule and a two-sided self-test
Sweep 1 failed because ring_row scans the MAIN MENU's gutter and these screens put
their cursors elsewhere; sweep 2 failed because a whole-frame identity test cannot
match once a crash dialog covers the centre. This rework fixes both: the decision
uses the region that CHANGED when the cursor moved, so no per-screen geometry is
assumed, and the back-on-the-menu test is the narrow ring row, which is what kept
reading correctly under the dialog.

The self-test is sylpheed-port's sharpened rule -- a control must construct the
failure it is named after. Mine was one-sided: I checked only that the rule reports
RESETS on a known-RESETS triple, so a rule biased entirely to RESETS would have
passed. It now constructs both verdicts from the same frames and exits 3 if either
is wrong.

Committed BEFORE running, which is the process fix: the previous run aborted
because I was editing and deliberately breaking this file while its own run was in
flight, and the sweep read the broken version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 01:00:56 +00:00
sylph-decoder
f4025b3164 tools: both of my checkers exited 0 no matter what -- neither could fail
sylpheed-port found an asserting step in their suite that could not fail: a
 swallowed the verdict. Tested the same thing here and both of mine had
it. check_refuted.py found a PLANTED unmarked revival, printed it, and exited 0.
impossibility_scope.py printed 'CONTROL FAILED' and exited 0 -- in a tool written
today, one message after they described the shape.

Now: unmarked assertions exit 1; a failed control exits 2. Suppressed mentions do
not fail, since they are unverified rather than wrong and failing on them would put
the clean state out of reach.

Controlled in both directions -- clean 0, planted revival 1, control passing 0,
control deliberately broken 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 00:40:24 +00:00
sylph-decoder
3d9309dace re: sweep for negatives stated as world-properties -- the tool failed its control twice first
The mission's third classification is 'undecodable, with reach', and a negative
written about the subject when it is about the method is the failure that put 'an
individual SE's audio is not extractable yet' at the head of a page whose own later
section located the waves.

The tool failed its control -- the one known instance -- twice. First the pattern
required a sentence-ending period, and headings do not end in periods, so it matched
nothing in any heading and a clean report would have been vacuous. Then the scoring
hid it, because 'yet' was in my list of scope words. It is not one: 'yet' and 'so
far' are temporal hedges naming no instrument, no search and no place looked, which
is exactly what made that heading read as bounded.

With the control passing, the two amplifier files are clean: every INDEX and HANDOFF
hit read, all legitimate. 130 unscoped candidates remain unread corpus-wide, and the
regex has a high false-positive rate -- recorded as reach, not as a clean bill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 00:14:23 +00:00
sylph-decoder
3f627eeeb4 re: first audit of INDEX against the pages it links -- 3 of 8 rows were stale
INDEX is read every iteration and is the first thing a new reader meets, and I had
admitted three times to never auditing it. Found by accident: I was about to spend
a boot measuring whether A skips a movie because INDEX said 🟡, when movie-binding.md
had it settled since 2026-08-28 with a three-boot baseline and a delivery counter,
and HANDOFF carried it correctly. The staleness was in the index alone.

Three stale rows fixed: movie skippability, SE audio extractability, and B leaving
the main menu. Five other hits read and left alone -- index and page were talking
about different clauses.

One of the three should have been caught by check_refuted.py: REFUTED holds the
same dead claim with a different second clause, and the register matches exact
wording. Its docstring documents that weakness; this is the first live instance.

Also flags a within-page contradiction not fixed here: menu-audio-cues.md line 81
still heads a section saying SE audio is not extractable, which its own line 189
refutes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 23:44:01 +00:00
sylph-decoder
7a72c1a5fa re: strike the resolution contrast and the skipping claim -- both refuted by the port's own probe
Their frame counter counts engine frames, an upper bound rather than a count: quiet,
ADV drew 6480 engine frames across a 4123-frame video, so above that crossover it
constrains nothing. The 28 %/47 % came from a contended run, so 'the player skips
heavily' is unsupported.

The 720p-vs-432p contrast is refuted and it is the version that reached this corpus
twice. Quiet, both videos run +6.7 %..+6.9 %, 5 runs, resolution-independent. The
-0.5 % was contention, not resolution.

My own error in the thread is kept rather than superseded: I corrected a correct
entry on an argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 23:28:02 +00:00
sylph-decoder
80ef6ac4e2 re: the container's audio clock is within 1.5 % of real time -- a uniform slowdown is refuted
BGM_103's loop bounds are bit offsets in the decoder context, and each wave's
duration follows from its declared byte rate, cross-checked against decoded PCM to
0.007 %. Cycle media length 62.34 / 63.29 s against 61.87 s wall: ratio 0.985,
where a uniform 8.5 % slowdown predicts 1.085 -- 10 % away and on the other side
of 1.0.

The method's error bar is its own 1.5 % self-disagreement between two stems that
must have equal duration, which is why the loop page refused this conversion for a
finer question. It is adequate for an 8.5 % effect.

Does NOT settle the frame clock: audio can hold real time on a timer while
rendering lags, and 27.6 fps and the 8.5 % splash excess are both frame-clock
numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 22:45:33 +00:00
sylph-decoder
036d5ef0bb re: two sweeps, two instrument faults -- the three submenus are still unmeasured
Sweep 1: ring_row.py scans x 500:542, the main menu's gutter. EXTRAS happened to
put its ring there; LOAD GAME, TUTORIAL and OPTIONS do not, so the reader found a
static element and all three voided on 'the ring did not move'. Differencing S1
against S2 shows the cursors moved at x 97..231, 338..1099 and 153..479.

Sweep 2: replaced the reader with a whole-frame comparison, controlled on the
EXTRAS frames whose answer is known. Then the guest crashed -- the already
documented STL map/set erase at PC 0x82307128, firing early in the boot, not
something B did -- and Xenia's crash dialog covers the screen centre, so a
whole-frame identity test can never match again. The narrow ring column the dialog
does not cover was reading correctly the whole time. I traded one blindness for
another, and did it because the first instrument had just failed.

Refutation attempt on 'B on a submenu restores the parent's focus' (4/4): it
SURVIVES, and the run I had written off is what corroborates it. The stuck frame
has the ring at y 303.5 = LOAD GAME, the item entered from. A fifth instance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 22:25:04 +00:00
sylph-decoder
1fa3ffd08b tools: focus_persistence.py used the reader that was two items out
Now uses the shared measured reader, which refuses to name a row outside the
calibration rather than guessing. Replaying this run's stored frames through it
gives NEW GAME / TUTORIAL / TUTORIAL, which is what the corrected record says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 22:08:10 +00:00
sylph-decoder
76013733c6 re: refutation attempt on Q2's 'shipped twice' -- it survives, with one real caveat
The doubt was my own artefact: the entry dump printed only the first two sprite
names in HashMap order, making 11 and 14 look like different studios. Full sets
are identical.

7 of 8 pairs declare identical sprite sets, control included. 4/7 does not: entry
7 carries nine sprites entry 4 lacks, including ptlogo_jp and ptlogo_jpeff, so the
Japanese title is a different element inventory rather than the same screen
localised. That matches the JP capture work from the other side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 22:05:03 +00:00
sylph-decoder
9b7a8b3237 re: the focus reader was two items out -- initial focus is NEW GAME, and my labels were wrong
menu_focus.py's row centres are design-space rows from screenshot output; my
probes fed it whole-display x11grab frames carrying Xenia's chrome and a surface
scaled 1.060. Caught by ground truth, not by a control: the probe announced 'on
EXTRAS', pressed A, and opened OPTIONS.

Measured directly with ring_row.py: initial focus on a fresh boot is NEW GAME,
2/2 fresh boots, both the first menu entry. That agrees with boot_menu.sh's own
line and menu-state-in-memory.md's four-downs, and withdraws this page's
'TUTORIAL 2/2' as the outlier.

Persistence stands and is now geometry-free -- 384.0 vs 385.5, 1.5 px apart. An
equality test is immune to a constant offset, which is why the conclusion survived
a broken reader when the published item names did not.

The control was structurally blind: 'two DOWNs move two items' tests relative
motion, and a constant offset preserves it exactly.

EXTRAS remains unmeasured; that run navigated to OPTIONS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 21:54:32 +00:00
sylph-decoder
12c9f04ff8 re: the main menu remembers its cursor across menu -> title -> menu
F1 TUTORIAL, two delivery-confirmed DOWNs to EXTRAS, B to the title, A back:
F3 is EXTRAS. Re-entry restores the item you left.

Reframes the initial-focus disagreement rather than settling it: if focus
persists, any 'initial focus' reading not taken on a fresh boot's first menu entry
measures history. It still says nothing about what the menu opens on -- this run's
F1 was itself carried over from a prior probe's press.

Reached on the plate-pulse gate, not boot_menu.sh, whose stillness test cannot fire
on this title -- TITLE at 422.7 s on a boot skip_intro could not gate at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 21:29:01 +00:00
sylph-decoder
4d954a8d0a re: two boots failed because the title gate tests for stillness on a screen that never stills
skip_intro.sh admits a static screen at d <= 1500 between grabs 0.6 s apart. Over
72 samples of the failing boot the MINIMUM was 1551 -- zero could ever pass. The
timeout is unreachable by construction, not bad luck about intro length.

The premise is in the script's own comment ('the resting title barely changes')
and it is refuted by this session's own draw capture: the title free-runs two
full-screen-height sweep leaves and pulses the plate. HANDOFF already said a
settled screen is not a static screen. wait_plate_pulse.py, which counts glyph
pixels instead of demanding stillness, reached TITLE SETTLED at 245.6 s on the
same game the same day.

Threshold deliberately NOT raised: 7 of 72 samples fall under 2000, so a gate loose
enough to admit this title would also admit movie frames -- the confusion the
script's own header records paying for once already.

Cost recorded: the focus-persistence question it was booted for is unanswered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 21:15:48 +00:00
sylph-decoder
7a9f5d3b82 re: read the suppressed set -- 7 correction contexts, zero live revivals
Also dedups the suppressed listing: two registered claims can be substrings of one
line, which reported that line twice (8 -> 7).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 20:45:33 +00:00
sylph-decoder
125a207fde re: check_refuted.py's clean run was not a pass -- 8 of 8 mentions were suppressed silently
A real revival planted inside a paragraph that merely discussed corrections was
missed: the words 'refuted' and 'withdrawn' in the surrounding prose vouched for it.
Measured reach: 100 % of registered-claim mentions in the corpus are suppressed by
marker language, so the reported 0 was 0 regardless of whether any was live, and I
had been reading it as a pass.

sylpheed-port's token-based hook has the opposite bias -- it over-reports on
well-written corrections, which is the safe direction. Under-reporting is disguised
as success.

Fixed by making the suppression visible rather than removing it: suppressed mentions
are counted and listed with --show-marked as not verified, only vouched for.
Controlled -- the planted revival moves the suppressed count 8 -> 9 and appears in
the listing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 20:27:17 +00:00
sylph-decoder
dcfb53bc9b re: a bank's wave 1 is not a filtered copy of wave 0 -- and my own discriminator cannot finish the job
Coherence on BGM_103, the menu's bank, with controls run first: a real linear
filter of wave 0 reads 0.93-0.94 in every band, a different bank reads 0.001, and
wave 0 misaligned by 1 s reads 0.004-0.057. The measurement reads 0.027 at 1-4 kHz,
so the 'wave 1 is wave 0 filtered' model is refuted.

The frequency structure is inverted relative to any mic-pair or reverb model:
coherence rises with frequency (0.169 -> 0.827) while energy falls (71 % -> 0.2 %),
and a rear pair decorrelates fastest at HF. In the midrange the two waves are 13x
further apart than the two channels of one wave.

But the L-R control is what limits the tool and it is recorded as such: within one
wave, genuinely one performance in two channels, coherence is only 0.221-0.497. So
'same performance' does not imply high coherence here, my positive control was the
wrong model of the rear-pair reading, and the 🟡 is NOT settled. The tool tests for
linear filtering and neither surviving reading requires it.

Also corrects MISSION's Q10 row, which still carried the refuted three-sub-wave
premise and had directed work at a dead question for days. Its gate is in fact met.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 20:17:23 +00:00
sylph-decoder
81e84d6ec6 re: the refuted register ignored the deaths I just wrote, and two false positives shared a cause
Three refutations written as prose under ### headings never entered the register:
check_refuted.py parses * "claim" lines, so the count stayed at 188. Registered
them properly (188 -> 192). A register that parses one syntax silently ignores
every other, and it is invisible from the author's side -- ask the register what it
holds, do not re-read what you wrote.

Both standing false positives were bullets under a header that retracts the whole
list, with no marker in the +-4-line window: scope marks them, not proximity. The
scan now includes the nearest preceding header and matches markers
case-insensitively ('An earlier version' was missed by the marker 'an earlier
version'). Controlled by planting a real revival and confirming it is still caught;
register now runs clean at 0.

Also records sylpheed-port's diagnosis of the phase-lock fallout: a number can be
inapplicable rather than wrong, and a tension built on one is manufactured. Plus
their point that some claims are not registrable in a substring register at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 20:02:21 +00:00
sylph-decoder
27ce59b3a7 re: the sweep IS drawn on the JP title -- and my gate was phase-locking the shutter
The occlusion hypothesis is refuted: build 7 draws the same three ROT strips at
higher alpha than English, so there was never an absence to explain.

The 0.32-vs-11.9 tension that motivated it was an artefact of my own instrument.
Both JP captures were shuttered on the plate pulse, and the plate's pulse is part
of the animation -- so the gate synchronises the shutter to the animation's phase.
Measured at the shutter instant, the sweep sits 25-26 px apart across two runs in
different locales and different sessions: 1.6 % of a ~1600 px traverse.

So the 0.32 I recorded as between-session capture noise measures my trigger's
repeatability, and I read it as evidence the title is still when it is evidence
the gate works. The era adjudication is unaffected -- margin 16.72 clears even the
un-locked 11.9 -- and unaffected for the reason that file already gave: correlated
noise cancels in a margin.

Refutation attempt on sylpheed-port's positional-mechanism rejection: FAILED, the
claim stands. Its residual sits inside lit logos, and the logo ROI is
byte-identical across five differently-phased frames in two sessions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 19:52:15 +00:00
sylph-decoder
668255118d re: build the enforcement check REFUTED.md lacked -- 0 real revivals, and why
sylpheed-port's check-claims fails their run when a refuted claim is quoted
without an explicit token, and feeding it four withdrawals flagged three still
asserted unmarked -- each inside a correction they had written. REFUTED.md
publishes deaths without enforcing them, which is the gap I named last iteration
and did not close.

check_refuted.py is the prose equivalent: for each quoted claim in REFUTED.md it
searches docs/ for that text and reports occurrences whose neighbourhood carries
no refutation marker. Controlled first -- a claim planted unmarked in a scratch
file is detected, so a clean run means something.

9 raw hits, ZERO real revivals. All false positives, and the kinds are the
finding: 2 were text explicitly DECLINING to revive a claim; 1 a duplicate report;
4 were BACKLOG.md entries under a 2026-08-12 header, an append-only log recording
what was believed then; 2 were the claim quoted inside its own correction.

The structural limit is worth more than the clean result. A neighbourhood-language
detector cannot separate "asserted now" from "recorded as believed then", because
a dated log entry and a revival read identically. The port's design avoids this by
testing for a token an author must PLACE rather than for language -- theirs fires
correctly inside a correction, which is what caught their three, while mine fires
incorrectly there and would miss a revival reworded.

Stopped tuning at two remaining. Each marker phrase added fits the detector to this
corpus's habits of expression and away from being a test of them; tuning until it
reads zero would be fitting the instrument to the answer. Left over-reporting,
which is the safe direction.

Reach: it matches a claim's exact wording, so "no verbatim revival" is not "no
revival".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 19:26:00 +00:00
sylph-decoder
596d21b882 re: a ninth transition -- the same origin gives gap 0 and 1 by destination
sylpheed-port confirmed "nothing declared predicts the gap" from their export
independently, and deliberately declined to search combinations: four pairs
against many candidate two-screen functions fits by construction. Right call, and
it applies to me unchanged. So this iteration adds a PAIR rather than a fit.

menu -> a second screen outside GP_TITLE, reached by stepping the cursor two
items before arming. The button is not controlled -- there is no focus readout --
so the destination is identified afterwards by its draw signature: incoming
primitive [255] at 7-9 draws/frame, against the first run's [127] at 12-13.
Different screens.

Outgoing quad rises 25, 51, 102, 229, 255 across frames 37-42, then at frame 44
the new screen is already drawing. NO empty frame anywhere. GAP = 0.

So the menu as origin gives four values across four destinations: title 0,
EXTRAS 1, other-1 1, other-2 0. The same origin yields both 0 and 1 depending on
where it goes, while the two repeated pairs stay internally identical (3,3,3 and
2,2). Further evidence for the ordered pair over the origin.

Recorded as an observation with its counter-example rather than fitted: the
incoming screen's own full-screen primitive is [255] where the gap is 0 and [127]
where it is 1, which suggests a screen beginning from opaque black needs no blank
frame. That FAILS on menu -> EXTRAS, which declares a black backdrop and still
gives 1. Nine transitions against many candidate functions is the construction
the port declined to search, and I am not searching it either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 19:13:23 +00:00
sylph-decoder
040b2a7855 re: the symmetric audit -- port-sourced claims in my corpus are attributed
sylpheed-port promoted my unverified "EXTRAS is stuck at n=1, a structural limit"
out of a message into DECISIONS.md as an established fact, while holding the file
that refuted it -- their own authored/flow.json, recording ptbtn11 ->
GP_MISSION_SELECT. Their corollary is sharper than my original entry: distrust
"structural" and "impossible" hardest when SOMEONE ELSE writes them, because they
arrive without the doubt the author would have had.

Swept this side for the same shape. It is clean: port-supplied figures are
attributed in the text ("port reports 866 keyframes ... 0 untimed"), the
ui_layout.rs comment on the unreachable fallback cites MY OWN measurement of 0
untimed of 24 811 across 965 builds rather than their 866, and their quantisation
floor of 0.41 appears in no document of mine at all.

Reach stated: this tests attribution WORDING and the port-supplied figures I could
enumerate, not every reliance. A negative from a naive check is not proof of
absence, and saying so is the point of recording it.

What protected it was a habit rather than vigilance -- writing the source into the
sentence. That is now the third instance of one remedy: state what the number is a
number of; write the index space into the token (e10 rather than "build 10"); write
the source into the claim. Put the qualifier in the text, never in the reader's
memory.

Also fixes the half-guard the port called out. The effective-config block reported
`arming on` from screen_id.py, which cannot separate the main menu from EXTRAS --
so it announced "menu" while the run was armed on EXTRAS, a field the guard could
not resolve for exactly the two screens in question. It now prints both that value
AND the discriminator with its margin, so the ambiguity is visible rather than
hidden. NOT yet verified in a run -- per the port's own --no-hold lesson, parsed
and edited is not working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 18:52:19 +00:00
sylph-decoder
826599cab8 re: break the EXTRAS n=1 cap -- which I had wrongly called structural
I recorded EXTRAS as able to supply only one gap measurement because "its sole
exit is (B) to the menu", and called that n=1 STRUCTURAL -- a word that retires a
question. The disc refutes it in one command: build 6 declares three buttons,
ptbtn11/ptbtn12/ptbtn13, all kind 0x3002. The cap was an unverified assertion I
had already written into HANDOFF twice.

Measured EXTRAS -> a screen outside GP_TITLE via (A): outgoing quad rises across
frames 36-40 (4-5 frames, matching build 6's declared 10-unit close), then THREE
empty frames at 42, 43, 44, then a different archive builds (23-28 draws/frame
against GP_TITLE's 11-14). Gap = 3.

So EXTRAS as outgoing gives {2, 3}, and seven transitions now group as:
menu {0,1,1} n=3, EXTRAS {2,3} n=2, title {3,3,3} n=3.

A pairwise control that holds the destination class constant: menu -> another
archive gives 1, EXTRAS -> another archive gives 3. Same kind of destination, gap
differs by outgoing screen. That is the strongest support yet for the
outgoing-screen dependence because it removes the destination as the variable.

But the clean ordering is GONE: EXTRAS {2,3} and title {3,3,3} overlap at 3, so
"menu < EXTRAS < title" no longer separates them. What survives is weaker -- the
outgoing screen constrains the gap to a ~2-wide band and the bands are not
disjoint.

METHOD: a structural limit is a claim and needs checking like any other.
"Structural" and "impossible" are the two words most worth distrusting in your own
notes, because they retire a question rather than answering it and nothing later
re-opens them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 18:49:29 +00:00
sylph-decoder
0e81cfacc9 re: a third menu-outgoing gap, and leaving the archive costs no extra black
sylpheed-port's BLOCKED row asks for a second value on one outgoing screen --
what would make "the gap tracks the outgoing screen" predictive rather than a
restatement of the data.

First, a correction their ask surfaced without needing a run: THE MENU ALREADY
HAD TWO VALUES AND THEY DIFFER -- 0 leaving for the title, 1 leaving for EXTRAS.
So "the outgoing screen determines the gap" was too strong and is withdrawn; what
holds is an ordering, not a determination. Also recorded: their ask is answerable
only from the menu, since the title's sole exit is (A) to the menu and EXTRAS's
sole exit is (B) to the menu.

Then took a third menu-outgoing measurement, to a screen outside GP_TITLE.
Confound named in advance rather than after: that transition leaves the ARCHIVE,
so a pak load could inflate the gap for reasons unrelated to the outgoing screen.

Result: outgoing quad rises across frames 24-28 (4-5 frames, matching build 5's
declared 10-unit close), then ONE empty frame at 30. Gap = 1, identical to
menu -> EXTRAS. So the confound is measured ABSENT -- leaving the archive costs no
extra black, which is worth having on its own.

Six transitions now group as: menu {0, 1, 1} over three different destinations
including another pak, EXTRAS {2}, title {3, 3, 3}. The ordering rests on 3+1+3
measurements and the menu's three agree to within one frame.

Still not predictive: outgoing closes are 5, 5, 4 frames against gaps {0,1}, {2},
{3}, and EXTRAS has n=1 with no way to obtain a second value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 18:42:51 +00:00
sylph-decoder
a9b6e78c8f re: the black gap tracks the OUTGOING screen -- direction and button ruled out
Applied my own log-line rule to my own instrument first, which is the "rule
written down is not a rule applied" failure I have on record three times. Every
branch of fade_decompose.sh now announces its EFFECTIVE configuration -- derived
from the variables in force, not from $WHERE, per sylpheed-port's sharpening --
and the run REFUSES if no branch announced itself. That is the guard that would
have caught last iteration's silent fall-through at a glance.

With it in place, took the measurement that failed last time: menu -> EXTRAS, the
reverse of the pair measured at a 2-frame gap. Navigation verified both ways
(extras margin 11.23, back on menu 11.60) and the config block confirmed the right
branch.

Result: outgoing quad rises across 5 frames matching build 5's declared 10-unit
close, then ONE logged empty frame. Quoted as 1 with an upward uncertainty, since
its two neighbours carry no frame header at all.

Five transitions now order by the screen being LEFT:

  menu -> title    (B)     0        outgoing menu
  menu -> EXTRAS   (A)     1        outgoing menu
  EXTRAS -> menu   (B)     2        outgoing EXTRAS
  title -> menu    (A) x3  3,3,3    outgoing title

Direction is ruled out: EXTRAS->menu (2) and menu->EXTRAS (1) are the same pair
both ways and differ. Button is ruled out: (B) gives 0 and 2, (A) gives 1 and 3.
Incoming screen is ruled out: an incoming menu takes 3 from the title and 2 from
EXTRAS.

NOT a rule, and recorded as such. Three outgoing screens with one value each means
"each outgoing screen has its own gap" restates the data rather than predicting,
and nothing declared predicts it -- outgoing closes are 5,5,5,4 frames against
gaps 0/1,2,3, inverted if anything, on three points.

What it changes for the port: a UNIFORM black_hold_units is now positively
excluded rather than merely unsupported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 18:33:40 +00:00
sylph-decoder
3e525756bf re: a third title->menu replicate, from a run that ran the wrong experiment
Set out to measure menu -> EXTRAS, the reverse of the pair measured at a 2-frame
gap, to test whether the black gap is a property of the screen pair or of the
direction. The run did not do that.

A three-part patch to fade_decompose.sh asserted two of its three replacements
and left the third -- the branch condition -- unchecked. It silently failed, so
WHERE=menu2extras fell through to the `title` branch. The capture is well-formed
and is of a different transition than intended, which is the build-ordinal error's
shape again: right-looking output for the wrong object. What caught it was the log
LACKING the navigation lines the intended branch prints; the data itself looked
entirely fine.

Salvaged, because the accidental transition is one already measured twice:

  run   outgoing ramp             black   incoming decay
  1     67-70: 63,127,191,255     3       73-77
  2     64-67: 63,127,191,255     3       70-74
  3     92-95: 63,127,191,255     3       98-103

Three independent runs, gap = 3 frames every time, outgoing ramp byte-identical
in all three. That takes "the black gap is not a load" from two replicates to
three, and makes the 4-frame outgoing ramp as solid as anything measured here.

menu -> EXTRAS remains open; the condition is fixed (with an assertion this time)
and the run has not been taken.

METHOD gains two entries. Assert every edit, not most of them -- and have each
branch announce itself in the log, so a run that took the wrong path says so
before its numbers are read. And: "appears nowhere in crates/" is a claim about a
TREE. sylpheed-port found SYLPHEED_KF_TIME_SHIFT live at ui_layout.rs:497 on their
branch, which carries the stale era; both statements are true of different trees.
With main 145 commits behind and each agent on a topic branch, any claim about
what the code contains needs its ref attached.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 18:21:20 +00:00
sylph-decoder
18f3c5d3dd re: verify the splash's opaque-black backdrop, and the noun amendment
sylpheed-port found the fifth member of our error family on their own side: their
"visible" test counted any element with alpha > 0, which includes palogo_eff0.
Verified from the disc rather than accepted --

  entry 10  [0] palogo_eff0.prm  1 kf  t=0  fade=0xff000000  scale=100x100  pos=(0,0)
  entry 11  same

Alpha 255 over RGB 000000: full-screen opaque black, drawn from t=0 and showing
nothing. So "any element drawn" reports these screens visible from t=0 while the
frame is black -- "visible" read as "drawn".

Worth having on its own: this verifies from the disc the premise behind
`screen render --black`, which its own help states as "what the game composites
over on a screen carrying its own background". On the splash builds that
background is DECLARED, not assumed.

METHOD gains their amendment, which is the sharpest formulation either of us
reached this week: all five instances are a failure of a NOUN, not of a number.
Extent, bounding box, duration, span, visible. The number was always correct FOR
SOMETHING; what went missing was which thing. Every other check in that file
tests whether a number is right, and not one tests whether it is a number of the
thing you think.

Also teaches fade_quads.py to address a PAK ENTRY directly (`e10`) rather than
only a build ordinal. The splashes are entries 10/11 and are not screen builds,
so no ordinal addresses them -- and writing `e10` states which index space is
meant, which is the standing lesson of build-ordinal-vs-entry.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 17:30:37 +00:00
sylph-decoder
f2a5392efb re: (A) on a settled boot title DOES reach the menu -- the withdrawal explained
Clearing my own debt: I withdrew navigation.md's "boot title accepts a single A"
counter-example as confounded by three concurrent emulators and never re-ran it,
which left the claim unsupported rather than settled.

Clean trial: exactly one emulator verified by count, gated on the plate pulse
(glyph in [500,2500] held 12 samples) so the press lands on the BOOT title rather
than the attract loop's, delivery confirmed at [file-pad] keystroke vk=5800
down/up. Glyph after the press is 0 at +2 s and +4 s -- the transition -- then 327
steady from +6 s through +39 s.

327 is a proxy and reading a proxy is the habit this corpus keeps cataloguing, so
the screen was checked with which_title_screen.py instead: main_menu at RMSE
19.91 and 20.08 with margin ~10, inside the 9.9-11.7 band its control establishes
on four known captures. The before frame gives the "neither" signature at margin
0.10, correctly, since the title is neither main_menu nor extras.

So the count is 3 of 3, the latency is 4-6 s -- which is why a script that
presses and looks 0.5 s later concludes the press was dropped -- and the two
earlier failures were the confound, not the game.

Refutation attempted: sylpheed-port's leaf segment rates. Derived independently
from the disc and they SURVIVE exactly -- pteff03 +4.0000 then +4.0000 then a
hold, pteff03a -4.0667 then -4.0625 then a hold. So their inversion stands: my
linearity gate fails on the leaf whose declared track is perfectly straight.

And records the third structural consequence of main being stale, which they
raised: HANDOFF.md is the delivery contract and it lives on an unmerged branch,
so their checkout contains none of this week's entries. Findings written into the
contract reach them only through messages -- the channel the protocol says does
not count as delivery. Writing it in the contract is necessary and not sufficient
when the contract lives where the other party cannot see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 17:17:41 +00:00
sylph-decoder
721de02723 re: the leaf is NOT frame-locked -- and a residual gate disqualifies half my fits
The frame-rate test sylpheed-port and I agreed was the only clean route left.
Same strips, same screen, --framerate_limit=15 against the default. The limit
demonstrably took effect: the title settled at 862 s against 241 s.

First, a gate this work should have had from the start. A slope is only a rate if
its residual is random, so count sign changes in the residual:

  default   1299x1303  -4.348  rms 3.33  43/111  OK
             883x1134  +4.284  rms 3.08  21/76   SYSTEMATIC
             890x1134  +4.284  rms 3.19  15/54   SYSTEMATIC
  limit 15  1299x1303  -2.032  rms 1.51  44/83   OK
             883x1134  +2.003  rms 1.12  36/61   OK
             890x1134  +1.999  rms 0.74  12/37   SYSTEMATIC

So one of the two strips I quoted as "agreeing to three significant figures"
FAILS the linearity gate at default fps: that agreement was between a rate and a
slope through a curve. The port had already caveated the claim for a different
reason; this weakens it further from my own side.

The result, on the one group passing the gate at both settings: -4.348 px/frame
at default against -2.032 at limit 15, a ratio of 2.14.

THE LEAF IS NOT FRAME-LOCKED. A fixed number of units per submitted frame
predicts px/frame unchanged; it changed by 2.14x. Dead.

A simple wall-clock model is dead too, in the other direction: fewer frames per
second means more wall time per frame, so a time-driven leaf should move MORE
px/frame at a lower limit. It moved LESS. Neither model fits and I have no third.

Reach: the effective frame rate was NOT measured. The timing instrument I added
polls for the capture log, which is created when the capture is ARMED rather than
when it finishes, so it reported 0.728 s and is void. The 3.6x boot slowdown says
the limit took effect, not that fps went 28 -> 15. The RATIO is measured; the
absolute rate still is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 17:03:15 +00:00
sylph-decoder
7ad96c1a64 re: make the one-emulator rule enforceable instead of remembered
The withdrawal last iteration was caused by `rm -f /tmp/xenia-canary.lock` --
the obvious way past a lock orphaned by kill -9, which also disables the guard
for every later launch. Three instances ended up live at once, sharing
/tmp/xenia_pad.txt and display :98, and silently confounded an input experiment.

Care is not a fix, so this is tooling. ensure_single_emulator.sh counts live
instances, stops them (plain kill, then -9, each with a bounded wait), verifies
ZERO, and only then removes the lock -- refusing loudly if any remain. The lock
is never removed before the condition it guards against is verified absent.

FOUR scripts did the bare `rm -f`, and only two were mine from today:
menu_loop_session.sh, title_draw_capture.sh, poke_control.sh and
resume_reliability.sh. So the footgun was corpus-wide rather than introduced
this session. All four now route through the guard.

The guard is controlled rather than assumed: run against a deliberately started
live instance it reports "1 instance(s) live -- stopping them", ends at 0 with
the lock cleared, and exits 0. A guard that only ever passes on an already-clean
slate would prove nothing.

It also kills by process NAME. `pkill -f xenia_canary` matches the shell running
it -- that has now cost this corpus three commands, one of them a cleanup that
died halfway and left the very instances it was meant to remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 16:23:59 +00:00
sylph-decoder
1bb4963ff0 re: WITHDRAW the (A)-press finding -- three emulators were live at once
While chasing the draw-stream question I found THREE xenia instances running
simultaneously (started 15:39, 15:44, 16:12), which violates the "one emulator at
a time" hard rule and confounds the finding I recorded last iteration.

All three read the same /tmp/xenia_pad.txt and share display :98. A press written
to that file is delivered to EVERY instance, while `screenshot` grabs whichever
window is topmost -- not necessarily the one that acted on it. So "(A) was
delivered and the screen did not change" may simply be two different emulators,
and the keystroke-level confirmation proves only that SOME instance received it.
The navigation.md entry claiming the boot title's "2 of 2" is no longer 2 of 2 is
withdrawn as unsupported, pending a clean re-run.

The cause was mine. run-canary's lockfile is the IMPLEMENTATION of the one-at-a-
time rule; a kill -9 orphans it, and the obvious unblock -- rm -f the lock --
also disables the guard for every later launch. I did that more than once today.

METHOD gains two entries. A lockfile is the rule, not an obstacle to it: clear a
stale lock only after confirming zero live instances, and COUNT them rather than
trusting a kill landed, because a plain kill is asynchronous and a -9 on a stuck
process can take seconds. When a guard blocks you, the question is whether the
condition it guards against is present, not how to remove the guard.

And a third instance of pgrep -f matching the shell that runs it -- this time it
killed a cleanup command halfway through, leaving the emulators alive and the
lock in place. Already recorded for wait-loops; promoted to "reach for -C first".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 16:20:24 +00:00
sylph-decoder
b6d4242068 re: WITHDRAW "ptloop01/02 do not free-run" -- I measured a pivot, not an extent
sylpheed-port noted that build 5's ptloop parent can be static while the leaf
record animates, and asked me to check it against my table. My own corpus
refutes my claim outright.

ptloop-leaf-sweep-positions.txt -- written earlier in this same corpus -- records
ptloop01.rat's nested record at loop length 600, whose leaf pteff03.t32 sweeps a
400 px-wide quad with its centre running x~921->1041 over t=340..370. The
parent's declared rect is (441,270) 200x90. The leaf draws 300 px outside it: the
parent rect is a PIVOT ANCHOR, not the drawn extent.

Checked against the two JP captures: my measured rect differs by 0 px -- and so
does the whole dead region y 270..450 x 480..960 around it -- while the band the
sweep actually occupies (x 721..1241) differs by 44 025 px. The zero was measured
where nothing happens.

So the port's reading is right and now confirmed from the disc: parent static,
leaf animates, and the two nested records cycle at DIFFERENT lengths, 600 and
720. My "single static keyframe" described the parent only. The era adjudication
is unaffected -- its box overlaps the sweep band only at x 721..776, which shows
no between-session differences.

The menu-loop question is still unsettled after a second attempt, and the second
attempt's failure REFUTES my diagnosis of the first. menu_loop_probe.py gated on
the plate pulse (glyph in [500,2500] held 12 samples), fired at t=484.5 s with
glyph 1723 -- a verified settled BOOT title, not the attract one -- pressed A,
and the press was delivered ([file-pad] keystroke vk=5800 down/up, 8 RE-INPUT
lines). Twenty seconds later all five frames still classified as the title
(rmse ~67-70, margins 0.06-0.16, the "neither" signature; screen_id says title).
So "the attract title accepts nothing" does not explain attempt 1, and the
corpus's "the boot title accepts a single A, 2 of 2 runs" is no longer 2 of 2.

METHOD: a declared rect can be an anchor, not an extent -- confirm an element
draws in a region before diffing that region to ask whether it moves.
navigation.md: confirm the screen changed, do not infer it from a delivered
press.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 15:55:31 +00:00
sylph-decoder
36c00f1a5c re: ptloop01/02 do not free-run on the settled title -- and the menu is not settled
sylpheed-port found ptloop01/02 free-running in their renderer on the menu path,
pinned them, and was explicit that pinning picks one pose rather than the game's:
"a capture question, not a harness one". It is, and it lands in my lane.

On the title it is now answered. Those leaves rest at (441,270) 200x90, INSIDE
the box the ptlogo_eff3 era adjudication uses, and across my two JP captures from
different sessions they are byte-identical: 0 of 18 000 px, max |d| 0, against a
whole-frame contrast of 116 492 px differing. So they are static at rest, and the
in-box between-session noise of 0.32 is not theirs -- the 645 differing pixels
all lie in a 30-row band at y 99..128, nowhere near the loop rect. That also
closes the reach caveat on the EN->JP noise transfer.

The MENU is a different bundle and is not settled. Build 5 declares the same rect
with a single static keyframe, and that is where their row drifted.
menu_loop_rest.sh was written to capture five settled menu frames and diff the
rect; it did not complete. The run reached a title at t=146 s and (A) did not
take across six attempts -- the documented intermittency where the attract loop's
title accepts nothing, unlike the boot title. Recorded rather than re-rolled.

Two committed main-menu captures cannot substitute: they differ across 57 % of
the surface (different geometries and capture paths), so the 88 % differing on
the loop rect measures the mismatch, not the loops. The control fails and the
comparison is void.

navigation.md gains the trap that cost this iteration a run: kill -9 on xenia
orphans /tmp/xenia-canary.lock, the next run-canary refuses to STDERR where a
polling script never looks, and a probe then sampled a dead display for 484 s
reporting `other` every 4 s -- because screen_id.py on an empty screen returns
`other` and "not the title yet" is indistinguishable from "there is no emulator".
Kill plainly so it clears its own lock, and assert the emulator is alive before
entering any wait loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 15:43:29 +00:00