Commit Graph

413 Commits

Author SHA1 Message Date
MechaCat02
a88e273c40 feat(port): adopt the measured held-direction repeat rate (F1)
REPEAT_DELAY = 0.402, REPEAT_INTERVAL = 0.134, from
docs/re/f1-repeat-measured-via-driver-patch.md -- 12 and 4 frames at the
run's achieved 29.87 fps guest rate, converted to seconds because this port
does not run at the guest's rate and it is the cadence that was measured.

The mechanism has been here since 2026-09-02 and inert on purpose. The
instruction it was waiting on is now vindicated in the most awkward way: the
draft it refused to ship had 0.40 / 0.20, so the guessed delay was nearly
right and the guessed interval was off by 50 %. The half that was wrong
would have been protected by the half that was right.

The delay and the interval are NOT equally well evidenced, and the code says
so at the constants. No physical controller exists in the Decoder's
container, so the measurement fed Canary's file driver the SDL driver's own
400/100 ms constants: the 402 ms that came back is the constant that went
in, and confirms the instrument. The 133 ms interval against a fed-in 100 ms
is the new fact -- the game paces repeats to its own frame consumption. One
run; the two-run minimum is not met and the finding says so itself.

Also: the prediction that this would turn verify-input's "a held stick is
ONE step, not six" red was wrong. It stayed green, because steps() never
advances a clock and so had never called repeat_due() at all -- the rate was
about to ship into a harness with no coverage of the feature, with a green
line that would have been read as coverage.

So verify-input gains a `repeat` subject: nothing before the delay, the
first repeat on the delay, the steady interval, cadence independent of frame
rate (the code claims this in a comment, so it is now asserted), and a
direction change restarting the delay. Each asserts THESE numbers, not the
shape -- a shape-only check would have passed on 0.40 / 0.20. The control
removes the premise, a held direction, and every controllable row inverts.

The first-repeat row measures from the arming tick, not from t=0: that tick
is the frame the press is handled, which is the origin the finding measures
its 12 frames from. Measured from zero it read 0.433 vs 0.402 and the
tolerance would have had to be widened to hide a units mismatch.

Closes #2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 13:34:14 +02:00
sylph-decoder
34fb1ddf1e re: F3 sting half closed -- no sting, measured with a working positive control
Continues the static lead from two iterations ago
(f3-title-sting-mechanism-found-not-value.md) with the dynamic half it
named as the next step. Booted with --xma_param_probe=true (the same
census menu-audio-cues.md used for the menu's SE cues), no pad input,
recording continuously from window-open: a glyph time series (not a
threshold trigger) and every newly-seen XMA-PARAM stream, stamped on
arrival since Xenia's own log carries no timestamps.

Positive control, and a real one: the probe caught the title's two BGM
stems starting at t=147.6s, matching f3-title-plays-bgm-102-and-103.md's
already-established finding exactly -- the instrument finds a real stream
before being asked to find nothing.

From the plate's first visible activity through 68 seconds of build-in
plus fully-settled pulsing (killed at t=220.1s), zero new XMA streams
appeared beyond the two BGM stems and three unidentified early ones (boot
splash, not this question). No SE-range stream, no second BGM, nothing --
measured, not the prior static reach limit.

Refutation attempt this iteration, recorded either way: my first read of
the fine-grained glyph series said BGM and the plate's build-in start at
"essentially the same moment". Checking the raw per-sample data instead of
a coarse table refutes that -- first non-zero glyph reading is ~0.67s after
BGM onset, and immediately noisy rather than a clean climb. Corrected in
the doc rather than left as an overclaim for someone chasing frame-accurate
sync later.

Reference data: docs/re/data/f3-sting-{glyph-timeseries,xma-param-arrivals}.tsv
-- derived numeric/log-line data, not a capture of rendered game content.
2026-09-12 12:40:34 +00:00
sylph-decoder
ed9a7d702d re: F1 answered -- 12 frames initial delay, 4 frames interval, by patching in the repeat the driver lacked
Direct follow-through on this session's own named next step. The prior
result (f1-held-down-measured-no-repeat-via-file-driver.md) concluded the
file driver cannot show menu repeat because its GetKeystroke() never emits
a REPEAT-flagged event, and that the menu's repeat is very likely driven by
that flag rather than raw polled state. Testable, so tested: patched
/canary/src/xenia/hid/file/file_input_driver.h to add opt-in repeat behind
a new --pad_file_repeat cvar (off by default, every other scripted script
unaffected), using the SDL driver's own constants verbatim
(HID_SDL_REPEAT_DELAY/_RATE = 400/100, guest-time ms via
Clock::QueryGuestUptimeMillis) rather than re-deriving them. Incremental
rebuild, ~1 minute (only xenia_main.cc needed recompiling).

Control: the driver's own log confirms repeated keystroke events fire as
designed, zero crashes.

Result: re-ran the identical held-DOWN capture. The cursor that moved once
and stopped in the null result now cycles continuously through the whole
5-item menu, wrapping, for as long as the button is held -- the null result
was real for that driver path, and giving the driver the one thing it
lacked reverses it completely.

Measured at this run's achieved 29.87 fps guest rate: 12 frames (~402ms)
initial delay from the press-triggered step to the first repeat step; 4
frames (~133ms) steady-state interval for 13 of 15 gaps, 3 frames (~100ms)
for the other 2 -- slower than the raw 100ms constant driving it, which
this page flags but does not trace further (most likely the game batches
drained keystrokes per its own frame tick rather than reacting to each one
instantly). The 4-frame figure is what matters for the port: it's what the
cursor visibly does.

Honestly scoped: this measures what the game does when FED repeat events
shaped like the SDL driver's, not a capture through an actual physical
controller (none exists in this container) -- classified measured, not
decoded, for exactly that reason. One run only; the corpus's two-run
minimum isn't met, flagged rather than overclaimed.

f1_hold_capture.py gains an optional `repeat` argument. The Canary source
patch itself lives in /canary, outside this repo (Canary source, not
sylpheed-formats) -- fully described inline in the finding doc so it can be
reapplied if that tree doesn't persist across a container reset.

Reference data: docs/re/data/f1-repeat-cursor-transitions.tsv -- every
transition's frame, guest tick and Y position, not the raw draw log.
2026-09-12 12:31:05 +00:00
sylph-decoder
6f08999859 re: F1 dynamic attempt -- harness debugged through four bugs, still no number
Tried to close out issue #1 with an actual draw-log measurement this
iteration: built f1_hold_capture.py to boot to the settled main menu via the
proven glyph-gated route, arm the F10 UI-draw capture, hold a direction, and
read cursor position per frame -- the instrument f1-menu-repeat-harness-
built-not-answered.md already validated but never got to run against a
reachable menu.

Four bugs found across four boot attempts:

1. tap() shelled out to pad.py without this script's own env, so the press
   went to /tmp/xenia_pad.txt while Canary watched OUT/pad.txt -- an
   unobserved press indistinguishable from a dead pad. Fixed with an
   in-process tap() using the same pad() the hold uses; confirmed working
   the next run (title 154.5s, menu 163.0s).

2. ui_draw_capture_frames/max were persisted at 3/20000 from a prior
   session in xenia-canary.config.toml -- log_ui_draws is now a documented
   no-op (F10 arms unconditionally) and these two cvars didn't visibly
   respond to command-line overrides. Bumped to 600/400000 directly in the
   config.

3. The real blocker: this container has no signed-in profile (no content/
   directory at all -- a fresh container after a restart, which every
   container is right after one). Without a profile the title's sign-in
   dialog sets IsUIActive() true, which reproduces
   structures/title-a-press-fault.md's already-diagnosed unbounded-
   keystroke-queue crash -- verified byte-for-byte against that page's own
   addresses (PC 0x868 past sub_82457038, 0x828F3xxx registers, identical
   host/guest address arithmetic), looping continuously from before F10 was
   ever pressed. Fixed by creating a profile (--create_profile_if_none) and
   signing in (--logged_profile_slot_0_xuid), matching boot_menu.sh, which
   already did this and so never hit it. Confirmed: zero crashes with the
   fix, dozens per run without it.

4. Found but not re-verified: no xsetroot blank before launch, so a stale
   X-root frame from a killed prior run gave a false "TITLE" read at 2.6s,
   before any real window existed -- skip_intro.sh already blanks the root
   for exactly this reason. Fixed in the script.

Ran out of budget before a clean end-to-end run landed. Still no number for
issue #1 -- the Port keeps -1.0. Flagged prominently (HANDOFF, REFUTED.md)
because bug 3 will hit any bare run-canary invocation in any fresh
container, not just this script.
2026-09-12 12:02:53 +00:00
sylph-decoder
b93d202f04 re: shape the F6 out-of-sample residue (issue #9) -- one open question, not three
f6-out-of-sample-RESULT.md left three failures unexplained beyond "n=2 wasn't
enough". Reviewing what's already decoded: two of the three (the ptcopyright/
parent ramp ratio, the sweep-leads-plate lead) fail in the SAME direction
(0.75x, 0.71x of predicted), and f6-unit10 already established the sweep and
plate families are gated by separately-triggered parents -- a ratio across
two independently-triggered elements has no structural guarantee of being a
constant, unlike a ratio internal to one element family (which is exactly
what the passing three checks are). The gross mislabeling that caused the
ORIGINAL 1.7x conflict was fixed two days before the prereg was written, so
that's ruled out as the cause here; whether the cross-group phase genuinely
varies boot to boot vs. an artifact in the frame-based ratio math is still
open, and needs more captures to tell apart -- filed as a follow-on, not run
here.

check_labels.py conflated two different claims under one "N LABEL(S) DRIFTED"
verdict: identity checks (which element -- clock-free, still 3-for-3 out of
sample) and timing checks (a cross-element ratio and a self-consistency curve
fit -- 0-for-2 out of sample). Split into two reported groups; only identity
gates the exit code now. Tolerances untouched -- widening them to pass f6c
would be tuning the check on the case that failed it, the same error class
already named twice in this corpus. Verified with synthetic data shaped like
the real f6c residue (3/3 identity, 0/2 timing, exit 0) and confirmed the
selftest's injected mislabel still fails an identity check (exit 1) --  no
capture exists in this fresh container to run it against real logs.

The withdrawn sweep->plate lead (0.138-0.141) stays withdrawn. Nothing here
reinstates a number.
2026-09-11 21:26:02 +00:00
sylph-decoder
5b850bb565 Merge remote-tracking branch 'origin/main' into auto/frame-blend-draw-path
# Conflicts:
#	crates/sylpheed-cli/src/main.rs
2026-09-11 19:23:13 +00:00
MechaCat02
2d5496f754 protocol: findings before citing code, and checks that were kind once
Two rules that look unrelated and are one failure, plus the change that makes
the second enforceable.

1. A FINDING REACHES `main` BEFORE THE CODE THAT CITES IT. A citation resolving
   only on a peer branch is dead the moment it merges. Not hypothetical: 495
   decoder and 366 port commits sit off `main`, and `port/scripts/boot.gd`
   already cites two docs/re pages present on neither its own branch nor main.

2. A CHECK MAY ONLY SOFTEN AGAINST A CONDITION IT CAN TEST -- the Pi agent's
   wording, and better than mine, because it is applicable while writing rather
   than a call to be vigilant. The mechanical form:

       Can this branch tell the difference between "not yet" and "no longer"?

   `gitea-protect --verify` printed  "not a collaborator (yet)" and continued,
   so the only instrument checking Write-not-Admin could not report that gate
   being REMOVED. `check-citations` reported peer citations instead of failing
   them, because under the old topology that was unfixable from the container.
   Both were correct AND kind when written; neither recorded that the kindness
   had a scope. Nobody edits these into being wrong -- the world moves and the
   allowance stays, which is why they survive review. The smell is leniency with
   an expiry date nobody set; the fix is the testable-condition rule.

check-citations gains `--for-merge`, which turns the peer class into a failure.
A flag rather than a new default because BOTH readings are still live: mid-work
on a topic branch the peer class really is unfixable noise. What the old code
could not express is where the code is GOING, and that is a condition the caller
can state. Measured on this tree: 19 citations resolve only on a peer branch --
which is the size of the #7-depends-on-#8 edge, not the 2 I had counted in
boot.gd.

The selftest gains that third class, because a flag whose classification is
unexercised is the shape this rule exists to catch. Controlled: emptying
PEER_REFS makes the peer case collapse into "nowhere" and the selftest reports
🔴 BROKEN, rc=2.

⚠️ Pre-existing and NOT from this change: the default run already exits 1 on 4
citations of `export/...` paths. Those are the generated tree, gitignored by
design, and main's copy of the tool fails identically. The CITE regex treats
`export/` as a repo prefix. Reported, not fixed -- it is the port's file and its
call whether the regex or the citations are wrong.
2026-09-04 18:27:39 +02:00
e55221f7d1 tools: a missing collaborator is a failure, not a blank
--verify's collaborator loop printed  and continued on 404 without touching
`ok`, so the one instrument that checks Phase 1.2 could not report Phase 1.2
being undone. An agent removed from the repository read as "nothing to say"
rather than as a gate that is no longer there.

It has never fired: Gitea answers that endpoint with permission "read" for a
non-collaborator rather than 404, so the case was caught by the role test two
lines down. Correct outcome, wrong reason -- the same shape as the check that
passed on an instance with no rule at all, and not worth keeping because the
luck has held so far.

Found by the port agent reading the file rather than running it, which is the
only way this one was ever going to surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-04 17:59:51 +02:00
b67b6243e6 agents: name what branch protection does not gate, and stop the tool contradicting it
Two things that read as protection while being none.

Phase 2's rule binds everyone who reaches Gitea through the API or the web, and
does not bind anyone with `gitea admin` in the container -- which includes the
supervising agent that created the agent accounts and minted their tokens. From
that shell the rule is editable and an admin token is one command away. That is
the boundary of what the phase buys, not a hole to plug there, and the document
read as though the gate were universal. Phases 1 and 2 gate the two CONTAINERISED
agents, whose design assumption is that policy lives where they cannot reach it;
a supervisor with a host shell is not in that set.

And `gitea-setup` finished by telling the reader to go and build a Gitea project
board by hand, four sections after the doc explains that a board is a second copy
of the state to hand-sync and is precisely the failure that produced a 1,227-line
BLOCKED.md. A tool instructing you to do the thing its own documentation argues
against is the drift this whole surface exists to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-04 17:48:58 +02:00
799fa93383 tools: apply and re-check the branch protection rule, rather than clicking it
Phase 2 as a file. Six settings where two are load-bearing and both were missing
from the first draft is the shape of thing that gets mis-clicked at 1am, so it
goes through the API: what was applied is readable in a diff, and `--verify`
can re-check it later instead of it being checked once.

--verify states its expectations INDEPENDENTLY of what the apply path sends.
A check derived from "whatever we posted" cannot fail -- it re-derives the
expectation from the thing under test, which is the same instrument-shaped
failure as a check that passes on an instance with no rule at all.

It also asserts both agents are still Write and not Admin, because an agent
promoted to Admin can edit the rule and then merge, so a green rule proves
nothing on its own. That is the `gitea-verify` card from "Still to build";
what is left of it is only putting it on a timer.

`block_admin_merge_override` stays false on purpose, and the reasoning is in
the file: approvals are whitelisted to `fabi`, and Gitea will not let `fabi`
approve a `fabi` PR -- so with the override blocked, a human-authored PR could
never reach one approval and could never merge at all. The override is not a
hole in the agent gate because the agents are Write, not Admin. Phase 1.2 pays
for that; this is where it is spent.

Reads the repository-scoped credential that already exists on the agent box
(~/.sylph-git-credentials) rather than the issue-only ~/.sylph-gitea-api-token,
which every branch-protection endpoint refuses. That keeps the setup needing no
new credential, and keeps push rights on one machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-04 16:49:58 +02:00
MechaCat02
a23c321831 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00
MechaCat02
ad96fe97b8 agents: move the working surface to Gitea -- issues, PRs, and where files live
The human wants to direct this project from a web UI rather than chat or Remote
Control, so Gitea becomes the working surface. No new store: adding a second
copy of the truth is this project's defining failure mode, and Gitea already
holds the code. Its first-party MCP server (gitea/gitea-mcp v1.7.0, checked) has
issues, labels, milestones, PRs, attachments and notifications.

ISSUES replace BLOCKED.md. Milestones are bundles the human defines; issues are
items agents propose and the human approves. The state labels end in
`needs-human`, which is the state the whole model turns on and the one no
off-the-shelf tool models -- the market has converged on removing the human.
`blocked` uses Gitea's DEPENDENCY EDGES rather than prose, so "the Port is
blocked on the Decoder answering X" becomes queryable and closes itself.

PULL REQUESTS, the human's proposal, adopted -- and a bigger improvement than it
looks. Today's long-lived auto/* branches have drifted 280 and 373 commits apart,
which is unreviewable by construction. One PR per item makes the human gate
NATIVE rather than a label convention, binds the change to its item, and enforces
the sizing rule: an item too big to review in one sitting was too big to be an
item.

🔴 Agents must not merge their own PRs, and pull_request_write includes merge --
so this goes in BRANCH PROTECTION on main, not in a document asking them not to.
Same principle that fixed the build-jobs cap: policy where the agent cannot reach
it.

WIKI -- the human suggested it for RE findings, and that half is declined with
reasons. A finding's value is that it sits beside its evidence, versioned with
the code that consumes it; the wiki is a separate git repo, so a decode
correction and the exporter change depending on it could never be one reviewable
PR. And wiki edits bypass review: the REFUTED.md reclassification changed the
file both agents read to decide what not to try, and as a wiki edit it would have
been an unreviewed mutation of shared ground truth. The wiki takes human-facing
orientation instead -- runbook, navigation, container notes, and a landing page,
which closes the real gap that there is no view of what is happening except
container logs.

FILES: three needs, three homes. Agent-to-agent transient stays in /exchange.
Evidence a HUMAN must look at attaches to the issue it belongs to -- it travels
with the item and cannot be orphaned from the claim. Evidence a finding cites
stays in git. Note the MCP exposes attachment_read only; upload needs a direct
REST call.

tools/gitea-setup creates the labels and bundles, idempotently, with --dry-run.
Blocked on a token with write:issue -- the push credential is write:repository
and every issue endpoint refuses it, checked rather than assumed.
2026-09-04 15:46:57 +02:00
sylph-decoder
bbe339d856 re: pre-register F6's numbers for an out-of-sample boot
Every F5/F6 figure comes from captures taken for other purposes and
re-analysed after the reader and three labels were corrected. None has been
tested out of sample. Six predictions registered before the capture, five of
them checkable by check_labels.py, which now takes --cap NAME=path.

Prediction 6 is the one already got wrong once (0.057 against ptcopyright
instead of 0.14 against the real plate), and is stated so a landing near
0.057 would show the correction itself was wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-04 13:17:26 +00:00
sylph-decoder
a4e7c69d7a re: refute my own batching hypothesis -- blend state, not linkage
Last iteration I proposed that the two sweeps share one indices=8 draw
because ptloop01 links to ptloop02, and said testing it needed a
loading-screen capture I lack. Wrong twice: a linked pair was already in
every capture, ptbtn00 -> ptbtn00f.

Measured: ptbtn00f is drawn ALONE in 899 (f6b) and 1441 (f6) draws and
batched in ZERO, while the sweeps pair up in 1092 and 1744. Linkage does not
batch. The constraint is blend state -- ptbtn00f is additive and its linked
partner alpha-over, which cannot share a draw. The sweeps batch because both
are additive on one page.

Page+blend is necessary but not sufficient: 8154/alpha-over appears as two
separate draws in a single frame, 2108 such draws in f6b. This removes a
wrong cause rather than supplying a batching rule.

Extends read_draws.py to preserve draw grouping (draw index and quad count
per draw); check_labels.py still passes unchanged as a regression control.

Refutation attempt on the port's 0x3002/0x3003 menu-item reading: survives.
958 of 970 stems contain "btn"; the 12 exceptions are psselect_slot and
psselect_slot_blank, which are menu rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-04 13:15:57 +00:00
sylph-decoder
943d890813 re: RETRACT the alpha-80 refutation -- it is declared, in ptbtn00f.rat
I claimed ptbtn00f's peak alpha of 80 was capture-only and refuted the
port's audit with it. Verified on the disc myself: ptbtn00f.rat, loop 120,
eight keyframes 0:0 6:6 29:74 35:80 50:80 58:74 97:6 105:0. ptbtn00 carries
TWO child records -- the leaf ptbtn00.rat (flat 255, which I read and
described correctly) and the focus record ptbtn00f.rat, which holds the
pulse. My "nothing declares 80" said only that nothing reached my
enumeration. METHOD.md has five entries on exactly this class.

Resolves the 🟡 with it: the corpus line attributing the 120-unit loop to
ptbtn00f is correct as written.

Turns the retraction into a stronger check. The amplitude is declared, so
check_labels.py now verifies the SHAPE: mean |alpha| error 1.14 levels (f6)
and 0.35 (f6b) against the declared 8-key curve. The first version assumed
the onset frame was t=0 and f6b FAILED at 3.24; the fix was aligning by
content rather than loosening the tolerance, since the 6->74 segment climbs
~6 levels per frame. Best lag is ~1 frame in both runs -- the element is not
drawn while its curve sits at alpha 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-03 20:31:58 +00:00
sylph-decoder
9873ca19e1 tools+re: a check that reads the label, and the alpha-80 refutation
Three of my errors were the label rather than the measurement, and a
correction in one document did not reach the next page I wrote. The port
built check-authored-vs-declared for values the disc can arbitrate and named
the gap: capture-only values that name an element rest entirely on my label.
check_labels.py closes that for the cases where the identification was
itself made by matching a declared quantity -- 8 checks over two captures,
all passing, with a --selftest that points the plate label at ptcopyright
(the real error) and must fail. It does, at 82-83% against a 5% tolerance,
with the other checks still passing so the failure is localised.

Refutation of the port's "all five figures are also declared": lands for
one. ptbtn00f's peak alpha of 80 is not declared anywhere -- ptbtn00.rat's
parent peaks at 255 and its leaf is one keyframe at 255 flat, with the
120-unit loop declared but no amplitude. The period checks out; the
amplitude is capture-only and their check cannot see it.

Opens a 🟡 not mine originally: the corpus attributes the 120-unit loop to
ptbtn00f, but it belongs to ptbtn00.rat whose leaf is ptbtn00.t32. Left
uncorrected since the identification rests on the period, which holds under
either name.

Adds examples/leaf_keyframes.rs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-03 20:28:37 +00:00
sylph-decoder
536206e89c re: F5 survives the full-quad reader, measured rather than asserted
Last iteration I asserted F5 was unaffected by the truncating-reader bug
because it compared like with like. Asserting that is the move that produced
the bug, so this measures it. The new reader sees 9.7 quads/frame vs ~7.5.

Scalar that needs no element identification: quads mid-ramp (0<a<250) per
frame goes 6,4,4,4,2,1,4,3 -> 0 at f436, while the control never reaches 0
anywhere in 48 frames of build-in. One frame with nothing part-way through a
ramp is the cut.

Bonus the old reader could not show: both sweeps enter at f436-438 at their
declared opening alphas -- pteff03 at 255, pteff03a at 1,2,3,4,6,11,17 from
its declared 0.

Refutation attempt on the port's "clock jumps to 236.0": tried and failed.
My bound is [100,238), which contains 236 -- consistent, not independent
confirmation.

Adds tools/re-capture/read_draws.py so the truncating regex is not re-rolled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 21:06:55 +00:00
sylph-decoder
12d26d59c9 re: F5 -- A snaps the plate, and does NOT advance the artwork's clock
Three captures, aligned by the sweep's position rather than by frame number.
The plate's alpha goes 23,46,69,92,115,139 over ~11 frames with no input,
and 255 in a single frame when A is pressed: zero intermediate values
against eleven. A cuts, it does not accelerate -- the human's prior holds.

The control is what makes it readable. Four elements ramp out right around
the press and I could have reported that as the effect; at sweep x=-1.42 all
three runs agree quad for quad, so those exits are the ordinary build-in.

Refutes clock:"shared" on F4's own discriminator: the artwork keeps
animating across the press, frame for frame identical to the control. Reach
stated -- that half rests on a 5-frame window, since everything else on
screen is already at 255 and cannot discriminate.

Also records the first attempt's miss: blind wall-clock timing put the press
after the settle, where A is accepted instead. The probe now gates on the
sweep appearing, which is the parent's declared t=70..100 gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 20:07:42 +00:00
sylph-decoder
d9a88b01e3 tools: F5 probe -- a second A during the title build-in
Presses A once to skip the attract video, then again during the build-in,
and records the press instant so a press that lands late is reported rather
than silently reinterpreted. Blind timing is the weak point: the build-in is
~5 s wide and run pacing has been seen to vary 2x.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 20:02:52 +00:00
sylph-decoder
15207b1c74 re: F6 unit 9 -- sweep period and onset, as ratios reproduced across two runs
Two captures of the same animation disagree on frame counts by 1.947x, so
frames are not comparable across runs. Expressed as ratios to a long
in-capture baseline, both runs agree: loop period = 13.93 baselines (0.35%
apart), and the sweep starts 0.79-0.80 baselines BEFORE the plate (0.9%).

This answers F6: the sweep does not start at t=0 as the port has it, nor
exactly with the plate -- it leads the plate by about 40 title units, under
a second, which is consistent with what the human reported seeing.

Surfaces a conflict rather than smoothing it: unit 8's title rate of 1.0
units/frame (from ptcopyright) and the plate's 0.571 units/frame are 1.75x
apart in the SAME capture. Either an element's declared ramp is misread or
the two are not on one clock -- the port's clock:"shared" premise. The
ratios stand without a clock; the title-unit conversions are 🟡 pending F4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 19:20:30 +00:00
sylph-decoder
a726391ead tools: make the sweep probe confirm it is actually recording before proceeding
Applies the fix unit 9 named. Twice a probe printed "armed", carried on and
wrote no draw log -- once because the window lookup failed, once with the
window found and the key sent. Sending the keystroke is not evidence the
logger responded.

After arming, the probe now polls for xenia_re_ui_draws_*.log to exist and
be non-empty, for up to 20 s, and aborts loudly if it does not appear. A
probe that cannot confirm its own instrument is recording is a probe whose
negatives mean nothing.

Narrower fixes were tried and were not enough: making the window lookup
fatal caught the first failure and not the second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 19:11:39 +00:00
sylph-decoder
7863ad3b62 re: F6 unit 9 -- wrap-to-wrap period NOT obtained, two harness failures
Unit 8's rate (0.514 units/frame) rests on one wrap, a start-to-wrap span
rather than a period. Hardening it needs a longer capture with two or more
wraps. Two attempts, both failed on the harness rather than the game.

First launch never started: the pgrep && echo || { } guard took the wrong
branch, no output directory was created and no process ran, while a stale
emulator from the previous iteration was still up. It looked like a running
capture for several minutes.

Second launch started, armed, pressed A and wrote no draw log at all --
canary.stdout stayed at 0 bytes and no xenia_re_ui_draws log appeared,
despite the script printing "armed at 8s". Likely a race with the orphaned
emulator from the first failure, not confirmed.

Unit 8's numbers are unaffected; they came from the intact f6 capture,
which is still on disk. The offset has two independent supports; the rate
still rests on a single wrap and the port should not ship on it.

The lesson, and it is the second harness failure of this shape: "armed at
8s" printed while nothing was being logged. The arming step reports success
on SENDING the keystroke, not on the logger responding -- the same silent
failure that cost the first F1 probe a run, which I "fixed" by making the
window lookup fatal. That fix was too narrow: the window was found, the key
was sent, and the log still never appeared. The check that would have
caught both is to wait for the draw log to exist and be non-empty after
arming, and abort loudly otherwise. A probe that cannot confirm its own
instrument is recording is a probe whose negatives mean nothing, and I have
now written that bug twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 19:09:59 +00:00
sylph-decoder
bc241f5d06 re: F6 unit 2 -- the parent's alpha IS multiplied into the sweep leaf
Question, look-at and not-covered written before the work, per PROTOCOL's
units section.

One boot, one A to skip the attract video, then no input so the title
builds in undisturbed. The sweeps are the two rotated 600%-scaled strips,
identified by size: NDC 1.38x3.15 and 1.39x3.15, matching the corpus's own
independently measured AABB height of 1134 px (2*1134/720 = 3.15).

Drawn alphas run 8, 24, 33, 50, 58, 74, 83, 100, 107 and 16, 41, 67, 91,
116, 128... pteff03's LEAF alpha is bounded below by 128 (keys 0:255
150:128 540:255 600:255). 8 < 128, so the leaf cannot produce it alone.
The parent's alpha is multiplied in.

That refutes the port's screen_view.gd decode that "the leaf runs on its
OWN timeline and the parent's alpha is NOT multiplied in" -- which is
exactly what that file asked for. It flagged honestly that every
observation behind it had parent alpha 0, so "the leaf wins" and "the
parent is ignored because it draws nothing" were never separated, and named
t=100..238 as the separating interval. This capture is in that interval.

Bears on F6's original report: the parent ramps 0->255 across t=70..100, so
if it multiplies in the sweep is invisible before ~70 and dim until 100. A
human reporting it starts late is seeing a real gate -- the parent's ENTRY
ramp, not the 238 cluster, which unit 1 showed is an exit.

Reach: one boot; quads identified by size against a previously measured
AABB, not by name, since the draw stream carries no names. The bound
argument needs only that the leaf minimum is 128, which is from the disc.
It does NOT establish the combination is plain leaf x parent rather than
some other one that also dips below 128 -- only that the parent
participates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 18:23:49 +00:00
sylph-decoder
85d2e93d05 re: F1 harness built and committed; the run did not reach the menu
The port needs an initial delay and a repeat interval and has shipped the
mechanism with -1.0 rather than invent them. This builds the instrument and
does not get the numbers.

The vertex stream is the right instrument: C_PAD_DECODER has no timer on
any direction path, so the repeat is in the layer above and cannot be read
out of that function; a cursor move is a quad changing position, and the
guest's vertex buffer carries that with no Canary processing in the path.

tools/re-capture/menu_repeat_probe.sh arms the logger, drives the boot with
pad-file presses and holds a direction. It ran its whole sequence. But the
boot did not reach the menu: in the settled era every quad shape holds a
constant y, and the shapes are near-full-screen rects rather than a button
list. No cursor, so nothing to repeat.

Two instrument errors worth more than the failed run.

The first attempt ARMED NOTHING and said nothing -- xdotool search --class
... --window %1 F10 behind a || true. The run completed normally and only
the ABSENT log revealed it. The working form looks the window up by name,
activates it, and sends F10 to the window and globally; a missing window is
now fatal rather than tolerated, because a silent arming failure is
indistinguishable from a screen that draws nothing.

My first cursor detector measured the wrong change: presents where the SET
of quads changed, which fired on 157 of 161 adjacent pairs because during a
build-in everything animates. Tracking one quad shape's POSITION instead
correctly reports no motion. That is the play-test's own lesson landing on
me -- an instrument that measures change can still measure the change of
the wrong thing.

Next run must not drive the boot blind: gate the presses on a classifier or
join with boot_menu.sh, which reaches the menu but takes screenshots rather
than draw logs. Blind was chosen because screenshots cost ~10 s each while
xenia runs; wrong trade, since a mistimed press costs the whole run.

Reach: the negative is about this run, not the game. F1 stays open and the
port should keep its -1.0 constants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 16:41:55 +00:00
sylph-decoder
dcaa322146 re: the decoder's timers are a double-tap latch on LB/LT, not key repeat
Answers H1's first half with a negative, and decodes what the timing
constants are for.

C_PAD_DECODER's +0xB4/+0xB8 and +0xBC/+0xC0 (10 and 90, twice) are not a
key-repeat delay/interval pair. They are two identical channels of a
DOUBLE-TAP detector, on cfg +0x74 (LT) and +0x70 (LB), firing output bits
0x20 and 0x40.

The mechanism, from the image: both timers tick down once per update; a
press arms the short timer to 10; a RELEASE adds 1000 to it as a flag
stored inside the counter, which is why the tick watches for exactly 1000
and clears both there; a second press while the flag is set arms the long
timer to 90; and while the long timer runs the output bit is asserted on
EVERY update, with the short timer re-armed to 1010 each time.

So it is a latch, not a repeat: a double-tap opens a 90-update window and
the bit is held for its whole duration. That is a dash or barrel-roll
shape, which fits the buttons it is wired to.

H1: the directions have NO timer. The D-pad and left stick reach the output
word through bare mask tests -- the four left-stick literals at 0x8220C458,
C474, C490, C4AC, and DPAD DOWN through cfg +0xA4 -- with no counter
loaded, decremented or tested on any of those paths. On the evidence of
this layer a held direction does not repeat.

Reach: one layer. A menu could implement repeat on top of a held bit, and
this says nothing about that -- but it does establish the repeat is not in
the shared decoder, so it would have to be per-screen. "One step per
deflection" stays authored for the port; this narrows rather than settles.

Refutation of my own earlier note that a decoder "is where a game normally
puts its repeat timing, its edge detection and its button remap": remap
survives, edge detection survives, repeat timing is REFUTED. That clause
was a prior about how games are written, not a reading of this one.

Also commits tools/ppc-dis, the minimal PowerPC disassembler this corpus
has been rebuilding in scratch and losing to container restarts three times
in one session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 16:20:50 +00:00
sylph-decoder
8580c8738b Merge remote-tracking branch 'origin/main' into auto/frame-blend-draw-path 2026-09-02 15:26:38 +00:00
MechaCat02
d394c55cbb agents: the splash does not animate, and three instruments could not see it
A human on a GPU at ~140 fps: "the logos just switch, there is no animation at
all." Measured from a real boot with --film at 0.05 s, then per-frame change:

  splash moves        1.30 s of 7.95 s = 16.4 %
  publisher splash    0.30 s of motion, then 3.20 s FROZEN
  developer splash    0.35 s + 0.25 s, then 2.40 s FROZEN
  distinct luma states in 7.95 s   26

A 45-unit build-in cannot be drawn in 26 states, and a fade does not hold one
picture for 3.20 s. The frame counter says 24.8 fps achieved; both are true --
the port is DRAWING 25 times a second and CHANGING almost never.

🔴 Why every check passed, which matters more than the bug:

  frozen sweep     drives the clock BY HAND -- proves the renderer can draw
                   pose N, never that the poses are drawn in sequence
  settled compare  0.01 % against the capture -- a screen frozen 84 % of the
                   time matches a settled reference PERFECTLY, that is what
                   frozen means
  achieved fps     counts frames DRAWN -- the same pixels 25x/s scores
                   identically to animating

Every one measured throughput or a pose. None measured CHANGE. Same shape as
InputEventAction bypassing the input map: the instrument sat below the thing
that was broken, so the break could not appear in it.

tools/motion-census closes the class. It measures change and nothing else, and
its --selftest asserts it separates a fade (97.4 % moving) from a switch (2.6 %)
from a frozen film (0.0 %) -- a detector that cannot tell those apart would
report the same green line on all three.

Both briefs: this is the SOLE focus. The port reproduces before changing
anything and gates every fix on a film rather than a still. The decoder maps the
whole pipeline end to end -- disc bytes, the game's per-frame update (does it
interpolate between keyframes or hold?), what is submitted per frame, and what
Canary does to it before a capture records it -- delivered as a SERIES, not a
settled value.

The port should also record the refutation against itself: H2 reads ANSWERED on
the strength of the frozen sweep. The mechanism half stands, the blur is a baked
companion texture. The behaviour half does not.
2026-09-02 17:24:42 +02:00
sylph-decoder
3f114a28af tools: export ALL FOUR container-only Canary commits, not just my own
Applying my own lesson to my own corpus rather than only stating it. I
exported the content-hash patch and stopped; auditing the docs for
"/canary sha" citations found three pages naming d90d14e02, and checking
/canary's history found the exposure is four commits deep, not one.

Bounded by measurement rather than guess: branch -r --contains puts
590912722 on origin/sylpheed-re and finds no remote for anything after it,
so the container-only stack is exactly these four.

  0f920e645  blend= per draw          <- the ENTIRE blend decode rests on this
  fa1e4c221  vertex dump 8 -> 64      <- at 8 the log silently dropped four
                                         quads of the EXTRAS 24-index batch
  d90d14e02  RT state, resolves, PS constants
  ab3203f79  h= content hash

The sharpest case is 0f920e645. Without it a draw log records no blend
state at all, so ui-blend-mode-decoded.md's 35-element oracle -- which
overturned a REFUTED entry and deleted the port's authored blend map --
could not be re-derived by anyone who cloned this repository.

The failure mode is silent: the recipe LOOKS complete and only fails for
someone on a different machine, long after its author could say what the
flag did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 19:59:14 +00:00
sylph-decoder
afec2ae592 tools: export the Canary logger patch the clock findings depend on
/canary is a separate checkout that lives only in the container and is
pushed nowhere. The content-hash field in the UI draw logger is what
separates "the buffer rotated" from "a frame was decoded", and two
committed findings -- guest-frame-rate-resolved.md and
clock-is-frame-based-one-unit-per-present.md -- cannot be reproduced
without it.

Committed in /canary as ab3203f79 on branch sylpheed-re for a nameable sha,
and exported here as a patch because that sha is not reachable from
anywhere outside this box.

Includes the rebuild recipe, since the build needs the /work/xenia-canary
symlink present and that is not obvious from the failure it gives without
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 19:56:29 +00:00
sylph-decoder
27f81d6d91 re: the guest presents at 30 fps -- 60 units/s, and the port was right
Answered against the pre-registration committed before the capture.

Units per second was the last open number on the PRESS (A) plate, and both
prior measurements of it were wall-clock readings off an emulator that runs
the guest slow by an unknown factor. They disagreed by 2.9x, because a 30 Hz
guest at full speed and a 60 Hz guest at half speed look identical on a wall
clock.

The ruler here is not a clock. ADV.wmv declares 30.0000 fps in its own ASF
header, so a decoded movie frame is a tick the emulator's speed cannot
stretch. Presented frames per decoded movie frame is guest_fps/30 with no
wall clock in the chain.

  predicted  H_A 30 fps ->  60 units/s -> 1.0
             H_B 60 fps -> 120 units/s -> 2.0
  measured                                1.0000

Both pre-registered guards pass. Guard 2: a perfect repeating 3-buffer
cycle, 52 uses each (exactly 156/3), 2 chroma planes per luma on 156 of 156.
Guard 1: run lengths are 156 runs ALL of length 1 -- no smear, so the
dropped-movie-frame bias that would have pushed the answer toward 120 is
measurably absent rather than argued away.

So H_A. The port keeps its 60 and changes nothing.

REFUTES the live H3 hypothesis that 120 units/s explains the play-test's
late plate. That hypothesis was well-formed and attractive precisely because
it would have explained the complaint, which is why it needed a ruler that
is not a clock.

Which means finding 3 still has no cause. The strongest remaining candidate
is decoded rather than speculative: the plate's declared onset is t=214, not
t=236 -- a 22-unit fade, matching the T=22 the oracle confirmed by measuring
+23 alpha per presented frame on that element.

The pre-registered control could NOT be run: this logger build emits vb=
addresses, not vertex contents, so there was no alpha to check +34 against.
A weaker control is substituted and labelled -- the splash shader/blend
census, which validates the log's structure (what this measurement uses) and
not alpha extraction (which it does not).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 18:49:40 +00:00
MechaCat02
3e60560344 register: reclassify under R1 -- name every instrument, re-open the ten our own killed
Both agents asked for this and neither could do it: the register is the file
they both read to decide what NOT to try, so two agents agreeing is not the
authority for changing it (RETRO-2026-08-31-agreed §7.1).

R1: a refutation whose instrument is one of our renderers is not a refutation,
it is "our renderer disagrees" -- 🟡, not . The motivating case was not
careless work. "Blending those sprites additively worsens every measure against
the capture" killed a real disc field for weeks, and read exactly like a
publishable negative; the renderer behind it had a stale keyframe association,
no leaf geometry and no rotation. Nothing in the entry could have told you.

All 222 entries now end with ⟨instrument⟩, read off each entry's OWN stated
evidence -- never inferred. An entry that states none gets `unrecorded`, which
is 83 of them.

Ten moved  -> 🟡, each naming what would settle it: 8 render-vs-capture,
1 our-reader, 1 harness.

Three things the pass turned up:

* The rest() question is OPEN and had been reading as settled in both
  directions -- "rest = last keyframe" was refuted by a sibling argument, and
  that refutation refuted by correlating our render against captures. Both legs
  are our renderer. Which one you believed depended on which entry you found
  first, and it decides the pose every plateau-less element is drawn at.
* A withdrawal never reached its sibling: "2 391 frames, max glyph 0" was
  withdrawn because a long-lived x11grab stream freezes and repeats a stale
  frame; the 1 674-sample negative three lines above it, same probe, was left
  standing as a reinstated measurement.
* 83 of 222 -- 37 % -- record no instrument at all. Not disputed, not safe:
  unauditable, and larger than every other group combined.

tools/stale-instrument is the --stale query the rule needs, because a colour
alone re-opens nothing: the failure was that nothing re-opened a claim when the
instrument that killed it improved. Its own --check found a real bug on the
first run -- sys.exit("text") exits 1, so three faults documented as exit 2
were reporting the same code as an ordinary miss.

R1 is now standing text in PROTOCOL.md, with R5's exception: our tool is the
right instrument for a question about our tool.
2026-09-01 17:21:17 +02:00
sylph-decoder
b86e82150d retro §6: route the Port's BLOCKED.md through HANDOFF, and make the pointer fail loudly
The Port's standing ask list is not in my loop brief, which BLOCKED.md itself
records as having cost three sessions. My brief does force HANDOFF.md open every
iteration and HANDOFF is mine to write, so the pointer lives there. One line in a
file I own; no brief change and no human needed.

R11 says a cross-agent pointer must fail loudly when it goes stale, because every
staleness incident here has been silent. peer_asks.sh exits 2, 3 or 4 with a
message for a missing remote, a renamed branch or a moved file, and lists the
branches it can see so the fix is obvious. A blank output is a failure, not an
empty ask list.

Its own first version was wrong in the way this retro is about. Written as
`git show ... || { echo missing; exit 4; }`, piping it into `head` closed the
pipe, git died of SIGPIPE, and the fallback printed 'the file is missing' for a
file it had just printed in full. An error path that fires on success is worth no
more than one that never fires. Existence is now checked with `cat-file -e` and
the content exec'd; all four paths were exercised before this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 11:16:10 +00:00
sylph-decoder
3d8b1c2d4b tools: the sweep leaf's declared ramp, and the slope measured off the GPU
sweep_leaf_ramp dumps the nested ptloop01/ptloop02 leaf keyframes -- position,
alpha, rotation, scale and time -- which is where the ramp the port asked for
actually lives.

sweep_positions now also pools alpha against position per strip and prints the
slope, with the quantisation stated: NDC prints to two decimals, so one frame's
dx is 6.4 px and alpha is one level, and at three or four frames the two
declared slopes (+0.0814 and -0.0651) are inside that noise. It is a direction
and magnitude check, not a discrimination.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 07:10:35 +00:00
sylph-decoder
871e80ba85 tools: reach OPTIONS by the WRAP, not by counting presses
options_draw_capture.sh counted three DOWNs. A dropped press lands the cursor on
TUTORIAL, whose (A) starts the tutorial. My two attempts to avoid counting were
both worse: 'press until the cursor stops moving' is unreachable on a wrapping
menu, and an earlier version of it read the same row twice after a lost press
and pressed (A) on NEW GAME.

The wrap is the landmark. Press down until the row DECREASES -- the cursor is
then on item 1 whatever it started on -- and take three steps, retrying any
press that does not move the cursor, aborting rather than pressing (A) if one
never lands. Needs the row to be monotone in the item, not calibrated.

Also: 1200 s title deadline, and the emulator is killed at the end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 07:06:49 +00:00
sylph-decoder
088df913b2 re: T8aD +0x04 bit 0x02 predicts the MEASURED blend, 35/35 -- and a prediction to test it
REFUTED.md kills this claim: 'T8aD +0x04 bit 0x02 selects an additive blend ->
mine, and refuted. Blending those sprites additively worsens every measure
against the capture.' That refutation rests entirely on our renderer, which the
corpus's own rule calls a hypothesis under test. The blend is now measured off
the GPU, so the claim can be tested against the oracle.

35 elements over three screens, every label an RB_BLENDCONTROL0 value read from
the command stream: 16 bit-set and additive, 19 bit-clear and alpha-over, zero
false positives, zero false negatives.

The control that makes it a decode rather than a coincidence: of every bit of
the first 12 header words, EXACTLY ONE separates those 35 elements without
error. Nothing ties with it. A perfect partition on a small sample is worthless
if half the header partitions equally well, which is the mistake +0x08 = 0x8050
was.

And the pair no confound survives: ptbtn00 = 0x0110, ptbtn00f = 0x0112 -- the
PRESS (A) plate and its own highlight, same screen, differing in exactly this
bit, drawn alpha-over and additive respectively.

Committed alongside is a PREDICTION for GP_OPTIONS, written before the capture
that tests it: a different archive, a different element set, and a MIXED
prediction -- po_menu_eff01/02/03 additive, 592 elements alpha-over. Falsified
if those three draw alpha-over or anything else draws additive. The developer
splash was considered first and rejected as a test: both its elements predict
alpha-over, so it can fail but cannot discriminate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 07:06:07 +00:00
sylph-decoder
fa887b4e5f re: EXTRAS complete -- ptframe4, pteff21/22/23 and pteff10 are all ADDITIVE
The four elements the port measured as the worst on EXTRAS, and which appeared
in no draw, were in a draw all along: the 24-index additive batch holds six
quads and Canary printed the first two. Cap raised to 64, screen re-captured,
all six named. Same draw as ptframe3, whose state was already measured -- the
one-way implication doing real work.

pteff10 is identified too, and it needed the resting SCALE: it ships as 409x144
and is drawn at 200 % x 500 % = 816x720. The matcher's 'try 1x and 2x' rule
could not name it at any scale and reported a near miss against something else,
which is a failure wearing the clothes of an answer. Candidates are now the
declaration's pivot*2 scaled by the resting keyframe as well as the texture at
1x and 2x, and the tolerance is the log's own NDC print quantisation rather than
a chosen number.

Flagged rather than buried: pteff10 measuring additive is in tension with the
port measuring it nearly exact under alpha-over. Both can be true for a dim
semi-transparent glow over a dark background, and it is the one row a rendering
check does not corroborate.

Also stated: the three full-screen alpha-over draws are NOT individually
identified -- four elements declare 1280x720 -- so the label on those rows is a
candidate, not an identification.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:59:11 +00:00
sylph-decoder
1ce4eca706 tools: the EXTRAS walk pressed (A) on NEW GAME -- fix the stop test
'Press until the cursor stops moving' broke on its first comparison: one DOWN
was dropped, the row read the same twice, and it concluded the cursor had
stopped while sitting on the first item. It then pressed (A) on NEW GAME. No
harm this time -- neither press registered and the screen stayed put -- but that
is luck, not the design.

A stop test that cannot distinguish 'at the end' from 'the press was lost' is
the press-counting bug wearing a different hat. Now: a non-move ends the walk
only after at least one move has been observed, four consecutive dropped presses
abort, and (A) is pressed only if the cursor demonstrably moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:40:12 +00:00
sylph-decoder
c8f488b3cd tools: separate 'submitted' from 'on screen' in a draw log
The blend map reports a quad's SIZE, which identifies an element and says nothing
about whether it is visible -- a parked quad is still a draw call. This prints
the NDC x/y range and per-vertex colour of every additive draw, per frame, so
movement between frames and overlap with the [-1,1] box are readable directly.

Written because sylpheed-port flagged exactly that conflation in my blend page:
the blend and the visibility arrived in the same artefact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:24:57 +00:00
sylph-decoder
698409fdfe 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
01025ab494 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
2353983df8 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
e61c60029a 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
2c7c6c255e 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
e568a3494d 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
df0d4f058d 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
1e0d819108 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
73bf90eb8d 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
2bc20ec31d 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
976d657808 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
dda1bc183b 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