Files
Sylpheed/docs/port/DECISIONS.md
Sylpheed port agent f7fee7a77f port: check the walk as well as the contract, and a defect I nearly filed off a debug pin
docs/game/navigation.md is a second document unreachable from main, and
authored/flow.json is its executable form -- nothing in the port fails when a
label drifts from it. Three more checks in contract-check, anchored on the walk's
own text: the five main-menu labels in order, EXTRAS' three items, the cursor
wrap. Ten checks now, ten known negatives, all passing.

The manual audit behind them found nothing else: initial focus is already
kind:authored citing Q5's instability, left_right is an explicit no-op,
auto_repeat is measured, unexported destinations are marked blocked with reasons.

Refutation target: the walk's claim that the ring is the ONLY thing moving on the
settled menu. Cannot be tested against the game from here, but can be tested
against my renderer, which is the direction that matters. Five renders across a
full ring cycle: 1428 of 921600 pixels vary, 0.155 %, one 46x44 cluster beside
the focused item. The port animates one ring, not five -- worth checking, since
all five ptbtn01f..05f declare the same 120-unit cycle and a renderer running all
of them would look identical until you diffed frames.

Then I nearly filed a serious P5 defect against myself: sweeping --leaf-time with
the ring pinned moves 10.4 % of the frame, full-screen. It is not a defect. That
pin addresses the build-in -- ptloop01 runs t=0..600, ptloop02 t=0..720 -- and at
settle both park off-screen at x=1521 and x=-839, with loop_leaf_on_screens
scoped to the title alone. The general form: a pin that can address states the
screen never occupies will manufacture defects on demand, which inverts what the
three pins are for.

The +0x08 ask came back answered and is not consumable. ui_layout::loop_length_units
is public at b5df02a and byte-for-byte what screen.rs holds, so the deletion is
one line -- but Cargo.toml pins a tag, no tag carries that commit, and swapping a
deliberate pin for a bare rev on an unmerged branch is not a move to make alone.
Asked for a tag; keeping the guarded local read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-30 21:17:59 +00:00

669 KiB
Raw Blame History

Decisions

One entry per decision that outlives the container it was made in. Newest last. A decision that lives only in an agent's context is lost when that container dies, which is what this file is for.


239 sections. Search this before re-deriving anything.

P0 — the exporter, 2026-08-28

The exporter reads one authored file, and stamps its provenance into the output

export/ is derived and authored/ is hand-written, and the natural reading of that is that the exporter never touches authored/. But a screen has to be called something, and the disc does not name its builds — the identification of build 5 as the main menu is HANDOFF Q2, measured against a live capture, not a field.

Two ways to handle that:

  1. the exporter emits build_05.json and the runtime renames it from authored/screen_names.json;
  2. the exporter reads that map and writes main_menu.json directly.

Chose 2, with a condition: every name it applies carries name_source: "authored" and a name_why quoting the evidence, and check rejects an authored name with no why. The file that lands in export/ is therefore still honest about which of its fields is a measurement — which is the property the derived/authored split exists to protect — while a human opening the tree sees main_menu.json rather than having to resolve a rename in their head. A build nobody has identified exports as build_NN with name_source: "index", which is a locator and not a claim.

This is the only authored input the exporter takes. Everything else in authored/ is applied by the runtime over export/.

Sprites are per screen, not a flat pool

main_menu and extras both ship a ptbase.t32 and they are different pictures. A flat sprites/ directory would have silently collided; whichever screen exported second would have won, and the loser would have drawn the wrong background with no error anywhere. sprites/<subdir>/<screen>/<name>.png.

The format is executable

sylpheed-export check --out export validates a tree against docs/port/FORMAT.md with no disc in hand. It exists because "the export is correct" is otherwise an assertion, and because the P0 gate is "validates against FORMAT.md" — which is not a thing anyone can confirm by reading.

It reads the tree the way Godot will: as a stranger, with no access to the disc, the decoders, or the exporter's internals. It deliberately does not check the export against the disc — that is what sylpheed-cli screen render is for, at P1.

Checked that it bites, rather than assuming: five mutations of a valid main_menu.json — a broken paint_order permutation, a dangling focus_sprite, a reversed buttons list, a #rrggbbaa colour, an invented name_source — are each caught with a specific message.

The highlight sprite pairs by name; opt is exported but not believed

FORMAT v1 said focus_sprite came from the element's opt link. That reading was measured and refuted by the RE agent, and this export shows why plainly: on the main menu, opt chains ptloop01 → ptloop02 → ptbtn01 — two decorations and then a button. It is a linked list of something, and it is not focus.

The highlight is paired by sprite name instead (ptbtn01.t32ptbtn01f.t32), which is HANDOFF's convention and holds for all 54 real pairs on the disc. It resolves all five main-menu buttons. The raw link is still exported as opt_link, renamed so that nothing downstream mistakes it for navigation, and so that whoever eventually decodes it has the data.

Note this is 🟡 a naming convention, not a decoded field. It is authored in effect, and lives in the exporter only because it is a rule over disc data rather than a value we chose.

The paint order is exported, not authored

Q3 decoded it — a u16 layer key at +0x0A of each T8aD sprite header, stable-sorted with declaration index. So it is read in the exporter, per the contract's own rule for a decoded answer, and paint_order in export/ is a derived field. "paint_order" is gone from unresolved; paint_order_ties replaces it, because the tie-break is still unknown and costs one element's blend on one screen.

Where an element has no T8aD header the key comes from the decoders' table of keys measured off the running game. That is a different kind of fact, so it is labelled: layer_source is "sprite", "implied" or "none", and a consumer that needs to know whether a layer is read or measured can tell.

Colours are exported as two fields with the byte order in the name

There are two modulate colours and they multiply: tint is RGBA, fade is ARGB and its high byte is the alpha that ramps. v1's single "#ffffffff" could not carry both and silently discarded the ramping alpha. They are exported as tint_rgba and fade_argb, raw hex, byte order in the key — because getting it backwards is silent and looks like an art bug rather than a parse bug.

t stays raw

HANDOFF Q1 is answered — linear ramp, 2 units per rendered frame, working conversion 1 unit = 1/60 s — but that conversion is measured off the running game, not read from the file, and the finding itself flags the 27.6 present- frames/second measurement as the part worth re-testing. If the game turns out to present at 60 Hz, every duration halves.

So t is exported exactly as the disc spells it, keyframe_time_unit stays in unresolved, and the conversion will live in one authored place at P2. One constant to change, in a file that says it is a decision.

The final keyframe has no t, and check enforces that

The disc has no time slot on the last keyframe of a group. A file that carries one there has invented it. check rejects it — this is the one place where the temptation to emit a plausible number is strongest and the resulting error is completely invisible.


P1 — Godot draws the screen, 2026-08-28

The Godot side reads the manifest, not a path

ExportTree is the only class that knows where export/ is: SYLPHEED_EXPORT if set, otherwise <project>/../export. Screens are addressed by their manifest name (main_menu), never by a file path, so the runtime never encodes the archive's subdirectory and a re-export that moves a file does not break it. It also checks format on both the manifest and each screen, and refuses a tree it was not built to read rather than half-drawing one.

Textures are read as bytes and decoded with load_png_from_buffer at runtime. They are deliberately not Godot-imported resources: export/ is gitignored and regenerated wholesale, and a .import sidecar per sprite would be derived state living next to derived state, invalidated on every re-export.

One CanvasItem draws the whole screen

ScreenView._draw walks paint_order and draws each element itself, rather than making a node per element and leaning on z_index. The export's paint_order is already back-to-front, so honouring it is a loop; expressing the same order through sixteen nodes' z-indices would hide the one thing that is still unresolved about it — the ties — behind Godot's own sibling rules, where a change in the export would silently become a change in Godot's tree order instead of a visible change in the draw sequence.

P1 draws rest and nothing else

Every element is drawn at its resting pose. No keyframe interpolation: that is P2, and it depends on the keyframe time unit, which is measured rather than decoded. A milestone whose gate is a pixel diff must not have a measured constant inside it, or the diff stops being evidence about the port.

For the same reason focused_id is empty at P1. Initial focus was measured as unstable boot to boot (HANDOFF Q5), so choosing one is an authored decision and it belongs to P5, where a human is pressing keys.

Nearest-neighbour, and why that is not a preference

TEXTURE_FILTER_NEAREST. The export is a 1:1 copy of the disc's own texels and elements draw at up to 500 %; a bilinear filter invents detail the disc does not have. It is also what the reference renderer does — ui_layout::blit maps destination to source by integer division — so a filter difference cannot masquerade as a placement difference in the diff.

The capture is the SubViewport, not the window

The screen is drawn into a SubViewport sized to the export's own design rectangle and shown through a container that scales it to the window. The first attempt captured get_viewport() and got 1235×695: there is a window manager on the Xvfb display and its title bar had eaten 45×25 px of a screen the export declares as 1280×720. A gate that compares a rescaled 1235×695 capture against a 1280×720 composite measures the compositor.

So --capture grabs the SubViewport texture: exactly the design rectangle, independent of the window, directly comparable with screen render with no crop and no resample. The windowed run is still worth doing — it is what proves a human sees the screen — but it is not what the numbers come from.

P1 gate — the diff, and what it found

tools/port/verify-screen renders every screen in the manifest both ways and reports the largest per-channel difference anywhere in the frame. Both renderers are held to the same inputs: the reference CLI built by build-reference-cli from the revision the exporter is pinned to (not /reborn/target/, which is a live mount that moves mid-iteration), --black because the screen carries its own background, and --primitives --animated because those are what make the CLI draw the same element set the port draws at rest.

screen build max per-channel Δ
main_menu 5 3 the P0/P1 gate screen
main_menu_jp 8 3
extras / extras_jp 6 / 9 4 / 3
press_start / press_start_jp 2 / 3 1
build_00 / build_01 0 / 1 3
build_10 / build_11 10 / 11 0 byte-identical
title 4 6 paint-order tie, below
title_jp 7 154 sampling phase, below

main_menu — the milestone's own gate — agrees to ≤3/255 on every channel of every pixel, RMSE 0.38 %, with no pixel differing by more than 4 %. 3/255 is what integer-truncating compositing in the CLI and float rounding on a GPU differ by; there is no structural disagreement anywhere in the frame.

Three screens exceed that, and each has a named cause rather than a threshold.

title: a tie in the paint order — neither renderer is wrong

Build 4 is the one screen where the CLI uses a paint order measured off the running game instead of deriving it. Compared against the order this port exports, every single disagreement is inside a tie — the two orders differ only among elements carrying identical layer keys (0x8083, the back2 glow group, and 0x80a0):

derived : … 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, …
measured: … 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, …

That is exactly the residual HANDOFF Q3 documents and this export already declares in unresolved: ["paint_order_ties"]. It is worth stating what it costs: 904 px in the glow band at (445,117)(1195,313), all of them 46/255. The port keeps the stable sort, per HANDOFF's own recommendation. Nothing to fix, and nothing to tune — a "fix" here would be fitting the port to one screen's capture.

Two of the reordered indices (0x80a0) are kind & 0x4 template instances that both renderers skip, so the only real reorder outside the glow group is ptlogo2 against ptlogo_tm, which do not overlap.

title_jp: nearest-neighbour sampling phase — the CLI is the one I would call wrong

title_jp is the only screen in the export with a drawn element at a scale that is not a whole multiple of 100 %: ptlogo_eff2 at 125 %. It is also the only screen with a difference above 6/255. The two facts are the same fact.

At a non-integer ratio the two renderers pick different source texels:

  • ui_layout::blit samples the source at the destination pixel's top-left cornersxi = col * sw / dw.
  • A GPU samples at the destination pixel's centrefloor((col+0.5)·sw/dw).

At 125 % those disagree on one column in five, which is why the differing pixels are ~30 above 100/255 strung along thin diagonal edges rather than a shifted region. At every whole multiple of 100 % they agree exactly, which is why the other eleven screens are clean.

Which is wrong: the CLI, I think. Corner-sampled nearest is a half- destination-pixel bias toward the top-left that no rasteriser produces, and the Xenon GPU that drew this screen sampled at pixel centres. But I have no framebuffer capture of title_jp and the disagreement is sub-pixel on one glow, so this is a reading, not a measurement — recorded in docs/port/BLOCKED.md rather than acted on. The port is not changing to match, because matching the CLI here would mean deliberately reproducing a half-pixel offset in order to make a number smaller.

extras: two pixels

Two pixels at 4/255. Rounding.

What the diff cannot tell us

The pivot question in docs/port/BLOCKED.md predicted that a P1 diff could not distinguish "anchor scale to the declared pivot" from "anchor to half the texture", because both renderers use the declared pivot. That prediction held: the port and the CLI agree on every scaled element, and that agreement is not evidence about which anchor the game uses. It stays open.

pteff05.t32 and pteff04.t32 have no sprite, and that is correct

RETRACTED 2026-08-29. This was wrong, and it was the most consequential thing on this page. See "The menu had no background" below.


P2 — keyframe animation, 2026-08-28

The time unit is authored, in one file, and says loudly that it is not on the disc

authored/timing.json. HANDOFF Q1 is answered — linear ramp, 2 units per rendered frame, 1 unit = 1/60 s — but that conversion is measured off the running game, not read from a file, which is exactly the case the derived/authored split exists for. It is expressed as keyframe_units_per_second: 60 rather than seconds-per-unit so the value is exact instead of a repeating decimal, and it carries the two independent lines that support it. t stays raw everywhere in export/; seconds appear only where this file is applied, which is one line of boot.gd.

exit_ramp_seconds is deliberately null. See below.

The timeline stops at the last timed keyframe, and never plays the exit

The last keyframe of every group carries no t — the disc has no time slot there. Across this export that final frame is an exit pose: for 116 of 134 elements it differs from the last timed keyframe in alpha only (a fade-out), for 12 it is the loading splash's scale-and-slide exit, and for 6 it is identical (no exit animation at all).

So the group is pre-roll → ramp in → hold → [exit], and the port plays it up to the hold and stops. Playing into the exit would mean inventing how long the ramp takes, because the disc does not say. That duration is the screen transition — HANDOFF Q7 measured it at ~0.4 s — and it belongs to P3, with its own evidence. This is why exit_ramp_seconds is null rather than 0.4: P2 has no business holding it.

The interpolation is checked by where it lands, not by inspection

For 8 of the 12 screens the settled timeline is byte-identical to the --pose=rest render. That is the useful assertion: the port walks the keyframes with an authored time unit and arrives, to the pixel, at the pose the pinned decoders independently identify as the resting one. tools/screen-strip reports this per screen, so a change to the interpolation that drifts by one unit shows up as a diff rather than as nothing.

The four that differ do so for two distinct reasons, below.

rest misidentifies six elements, and the running game says so

On main_menu, the settled timeline and rest differ in exactly one region: 400×470 at (440,108) — the bounding box of ptframe1 and ptframe2, and nothing else on the screen.

rest puts both at their first keyframe: off-position and fully transparent. The keyframes say they slide (620,108)→(440,108) and (403,267)→ (583,267) while fading 0x00→0xff, and then hold that pose for their last three keyframes including the untimed one.

/reborn/docs/re/captures/main-menu-oracle.png, a capture of the running game, shows them: the bright circuit-frame bracket around the menu, with a ring at the bottom right. Cropping the same 250×180 region from the capture and from both renders puts the ring and its elbow trace in the port's timeline render pixel-aligned with the game's, and absent from the rest render. That is geometry, not luminance, so it does not depend on the capture's gamma or on the fact that it was taken with NEW GAME focused.

Why the decoders get it wrong, precisely

ui_layout::rest_plateau excludes a run of identical keyframes that ends the group, because that run is normally the exit — the comment cites the pause menu, where taking the trailing run erased the word PAUSE. That exclusion is right in general and wrong for an element with no exit animation, where the trailing run is the hold. The rule then falls back to an earlier run, which for a slide-in is the invisible pre-roll.

The condition that identifies the affected elements exactly, with no false positives in this export, is:

the final untimed keyframe has the same pose as the last timed keyframe

Six elements match it and rest misses all six: ptframe1/ptframe2 on main_menu and main_menu_jp, and pteff02 on title and title_jp. This is a finding for the RE agent about sylpheed-formats, not something this port fixes: the decoders are pinned and must not be reimplemented here. The port simply does not use rest — it derives the arrived pose from the keyframes, which needs no heuristic — and verify-screen still asks for --pose=rest so that renderer-vs-renderer diffing compares like with like.

Note what this says about P1: the port and the reference renderer agreed on main_menu to 3/255, and both were missing two elements the game draws. Two renderers reading the same field through the same decoder agreeing is not evidence that the field is right. docs/port/BLOCKED.md had already said that about the pivot; here it bit for real.

The title is not settled, and P2 does not claim it

title and title_jp differ between the two modes by much more (max 142 and 247), and there the disagreement is not the six-element bug alone. rest picks a mid-timeline hold for several glows (pteff01, ptlogoall_eff, ptlogoall_eff2, ptlogo_back2eff5) where the timeline runs on to a much brighter pose.

I could not settle which is right, and did not try to make the numbers agree:

  • No element's alpha ever reverses direction anywhere in this export, so the title's 4.48 s timeline is a slow one-way ramp, not a pulse — which removes the obvious reason to expect a loop, but does not prove there is none.
  • The only live title capture composites the PRESS Ⓐ plate (build 2) over the title (build 4), so it cannot be diffed against build 4 alone. Mean luminance is oracle 64.1, rest 62.8, timeline 80.0 — which looks like it favours rest, except that the plate adds brightness and rest is carrying a 25 % black dim quad (pteff02) that is itself one of the six misidentified elements. The comparison is confounded in both directions and settles nothing.
  • Both modes are visibly wrong anyway. Side by side with the capture, the port draws a washed-out cyan glow slab across the logo that the running game does not have — in rest mode too. That is a third problem, independent of this one, and it is P3's.

So: the timeline is the default because it is derived from the disc's own keyframes with one measured constant and no heuristic, and because it is proven right on the screen this milestone gates. On the title it is unverified, and P3 should not assume P2 settled it.


P2, corrected — the pin moved, and the settle rule was wrong, 2026-08-28

Answering the RE agent's question: which six, and on what screens

They asked, having found only two elements on the English main menu satisfying the condition this port proposed. The six span the whole 12-screen export:

element screens trailing run
ptframe1, ptframe2 main_menu, main_menu_jp alpha 0xffvisible
pteff02 title, title_jp alpha 0x00transparent

So four of the six are the pair they already found, once per language build, and their alpha rule accepts exactly those. The other two are pteff02, whose trailing run is transparent, so their rule excludes it and leaves rest at 0x40.

That exclusion is right, and their own measurement proves it. pteff02 is the 25 % dim quad; they measured the title render going from +13.14 to +0.55 against the plate-free capture once the dim is drawn. rest must therefore stay at 0x40 and must not move to the transparent trailing run — which is what their rule does. Two investigations converging from opposite directions.

The condition this port proposed was too loose; the alpha discriminator is the correct rule and the port has no amendment to offer.

The pin moved 8b6dbcf5414db3

Its own commit, and what I wanted from it is the fixed ui_layout::rest(). Pinned at 5414db3 rather than 4bc9706 where the fix was written, because 5414db3 is where it carries its disc-wide check — 30 of 13 991 elements move, 4 become visible, 0 become invisible.

The re-export is the evidence the change was contained: two files changed, and within them exactly four rest blocksptframe1/ptframe2 on both main menus moving from (620,108)/(403,267) at t=16 and alpha 0x00 to (440,108)/(583,267) at t=62 and alpha 0xff. Every diff line pairs; the other ten screens are byte-identical, pteff02 did not move, and no sprite changed.

The settle rule was wrong, and their title finding is what showed it

P2 shipped "hold the last timed keyframe", on the reasoning that the exit is the final untimed frame. That is wrong, and the title is the counter-example: pteff02 holds at t=46 with the dim at alpha 0x40 and then ramps to 0x00 by t=236. The exit is not only the untimed frame — it can be a long run of timed ones. Running to the end drops the dim and makes the whole screen ~13/255 too bright, which is exactly the luminance excess P2 recorded (oracle 64.1, rest 62.8, timeline 80.0) and could not explain.

A group is pre-roll → ramp in → hold → ramp out → post-roll, and a screen that has arrived sits on the hold. So the timeline now plays in and stops at rest, which is the decoders' identification of that hold and carries its own t. settle_units() is rest.t.

The check is that the disagreement vanishes: on all twelve screens the settled timeline is now byte-identical to the --pose=rest render, where before this change four of them differed by up to 247/255. The timeline's endpoint should be rest — the animation is what the timeline adds, not a different destination — so this is the property to want, and it now holds without a special case.

That also retires P2's open question about looping, from the other side: the RE agent measured that groups hold rather than loop (ptloop01/ptloop02 park off-screen at x=1521 and x=839; 18 s of settled title sits at sd ≤ 0.01).

The reference renderer was stale for three diff runs

Worth recording as a process failure, because it defeated the project's whole verification method for a while and it failed silently.

After bumping the pin I rebuilt the reference CLI, and build-reference-cli reported success at rev 5414db3. verify-screen then showed main_menu jumping from 3/255 to 72/255. The natural reading — the port had regressed — was wrong. The port was right and the reference was a revision behind: the shared CARGO_TARGET_DIR still held a sylpheed-cli built from 8b6dbcf, and cargo reported Finished in 0.13s and left it in place. Building into a clean target directory produced a binary that resolves ptframe1 to (440,108) t=62; the shared one still said (620,108) t=16.

The old check — "does screen list run?" — cannot catch this, because a stale binary runs perfectly.

Two changes:

  • build-reference-cli builds into $CARGO_TARGET_DIR/reference-cli/$rev, a tree keyed by the pinned revision, so a new pin has no artifacts to reuse. A stable copy is placed alongside for consumers.
  • It then checks the binary against export/: both come from the same pin, so if the CLI resolves ptframe1's rest differently from what the exporter wrote, the two halves of the verification are not the same revision and it fails loudly. It compares the two rather than asserting a literal, so it stays true when the pin moves again.

docker/bin/ is baked into the image, so this takes effect on the next image build; until then the repo copy has to be invoked by path. The RE agent hit the same class of trap this session from the other side (./target/debug stale against a redirected CARGO_TARGET_DIR). It is worth naming the general shape: a build system reporting success is not evidence that the artifact you are about to trust is the code you pinned.

What this did not change

title (6/255), extras (4/255) and title_jp (154/255) are unchanged, and their diagnoses stand — a paint-order tie, two pixels, and nearest-neighbour sampling phase at 125 % scale. The title's swoosh defect the RE agent localised (drawn thick and white where the game draws it thin and pink) is untouched by any of this and remains P3's.


The menu had no background, and P1 called that correct, 2026-08-29

The pin moved 5414db3 → f817dd5 for 56cc7ac, "a RATC child's name is stated, not inferred". ratc::parse had named each child by scanning backwards for the last printable run of bytes before its magic. For pteff05.t32 the three trailing payload bytes are 38 41 588AX — which beat the real name, so the child registered under a name no element declares and resolved to no sprite.

pteff05.t32 is the full-resolution background of all five menu screens.

So every render this port has produced of a menu screen has been missing its background, and P1 wrote that up as a property of the disc: "the bundle declares them and carries zero RATC children for either, so there is no texture on the disc to export." That sentence was false. The bundle carries the child; the decoder was handing back the wrong name for it. Retracted above rather than edited away.

What the re-export shows

Six new sprites and nothing else: pteff05.png on main_menu, extras and their Japanese twins, pteff04.png on both titles. Per screen the JSON gains a sprite line and layer_source moves "implied" → "sprite" — the layer key is now read from the file instead of taken from the decoders' table of keys measured off the running game. That is the derived/authored ratchet turning the right way, in the exporter rather than in authored/.

pteff05.png is 1280×720; ptbase.png, which had been carrying the background alone, is 640×360 drawn at 200 %. The screen was being shown its own art at half resolution.

Measured against the live capture, not against the other renderer

Whole-frame RMSE of the settled main_menu against captures/main-menu-oracle.png:

RMSE
before this pin 8.05 %
with the real background 5.92 %

A 26 % reduction, and it is the right kind of evidence: the reference renderer was missing the same element for the same reason, so a renderer-vs-renderer diff could not have found this. It is the third time on this project that the capture caught something both renderers agreed on — the bracket, the title dim quad, and now the background.

verify-screen after the bump is unchanged in character: everything at 34/255 except title (6, the paint-order tie) and title_jp (155, the sampling phase). Both renderers gained the background together.

One thing the comparison says that I did not expect

Rendering with --focus=ptbtn01, which is how the capture was taken, makes the RMSE worse — 5.92 % → 7.00 %. The port replaces an element's sprite with its *f twin; sylpheed-cli's own --focus is documented as drawing the focused record over the base element. Those are different operations, and the capture shows a ring marker beside NEW GAME that the port does not draw.

This is P5's, not P2's, and it is not being guessed at here. Raised in docs/port/BLOCKED.md.


P3 — splash → title, unattended, 2026-08-29

The splash is located by entry index, because no rule can find it

The RE agent looked for a content predicate and there is none: design size fails (every extra composable bundle sampled is 1280×720, the same as every screen) and element count fails (fragments run 2…15 elements in GP_OPTIONS/GP_SAVE_LOAD while the splash halves are 3 and 7 — the ranges overlap).

So screen_builds is now is_build plus an authored allow-list of entry indices, in authored/screen_names.json under also_export, each with a why that says it is a locator and not a claim. This is safe in GP_TITLE and would not be in general: there, widening adds exactly four bundles and all four are real screens with zero fragments. That is why it is an allow-list rather than a loosened predicate.

There were two splash screens and the port had neither. Entries 11/14 are the developer logos (GAME ARTS / SETA / studio anima); entries 10/13 are the SQUARE ENIX publisher wordmark, the first thing the boot shows, and nothing in this project had noticed them. Both pairs are region twins — ™ on 10, ® on 13 — and the port shows one of each, not both.

authored/screen_names.json is now keyed by pak entry, not by ordinal

Widening the enumeration renumbers the ordinals, and a name that moves when the enumeration rule changes is not a name. The file had always called the entry "the stronger locator"; it is now the only stable one. In GP_TITLE the two coincide across all 16 entries, which is also the numbering sylpheed-cli screen --build N --all takes — so verify-screen now passes --all, and without it --build 10 would have landed on entry 12.

The two previously-unnamed plates therefore renamed build_10/build_11build_12/build_15. Their names were always locators; now they locate the right thing.

The exit is the group playing itself out, not a black rect over a freeze

HANDOFF's answer to ask 2 was (a), and it came with a test that discriminates rather than a plausibility argument. Under "a black quad over a frozen screen" every region is scaled by the same 1α, so the button-region / background-region brightness ratio stays constant through the fade. Measured, it falls 6.495 → 5.574 → 3.105 → 2.125 → 1.935 — a 3.4× monotonic drop. The screen plays out: pteff00.prm ramps to opaque black while the labels, ptmsg, pteff10 and pteff12 ramp to transparent, and ptframe1/ptframe2 hold.

Implemented by giving the final untimed keyframe a synthetic time, exit_ramp_units after the last timed one, and then interpolating it like any other. One code path: the difference between arriving and leaving is only how far t is allowed to run, not a second kind of animation.

exit_ramp_units = 24 (~0.4 s) is authored, and authored/timing.json carries the RE agent's own reach caveat rather than smoothing it: the filmstrip is downsampled and the button region contains some background, so this pins the direction, not 0.4 s to ±0.05 s, and it is one transition pair.

Nothing waits on a timer the disc does not carry

dwell in authored/flow.json is deliberately empty. Each screen's dwell is its own keyframe group — the publisher wordmark reaches its hold at t=235 (3.92 s), the developer logos at t=190 (3.17 s), both read from the disc. Adding a hold on top would be inventing a number nobody measured. The pacing you see is the disc's own, and the file says where a measured number would go.

The last screen holds

A screen plays itself out because something is taking its place. Nothing takes the title's place yet, so the sequencer holds there. A boot that ends by fading to black is a boot that looks like it crashed. P4 puts the intro video in front of the title and P5 gives the title somewhere to go.

flow.json reproduces an observation and says so

Q6 closed with a negative: the order is in none of the four places it could have been, and a transition is a call with a name argument chosen by code. So this file is authored and its header says plainly that it reproduces what was watched, not what any file states. The intro video's place in the real boot is named as a gap rather than the order being quietly rewritten to hide it.

P3 gate

godot --path port -- --boot --film=/tmp/boot runs unattended:

publisher_logo  →  developer_logos at 4.65 s  →  title at 8.57 s
boot sequence complete after 13.05 s, holding on title

The filmstrip shows each screen fading in, holding, and fading through black into the next, and the title staying up. verify-screen covers all 16 screens now; the four new splash bundles come in at max 12/255 against the reference renderer. The three known differences are unchanged: title 6 (paint-order tie), main_menu 4, title_jp 155 (sampling phase at 125 % scale).

Answers taken from the RE agent without re-deriving them

  • Focus stays "replace". Over-vs-instead is unobservable: the focused sprite covers the base at 100 % of base-visible pixels, and the two compositions differ by RMSE 1.1 inside the button rect — under the gamma floor. The port's guess was right for the wrong reason, and the actual gap is that ptbtn0Nf.rat declares two sprites — ptbtneff01.t32, a glowing ring, and then the bright label — where ptbtn0N.rat declares one. The ring is P5's, and its placement inside the record is not decoded, so it will be authored from the capture and marked as such.
  • RMSE against captures has a floor, so stop chasing it. The capture is ≈ 255·(render/255)^γ with γ ≈ 1.49 on the menu and EXTRAS, 1.34 on the title, and it is a ramp the game installed (VdGetCurrentDisplayGamma at video init), not a capture-path artefact to subtract. Its reach is narrow — the flat patches it was fitted on are almost all dark — so the port will not extrapolate it across the range, and will not apply it to rendered output on this evidence. It is a comparison constant, not a rendering one.
  • Rotation is escalated to a human and the port has not acted. The RE half is answered — rotate about the declared pivot, measured against the GPU capture — and it has zero effect on the five screens at rest. The port will carry rotation_deg in a future FORMAT v3 because carrying a decoded field the renderer ignores beats dropping it, but it will not draw it until the divergence question is settled.

P4 — the intro video, 2026-08-29

Theora at 720p is fine here, and no runtime dependency is requested

MISSION §6 anticipated that Theora might be too poor at 720p and permitted the FFmpeg-GDExtension fallback to be proposed. It is not needed, and this was measured rather than judged by eye alone. SSIM against the decoded source over a 10 s sample: 0.9863 at -q:v 6, 0.9896 at 8, 0.9924 at 10. At 200 % zoom on the reel's hardest case — fine serif text and soft gradients over near-black, where Theora breaks first — q8 is indistinguishable from the source.

-q:v 8, and no GDExtension is being proposed or adopted.

-ac 2 because the source is 6-channel WMA Pro and Godot's Theora path is not a surround one. That downmix is a decision, so it lives in the recorded command where a modder can see and change it rather than in prose.

The exact command is in the manifest, per MISSION §6

export/manifest.json gains a videos array, each entry carrying the verbatim ffmpeg line that produced it. A modder who dislikes the quality re-runs one line instead of reverse-engineering what was done to their video — which is the whole reason this project converts the disc rather than reading it at runtime.

A cache, and why that is not a hand-edit

export/ is regenerated wholesale, but re-encoding 232 s of video on every run costs ~4 minutes to produce a byte-identical file, and an exporter nobody re-runs is worse than a cache. So each movie gets a .cmd sidecar recording the command and the source size, and the encode is skipped only when both match exactly. Any change to either re-encodes. This is derived state validating derived state, not a hand-edit.

The player renders into the design viewport, not beside it

First attempt parented the VideoStreamPlayer to the Boot node. It played, and every captured frame was black: the capture reads the SubViewport, and the player was rendering to the window. Worth stating as more than a capture bug — everything this port draws composes in the export's own 1280×720 design space, and a movie outside that space is outside the coordinate system every screen is expressed in.

Ⓐ skips, because Q9 measured it

The only input the port handles so far. HANDOFF Q9: one Ⓐ press skips a movie, measured — the title was reached at 57 s against a 193 s baseline. Menu navigation is still P5.

P4 gate

godot --path port -- --boot --film=… runs publisher_logo → developer_logos → ADV.ogv → title, unattended. The filmstrip shows the SQUARE ENIX ident, then the reel's live-action-styled CG, then the title. The movie's place in the boot is measured, not decoded — Q9 decodes ADVERTISE_MOVIE → ADV.wmv from the movie manifest, but where it sits in the boot order is what the RE agent watched, and authored/flow.json says so.

What I cannot verify from here

Audible playback. This container has no audio device — Godot falls back to the dummy driver. What is verified is that the Vorbis stream exists in the transcode, is 2-channel, and decodes. Whether Godot emits it audibly is unconfirmed and is stated as unconfirmed rather than assumed from the stream's presence. It is a cheap check for anyone with a sound device and an impossible one here.


RETRACTION — sylpheed-cli is not the oracle, 2026-08-29

This corrects a framing that runs through everything above, so it is a retraction rather than an edit. Every place this file called sylpheed-cli screen render "the reference renderer" — and it does so repeatedly, starting at P1 — overstated what it is.

The correction comes from the human, via the RE agent, in their words: Reborn "was/is just a GUI explorer and extraction CLI for verifying the decoding of the various files. It may very well be wrong." The oracle is the Xenia Canary capture and the game.

So tools/port/verify-screen is a consistency check between two decoders that share their assumptions, and a regression detector. It is not a correctness check, and agreement in it is not evidence of correctness.

The embarrassing part is that this file already knew

After the ptframe1 case, P2's write-up says: "Two renderers reading one field through one decoder agreeing is not evidence that the field is right." Then P1's numbers kept being quoted as though 3/255 against sylpheed-cli meant the port was right. Having the principle written down did not stop me leaning on the agreement — which is worth recording, because that is the failure mode, not ignorance of the principle.

Three times both renderers agreed and both were wrong, all three caught by a capture and catchable by nothing else:

what both got wrong how it surfaced
pteff05 the menu screens had no background the RE agent decoded the RATC child name
scale 0 drawn at full size instead of collapsed RE agent's control run
rest() ptframe1/ptframe2 invisible; the menu bracket missing main-menu-oracle.png

What changes

  • tools/port/verify-screen says all of this in its own header, calls the CLI the comparison renderer, and a DIFFERS row now means "we moved apart, find out which of us moved" rather than "the port is wrong".
  • The correctness question moves to the captures. The RE agent has committed nine of them with an index at docs/re/captures/ORACLE-CAPTURES.md, covering all five screens in scope — including a main menu with OPTIONS focused, whose difference from the unfocused menu isolates exactly what focus changes.
  • Three cautions travel with any capture comparison, and they are the RE agent's: the captures are not gamma-neutral (γ ≈ 1.49 menu, 1.34 title — there is a floor, do not chase it); geometry is sound (best alignment 0,0 at corr 0.9466, so a positional disagreement is real); and each is one moment of a still-animating screen, so compare settled poses or regions known to be at rest.

What does not change

The port keeps running verify-screen over all 16 screens every iteration. A consistency check is still worth having — it is total, it is cheap, and it is what catches a divergence the RE agent introduces on their side. It is simply not a grade, and this file will stop quoting it as one.

P5 groundwork — the focus record, checked against a capture, 2026-08-29

P5 is the lowest unfinished milestone (P0P4 are gated above). This iteration did not implement navigation; it did the measurement P5 needs first, because the one thing P5 is built on — how a focused button is drawn — had three claims attached to it and none of them had been checked from this side.

The exporter already emits the focus record's second element

HANDOFF ask 3 answers with a 🔴: "what you are actually missing is the focus record's SECOND element"ptbtneff01.t32, a 42×46 glowing ring, declared before the bright label in ptbtn0Nf.rat.

That gap is in the renderer, not the exporter. export/screens/title/main_menu.json already carries both, in declaration order, under focus.elements, for all five buttons — ptbtneff01 then ptbtn0Nf, each with its own pivot, rest pose and keyframes. Nothing needs to change in crates/sylpheed-export for the ring. What is missing is that screen_view.gd draws only one sprite per focused button. Recording this so P5 does not re-open the exporter looking for it.

The ring's size checks out exactly: ptbtneff01.png is 42×46, as stated.

The (7,7) focus offset survives a refutation attempt, uniquely

Ask 3 states the focused sprite covers the base at 100.0 % of base-visible pixels "once aligned properly (the true offset is (7,7), and at the centre alignment it reads a misleading 7884 %)". P5 builds directly on this, so it was worth attacking.

Re-ran the RE agent's own metric on the exported PNGs — over every pixel where the base sprite is visible, the fraction where the focus sprite's alpha ≥ the base's — scanning the whole offset space, not just the stated answer:

alignment ptbtn01 ptbtn02 ptbtn03 ptbtn04 ptbtn05
(7,7) 100.00 % 100.00 % 100.00 % 100.00 % 100.00 %
geometric centre 80.58 % 79.20 % 79.58 % 79.45 % 79.45 %
pivot-to-pivot 80.58 % 86.59 % 87.40 % 79.45 % 84.58 %

The refutation fails, and more strongly than the original claim. Over a 15×14 offset scan, (7,7) is a unique isolated cell at 100 % on every one of the five buttons — every neighbouring offset, including (6,6) and (7,6), falls below 90 %. The centre and pivot alignments reproduce the 7884 % band the RE agent reported as misleading. A claim that survives a search of its whole parameter space is worth more than one checked at a single point, so this is recorded as strengthened, not merely unrefuted.

(7,7) is not a constant to apply — it is what the declared positions already say

An earlier version of this analysis had the port disagreeing with the capture by 6 px. That was my arithmetic error and it is worth writing down, because it is the mistake this format invites: I computed each element's top-left as pos - pivot, which gives base→focus deltas of (13,13) and a 29 % coverage — a confident wrong number.

pos is the top-left. screen_view.gd:121 is Rect2(pos - pivot*(s - 1), natural*s): the pivot is the anchor scale grows about and it cancels at 100 %, which is exactly the "can be got wrong invisibly" the comment there warns about. Getting it wrong invisibly is what happened.

With pos as the top-left, base focus is (542,162) - (535,155) = (7,7) directly, on four of the five buttons. So P5 draws each focus element at its own declared pos and needs no offset constant at all. Nothing to author.

The one real find: ptbtn04 is 1 px off the grid on the disc

The focus records sit on a clean 80 px pitch — 155, 235, 315, 395, 475. The base records do not: 162, 242, 322, 401, 482, i.e. spacings 80, 80, 79, 81. So ptbtn04's declared base→focus delta is (7,6), while the art itself aligns at (7,7) — the coverage scan puts ptbtn04 at 100 % on (7,7) and below 90 % on (7,6), the same as every other button.

This is 1 px of authoring jitter on the disc, not a decode error, and it has one consequence worth stating: do not derive the focus placement from the base by a constant. Draw the focus record at its own declared pos. A port that "simplified" this to base + (7,7) would put ptbtn04's focus art 1 px off, and would look right on the other four.

Verified against a capture, not against our other renderer

Diffing two oracle frames isolates what focus changes without any instrument in the path: live-main-menu.png vs live-main-menu-options-focused.png differ in one tight cluster of 6 338 px at x 506..702, y 398..445. ptbtn04 is the OPTIONS button, and the union of its focus record under the pos-as-top-left reading — ring ptbtneff01 at (500,396) 42×46 plus label ptbtn04f at (535,395) 172×56 — is x 500..706, y 395..451. Those agree on all four edges to within a few px of near-transparent sprite border.

Under the pos - pivot reading the same record predicts x 433..604, y 367..422, which matches nothing in the capture — and no button matches that cluster. The capture, not our renderer, is what settles it.

An instrument that failed its own control, and was therefore discarded

To locate the buttons independently I wrote a masked normalised-cross-correlation template matcher and ran it as PROTOCOL requires — through a control first: match each base sprite against the plain capture, where the declared position is known and the answer must be a (0,0) delta.

It returned deltas of (13,5), (6,19), (12,21), (16,22), (6,8) at NCC 0.0960.206 — noise, with three of five pinned to the ±22 search boundary. The control fails, so the instrument is dead rather than tuneable, and none of its output is used above. The button art is dark, low-contrast and further crushed by the capture's γ ≈ 1.49 ramp, which is the likely cause; a matcher for this corpus would have to work on gradients rather than luminance. Filed so the next iteration does not rebuild the same broken tool.

What P5 still needs, and has not got

  • Initial focus is not stable across boots (Q5: 2× TUTORIAL, 2× NEW GAME). That is a value to author, with a why naming Q5 — it is not written yet.
  • The ring's own animation is unread. ptbtneff01's two keyframes go rotation_deg 0 → 360 at t=120 with no second timed keyframe, i.e. a full turn. Whether it spins continuously while focused, or turns once and holds, is the group-loop question again — answered "groups hold" for build-in animations, but a 360° hold and a 0° hold are the same pose, so this group cannot be told apart by its rest pose. Not guessed; raised below.

P5 — navigation, 2026-08-29

The gate is "a human clicks through it", and the artifact is a walk that proves the wiring rather than the intent: up (which wraps 01→05), five down, Ⓐ into EXTRAS, down, Ⓑ back — landing on the main menu with focus restored to EXTRAS.

xvfb-run -a godot --path port -- --menu \
  --script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5

Ten PNGs, one per step, each taken after the screen it produced had settled. Contact sheet handed over as share id 1788002507-ef4468a0a33a.

The scripted walk goes through the input system, not around it

--script posts InputEventAction through Input.parse_input_event and lets it arrive at _unhandled_input exactly as a d-pad's press would. Calling MenuFlow.move()/accept()/cancel() directly would have been shorter and would have proved nothing: the thing most likely to be broken is the wiring between a press and the cursor, and a direct call is precisely the part that skips it. The same reasoning says the settle wait must be real — a shot taken before the screen stops moving photographs a fade and calls it a menu.

What is authored here, and what is derived

Split deliberately, because P5 is where the two are easiest to blur:

where why
the ORDER of the items derived — each screen file's buttons, filled by the exporter from the button-role elements sorted by resting Y it is on the disc
where an item goes authoredauthored/flow.json HANDOFF Q4 measured the destinations; they are not in the file
which item opens focused authored Q5 measured that it is not stable boot to boot
what Ⓑ does authored Q5, measured — except on the main menu, see below
⬅➡ do nothing authored, written as an explicit no-op so that "the game ignores it" and "we never wired it" are different lines of code

Four of the five main-menu destinations are goto: null with a blocked note. That is not an unknown: DIFFICULTY, the save-slot list, the lesson list and the settings menu were all measured, and they live in archives this export does not carry. blocked and none are kept apart so a later reader does not "discover" a gap that was a milestone boundary.

EXTRAS is the only main-menu destination inside GP_TITLE, and therefore the only Ⓐ-into-a-submenu this gate can actually walk.

The one navigation rule with nothing behind it

Ⓑ on the main menu → title. HANDOFF Q5 states it, and flow.json marks it authored — likely but UNPROVEN, because the title also self-returns after ~810 s idle and a single unrecorded observation cannot separate the two. The port implements it anyway — a menu with no way out is worse than a menu with a plausible one — and says in the file that it did. Asked of the Decoder this iteration; see BLOCKED.md.

Independent corroboration that the main menu is different from its submenu: the main menu's footer advertises only Ⓐ : OK, while EXTRAS' footer advertises Ⓑ : Back. That is on the disc, in ptmsg.png vs ptmsg2.png, and it is visible in both the port's render and the captures.

A press during a fade is dropped

Authored, and not measured. Nobody has watched what the game does with a button pressed mid-transition. Dropping invents less than queueing does: it cannot manufacture a press the game might have discarded. flow.json says so under navigation.input_during_transition.


--headless cannot draw, and the port hung instead of saying so, 2026-08-29

docs/port/PORT-MISSION.md and the loop prompt both name godot-headless as how this project runs unattended. It does not work, and the way it failed was the worst available shape.

Measured, not assumed. Under --headless Godot's dummy renderer never emits RenderingServer.frame_post_draw. Every capture path in boot.gd awaits it — --capture since P1, --film since P3, --shots as of this milestone — so all three blocked forever. Isolated by the difference between two runs:

godot --headless --path port --quit                                  # prints, exits 0
godot --headless --path port -- --screen=… --capture=…               # no output at all, killed at 40 s

The second produces zero bytes of output before it is killed, because Godot's stdout is block-buffered and never flushes. So the observable behaviour of an unattended headless capture was: silence, forever. In a loop, a job that waits reads as a job still working — this is the failure mode that costs a whole iteration and leaves nothing behind to say what happened.

Two changes, and deliberately not one:

  • --capture, --film and --shots refuse at startup under --headless, naming the flag and printing the xvfb-run line that does work. Refusing early rather than at the first frame means the run does not die halfway through a filmstrip with some frames written.
  • --script no longer waits for a drawn frame when it is not going to photograph one. Navigation is checkable where nothing draws, and that is worth keeping: godot --headless --path port -- --menu --script=… now walks the menus and exits 0 in about four seconds, which is a cheap regression check that needs no X server at all.

The Xvfb path is unchanged and is what produced the P5 artifact.


Refutation — the focus ring IS drawn rotated, and it is not at 0° in either capture

Attempted against the Decoder's 7eeae30 ("re(ui): the focus ring SPINS, the game draws it, and the leaf owns the f record"), point 2: that in the OPTIONS-focused capture the ring's bright head sits in a different angular position from the sprite's own, caught mid-spin. It survives, and the evidence is stronger than what was claimed.

Chosen for refutation because it is exactly what PROTOCOL says to aim at: a claim the port is about to build on, resting on an estimator (a brightest-region centroid) whose own control the Decoder reported as ±19.8°.

The test, and why it needs no absolute registration

live-main-menu.png has ptbtn01 focused; live-main-menu-options-focused.png has ptbtn04 focused. Both draw the same sprite, ptbtneff01.png — the export confirms the two focus records name the same file. So the two captures contain two instances of one 42×46 image, 240 px apart in design space, and the question "is it drawn rotated" becomes "are these two crops the same image at a different angle" — which needs no crop offset and no reference to our own renderer.

Method: sample each ring into a 360-bin angular luminance profile over the annulus band (r = 9…15 px, bilinear, 0.5 px radial step) and circularly cross-correlate. A rotation about the centre shifts that vector and changes nothing else.

The instrument was run through two controls before it was believed

control result
rotate a capture's own ring by a known 0/30/90/150/210/270/330° and recover it 0° error on all seven, peak corr 1.000
the same estimator on a ring-free 60×64 patch of the same capture peak corr 0.369 — it does not manufacture a match

The measurement

On one shared centre for all three images, so a centroid difference cannot masquerade as a rotation:

pair best shift peak corr corr at 0°
capture A vs capture B 134° 0.968 0.064
sprite (unrotated) vs capture A 76° 0.969 0.295
sprite (unrotated) vs capture B 210° 0.948 0.181

210 76 = 134: the three measurements are internally consistent, which nothing in the method forced them to be. Sweeping the centre by ±2 px moves the A-vs-B answer over 117…161° while the peak correlation stays 0.9+ across the middle of that range, so the magnitude is ~134° ± ~15° and the precision claim stops there.

Evidence sheet — sprite, capture A, capture B, each cropped at the declared 42×46+500+156 / +500+396 — handed over as share id 1788002507-afe1ad843789. The phase difference is obvious by eye; the numbers are here so it is not only obvious by eye.

The two things this settles for the port

  1. The game draws rotation_deg on an element the English boot path shows. This is a second, independent confirmation on a different screen and a different element from the ptloop sweeps, and it moves HANDOFF ask 4 (should the port draw rotation) off "changes nothing at rest" — it changes the main menu's focus marker, in every frame.

  2. 0° is not a pose the running game shows. screen_view.gd currently draws the ring at its rest pose, which is rotation_deg 0, and both captures put it at 76° and 210°. So the port's focus marker is known to be wrong, not suspected — and the comment in screen_view.gd now says which two numbers it is wrong against.

Registration, as a by-product

The ring's annulus centroid lands at (32.94, 36.63) and (33.30, 38.90) in windows whose design-space prediction under a zero crop offset is (33.0, 37.0). Within ~0.4 px on the better-thresholded of the two. That corroborates ORACLE-CAPTURES.md's "1279×675, top-left aligned" directly, on a feature nobody chose for the purpose.

⚠️ Do not read the earlier P5-groundwork note "button text bands land at design y + 23" as a crop offset — it is an offset within the button sprite, and the two were nearly confused here.

What the port did NOT do about it

It did not start spinning the ring. The period is a guess with two unknowns and both belong to the Decoder:

  • the keyframes are t=120, rot 0 then an untimed rot 360. Under HANDOFF Q1's replicated reading ("+36 is the time the NEXT pose is reached") that is one revolution in 120 units = 2.0 s — but this port's pose_at implements the other reading, and switching it is a change to every screen's animation timing, not a P5 change;
  • "groups hold" (settled 2026-08-28) predicts the ring stops at 360° = 0°. Both captures show it elsewhere. That is either a spin that loops, or two captures both taken inside the first two seconds of focus. The port cannot tell those apart, and a wrong answer here is a visible continuous rotation on whichever button the player is sitting on.

Filed in BLOCKED.md and asked over the message channel. What settles it is two frames of one focused button a known time apart.


P5 end to end — and the title does not say PRESS Ⓐ, 2026-08-29

The gate walk above starts on a screen. This is the whole thing, unattended, in one run — the sequence PORT-MISSION names as the objective:

xvfb-run -a godot --path port -- --boot --play \
  --script=accept,down,down,down,down,accept,cancel,cancel --shots=/tmp/e2e
screen publisher_logo …           settles at t=235 (3.917 s)
  -> developer_logos at 4.70 s
  -> video ADV at 8.60 s
     video ended at 151.91 s
  -> title at 151.91 s
boot sequence complete after 156.30 s, holding on title
  menu on title
script[1] accept   (A) -> main_menu
…
script[6] accept   (EXTRAS) -> extras
script[7] cancel   (B) -> main_menu   focus restored to ptbtn05
script[8] cancel   (B) -> title
script complete after 166.76 s on title

Publisher wordmark → developer logos → ADV → title → Ⓐ → main menu → navigate → Ⓐ → EXTRAS → Ⓑ (focus restored) → Ⓑ → title. Contact sheet shared.

Two smaller things this run found, both fixed here:

  • the boot step's why in authored/flow.json still said "nothing takes the title's place until P5 gives it somewhere to go". P5 has. Rewritten to say what is actually true — --boot still stops on the title, and --play hands the held title over; the stop is not a bug and the handover is not another boot step.
  • an empty focus printed as a line that trailed off, which reads like a value went missing rather than like there is none. The title is a screen with no buttons that still takes Ⓐ, so it prints (none -- this screen has no focusable item).

Also confirmed on the way: entering a submenu directly (--menu=extras) and pressing Ⓑ enters the parent at its authored initial focus, not at a restored one — there is no history to restore, and MenuFlow.cancel only claims a restored focus when the stack agrees about where it is going.

🔴 The port's title does not tell the player to press Ⓐ

Found by running the objective end to end, which is the only thing that would have found it: the boot's last step is title (build 4), and build 4 has no PRESS Ⓐ BUTTON plate. P5 has now made Ⓐ the only way off that screen.

This is not a guess about the art. Both states are captured off the running game and they differ by exactly that plate:

capture
title without the plate title-builds/live-title-build4-no-plate.png
title with the plate title-builds/live-title-press-a.png

And the plate is already exported — press_start, GP_TITLE build 2 (HANDOFF Q2), sitting in export/screens/title/ unused by anything.

This is P3's gate, not P5's, and P5 is what exposed it. Recording rather than fixing, for two reasons:

  1. Which state an idle post-boot title shows — build 4 alone, build 4 with the plate over it, or build 4 then the plate after a delay — is behavioural, and the port has no oracle for a sequence. The game demonstrably has both states; nothing here says which one follows the intro movie. That is the Decoder's.
  2. Showing it would mean drawing two builds at once, which this port has never done — every mode loads exactly one screen. That is a real change to ScreenView, not a line in flow.json, and it should not be smuggled in under a navigation milestone on the strength of "it looks more right".

Filed in BLOCKED.md. Not blocking: P5's gate is Ⓐ into a submenu and Ⓑ back, and both work.

P6 — menu audio, 2026-08-29

The disc's menu sound reaches Godot as Ogg Vorbis: three cues and one music bed. Nothing in port/ has heard of XMA, sound.pak or Static.slb, and nothing in it reassembles anything — sylpheed_formats::media does that and the exporter converts what it hands back.

The cue offsets moved OUT of the exporter, into authored/

The previous iteration left crates/sylpheed-export/src/audio.rs holding the three Static.slb offsets as a Rust const CUES. That is wrong under MISSION §3 and the fix is the first thing this iteration did.

Those offsets are measured, not decoded. Static.slb has no RIFF, no seek chunk and no container: it is a packed run of whole 2048-byte XMA1 packets, and a wave is defined only by (offset, packet_count). Both numbers came from the running game — Canary with --xma_param_probe=true prints a stream's packet count and first 32 bytes when it is played, and searching those bytes in the bank gives the offset (HANDOFF Q8).

A measured value compiled into the exporter is a measurement wearing the costume of a decoded field. It reads as though the exporter derived it from the disc; nobody deletes it when the real answer lands, because nobody can see that there is anything to delete. So the table is authored/audio.json se.*, each row carrying its own why, and the exporter holds no cue table at all.

crate::video::MOVIES stays a const in the exporter, and the contrast is the point: Q9 decoded that mapping off the movie manifest on the disc. Same shape, different provenance, different home.

name_match is a field, and its absence means something

Q8 names SE_UI_CURSOR for the move cue by name match against the authors' own identifiers — a plausible guess, not the measurement. For Ⓐ, Q8 is explicit that the wave was not separated between SE_UI_DECIDE and SE_UI_SUB_WIN_OPN, so no name is claimed at all.

name_match therefore travels beside every cue in authored/audio.json and in manifest.json, and an absent one means nobody claimed a name — never that the binding is unknown. The binding is the measured part. Collapsing the two would turn "we did not separate two candidates" into "we do not know what this sound is", which is a different and much weaker statement than the one the RE agent actually made.

The BGM is NOT a choice, and this port spent an iteration believing it was

The first draft of authored/audio.json picked BGM_001, wrote a careful why explaining that the choice was arbitrary, and was wrong.

docs/port/BLOCKED.md carried the row that caused it: "not on the disc … the port is choosing a track, and that choice is authored." The menu's music is BGM_103, and it is in HANDOFF at 9ca1eb5 — the exact commit that page says it was reconciled against. So this was not staleness. The row was wrong when it was written.

What HANDOFF says is a negative with a bound, and the bound is the entire content of it:

the tables cannot say — SOUNDS, FILES and the bank headers name no screen. GamePart_Title's phase handler sub_821C5580 carries li r5, 1103 into a sound call; cue 1103 is BGM_103; and BGM_103.slb's two declared waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe saw decoding at the main menu. Static code, disc census and runtime all agree. "The port does not have to choose a track."

The failure is worth naming precisely, because "read HANDOFF more carefully" is not the lesson — BLOCKED.md's own staleness check passed, twice, and would pass again. A negative summarised without its reach reads as a bigger negative than it is. "The tables cannot say" became "it is not on the disc", and one word of scope was the whole answer. A row in BLOCKED.md must quote the reach.

It also cost a second thing worth recording: the port would have shipped a menu playing the wrong music with a confident why beside it saying the choice was deliberate. That is exactly the shape of error this project's vocabulary exists to prevent, produced by the machinery meant to prevent it.

The bank name carries .slb, and that is how the mistake surfaced

BGM_001 is not in sound.pak. BGM_001.slb is — media::read_sound_bank looks up name_hash(name) against the TOC, and the TOC hashes the file name. Static.slb worked from the first run only because the RE finding happens to write it with its extension.

So the wrong track never played: the export failed loudly with "BGM_001: not present in sound.pak". That is luck, not design — had the draft picked a name that happened to resolve, nothing would have complained. The why in authored/audio.json now records both the correct name and why the short form fails.

export_bgm now distinguishes the two cases it was conflating. A bank that is not in this disc's sound.pak is a missing asset: the manifest takes a warning and everything else still exports. Any other failure — a short read, a malformed bank — still stops the run, because a partly-read bank produces a file that plays.

The two stems are summed. That part is not a choice

Q10 also measured that a bank's sub-waves are two stems of one performance, played together — sample-synchronous, equal duration, on all 32 banks. Concatenating them is explicitly wrong.

Emitting them as two files would be wrong for a second, independent reason: MODDING rule 1 is one logical asset, one file, and handing a modder two stems to line up by hand is precisely the reassembly the exporter exists to have already done. amix=normalize=0 sums at unity rather than halving, because halving is a mix decision nobody made — and because a sum can clip, the peak is measured and reported rather than silently corrected.

The loop seam is ugly on purpose

No loop-point field has been identified. loop: "restart" replays from sample 0, so a listener hears the track's own fade-out and its trailing silence before the music comes back.

Trimming to the fade would sound better and would be worse. It would invent a loop point, and an invented one is indistinguishable from a decoded one a month later — which is the failure mode this whole project is organised against. The seam stays audible until a loop point is measured or a capture of the real menu looping settles it.

When a cue fires — two rules measured, one authored

  • Move fires on a press that actually moves the cursor. MenuFlow.move() already returned whether it did, which is why left/right stay silent by construction rather than by a rule written twice (Q5: ⬅➡ do nothing, and Q8: they play nothing).
  • Ⓐ and Ⓑ fire when the press does something, and not when nothing is bound. 🟡 This half is authored and NOT measured — nobody has watched the game take a dead press. Silence invents less: a sound the game does not make is a wrong fact you can hear, while a missing one is a gap. blocked counts as doing something, because those destinations were measured off the running game and are missing from this export, not from the game.
  • The bed starts when the menu becomes live and carries across submenus. play_bed is idempotent, because music that restarts every time you press Ⓑ is the kind of wrong that reads as "the audio works".

--audio= records the Master bus, because neither container has a sound card

docs/port/AUDIO-VERIFICATION.md §2. An AudioEffectRecord on the Master bus captures the mixed output from inside a headless run with no device at all, and that is the only thing that closes the loop the file opens: comparing an exported Ogg against the disc proves the asset is right and says nothing about whether the engine ever reached it.

The run prints AudioServer.get_driver_name() beside the file it wrote, because "recorded under a dummy driver" is a weaker claim than "heard" and the write-up has to be able to say which one it is making.

The WAV is saved in _exit_tree rather than beside each quit(). There are eight of those, and the one that would get missed is an error path — exactly the run whose audio somebody wants to look at.

check now refuses silence and clipping

sylpheed-export check gained an audio pass, and two of its rules are content checks rather than schema checks. That is deliberate. Silence is the audio failure that looks like success — a file of the right duration, the right channel count and the right size, full of zeroes — and it passes every structural check there is. Clipping is the other one, and the BGM can produce it because it is a sum at unity gain. The exporter measures both at export time; check refuses a tree whose peak is ≤ 90 dBFS or ≥ 0 dBFS.

Neither is a judgement about whether the audio is the right audio. Nothing in that binary can know that, and BLOCKED.md says which parts are still authored guesses.

A bug worth naming: the temp name ate the file extension

run_ffmpeg wrote to .back.ogg.partial — the temp-name-then-rename discipline this project uses everywhere, and which AUDIO-VERIFICATION.md records as already having caused a confident wrong number once.

ffmpeg picks its muxer from the output filename, so that is not a slightly uglier temp name; it is a hard failure before a byte is written: "Unable to choose an output format for '.back.ogg.partial'". video.rs already had the right shape (.ADV.partial.ogv) and this function was written from scratch without looking at it. The extension goes last.

Refutation — the three Q8 cue durations, checked end to end

The claim: HANDOFF Q8 publishes three cue lengths — move 0.533 s (8 192 B, 4 packets), back 0.344 s (4 096 B, 2), confirm 1.016 s (12 288 B, 6). P6 is built directly on top of these, which by PROTOCOL's own rule makes them the right thing to attack: refutation is cheapest where the other agent is most confident, and most valuable where the port is about to build.

Why they looked attackable. The three do not share a rate. Seconds per packet is 0.133, 0.172 and 0.169 — the move cue is 22 % off the other two. If a packet were a fixed span of audio, at most one of these numbers could be right.

Why that is not a refutation. An XMA1 packet is 2 048 bytes of bitstream, not a fixed span: it carries a variable number of 512-sample frames. At 48 kHz a frame is 10.667 ms, and the three durations come to 50.0, 32.3 and 95.3 frames — near-integers, which is what a variable-frames-per-packet encoding looks like and is not what an arithmetic slip looks like.

The measurement. The exporter reads (offset, packet_count) through media::se_wave_riff, decodes, and ffprobes the finished Ogg:

cue Q8 claims exported file measures
move 0.533 s 0.533 s
back 0.344 s 0.344 s
confirm 1.016 s 1.016 s

Verdict: survives, exactly, at every published digit. Recorded as a survival rather than a pass, because that is what PROTOCOL asks for — a claim that has survived an attempt is stronger than one nobody challenged, and the corpus should say which it is.

⚠️ Reach, stated so nobody over-reads it. This is not independent of Q8: the durations were derived from the same packet counts the exporter feeds in, so what it confirms is that reading those (offset, packets) through sylpheed_formats::media yields streams of exactly the claimed length — i.e. that the transcription into authored/audio.json and the assembly path are right. It does not confirm that these three waves are the sounds the game plays on those three events; that is Q8's own measurement, taken by playing them, and this port has no oracle to re-take it with.

The attempt did find something, just not here: see the BGM section above, where the port's own BLOCKED.md row failed the same kind of check.

The BGM bank has three sub-waves and HANDOFF says it has two

media::sound_bank_riffs("BGM_103.slb") returns three. HANDOFF Q10's census says a music bank is "exactly two waves of identical duration (32/32 banks on the disc)" — and that census is itself a correction, of an earlier reading that called BGM_001 three sub-waves and was refuted with "the 10 KB is the bank header".

The third comes from sylpheed-formats/src/slb.rs:380, to_xma_riffs: when a bank has a leading headerless packet region ahead of its first RIFF, that region is emitted as a sub-wave. It exists because the voice path needs it — VOICE_D_453 decoded to 0.14 s without it. docs/re/REFUTED.md already records the same region as what makes BGM_106BGM_109 "break the two-wave rule".

The port sums all three and says so in the manifest. That is not the appealing answer — dropping sub-wave 0 would give a file matching the census, and it would have been one line. It is the correct one: which bytes belong together is the question sylpheed_formats::media owns, MISSION §2 names re-deriving it here as the single easiest thing in this project to get subtly wrong, and "the decoder returned something the corpus does not predict" is a finding to report, not a number to quietly adjust. Adjusting it would also have destroyed the evidence: a corrected export looks exactly like a correct one.

So the export ships the decoders' answer, the manifest carries a warning naming the contradiction, BLOCKED.md has the row, and the Decoder has the pointer. Until it comes back, the menu plays a sum of three things where the census predicts two, and every one of those places says so.

Clipping — and a comment of mine that argued for the thing that clipped

The BGM came out at +1.8 dBFS. The comment above the code that produced it said amix=normalize=0 sums at unity "because halving is a mix decision nobody made".

That was wrong in both halves. Unity summing is a decision, and it is the one that clips. And 1/n is not a taste call: it is the smallest constant that makes an n-input sum of unity-scale signals provably clip-free, which is precisely the reasoning video.rs already carried for its 0.4142-normalised 5.1 downmix — in this same repository, written by this same port, and not looked at. It preserves the stems' relative balance exactly, which is the only thing about the sum that Q10 settles.

It is written as an explicit volume= rather than left to amix's normalize=1 default, so the coefficient appears in the manifest's command line. A default is a decision nobody made and it can move under an ffmpeg upgrade — the same argument MISSION §6 makes about the downmix matrix.

The confirm cue is a different case and is not "fixed". It lands at +0.18 dBFS, and it is a single wave off the disc with no arithmetic of ours in it: the disc masters it near full scale and a lossy decode of a near-full-scale signal overshoots by a fraction of a dB. Attenuating it would mean altering a game asset to make one of our own numbers smaller. So check bounds the two kinds differently — a bgm peak ≥ 0 dBFS is refused outright, because it is our sum; an se is refused only above +1.0 dBFS.

🟡 That +1.0 is a judgement and not a measurement, and it is the weakest number in P6. Nobody has measured the overshoot distribution across a corpus of cues. If a cue ever trips it, the right response is that measurement, not a looser bound.

P6 gate — the audio is in the mix, and a null control says which part

No container here has a sound card, so "P6 works" cannot be answered by listening. docs/port/AUDIO-VERIFICATION.md splits the question into three, and these are the two that need no device.

1. The exported files, measured off the finished assets

se    back      -> audio/se/back.ogg    (0.344 s, peak  -5.7 dBFS)
se    confirm   -> audio/se/confirm.ogg (1.016 s, peak  +0.2 dBFS)
se    move      -> audio/se/move.ogg    (0.533 s, peak  -1.4 dBFS)
bgm   main_menu -> audio/bgm/main_menu.ogg
                  (87.744 s, peak -7.7 dBFS, bank BGM_103.slb, 3 sub-waves)

sylpheed-export check export passes: 16 screens validate, and every audio entry carries a peak and a duration inside its bounds. The three cue durations match HANDOFF Q8 at every published digit — see the refutation record above.

2. The engine, recorded off the Master bus

godot --path port -- --menu --script=down,down,accept,cancel --audio=…/p6.wav
  → recorded 6.037 s of Master bus (driver Dummy)
    peak 0.0 dBFS, RMS 21.1 dBFS

Non-silent is not the claim. A WAV of the right duration full of the bed would look exactly like this, and the cues could be missing entirely. So the cue was isolated with a null control: the same scripted walk with ⬅ in place of ⬇. Left/right are measured no-ops (Q5) and fire nothing, so the two runs differ by exactly two move cues and nothing else — same screens, same transitions, the same Ⓐ and Ⓑ cues in both, the same bed.

RMS
walk with two ⬇ presses 21.9 dBFS
walk with two ⬅ presses (null) 22.1 dBFS
difference 34.6 dBFS

The difference is not spread over the run. It is one burst beginning at t = 1.10 s and lasting 0.55 s — two overlapping 0.533 s move cues — with 22 of 237 windows above 70 dBFS and silence everywhere else, including across the Ⓐ and Ⓑ presses, which cancel because both runs make them. That is the cue reaching the bus, separated from the music that was playing over it.

The control that proved nothing, kept because it nearly passed

The first attempt paired --script=down,down against --script=left,left. The difference was bit-identical zero, which reads as "the cues never reached the bus" and would have been reported as a bug.

It was neither. Both runs recorded 1.115 s while the first press lands at ~1.17 s: the control ended before the event it was controlling for. A null result from an instrument that was not running is not a null result — PROTOCOL's "run your own instrument through a control" applies to the control too.

What this does NOT establish

  • That it sounds right. Everything above is correspondence and separation, not judgement. A ten-second human listen still answers something no measurement here does.
  • That the bed is at a sane level against the cues. 🔴 The Master bus peaks at 0.0 dBFS in the four-step run — the confirm cue is +0.2 dBFS on its own, so any music under it puts the mix on the ceiling. Per-file levels are the disc's and are fine; the runtime mix has no headroom. The port has not set a bus balance, because nothing measures one and an invented balance is the same class of mistake as an invented loop point. Recorded here rather than fixed quietly.
  • That "Dummy driver" means heard. It does not, and the run prints the driver name so a write-up cannot forget to say so.

One bug, in two dialects, both about a temp filename

The temp-name-then-rename discipline this project uses everywhere broke twice in this milestone, in two different tools, for the same underlying reason: tools dispatch on the extension, so a temp name must preserve it.

  • run_ffmpeg wrote .back.ogg.partial"Unable to choose an output format", a hard failure before a byte was written.
  • boot.gd wrote p6.wav.partsave_to_wav appends .wav when the path does not end in it, producing p6.wav.part.wav; the rename then failed to find its source, its return value was not checked, and the run printed a success line naming a file that did not exist.

The second is the more dangerous shape, and it is the one this project has already warned itself about: a confident line of output pointing at nothing. The rename's return is now checked and the failure is loud.

P3, reopened — the boot title was missing the PRESS Ⓐ plate, 2026-08-29

P3 passed its gate with a boot that ended on build 4 alone. BLOCKED.md carried that as 🔴 from the start: both states were captured, so the art was never the question — the sequence was, and it is behavioural, so the port had no oracle for it.

It is answered. docs/re/title-plate-delay-measured.md (auto/no-disc-and-menu-captures at fb536df, not on main at the time of writing) measures two independent boots: the title presents without the plate, and the plate arrives 2.13 s later, the two runs agreeing to 6 ms.

Two builds at once, as two ScreenViews

ScreenView draws one screen. The obvious change was to teach it about a subordinate overlay screen; the change made was to put a second ScreenView in the same SubViewport, after the first.

That is what "two builds at once" actually is. Each build has its own timeline, its own textures and its own hold — the plate's group runs independently of the title's, which is the entire content of the finding — and Node2D siblings already paint in tree order. The alternative would have put an if overlay in every method that walks elements, and would have expressed the same information less directly. The export's paint_order still means what it always meant: an ordering within a build.

The delay is timed from where build 4 stops animating

Not from where the title first appears. This is the finding rather than a detail: measured from first-draw the two oracle runs differ by 0.48 s, because the build-in itself ran 1.64 s and 2.13 s and the emulator's frame pacing during an animation is not the game's clock. Measured from settle they differ by 6 ms.

So _boot_done — the moment the sequencer already had for "this screen has reached its hold" — is the landmark, and the overlay is due after_settle_seconds later. A number taken from the wrong instant here looks exactly like a measurement.

The overlay is attached to the BOOT STEP, not to the title screen

What was measured is the boot title. Whether the plate is there when the title is reached again — by Ⓑ from the main menu, or after the attract movie — is not measured, and hanging the overlay on the screen would quietly claim that it is. So it lives on the boot step in authored/flow.json, and _drop_overlay takes it away with the screen it belongs to. BLOCKED.md carries the gap.

Refutation — the RE agent's instruction contradicts the RE agent's measurement

The claim under test, quoted from the finding's "What the port should author": draw build 4, "when build 4 has settled, wait 2.13 s, composite build 2 over it".

It does not reproduce the measurement it came from, and the gap is 3.97 s. Build 2 is not a static plate: it has a group, and this port plays groups. press_start has one element, ptbtn00, and its fade_argb reads

t=214  0x00ffffff   pos (383, 560)      invisible
t=236  0x00ffffff   pos (383, 550)      still invisible, having slid 10 px up
t=238  0xffffffff                       full alpha
t=244  0xffffffff                       holds
  —    0x00ffffff                       the exit, untimed

At the measured 60 units/s that is 3.967 s from the group's start to full alpha. Compose the instruction with the group and the plate is first visible at settle + 2.13 + 3.97 = settle + 6.10 s. What was measured — the glyph counter leaving its no-plate value of 154 — is the plate becoming visible at settle + 2.13 s.

Neither obvious reconciliation works:

reading plate visible at measured
both groups start together 3.97 s (build 4 settles at 4.350 s) — i.e. 0.38 s before settle settle + 2.13 s
build 2's group starts at settle settle + 3.97 s settle + 2.13 s
build 2's group starts at settle + 2.13 s (the instruction) settle + 6.10 s settle + 2.13 s

To land on the measurement, build 2's group has to start 2.51 s after build 4's, which is not a landmark of anything.

Verdict: the instruction is refuted as written; the measurement is untouched. The measurement is an observation of the running game and this port has no standing to doubt it. What is refuted is the step that turns it into an authoring rule, and that step is an interpretation.

So the port ships the instruction, not its own arithmetic, prints the discrepancy on every boot, and files the row. This is the same call as the BGM sub-waves and for the same reason: reconciling two of the RE agent's numbers is a decoding question, and a port that quietly picks the one that looks right destroys the evidence — a corrected boot looks exactly like a correct one.

The first thing to check is about the instrument rather than the game: is "title settled", the glyph counter first reading 154, the same instant as the port's last-element settle (t=261, 4.350 s into the group)? If that landmark is earlier, the gap closes with nothing else moving.

Refuting the port's own claim: things in this export DO pulse

BLOCKED.md has carried this since P2, under the port's own raised question about whether groups loop:

no element's alpha reverses direction anywhere in this export, so nothing pulses, which removes the obvious reason to expect a loop without disproving one.

ptbtn00 reverses. 0x000xff0x00, in the table above, in the export, the whole time. The claim was never checked against press_start; it was checked against the screens P2 happened to be animating. The RE agent has now measured the running game pulsing this exact element at a mean 2.24 s.

So the reason to expect a loop is back — and the port still does not draw one, because no reading of this group produces 2.24 s: the whole group is 268 units = 4.47 s, and from its first keyframe 54 units = 0.90 s. The plate is drawn arriving and then holding at its settle (t=238, alpha 0xff), which is what every other screen does and what the static oracle capture live-title-press-a.png shows. Which instant a repeat restarts from is filed, not guessed.

--boot --capture= — one frame instead of six hundred

The boot had no artifact of its own except --film, a PNG every 0.25 s for the whole 156 s run, to answer one question: is the plate on top of the title at the end. --capture was a --screen-only flag taken in _ready, which for a boot run is 150 s too early. It is now deferred to the end of the sequence when --boot is given.

P3 gate — the boot ends on two builds

godot --path port -- --boot --capture=…/p3-plate.png
  → boot sequence complete after 155.86 s, holding on title
    overlay press_start due at 157.99 s (+2.13 s after settle)
    overlay press_start raised at 158.00 s, 1 element(s), settles at t=238
    ⚠ plate raised at settle+2.13 s but its own group reaches full alpha 3.97 s
      later, so it is first VISIBLE at settle+6.10 s -- the measurement is
      settle+2.13 s.
    boot ends on title + press_start at 161.99 s
    drew 16: ptbase2, ptloop01, …, ptcopyright
    overlay press_start at t = 261.00 units, drew 1: ptbtn00

The PNG shows the title logo with PRESS Ⓐ BUTTON under it — build 4 and build 2 in one frame, which this port had never drawn.

Two things the run made obvious and that are now fixed:

  • The capture reported only the base build's elements. The first composited capture printed drew 16 and no mention of the plate, which reads as though the overlay had not drawn at all. The overlay gets its own line; folding its elements into the first list would have reported a screen that does not exist.
  • --screen=<a> --overlay=<b> raises the same composite immediately, by the same code path, with no delay. It exists because the only other way to see two builds was a 156 s boot of which 137 s is the intro movie — and under Xvfb's software Theora decode that is several minutes to answer "is the plate on top of the title". It applies no delay: the delay is a measurement and lives in authored/flow.json. The boot-mode narration is suppressed there, because a log line that describes a sequence it is not running is worse than no log line.

P5 — the focus ring spins, 2026-08-29

The ring was drawn at 0° and the file said so: "THIS IS KNOWN TO BE WRONG, and is drawn anyway because the right answer is a guess." What was missing was the period, and it is now measured — docs/re/focus-ring-spin-measured.md (auto/no-disc-and-menu-captures at 4fa3099): a continuous spin, from eight evenly spaced autocorrelation peaks over nine revolutions, with no angle estimated anywhere — both angle estimators failed their own controls and were not used.

The period comes off the disc; the RE agent supplied only that it repeats

ptbtneff01 declares two keyframes that differ in nothing but rotation_deg, 0 → 360, the first timed at t = 120 and the second untimed. The port turns once per 120 units. Nothing is authored: the number is on the disc, and what the measurement adds is that the turn repeats rather than stopping at 360 = 0, which "groups hold" could not distinguish because those are the same pose.

ScreenView.spin_period_units is the rule, and it is structural and narrow: exactly two keyframes, differing only in rotation, by a full 360, first timed and second untimed. Disc-wide check over this export: 16 of 212 elements match, and all 16 are focus ringsptbtneff01 on the five main-menu buttons and ptbtneff02 on the three EXTRAS buttons, in both locales, every one declaring t = 120. Zero false positives.

That check is the point rather than a formality. The measurement was taken on one button of one screen; a rule that also caught something else would be extrapolating it to elements nobody watched.

⚠️ It is a rule about shape, not a decoded field. Nothing on the disc says "this loops". The day a loop flag is decoded, this goes.

Verified on the port's own render, with the RE agent's own control

Captures at --time= 2.0 … 4.0 s on the settled main menu, ptbtn01 focused:

t=2.0 vs t=4.0 (one full period apart), whole frame 0.0000 / 255 — bit-identical
t=2.5, 3.0, 3.5 against t=2.0, inside the ring's box 3.60, 3.71, 3.58 / 255
sum of box luminance across eight phases spread 0.027 % of the mean

The last row is deliberately the RE agent's own observable: they separated rotation from a brightness pulse by showing total annulus brightness is conserved while per-bin brightness moves. The port's render conserves it to 0.027 % (theirs was 0.4 % over 16 s, with capture noise in it). A filmstrip of the four quarter-period phases shows the bright head at top, right, bottom, left.

Two things it does not settle

  • Direction. The port turns 0° → +360°, the sign the disc declares. No signed angle was ever measured — the estimator that would have given one failed its control and was not used.
  • Phase across a focus change. The port drives the ring off the screen clock, so moving the cursor does not restart the turn. The alternative — the record's group restarting when the record is instantiated — is the stronger claim, and the oracle run held focus on one button throughout, so nothing separates them. Two frames straddling a focus change would.

P3, corrected — the plate needs no authored delay at all, 2026-08-29

Last iteration the port refuted the RE agent's authoring instruction ("when build 4 has settled, wait 2.13 s, composite build 2") with arithmetic off the disc, shipped the instruction anyway rather than pick between two of their numbers, and printed the discrepancy on every boot.

The refutation held, and the answer that came back is better than either option the port offered: author nothing. 5b0a6e6.

The premise that failed was the port's, and it will bite again

🔴 rest.t is not when a screen settles. It is the last hold keyframe before the exit.

Checked here rather than taken on trust. title's ptlogo1:

t=26  (-116,-7) 150%  a=0x00      the pre-roll
t=42  (179,186) 101%  a=0xe0      it has arrived
t=251 (184,193) 100%  a=0xff      5 px and 31 alpha steps later, 3.5 s on

It stops moving at t=42 and then creeps for 209 units. rest.t = 251 is the end of that creep, not the arrival. The title's visible build-in is over at t = 118, where pteff01, pteff02 and ptlogoall_eff finish together.

Every reconciliation the port computed last iteration was wrong by exactly that error: reading rest.t put build 4's arrival at 4.350 s instead of 1.967 s, and the "2.51 s, which is not a landmark of anything" that looked so damning is (4.350 1.967) + 0.13 — the error itself, wearing a decimal point.

One clock, and the interval is declared

units
build 4's last build-in ramp t = 118
ptbtn00 reaches a = 255 t = 238
difference 120 units = 2.000 s

Measured: 2.138 s and 2.132 s. The 6.7 % is presentation rate — 120 units in 2.135 s is 56.2 units/s, the emulator running 28.1 fps against a nominal 30, and the corpus had independently measured the idle title at 28.5 fps before these runs.

So authored/flow.json carries "clock": "shared" and no delay, boot.gd raises the overlay when the step's screen loads rather than at its settle, and overlay.time_units = view.time_units — assigned, not accumulated, because two independently advanced clocks drift by a frame here and there and the whole content of the finding is that 120 units is a fixed interval on one timeline.

⚠️ The general hazard, stated by the RE agent and worth repeating where the port will read it: discount a wall-clock number off that oracle by ~6 %. It is Canary's presentation rate baked into whatever it measures. A port at a true 30 Hz that authored 2.13 s would be visibly late.

Refutation — two of the RE agent's numbers for the same 120 units disagree by 2 %

Both findings measure the same declared quantity: 120 keyframe units of wall clock, during a static hold, in Xenia Canary.

implied presentation
plate: settle → plate, two runs 2.138, 2.132 s → mean 2.135 s 28.10 fps
ring: one revolution, seven spacings 2.18 2.16 2.18 2.16 2.16 2.20 2.20 → mean 2.177 s 27.56 fps
disagreement 0.042 s = 1.97 %

That is seven times the plate finding's own run-to-run agreement of 6 ms, and it lands on the argument that finding uses to justify trusting itself: "the build-in is where frames are dropped; the static hold is not. A model in which the game's own timing varied would have to move both." Two static-hold measurements are exactly what should agree under that model.

A second, smaller arithmetic slip in the same place: the ring page reconciles 2.177 s against a band of "27.628.8 fps", saying the measurement "sits at the top of that band". It does not sit in it. 60 rendered frames at 27.6 fps is 2.1739 s; the mean needs 27.56 fps and the two slowest spacings (2.20 s) need 27.27 fps. Four of the seven spacings are above the band's top.

Verdict: the containment claim is refuted; the spin, the period and the reconciliation are untouched. Either the presentation rate genuinely differed between the two sessions — which the plate page's own corroboration argues against for static holds — or the ring's revolution is not exactly 120 units. The corpus should say which, because they are the same claim measured twice.

🟢 Nothing in the port moves either way. spin_period_units uses the declared 120 units at 60 units/s = 2.000 s of port time, which is the true-30 Hz value both readings agree the disc means. This is a corpus consistency problem, not a port one — which is why it is filed rather than worked around.

The corrected boot ended one build too early, and the capture showed it

Moving the plate onto the shared clock also moved the boot's exit, and the first capture taken afterwards was visibly darker than the one before it. The cause is pteff00, the title's black fade quad: it ramps 0xff0000000x00000000 over t=16…261, so at t=243 — where the run was quitting, overlay.settle_time() after the overlay was raised — the frame is still ~7 % black.

The plate arrives at t=238; build 4 is not finished until t=261. The boot now ends at the later of the two, and says which in the log:

  -> title at 145.79 s
  overlay press_start raised at 145.79 s, 1 element(s), settles at t=238
  boot ends at 150.14 s, once both builds have arrived (t=261)

Worth recording because of how it presented: nothing failed, no warning was printed, and the only symptom was a frame slightly darker than the previous run's. A gate artifact that silently drifts is the failure mode this project keeps meeting — and it was caught only because there was a previous capture to compare against.

P7 — the new-game intro, 2026-08-29

S00A.wmv has been in export/video/ since P4 (MISSION §6 put both movies in scope at once). What P7 needed was for something to play it and for the run to end somewhere defined.

The port skips two measured screens, and says so on screen

The real chain is NEW GAMEDIFFICULTYSELECT DATA → Ⓐ on a save slot → ~4.5 s → S00A (HANDOFF Q4 measured the screens, Q9 decoded the movie and then measured its onset off the running game at 0.961.000 with a strictly monotone playhead over 25 consecutive 0.5 s samples).

DIFFICULTY and SELECT DATA are measured destinations that are not GP_TITLE builds, so no screen file exists to go to. The port therefore jumps from NEW GAME to the one thing in that chain it has.

That is a gap, not a sequence, and the whole design here is about not letting it read as one:

  • MenuFlow.accept returns a new kind, video, rather than folding this into blocked. The caller has to announce the skip, and a distinct kind is what forces it to.
  • The runtime prints it every time: (NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie. Neither screen is in this export.
  • authored/flow.json carries skipped_chain as data, so the names of what is missing live beside the decision rather than inside a GDScript string.

A port that quietly jumped from NEW GAME to the intro would be showing a sequence the game does not have, with nothing on screen saying so. That is the exact failure this project keeps meeting from the other direction.

What happens after the movie is authored, and had to be

The game goes into mission 1. Gameplay is out of scope (PORT-MISSION §7), so "returns to a defined state" is a decision, and P7's gate says as much. The port returns to the title: the boot's own end state, so a run that finishes the intro lands somewhere a player can start again from. Nothing measured says the game does this, and after_video.kind is "authored".

The 4.5 s gap is left empty on purpose

Q9 measures the movie starting ~4.5 s after Ⓐ on the save slot. What is on screen for those 4.5 s was never observed — the run that would have shown it hit the documented sub_823070B0 cache crash after SELECT DATA.

GP_TITLE does carry a loading screen (below), and 4.5 s is about the right shape for one. That is precisely why it is in BLOCKED.md and not in flow.json: a plausible filler that nobody watched is the kind of thing that is indistinguishable from a measurement a month later.

A script timeout that would have failed every movie

--script's per-step timeout is 20 s, to stop an unattended run waiting forever on a screen that never settles. S00A is 93.9 s, so the first scripted new-game run would have been killed at step 1 and reported as "never settled".

Raising the constant would have been wrong in the other direction: a movie stuck at frame 0 would then hang the job, and a job that waits is worse than a job that fails, because it does not look like a failure.

So the test is liveness, not duration: while get_stream_position() advances, the deadline moves with it; a stalled movie still trips the same 20 s.

Found while looking: GP_TITLE's four unnamed builds are LOADING screens

build_00, build_01, build_12, build_15 have never had names. Every element in all four is pgloading_*pgloading_processing.png, pgloading_circle1, pgloading_delta, pgloading_ring — and LOADING is one of the three screen names the RE agent read out of the title part's state function.

Two variants: 0/1 carry 7 elements, 12/15 carry 10.

They are not renamed here. The archive's own pairing (adjacent for 2/3, +3 for 4…9 and for 10/13, 11/14) suggests 0 is 1's twin and 12 is 15's, but which member of each pair is which locale is an inference, and a name is exactly the kind of thing that stops being questioned once written. Handed to the RE agent, who can answer it from a capture in one look. BLOCKED.md has the row.

⚠️ And one of them is a second casualty of the rest.t problem. pgloading_eff00.prm on entries 12/15 is a full-screen black quad whose group runs 0xff000000 at t=38 → 0xff000000 at t=48 → 0x00000000 untimed: black, held, then clear. Its rest.t is 38, where it is fully opaque. A port that draws that screen at its declared rest draws a black rectangle over the entire loading screen. The title's case only dimmed a frame; this one hides everything. Filed with the settle_time() row it belongs to.

Refutation — attempted on the fade-quad census; it survives, with a caveat

The claim (HANDOFF, on transitions): "in GP_TITLE exactly the six screen builds carry it while the six overlays do not", where "it" is the full-screen black .prm quad whose keyframe group is the transition.

The test, over the whole export: count builds carrying a full-screen primitive with black in its keyframes.

16 builds exported; 12 carry one.
Of the 12 `is_build` bundles (excluding the 4 authored splashes): 8 carry, 4 do not.
  carry:  title, title_jp, main_menu, main_menu_jp, extras, extras_jp,
          build_12, build_15
  do not: press_start, press_start_jp, build_00, build_01

8 and 4, not 6 and 6. But the two extras are build_12 / build_15, and their quad is a different shape:

transition quad (pteff00.prm, title) loading quad (pgloading_eff00.prm)
0xff000000 t=16 0xff000000 t=38
0x00000000 t=261 0xff000000 t=48
0xff000000 untimed 0x00000000 untimed
shape black → clear → black black → held → clear

The transition quad returns to black on exit; the loading quad does not. Read strictly — the quad whose group is the transition — the claim holds.

Verdict: survives. The refinement is worth recording anyway, because the naive test over-counts by two and somebody will run the naive test. There are two kinds of full-screen black .prm in GP_TITLE, and only one of them is a transition.

P7 gate

godot --path port -- --menu --script=accept --audio=…/p7.wav
  menu on main_menu, focus ptbtn01
script[1] accept
  (NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
               Neither screen is in this export.
  -> video S00A at 1.18 s (/work/export/video/S00A.ogv)
     video ended at 94.93 s
     -> title (authored: authored)
  menu on title, focus (none -- this screen has no focusable item)
script complete after 99.28 s on title
recorded 98.453 s of Master bus (driver Dummy)

The movie ran 93.75 s against a declared 93.9 s, the run ended on the title, and the Master bus recorded 98.453 s: pcm_s16le, 44.1 kHz stereo, RMS 22.2 dBFS.

What this does not show, stated because it would be easy to imply otherwise: the recording contains the menu bed and the movie together, and this run did not separate them. So it establishes that the engine reached an output for 98 s of a run whose middle 94 s was a movie — not that S00A's own audio track is in the mix. Separating them wants the P6 null-control method (a paired run that differs only in the movie), and that is not done here.

🔴 Peak 0.0 dBFS again. The same runtime-headroom problem P6 filed: per-file levels are the disc's and are fine, the Master bus has no headroom, and the port has set no bus balance because nothing measures one.

One more file read while it was being written

ls reported the recording as 3 702 828 B; ffprobe on the finished file reports 17 367 084 B / 98.452608 s — a factor of 4.7. ffprobe is right and the ls caught it mid-flight.

AUDIO-VERIFICATION.md opens by naming this failure and the port has had the temp-name-then-rename discipline since P6, which is what makes it worth writing down rather than shrugging off: the discipline protects a reader who opens the path, and it does not protect a reader who stats it at the wrong moment. Size on disk is not a measurement of a file somebody else is still writing. Ask the decoder, not the directory entry.

Modding — rule 4 was never implemented, 2026-08-29

docs/port/MODDING.md is explicit that modding is "a design constraint on the exporter today — not a milestone to add later", and its rule 4 is base-and- overrides: a mod replaces a file by shadowing its path, so a modder edits nothing under the derived tree and re-exporting is always safe.

Nothing read data/mods/ at all. The directory has existed since the monorepo merge with a .gitkeep in it and no code path anywhere — exporter or runtime — that looked at it. Eight milestones shipped past that.

One resolver, and every read goes through it

ExportTree.resolve(rel) returns the mod tree's copy when one exists and the derived tree's otherwise. read_json, texture, video and MenuAudio all call it, so a mod can replace a screen's JSON, a sprite, a cue, the music bed or a movie — every asset kind the port reads.

MenuAudio was reading tree.root.path_join(...) directly and had to be changed. Left alone it would have made audio the one asset kind a mod could not touch, for no reason a modder could have guessed — which is the failure mode rule 4 exists to prevent.

There is deliberately no manifest of what a mod contains and no registration step: the path is the registration, which is the whole of the rule.

⚠️ One tree, not a stack. Several mods layering over each other needs a load order, and a load order needs a rule nobody has asked for. Said out loud in data/mods/README.md rather than answered.

A modded run must not look like an unmodded one

Every shadowed file is printed the first time it is read:

mod: sprites/title/main_menu/ptbtn01.png  <-  /work/data/mods/sprites/…/ptbtn01.png

MODDING says "did I break it?" is answered by disabling a mod. That is a fine last resort and a poor only resort, so the log names the replacement instead.

The first version of this got it wrong in an instructive way: it printed a summary in _ready, before a single asset had been read, and so always said (nothing shadowed yet). A report structurally incapable of reporting anything is worse than no report, because it looks like an answer. It now announces each shadow at the moment it happens.

Gate

A synthetic 203×43 magenta PNG — nothing disc-derived — dropped at data/mods/sprites/title/main_menu/ptbtn01.png:

pixels changed between the two renders 8 501 of 921 600 (0.92 %)
bounding box of the change x 542…744, y 162…204 — 203×43, the sprite's own size
sylpheed-export check export afterwards 16 screens still validate

The changed region is exactly the sprite and nothing else moved.

data/mods/ was not gitignored, and that is a hole in a hard rule

"Never commit game assets" has been enforced on export/ and data/base/ since P0. But a mod is usually an edited game asset, and data/mods/ was fully tracked — so the one directory a user is invited to put modified sprites in was the one directory git would happily take them from.

.gitignore now excludes everything under it except the README.

The naming split is not mine to resolve

MODDING.md describes the tree as data/base/; PORT-MISSION.md §3, the exporter, ExportTree and .gitignore all say export/. Both are mission files, and PROTOCOL is clear that only the human changes a mission, so this is raised rather than picked. .gitignore has ignored both names on purpose since P0.

It matters here for one concrete reason: MODDING's layout has base/ and mods/ as siblings, and today they are not — the tree is export/ at the repo root while mods are data/mods/. The resolver takes SYLPHEED_MODS or defaults to data/mods/, which is what exists; if the tree is ever renamed to data/base/ the sibling rule becomes natural and that default can go.

Refutation — the paint-order key, and the reach of its tie-break

The claim (HANDOFF Q3): paint order is "a u16 layer key at +0x0A, decoded", with the tie-break filed 🟡 as "eight candidates refuted; costs one element's blend on one screen".

First pass: 2 of 16 screens did not match a stable sort by layer key — both loading screens, build_12 and build_15.

That was my test, not the claim. pgloading_eff00.prm carries no layer key at alllayer: null, layer_source: "none": it is a primitive with no sprite header, and the exporter's implied-name fallback produces nothing either. My sort put a keyless element first; the decoders put it last.

Completing the rule as "stable sort by layer key, elements with no key last" gives 16 of 16. And last is right: pgloading_eff00 is the full-screen black quad, and HANDOFF's own sentence is that the fade quad paints last.

Verdict: survives, with the rule completed. Worth recording because the published statement does not say where a keyless element goes, and there is at least one in the archive.

🟡 But the tie-break's reach looks understated. Census over this export:

elements sharing a layer key with another element: 105, across 12 of 16 screens

HANDOFF characterises the cost as "one element's blend on one screen". 105 elements on 12 screens is a much larger surface than that. Most of those ties are probably invisible — two elements that share a key and never overlap cannot show a difference — but probably is doing the work in that sentence, and nothing has measured which. The port is unaffected either way: it draws ui_layout::derived_paint_order verbatim and derives no order of its own.

Correction — the runtime "clipping" I flagged 🔴 twice was overstated

P6 and P7 both filed 🔴 "the runtime mix has no headroom" on the strength of a peak reading of 0.0 dBFS off the Master bus. Measured properly:

samples at full scale of total longest clamped run
P6 walk (5.944 s) 43 0.0082 % 10 samples — 0.23 ms
P7 new-game run (98.453 s) 24 0.00028 % 11 samples — 0.25 ms

That is not a headroom defect. It is the disc's own confirm cue, mastered near full scale (+0.18 dBFS after a lossy decode), touching the ceiling for a quarter of a millisecond on a transient — and possibly only in the recording's 16-bit conversion, since Godot mixes in float and AudioEffectRecord saves s16.

Nothing is changed, and that is the point. Attenuating the mix to buy headroom would be an unmeasured decision about level — the same class of thing this port refused for the BGM loop point and the stem balance. Refusing it there and taking it here would be inconsistent, and it would trade an inaudible 0.25 ms clamp for an audible change nobody measured.

A peak reading is not a clipping measurement. One sample at 0 dBFS and two seconds of square wave give the same number, and I reported the first as though it were the second — twice, in red, in two milestones' write-ups.

The P1 regression harness had been broken since the monorepo merge, 2026-08-29

tools/port/verify-screen is the P1 gate's regression detector: Godot's drawing of a screen against sylpheed-cli screen render of the same build. It had not been run since P1, across four milestones that changed the renderer — rotation, the focus record, the spinning ring, two builds composited at once.

It could not have been run. It resolves its reference binary to a path that build-reference-cli stopped being able to produce. That script greps crates/sylpheed-export/Cargo.toml for

sylpheed-formats = { git = "…Syplheed-Reborn.git", rev = "…" }

and the monorepo merge (65cefa7) replaced that line with { path = "../sylpheed-formats" }. The grep returns nothing, the script exits 1, and the binary left at reference-cli/sylpheed-cli is whatever predated the merge — here, three hours older than the sources and built from a revision nothing in the tree points at any more.

Running the diff against it would have compared the port to a decoder from another era and called the result a regression check. DECISIONS.md already carries "The reference renderer was stale for three diff runs" from P2. This would have been the fourth, and the mechanism was different: not a forgotten rebuild, but a build step that could no longer succeed and a consumer that only checked whether the file existed.

The fix is a deletion, not a repair

The revision-keying solved a two-repo problem: /reborn's target/ was a live mount of the other agent's checkout and moved mid-run, so a pixel disagreement against it had a free variable in it. The monorepo removed that problem by construction — the exporter, the reference and the port now read one decoder, the working tree's. So verify-screen builds sylpheed-cli from the workspace. SYLPHEED_CLI still overrides for anyone who wants to pin one deliberately.

The baseline, all 16 screens

build_00/01        max 3    over3 0       OK
press_start(_jp)   max 1    over3 0       OK
title              max 6    over3 790     DIFFERS
main_menu(_jp)     max 4    over3 0       DIFFERS
extras(_jp)        max 3    over3 0       OK
publisher_logo(_r) max 1-2  over3 0       OK
developer_logos(_r) max 2   over3 0       OK
title_jp           max 155  over3 20498   DIFFERS
build_12/15        max 0    over3 0       OK

No new drift. Four milestones of renderer change and the only screen with a substantial disagreement is title_jp — which is the same one P1 recorded and left open: ptlogo_eff2 is the single drawn element in the whole export at a scale that is not a whole multiple of 100 % (125 %), and the two renderers pick different source texels there. ui_layout::blit samples at the destination pixel's top-left corner, a GPU at its centre. The port has still not changed to match, because matching would mean reproducing a half-pixel bias on purpose to make a number smaller. Only an oracle capture settles it.

title's 790 pixels at ≤ 6/255 are the same class, one texel wide, on the logo's scaled edges. main_menu and main_menu_jp say DIFFERS on a max of 4 with zero pixels over the bar — a couple of pixels differing in a single channel.

max alone could not tell 2 pixels from 25 000

The script reported only the largest difference anywhere in the frame, so main_menu (two pixels) and title_jp (2.8 % of the frame) produced the same verdict. It now also reports how many pixels are over the bar.

The bar itself is not raised. Tuning a threshold until things match is the failure the script's own header warns about; adding a second number is information, not a loosened bound. ⚠️ The count is thresholded on greyscale luma while max is a per-channel maximum, so they are not two views of one measurement — a per-channel check counts 957 on title where the luma count says 790.

What this harness cannot see, stated because the OK rows look reassuring

It renders --pose=rest. That is deliberate — it holds both renderers to the same declared pose so the test is port vs reference and not rest vs timeline — but it means none of this iteration's or the last four's visible work is under test: not the spinning focus ring, not the plate composited over the title, not any timeline behaviour, not audio. Sixteen OK rows are a statement about the resting composite and nothing else.

And it remains what its header says: a consistency check between two renderers that share their assumptions. Both have been wrong together three times — pteff05, scale-0, rest() — and each time only a capture caught it.

Refutation — "builds 0/1 and 10/11 are the loading screen" is false in the index space this export uses

The claim, from the RE agent 2026-08-29, answering the port's ask to name GP_TITLE's unnamed bundles: "builds 0/1 and 10/11 are the loading screen, decoded from their own pgloading_* element names."

In this export, entries 10 and 11 are the splash screens, and it is not close:

entry elements
10 palogo_eff0, palogo_sqex, palogo_sqex_eff
11 palogo_eff0, palogo_gamearts, palogo_seta, palogo_anima
12 / 15 pgloading_eff00, pgloading_loop1, pgloading_str

Entry 10 is the SQUARE ENIX wordmark and 11 the developer logos — which the same agent identified, in the answer to the port's ask 1, as "entries 10/13 are the SQUARE ENIX publisher wordmark, the first thing the boot shows".

Verdict: the finding is almost certainly right and the index space is wrong. Over the twelve bundles is_build accepts — entries 0,1,2,3,4,5,6,7,8,9,12,15 — ordinals 10 and 11 are entries 12 and 15, which are exactly the two dressed loading variants. So "0/1 and 10/11" is the is_build ordinal, and this export addresses by pak entry index.

Why this is worth a section rather than a shrug. authored/screen_names.json is keyed by entry index, and the exporter's own comment says why: "keyed by ENTRY, not by the enumeration ordinal — widening the enumeration to reach the splash renumbers the ordinals, and a name that moves when the rule changes is not a name." Someone reading that message and writing keys "10" and "11" would name the publisher wordmark and the developer logos as loading screens, and the export would validate, and the boot would still run.

Two enumerations of the same archive differ by exactly the four bundles the port had to add an allow-list to reach. That is the sharpest possible demonstration of why the exporter switched, and it has now nearly caused the error it switched to prevent. Reported; the names are still the RE agent's to give.

The intro's missing dialogue was an export gap, not a transcode bug, 2026-08-29

A human play-test heard music under the boot intro and no voices. The obvious reading is that the 5.1→stereo fold dropped the centre channel, and it is wrong.

ADV.wmv carries music and effects only. On this disc a cutscene's voice is a separate asset: one continuous XMA stream in sound.pak, bound to the movie by the manifest in tables.pak (ADVVOICETRACK = VOICE_ADV). Nothing was dropped — grep -rn voice crates/sylpheed-export/src/ returned nothing, because the exporter had never been asked for it. The transcode was correct the whole time, which is why every measurement on it passed.

That is worth stating plainly because the failure looked exactly like a codec bug, and docs/port/AUDIO-VERIFICATION.md is full of ways to measure a transcode against its source. Every one of them would have come back clean.

The binding is resolved, and must never be matched by name

audio::export_voice takes exactly one route: media::resolve_movie_voice_region(source, movie, VoiceLang::English), which walks movie → cue token (manifest) → sound id (registry) → a [start, end) byte region of the continuous stream. The cheap route — read VOICE_<movie>.slb — was not taken, and the reason is a measurement:

movie resolved region inside the bank named after it?
ADV 433 930 240…437 044 592 yes
S00A 452 798 464…455 499 120 yes
RT01A 437 044 592…437 345 648 no — it is inside VOICE_ADV.slb

⚠️ Name-matching is correct on exactly the two movies this port ships, and wrong on the radio cutscenes. It would have exported clean, verified clean against both in-scope movies, and returned the wrong recording the moment anybody widened the export. This is the failure mode MISSION §2 names — one playable thing is not one archive entry — in its most convincing disguise: the spot-checks a person would actually run are the ones it passes.

Three choices, and why none is a guess

  • One file per movie, per MODDING rule 1, and the region's chunks are summed — see the correction below, because the first version of this paragraph said the opposite and was wrong.
  • Mono, folded from the stream's own declared channel count, probed with ffprobe rather than assumed. This is not pedantry: pan silently ignores a channel the input does not have — measured this iteration on the 5.1 fold below, where FLC/FRC/SL/SR vanished with no warning at all — so a stereo matrix applied to a mono voice track is not an error, it is a 6 dB attenuation that nothing reports. A track that is already mono is passed through untouched.
  • No sync offset, and no length clamp. The voice plays from the video's first frame, so nothing is authored. The decoded length is recorded in the manifest beside the movie's own length rather than trimmed to it: the voice has no shared container to disagree with, so a length mismatch is the only symptom a resolution error would ever show, and clamping would delete it. That decision is the reason the error below was caught in the same hour it was made.

Correction, within the hour — the chunks are stems, and I had concatenated them

The first version of export_voice joined the region's chunks end to end and produced 359.201 s of voice for a 137.437 s movie, and 255.460 s for a 93.779 s one. Both ratios sit near 3, and both regions decode to 3 chunks.

The manifest said so on the first run, because the length was recorded against the movie's instead of being clamped to it. A clamp — which is what sylpheed-viewer does, and what media's own doc comment invites with "trimmed by the caller's length clamp" — would have produced a file of exactly the right duration containing the wrong audio, and every check in docs/port/AUDIO-VERIFICATION.md would have passed it.

Decoding each chunk and timing it (crates/sylpheed-export/examples/voice_chunks.rs):

movie movie length chunk 0 chunk 1 chunk 2
ADV 137.437 s 84.553 137.324 137.324
S00A 93.779 s 68.072 93.694 93.694
RT01A 0.009 34.034

Chunks 1 and 2 are equal to six decimals and each span the whole movie. That is HANDOFF Q10's decoded shape — two stems of one performance, played together; do not concatenate — showing up on a second asset kind. They are summed at 1/n, exactly as export_bgm sums a music bank.

⚠️ Chunk 0 is dropped and its status is open. Its duration matches nothing: 84.6 s under a 137 s movie, 9 ms under RT01A. docs/re/REFUTED.md records to_xma_riffs's hybrid branch emitting a leading headerless packet region ahead of the real RIFF waves, and docs/port/BLOCKED.md already carries that as an open row against BGM_103, where media returns three sub-waves against a census of two. This is the same signature on an independent asset kind — good corroboration, not proof, and the port is not entitled to close it. So the selection rule is written in terms of the measurement (keep the longest duration and everything tying with it), and every dropped chunk is named in the manifest with its length.

This is the media-assembly trap MISSION §2 names, and it caught me: I wrote a doc comment asserting concatenation, gave the reason, and had it wrong. What saved it was refusing to clamp — the one decision in the first version that was made for the right reason.

What a None means

A movie whose region does not resolve is genuinely unvoiced — the honest answer for most hokyu_* resupply cutscenes — and gets a manifest warning, not a substitute. The corpus already paid for the alternative: resolving unbound movies through a shared demo line played the wrong recording.

This is decoded, not authored, so it runs outside the authored/audio.json block in main.rs. Nothing new goes in authored/; there is nothing here we decided.

Refutation, of my own exporter — MISSION §6 pins a downmix matrix, and the exporter ships a different one

The claim under test is the port's, not another agent's, and it has been in video.rs since P4: that the 5.1 fold is normalised by 1/(1 + √½ + √½) = 0.4142 because "the unnormalised form was measured too and clips: peak 0.0 dBFS."

That sentence rests on a peak reading. docs/port/BLOCKED.md records this port withdrawing a 🔴 runtime-clipping flag on precisely the grounds that a peak reading is not a clipping measurement — one sample at full scale and two seconds of square wave give the same number. So the justification for deviating from a matrix a human pinned was produced by an instrument this port has already declared unfit for the question.

Measured properly, over the whole of both movies

Decoded to 32-bit float so nothing is pre-clamped, then counted: samples at or over full scale, how many exceed it by more than 1 dB, and the longest consecutive run.

peak RMS ≥ full scale > +1 dB longest run
ADV, MISSION §6 matrix +4.26 dBFS 14.55 4 406 / 13 187 900 1 874 16 samples (0.333 ms)
ADV, exporter's matrix 3.39 dBFS 22.21 0 0
S00A, MISSION §6 matrix 1.34 dBFS 18.73 0 0
S00A, exporter's matrix 8.99 dBFS 26.39 0 0

The claim survives, and the reasoning behind it does not. The pinned matrix genuinely overloads ADV: not one stray sample but 4 406 of them, 1 874 more than a full dB over, wanting 4.26 dB more headroom than the container has. That is a different animal from the 43 samples and 0.25 ms transient I withdrew a flag over, and the number that separates them is the magnitude, not the count.

But the same table refutes the scope of the fix. S00A never clips under the pinned matrix — it peaks at 1.34 dBFS. The exporter attenuates it by 7.65 dB to solve a problem it does not have, because 0.4142 is derived from a theoretical worst case (every channel correlated at full scale at once) that neither movie comes near.

Control, before believing any of it

The pinned matrix names FLC, FRC, SL and SR, and a 5.1 source has none of them. ffmpeg neither errors nor warns — measured at -loglevel warning, the output was empty. So the literal string was decoded alongside its three-term 5.1 reduction (FL = 1.0·FL + 0.707·FC + 0.707·BL) and the two outputs compared: bit-identical, 52 751 600 bytes. The reduction is what runs, and it is the matrix §6 intends. That silence is itself the trap the mono fold above guards against.

Not changed, and deliberately so

MISSION §6 is a human decision of 2026-08-29, and the level of a mix is exactly the kind of thing §6 reserves — "adjust it deliberately, as a commit". Three options, and choosing between them is not mine:

  1. Keep the pin. ADV clamps on 4 406 samples. Rejected on the measurement.
  2. Keep the exporter's 0.4142. Preserves the two movies' relative loudness exactly, costs 7.65 dB, and is safe by construction for any movie a modder drops in.
  3. One measured constant, 1/1.6339 = 0.612. The smallest single scalar under which no in-scope movie clamps: +3.39 dB over today, still one constant so relative loudness is untouched. Tuned to two files, but the exporter's own check refuses any export whose peak reaches 0 dBFS, so a third movie that needed more headroom would fail loudly rather than clamp quietly.

Per-file normalisation is not on that list: it would put ADV 4.26 dB below S00A and change how two cutscenes sit against each other and against the menu bed, which is an aesthetic decision with nothing measured behind it.

What changes today is only that the deviation is visible: video.rs now cites MISSION §6 by name and says it departs from it, and the export carries a manifest warning with these numbers. Before this, a reader of the manifest could not tell that a pinned human decision had been overridden at all — the command line was recorded faithfully, and recording the command you ran does not disclose that it is not the command you were given.

The voice reaches the output, and a null control says so quantitatively

+ voice ADV in the log proves only that play_voice found a stream and called play(). Whether the audio arrives at the Master bus is a different question, and docs/port/AUDIO-VERIFICATION.md §2 exists because it is.

The control needed no test-only code: MODDING rule 4 already shadows any exported asset by path, so 140 s of silence dropped at data/mods/audio/voice/ADV.ogg mutes the dialogue and changes nothing else. Two --boot --skip-at=25 --audio=… runs, then astats over the same 14 s of movie:

peak RMS
ADV.ogv's own audio (the bed) 6.239 24.941
the exported voice alone 7.614 27.965
run with the voice muted 6.251 25.126
run with the voice playing 5.415 22.913

The muted run reproduces the bed to 0.01 dB peak / 0.19 dB RMS, which is what makes the other row worth reading. And the mixed run is not merely louder: two incoherent sources at 24.941 and 27.965 dBFS predict a sum at 23.184, and the run measures 22.9130.27 dB out. The voice is in the mix, at the level its own file says it should be.

⚠️ Under the Dummy driver. Per AUDIO-VERIFICATION, "recorded under a dummy driver" is a weaker claim than "heard", and no measurement here says the recording is the right dialogue for this cutscene — only that the file the exporter resolved is the one reaching the output at the expected level. The two runs are also not sample-aligned (they differ by 1.7 s of wall clock), which is why the RMS trough column is omitted: it moved by 40 dB between runs on window placement alone, and peak and RMS are the two numbers that survive that.

does skip the intro in this build, so the play-test's report is not this bug

--skip-at=25 on a --boot run: video skipped at 25.02 s, video ended at 25.02 s, title at 25.02 s. The press goes through Input.parse_input_event and arrives at _unhandled_input exactly as a pad's would, so the wiring from press to skip is live. What that does not cover is a real key event from a focused window, which is the difference between this run and the human's — and, separately, whether the game permits skipping an attract movie at all is HANDOFF Q9 and still 🟡. If the answer is no, this path is deleted rather than debugged.

Refutation of my own two-stem reading — and it had already been adopted elsewhere

Two hours after writing that a voice region's equal-length chunks are "HANDOFF Q10's decoded two-stem shape", the Decoder asked me to decode the leading chunk — it has no XMA1 decoder in its container — and the decoder run refuted the claim I had made.

Equal duration was a shape match, and I carried Q10's music census across to voice on the strength of it. The content does not support it:

S00A chunk 2 digital silence — 4 497 300 samples, peak inf
ADV chunk 2 0.60 × chunk 1 (best-fit scalar), residual 26.8 dB below the target

About 95 % of ADV's second chunk is a 4.4 dB copy of the first. Two chunks of equal length, one silence and the other a scaled near-duplicate, are not two stems of one performance. ⚠️ The claim had already travelled — it is quoted in the Decoder's voice-region-leading-chunk.md — which is the failure PROTOCOL names: a wrong belief moving faster than its correction, through two documents that share a source.

What it cost, and what changed

Summing chunk 1 with silence at 1/n put S00A's dialogue 6.02 dB down for nothing: the exported file peaked at 16.2 dBFS against a source chunk peaking at 4.2. export_voice now drops a digitally silent chunk before the sum. That is arithmetic, not a content judgement — a silent input contributes nothing to a mix and counting it in the normalisation is simply my error.

What ADV's near-duplicate chunk 2 is remains open and it is still summed. Whether the game plays both is a decoding question; 26.8 dB of residual is not nothing, and dropping a chunk because it correlates with another would be answering it.

The leading chunk, decoded — structure, and not one word about content

The Decoder's ask was "cutscene dialogue or mission dialogue". ADV region

  • 1392, 394 packets: 84.553 s, stereo, 48 kHz, peak 2.48 dBFS, RMS 24.80, with 6 silent gaps over 0.4 s below 50 dB totalling 45.3 s — 54 % silence, the same duty cycle as the two full-length chunks (54 %, 55 %). So it is speech-structured audio: not a header, not padding, not noise.

🔴 Which is as far as a measurement goes. Cutscene or mission is an identification and this agent has no ears and no oracle. Envelope cross-correlation against the full-length chunks peaks at 0.768 at the last lag in the search range, which is where a statistic lands when it has found nothing, and it is not evidence. The Decoder's 🟡 stands, and its own leading hypothesis — an in-mission VOICE_D_* line — is untouched by any of this. The byte-span test it already built settles it the moment those regions are enumerated; nobody has to listen.

Taken from the same message: bank_header_len, not riffs.len()

The Decoder's census warns that eight bank-header regions also yield three chunks, so the chunk count cannot say which structure you are in. This exporter never used the count — it selects on decoded duration, which is why it already handles both cases: RT01A's 10 300 B leading chunk decodes to 9 ms and falls out on its own. But a duration tie is an observation and bank_header_len is decoded, so the rule switches the day c1f3608 reaches main. sylpheed-formats is a path dependency and merging another agent's topic branch is not the port's to do.

The mono fold I warned about, in the comment directly above the code that did it

export_voice's first version folded to mono by averaging every declared channel, and the doc comment above it said, in as many words, that "pan silently ignores a channel the input does not have — so a stereo matrix applied to a mono voice track is not an error, it is a 6 dB attenuation that nothing reports."

It then did exactly that. Per-channel astats on both voice streams:

channel 1 channel 2
ADV chunk 1 peak +0.000 dBFS peak inf
S00A chunk 1 peak 4.207 dBFS peak inf

The voice is a mono recording carried in a nominally stereo stream, and averaging it with silence cost 5.94 dB — which is most of why S00A's exported dialogue sat at 16.2 dBFS against a source chunk peaking at 4.2 (the other 6.02 dB was summing a silent chunk, corrected in the same iteration).

Checking the declared channel count is not checking the content, and only the content is the fold. live_channels now measures which channels carry signal and averages only those. sylpheed-viewer's pan=mono|c0=c0 reaches the right answer here for a reason it does not state; this reaches it for a stated one, and would still be right if a stream ever did carry two live channels.

Worth recording as a pattern rather than a bug: three defects this iteration were all the same shape — a silent chunk in a sum, a silent channel in a fold, and a pan matrix naming channels that do not exist. Each is an input that contributes nothing being counted in a divisor, and none of them is visible in anything but a level.

The leading chunk is the TAIL of the full one — measured, and it is why the region over-covers

The Decoder settled by byte-span analysis that a voice region's leading chunk is the movie's own dialogue, 17 of 17 — killing its own standing hypothesis that it was an in-mission VOICE_D_* line — and asked whether dropping it is therefore a truncation. It has no XMA1 decoder; this container does.

Envelope cross-correlation, sliding with overhang allowed at both ends and normalised over the overlap only. ⚠️ This corrects an earlier number of mine: a first pass scored 0.768 and I called it nothing, correctly — that search only tried lags where the shorter chunk fitted wholly inside the longer one, and it peaked on the boundary of its own range.

best r at lag overlap
ADV chunk 0 → chunk 1 0.998 +52.8 s 84.5 s
S00A chunk 0 → chunk 1 0.932 +25.6 s 68.0 s
control — ADV chunk 0 against itself 1.000 0.0 s
control — ADV chunk 0 against S00A chunk 1 0.289 28.2 s

Both lags put chunk 0 flush against the end of chunk 1: 52.8 + 84.55 = 137.35 s against chunk 1's 137.324, and 25.6 + 68.07 = 93.67 against 93.694.

Confirmed in the sample domain — lag refined to ±1 sample on the loudest second, then a scalar best-fit over the whole overlap: ADV +52.8000 s, gain 0.833, residual 16.70 dB below the target; S00A +25.6320 s, gain 0.365, residual 23.15 dB. 9899.5 % of the energy is a scaled copy: the same material at a different gain, not bit-identical, which is what a lossy decode at two gains should look like.

So dropping chunk 0 removes a duplicate, and is not a truncation — the exporter's existing behaviour is right for a better reason than the one it gave. 🟡 The manifest note has NOT been rewritten to say so. The structural claim — that the region over-covers because it re-presents its own tail, and that this accounts for the whole 2.6× — is the Decoder's to write down; this page reports the measurement and says which is which. The note stays hedged until its page carries the conclusion, and the hedge is true either way.

⚠️ The 504 464 B constant was deliberately not converted. The Decoder found the region anchor sitting that far after the true predecessor trailer on all 17 and pointedly declined to call it missing dialogue. Converting it needs a byte↔time mapping, and the numbers above are the reason there isn't one: chunk 1 is 1 118 268 B and chunk 2 is 1 171 516 B for the same 137.324 s, so bytes per second is not constant even inside a single region. Any figure in seconds off that constant would be invented.

Third reading of a voice region, and this one is decoded: three presentations of one take

export_voice has now read the same bytes three ways in one session, and each reading was ended by a measurement rather than by an argument:

  1. Concatenate the chunks — 359 s of dialogue for a 137 s movie.
  2. Sum them as HANDOFF Q10's two stems — refuted here: S00A's second full-length chunk is digital silence, ADV's is 0.60 × the first with 26.8 dB of residual.
  3. Keep one stream. This one is decoded, and not by me.

The Decoder settled the shape disc-wide without a decoder, by counting stream starts inside every inter-descriptor span: 258 spans hold one stream, 28 hold three, and nothing holds two or any other number. The 95 movie-voice regions decompose 70 + 8 + 17, and the 8 are independently the same 8 its first census flagged. So a region carries three presentations of one take — which is exactly 359 = 84.55 + 137.32 + 137.32, the first clipped by its crate's own 1.5 MB predecessor guard.

It also cross-checked my correlation by a route needing no decoder: if the leading chunk is the tail of a full stream, the whole leading stream should be one complete take, and ADV's 504 464 + 808 304 = 1 312 768 B at chunk 0's byte rate is 137.323 s against my measured 137.324. Two instruments, no shared assumption.

So summing was wrong for a third reason: a take plus a 0.60 × copy of itself is ~4 dB louder and coloured, not a mix of parts. The exporter keeps one stream and performs no arithmetic on it.

🟡 Which stream is a recommendation, not a decoded field. The selector is the highest byte rate among the equal-duration survivors, on the Decoder's advice. Nothing on the disc says which presentation the game plays, and on ADV this picks the quieter of the two — 8.3 dBFS against 0.0. That is in the manifest in those words so the choice is visible and reversible; it is the one part of this that a capture could still overturn.

check moves voice off the strict peak bound as a consequence. It sat with bgm because it was a sum this exporter produced; it is now a single wave off the disc, mastered near full scale — ADV's louder presentation measures +0.0003 dBFS at source — and refusing that would be refusing the disc's own mastering.

The 504 464 B constant: I refused the conversion, and refusing was right

The Decoder asked whether I would spend a decode converting its anchor offset to seconds, and I declined because bytes per second is not constant even inside one region. It has since found the stronger reason and withdrawn the ask: the constant is structural, not proportional — identical on all 17 regions despite their differing durations. A proportional prediction lands within 8 bytes on ADV, which is a coincidence, and is 4 305 B out on S00A. A seconds figure off that constant would have been invented, and it would have looked corroborated on the first movie anybody checked.

Why the disc stores three presentations at all is unanswered, by either of us.

The transcode cache had never hit, because the wipe ran first

video::transcode has carried a cache since P4. It writes a .cmd sidecar with the exact ffmpeg command, the source's byte count and its channel count, and skips the encode when all three still match. Its doc comment says why: "without it every re-export pays ~4 minutes to produce a byte-identical file, and an exporter nobody re-runs is worse than a cache."

It had never hit once. main.rs clears the output tree wholesale — and the remove_dir_all runs immediately before the check, deleting the sidecar and the output it stamps. The cache tested a file it had just erased.

This session ran the exporter six times and paid the full Theora encode every one of them, producing five byte-identical files. Roughly 48 minutes. Nothing reported it, and nothing could have: a cache is silent when it works and silent when it does not, and the only symptom is a wall-clock cost that looks like the job simply being slow.

⚠️ It is worth being specific about how this hid, because the ingredients are ordinary. The cache is correct. The wipe is correct. Each carries a doc comment explaining itself, and neither mentions the other. The defect exists only in their ordering, which is stated in neither, and the cost is invisible in every artefact the export produces — the tree is byte-identical either way.

The fix keeps the wholesale guarantee rather than trading it away

The obvious repair — stop wiping — would break what the wipe is for: a screen that stops being exported stops existing, rather than lingering as a stale file that still validates. So the wipe now spares exactly video/, and prune_videos deletes anything in it this run did not claim. Everything else is still cleared outright.

That is a cache, not a hand-edit, and the distinction matters against MISSION §3: nothing in export/ is authored, the sidecar is derived from the command the exporter itself computed, and any change to the command, the source size or the channel count re-encodes. A modder who edits an .ogv by hand gets it overwritten on the next export, exactly as before.

🟡 Not measured yet: that a cached run reproduces the same tree. The claim is structural — the skip is keyed on the whole command string — but "the second run produces the same bytes" is checkable and has not been checked.

settle_time() — the answer arrived, and it refutes my own 🔴 more than it confirms it

The Decoder took the port's top ask and measured the boot on a cold profile with no shader cache (auto/no-disc-and-menu-captures at 4bd4779, docs/re/boot-settle-times-measured.md). It confirms the principle I filed: the title's rest.t is 251 units = 4.183 s where its art is finished at about 2 s, so rest.t is not when a screen arrives.

But my row said more than that, and the extra part is wrong. It said "everything the boot sequencer paces off that landmark is therefore late", and named publisher_logo and developer_logos. So I measured the port the way the game was measured — visible span, not arrival-to-arrival — with --film at 4 fps and a per-frame greyscale mean:

port, visible span game, three cold boots
publisher wordmark 4.25 s 4.297 / 4.604 / 4.370 0.05 s under the lowest
developer logos 3.50 s 3.508 / 3.503 / 3.366 dead on
black hold between ≈0.25 s 0.2 0.3 s inside
title settled → plate 2.000 s (declared 120 units) 2.247 s inside, at ~28 fps presentation

The splashes are not late. They match. ⚠️ And the reason my earlier reading said otherwise is worth keeping: I had compared the port's transition timestamps — 4.68 s and 3.94 s, arrival to arrival — against the game's visible spans. Those differ by the exit ramp plus the black hold, about 0.6 s, which is the whole of the discrepancy I was about to chase. This corpus has been bitten by exactly this before, in the plate delay: "timed from where build 4 stops animating, not from where it first appears — measured the other way the two runs differ by 0.48 s against 6 ms."

So the port paces the boot correctly, and rest.t is a wrong landmark whose blast radius is much smaller than I claimed: on the screens the sequencer actually advances off, rest.t plus the 24-unit exit ramp lands where the game lands. What it still affects is _script_settled, which waits longer than it needs to before photographing — a slow test, not a wrong frame.

dwell_seconds stays null, and the question is now closed rather than open

authored/timing.json says of it: "If a capture ever times the real boot, this is where that number goes." A capture has now timed the real boot, and the answer is that nothing goes there — the disc's own keyframe groups reproduce the game's dwells to 0.05 s and 0.01 s. The field stays null for a measured reason instead of an absence of one.

Taken from the same page, and not taken

  • The 120-unit plate delay stands. The Decoder ran a refutation of it that failed instructively: its probe's title_static mark gave 3.203 s, which on a cold boot looks like a real effect. It was the instrument — the mark fires during the crossfade out of the attract movie, with the glyph count still 0. Re-measured from content: 2.247 s. The port changes nothing, and the declared 120 units is what it keeps.
  • 🔴 No Ⓐ→menu dwell is authored. It measured 3.763 s and contains a 1.53 s guest load stall — the third independent reproduction of that stall, this one on a cold cache, so it is not a warm-cache artefact. It is emulator time, not a game constant.
  • 🟡 Menu build-in 0.531 s and Ⓑ→title 0.482 s are not authored either, and that is the Decoder's own caveat rather than my caution: they rest on one run, where the plate delay and the load stall are each cross-checked against independent prior evidence. The port is within ~0.1 s of both with its existing 24-unit exit ramp, so authoring them would replace a disc-derived number with a provisional measured one and gain nothing measurable.

The voice presentation is now unambiguously the port's choice, and the recommendation behind it was withdrawn

The Decoder has withdrawn "highest byte rate": its sentence read "the highest-rate, highest-gain one is chunk 1", and those two criteria select different streamsADV chunk 1 is 1 118 268 B at 0.0 dBFS, chunk 2 is 1 171 516 B at 8.3. The rule named one and the parenthetical named the other. I implemented the rule faithfully and got the quieter presentation.

What the file can still say is decoded and does not adjudicate it: the fmt chunk is a 32-byte XMAWAVEFORMAT whose +0x20 is a declared PsuedoBytesPerSec — 8 142 and 8 530 on ADV's two, matching the computed rates to 0.02 % — but wEncodeOptions (0x10d6), channel count and channel mask are byte-identical across the presentations. Nothing in the header ranks them.

⚠️ One more observable, measured here and not in that page: the two presentations differ in channel layout. ADV chunk 1 is mono-in-stereo — channel 2 digitally silent — while chunk 2 is dual-mono, both channels identical at 8.318574. So they are not two encodes of one file differing only in rate.

Also recorded, because it cost the Decoder time: sylpheed-cli audio info is not to be trusted on these. Its "16 channels / 4310 Hz / 2-bit" is wBitsPerSample, wEncodeOptions and the channel fields read at the wrong offsets — its XMA1 reader is misaligned. That is a tool in this repository reporting confident nonsense, and it is the second time a renderer or reader of ours has been believed before it was checked.

Refutation of my dual-mono inference — the measurement stands, the generalisation does not

I argued that highest_rate had no case because ADV's higher-rate presentation is dual-mono while its louder one is mono-in-stereo, so the extra bytes buy a duplicated channel rather than fidelity. The Decoder tested that disc-wide, as a refutation attempt, and it fails.

Over the 28 three-stream cues, the stream-3 / stream-2 size ratio runs:

min median max sd within 15 % of 1.0
0.0778 1.2565 2.9163 0.5057 12 of 28

Declared rates scatter with them — S06A is 5 661 against 16 513 B/s. A 37× spread is not a duplicated channel.

The channel measurement itself stands: ADV chunk 1 really is mono-in-stereo and chunk 2 really is dual-mono at 8.318574, and that is this port's own decode, which the Decoder could not re-run and did not dispute. What fails is the step from one asset to the format.

What this changes, and what it does not

Nothing in the export changes. loudest is a per-asset content rule — it reads the peak of the actual streams in front of it — so a scattering structural ratio cannot undermine it, and ADV's dialogue at +0.3 dBFS instead of 8.7 is plainly the better outcome either way.

What changes is the reason, in four places: authored/audio.json's presentation_why, the selector comment in audio.rs, BLOCKED.md's row, and this page. The honest statement is narrower and slightly less satisfying: highest_rate was never refuted — it was never argued for, and neither is loudest. Which is exactly why the entry is marked chosen rather than measured, and why one capture deletes it.

⚠️ This is the third claim of mine in two iterations that generalised a single-asset observation — after "the chunks are two stems" and "everything the sequencer paces off rest.t is late". All three were true of the thing I looked at. The pattern is not carelessness about the measurement; it is reaching for the rule the measurement would imply if it held everywhere, and writing that down in the same breath. The corpus catches it because someone else runs the census.

Two things in that data that are not mine, recorded so they are not lost

  • S12B's three streams are byte-size identical (14 396 each).
  • BIRD_224 is three-stream and is not a movie cue — so the three-stream shape is not exclusive to cutscenes, which narrows how it was described to this port earlier. Neither affects export_voice, which only resolves movies.

Two rows of the P1 baseline were comparing blank frames and reporting OK

docs/port/BLOCKED.md has carried a 🔴 since P3: "the loading screen's fade quad rests OPAQUE BLACK … it will bite whoever first draws a loading screen." It had already bitten, in the one place nobody looks — the regression harness.

build_12 and build_15 render as pure black in both renderers: mean 0, max 0, on the Godot side and on sylpheed-cli's. The difference between two blank frames is zero, so verify-screen scored them max 0 over3 0 OK — the strongest verdict it has. Two of sixteen rows were comparing nothing against nothing, and the committed baseline reads as sixteen passes.

That is worse than a missing test. A missing test is visible in the count.

The cause, isolated by a control rather than by reading

build_00 and build_01 are the plain loading variant — the same screen minus three elements. They render: mean 1.913, max 214.5. build_12/build_15 add pgloading_baseeff, pgloading_loop5 and pgloading_eff00, a 1280×720 primitive whose rest is 0xff000000 — opaque black — at t=38, inside its own opening black hold (0xff000000 at 38, 0xff000000 at 48, clear on the untimed final). It carries layer_source: "none", so paint order puts it last, over everything.

Same screen, one element different, one renders and one does not. That is the diagnosis, and it did not require an opinion about rest.

The rule I was about to write, and the census that killed it

The obvious reading is that rest.t = 38 is wrong because it precedes the element's last timed keyframe at 48 — so "rest.t before the last timed keyframe" would flag the pathology. I ran the census before writing the rule, and it does not survive: 152 of 212 elements in this export have rest.t earlier than their last timed keyframe. It is the norm.

What actually distinguishes this element is its content, and the reach of that is one:

screen full-frame primitive rest.t last rest fade
build_12 / build_15 pgloading_eff00 38 48 0xff000000
extras / extras_jp pteff00 64 74 0x00000000
main_menu / main_menu_jp pteff00 70 80 0x00000000
title / title_jp pteff00 261 269 0x00000000
title / title_jp pteff02 46 236 0x40000000

pgloading_eff00 is the only element in the whole export whose resting pose is a fully opaque full-frame quad — 1 of 212. Every other full-frame primitive rests clear or at 25 %. One instance is not a rule about rest, and keying the renderer on "an opaque full-frame quad at rest is probably wrong" would be a content heuristic of exactly the kind this port refuses elsewhere.

So nothing in the renderer changed, and the harness did

The screens stay black. Nothing draws a loading screen, and the honest position is that either rest is mis-identified for this one element — a decoding question, asked — or the screen really does begin fully black and --pose=rest is simply the wrong thing to photograph it at.

What changed is that a blank pair can no longer score. verify-screen now checks both frames for ink first and reports BLANK -- both renderers drew nothing; this row proves nothing. It is not a failure — the port may legitimately have nothing to draw — and status is untouched, so an unrelated DIFFERS still fails the run. The corrected baseline:

build_00/01        max 3    OK          press_start(_jp)   max 1    OK
title              max 6    DIFFERS     main_menu(_jp)     max 4    DIFFERS
extras(_jp)        max 3    OK          title_jp           max 155  DIFFERS
publisher_logo(_r) max 1    OK          developer_logos(_r) max 2   OK
build_12 / build_15                     BLANK  ← previously OK

Fourteen rows, not sixteen. No new drift among the fourteen.

Refutation attempt — the loading-screen variants, and it survived

The Decoder's ui-title-build-map.md says entries 0/1 are the plain loading variant at 7 elements and 12/15 the dressed one at 10, the three additions being pgloading_eff00, pgloading_loop5 and pgloading_baseeff. Checked against this export, which addresses by pak entry:

entry elements
0, 1 7 — eff01 eff02 line loop1 loop3 loop4 str
12, 15 10 — the same seven plus baseeff, eff00, loop5

Exact, in both the count and the identity of the three. The claim survives, and it paid for itself immediately: the two variants differing by exactly the black quad is what made build_00 a control for build_12 and turned "the loading screen is black" into "this one element blacks it out".

🔴 The voice export is known incomplete — the game decodes all three streams at once

The Decoder booted with Canary's --xma_param_probe=true — the cvar whose own comment exists to say which sub-wave a movie's .slb the game decodes — and the answer is that it does not pick one. It decodes all three, concurrently, in three separate XMA contexts.

ctx packets byte_size disc payload (RIFF 60)
0 632 1 294 336 1 294 396
1 546 1 118 208 1 118 268
2 572 1 171 456 1 171 516

Three-way, byte-exact. So "three presentations of one take, pick one" is refuted by the running game, and the question I had been arguing about — which presentation — has no answer, because its premise was wrong.

This one was not caught by a census, and could not have been

The last three claims of mine that overreached were all killed by counting something. This one survived every count available: the streams really are equal-duration, one really is silence, one really is 0.60 × another with the residual 26.8 dB down. Every measurement was right and the frame around them was wrong, and no amount of looking harder at the file would have moved it — the file says ChannelMask = 0x0002 on all three. It took the running game.

That is the mission's own sentence arriving in practice: the Port has no oracle — if it needs to know what the game does, it asks. I did ask, repeatedly, and each time for the wrong thing: which stream, rather than whether the premise held.

What changed, and what deliberately did not

The behaviour is held. Reverting to the 1/n sum is not obviously less wrong: an equal-gain sum of channel pairs is not a downmix — MISSION §6 makes exactly that point when it pins an explicit matrix for the movies' 5.1 fold rather than letting ffmpeg default — and the sum cost S00A 6.02 dB when one stream was silence. Swapping one guess for another on the strength of a message is what produced this entry twice already.

What changed is that the wrongness is now loud. ⚠️ This failure sounds like success: a single stream decodes to clean, audible dialogue, so nothing a listener hears reveals that two streams are missing. So it is stated in three places a reader cannot miss — a top-level manifest.json warning per movie, the console line (1 of 3 streams [refuted] -- KNOWN INCOMPLETE), and the entry's own why — and 🟡 became 🔴 in authored/audio.json and BLOCKED.md.

🟡 "They are 5.1" is the Decoder's hypothesis and is not established. Three concurrent stereo streams is six channels and N stereo streams is how XMA carries multichannel on the 360, which would explain the differing byte rates, the near-silent stream, and why cues are 1-stream or 3-stream and never 2. Against it: all three declare ChannelMask = 0x0002 identically, which is odd for distinct channel roles. Nothing here builds on it.

What settles it, and it is asked: a recording of the game's own output over ADV, through the PulseAudio null sink (AUDIO-VERIFICATION §3). Candidate combinations of the three decoded streams can then be correlated against what the game actually played — which turns the channel-role question from a decode into a fit against an oracle. Twenty seconds over dialogue is enough.

The measurements survive; only their meaning moved

S00A's silent stream and ADV's 0.60 × relationship are untouched and now read as facts about channels: 0.60 × with the residual 26.8 dB down is what a correlated channel pair at a lower level looks like, and a silent channel is an unused one. Nothing measured here is retracted. What is retracted is every sentence that called them presentations.

🔴 The oracle capture does not contain the intro — a controlled negative

The Decoder took the capture I asked for — adv-game-output-6ch.wav, 70.2 s, 6 ch, 48 kHz, shared as 1788018994-16f9d19d90b8, taken at 68aa192 — described as "the FULL mix, the movie's own WMA track plus the three XMA streams".

It contains none of them. Envelope cross-correlation, sliding with overhang, normalised over the overlap, minimum 30 s of overlap so a short window cannot win on an edge:

capture ch against best r runner-up margin
c0 ADV bed 0.361 0.359 +0.003 no match
c1 ADV bed 0.407 0.403 +0.004 no match
c0 voice stream 1 / 2 / 3 0.42 / 0.26 / 0.27 ≤ +0.006 no match
c1 voice stream 1 / 2 / 3 0.47 / 0.34 / 0.34 ≤ +0.006 no match
c0 BGM_103 (menu bed) 0.271 0.260 +0.011 no match
c0 S00A 0.351 0.349 +0.002 no match

The margin is the number that matters, not r. A match has a peak; these have a plateau — best and second-best differ by 0.0010.016 across every pairing, which is what a statistic does when no alignment exists.

Three controls, because a negative from an uncontrolled instrument is worthless

  1. The instrument finds matches on this data. bed vs bed → r = 1.000, margin +0.115. Voice stream 2 vs stream 3 → r = 1.000, margin +0.300.
  2. My reference really is the movie. The .ogv transcode against the disc's own ADV.wmv → r = 1.000, margin +0.114. So a failure to match is not my transcode.
  3. Time drift is ruled out. A stretched playback would break a long correlation while still matching locally, with the best lag climbing monotonically. Five-second windows of the capture slid over the whole bed give best lags of 4.95, 15.30, 119.35, 50.75, 29.35, 83.95 s — scattered across the movie, not monotonic, and every margin ≤ 0.017.

What I can say, and what I will not

Said: this capture cannot answer the channel-role question, and it is not the intro's audio. Not said: what it is. It is 70 s of something, all six channels carrying signal at RMS ≈ 27 dBFS, matching nothing this port exports. Diagnosing it is the Decoder's side of the wall and I have handed it back rather than guessing.

⚠️ One measurement on the file that may help them. Split as 5.1, channels 3 and 6 are byte-identical — same MD5, not merely the same peak and RMS to six decimals. An exact duplicate pair inside a six-channel "surround" output is consistent with the Decoder's own warning that the 6-channel frame is Xenia's hardcoded kFrameChannelsDefault, not the guest's request. It weakens, further, any reading of that file as evidence of a 5.1 game mix.

The voice export stays exactly as it is

Still one stream of three, still marked 🔴 in the manifest, the console line and authored/audio.json. The capture changed nothing, which is the correct outcome for a measurement that failed: the question is open, and it was open before. What would have been wrong is treating a 70 s recording as an oracle because it was expensive to obtain.

Resolved the same day — it was the capture path, and the duplicate pair was the thread

The Decoder found the cause and withdrew the capture: PulseAudio was remapping between two mismatched channel maps, and a 6-channel remap silently drops and duplicates. Its control needs no emulator and no disc — six channels, six different tones, the same sink and the same parec invocation — and came back 400 / 3200 / 200 / 800 / 800 / 200 for an input of 400 / 800 / 200 / 1600 / 3200 / 6400. Two source channels were gone entirely. Setting the sink's channel_map to the guest's own returns all six.

So the negative was right and, more usefully, the byte-identical pair I reported was the thread that unravelled it. That is worth recording precisely, because it was nearly not reported at all: it began as an idle check of two channels whose peak and RMS matched to six decimals, and the only reason it became evidence is that a coincidence at six decimals is cheaper to hash than to explain.

Withdrawn with the file, both the Decoder's: "all six channels carry signal", and the non-zero-surround observation offered as weak support for a 5.1 guest mix. Unaffected: the three-XMA-context concurrency result, which is read from the emulator's own log rather than the audio path, on two independent boots.

What the port took from it: tools/port/check-capture

A capture now has to pass a provenance check before anyone analyses it, and it is one command. It splits the file, hashes every channel and fails on any duplicate pair. Documented in docs/port/AUDIO-VERIFICATION.md §5.

Run through its own controls, both directions, because a checker nobody controlled is the thing this whole incident is about:

  • six distinct tones → PASS;
  • the remap's own output pattern → FAIL, naming all four duplicate pairs;
  • the corrupt game capture → FAIL on ch2 == ch5.

⚠️ The known-bad control is the part worth reading. All six of its channels report a peak of 18.063656 dB — identical to six decimals — while containing three duplicate pairs. A level check cannot see this failure at all. That is why the tool hashes rather than measures, and it is why the corrupt capture's "plausible per-channel levels" were never evidence of anything.

The tool says so itself: it is necessary, not sufficient. Passing means no channel was duplicated; it says nothing about whether the right thing was recorded. A capture should survive both that and §1's correlation against a known source before anything is concluded from it — and the one that was analysed here would have failed the cheap check in thirty seconds.

The corrupt file is withdrawn from the exchange (share drop), so the next agent cannot pick it up and repeat the work.

🔴 Take 2 is clean, my instrument was not, and the negative had to be re-earned

The Decoder's second capture passes check-capture — I re-ran it myself rather than cite theirs — carries a screen log, and was recorded with the sink's channel_map set equal to Canary's own. It is a good file.

⚠️ One provenance discrepancy, minor but worth stating: the message gives 253.3 s; the file is 318.539 s. The screen log runs to 316 s and is consistent with the file, so this is a mis-stated number rather than a bad capture — but a length quoted in a provenance claim should match the artefact.

The retraction that came out of measuring it

Take 2 also showed no alignment with the bed or the voice streams. Before reporting a second negative I asked whether the method could do the job at all, by building a synthetic mix — the bed plus the three voice streams — and hunting the bed inside it.

It failed. r = 0.415, against the r > 0.8 bar my earlier negatives were judged against.

So the instrument that produced "the capture contains no ADV audio" could not have found ADV audio in a mix even when it was certainly there. That conclusion was right — the Decoder's tone control proved take 1 corrupt independently — but it was right by luck, and I reported it as measurement. The three controls I was pleased with tested the wrong things: that the method finds a clean signal in a clean reference, which was never the task.

The rebuilt instrument, calibrated in both directions

Band-limit so the target dominates, then judge on lag and margin, not on absolute r — the r > 0.8 bar is correct clean-against-clean and meaningless for a component in a mix.

hunting band against r lag margin
the bed 40180 Hz mix containing it 0.663 0.0 s +0.111
the bed 40180 Hz voice-only mix 0.262 wrong ✗ +0.005
voice stream 2 3003000 Hz mix containing it 0.810 0.0 s +0.248
voice stream 2 3003000 Hz the bed alone 0.358 wrong ✗ +0.005

A 2050× separation in the discriminating statistic. Documented as AUDIO-VERIFICATION.md §6.

And now the negative, supported

Every one of take 2's six channels, against both targets, sits in the known-absent regime:

bed (40180 Hz) voice stream 2 (3003000 Hz)
margins +0.000 … +0.014 +0.001 … +0.017
lags 58 … +255 s, scattered 72 … +183 s, scattered

Take 2 contains neither the movie's WMA bed nor the cutscene voice, on an instrument that demonstrably finds both when they are present.

What that leaves, and it is not mine to answer

Two captures, differently configured, the second provably free of the channel-map fault, with a screen log saying the movie was on screen — and neither carries either audio source. That points away from a one-off setup error. The possibilities I can see are a capture path that still loses the guest's mix, or the guest not emitting these sources at all during the movie, and only one side of that wall can tell them apart. Handed back with the numbers.

⚠️ If it is the second, it reaches the port directly: the export's movie audio comes from the .wmv's WMA track, and if the game never plays that track, then ADV.ogv's audio is wrong in a way no amount of transcode fidelity would fix. I am not asserting that — it is a question about what the game does — but it is the reason this is worth another boot rather than being written off.

Every music bank was summed at 1/3 when only two sub-waves are music — 3.52 dB, since P6

The Decoder's message about BGM_102 came with declared durations from the corrected XMA1 PsuedoBytesPerSec, and checking my export against them turned up a defect of mine that had been shipping since P6.

export_bgm summed every sub-wave media returned and scaled by 1/n. Decoded and timed, the three banks are identical in shape:

bank sub-wave 0 sub-wave 1 sub-wave 2
BGM_103 10 300 B → 0.009 s, peak inf 3 876 924 B → 87.744 s 3 930 172 B → 87.744 s
BGM_102 10 300 B → 0.009 s, peak inf 1 151 036 B → 37.482 s 1 269 820 B → 37.482 s
BGM_001 10 300 B → 0.009 s, peak inf 4 466 748 B → 173.809 s 4 673 596 B → 173.809 s

Sub-wave 0 is digitally silent in all three, and 10 300 B is 10 240 + a 60-byte RIFF wrapper — 10 240 B being exactly what the Decoder's disc-wide census identifies as the bank header. So it is not a stem. Counting it in the divisor put every real stem at 1/3 instead of 1/2: 3.52 dB of attenuation on all the menu music this port has shipped since P6.

Dropping it is arithmetic, not a decoding decision — a silent input contributes nothing to a sum, and this is the same rule export_voice already applies. Measured after the fix: main_menu.ogg goes 7.69 → 4.20 dBFS, +3.49 dB against 3.52 predicted, the remainder being Vorbis.

⚠️ This is the third instance of one defect in this pipeline — a silent chunk in the voice sum, a silent channel in the mono fold, and now a silent sub-wave in the music sum. Each was invisible in every check except a level, and each time the divisor was computed from how many inputs there are rather than how many carry signal. That is the shape to look for, not the individual bug.

It also closes a 🔴 that has been open since P6

docs/port/BLOCKED.md carried "media::sound_bank_riffs returns three sub-waves where HANDOFF Q10's census says two" as a disagreement the port shipped deliberately. The census was right; the third was never a stem. The export now reports 2 sub-waves and the warning is gone — closed by measurement on my side, corroborating the Decoder's c1f3608 from a different direction (decoding it, rather than counting headers).

The declared-rate method, cross-checked a third time — and one correction

Their declared lengths against my decodes: BGM_103 87.750/87.749 vs 87.744; BGM_102 37.487 vs 37.482; BGM_001 173.821 vs 173.809. Agreement to 512 ms on three banks. The method is good for lengths.

🟢 Refutation attempt, and the conclusion survives while the reasoning does not. The Decoder wrote that BGM_001 reads "173.821 s declared against your decoded 167.663 s — a gap of 6.158 s", explaining it as "declared is the encoded stream, decoded is where the audio stops." A full decode of BGM_001 yields 173.809 s of PCM, not 167.663 s. The 167.663 figure is where the music fades out, measured from the audio; the stream then continues, silent, to its declared end. So declared and decoded agree to 12 ms and the trailing silence is inside the decode, not the difference between two methods. The cross-check stands — better than stated, since it is now three banks rather than a coincidence — and the sentence explaining it should go.

Take 2 was starved, my correlator was fine, and check-capture was incomplete

The Decoder diagnosed take 2: a starved capture. Verified here independently rather than taken on trust — 35.6 % of frames silent on all six channels, 10 482 alternating runs, median burst 13.5 ms and gap 3.9 ms, a 17.4 ms period at 57 Hz. Their untruncated original reads 39.3 % and 10 595 runs; the difference is exactly the truncation, and every other number agrees.

So my rebuilt correlator was working correctly on a file that could not carry the signal. And the alarming reading it produced — "the game may not play the .wmv's WMA track, so ADV.ogv's audio has been wrong since P4"is not supported by this capture, and is not refuted either. It is withdrawn as a concern arising from evidence, and nothing is changed on account of it in either direction. That matters more than it looks: it was the most expensive-to-act-on hypothesis in the port, and it came from a file that could not speak to it.

The real deliverable: my own checker passed the starved file

check-capture tested only for duplicated channels, so it cleared a recording that was 36 % holes. A provenance check that passes the artefact it was built in response to is not a check.

It now measures starvation too. ⚠️ Two thresholds I invented were both wrong, and the controls caught both — which is the part worth recording:

  1. Counting exact-zero frames. Real audio crosses zero constantly; a clean voice track scored 5 947 "gaps" of median 0.0 ms and was called starved. A gap is a run, not a sample. Only runs ≥ 1 ms count.
  2. Gap count and median length. A genuine music bed shows 454 gaps at a median of 1.4 ms — quiet 16-bit passages really are zero for milliseconds — so neither statistic separates it from a starved file.

What separates them is the rate:

gaps/s median gap all-channel silence
the starved capture 32.9 3.9 ms 35.6 %
a real music+SFX bed 3.3 1.4 ms 1.1 %
a voice track, 53 % pauses 0.03

Bar set at 20 gaps/s — 1.6× below the bad case, 6× above the worst good one, derived from the controls rather than chosen and then justified. Controlled in both directions: real stereo bed PASS, six distinct tones PASS, starved capture FAIL. It also now reports a data chunk declaring 0 bytes, which is what a file copied while still being written looks like.

🔴 The voice channel roles are not obtainable this session

Both capture routes are closed and the Decoder has said so plainly. The monitor sink is starved by construction — it advances at wall-clock rate and substitutes silence, so every moment the emulator runs slow is a hole, and deleting the holes warps the timebase rather than repairing it. The route that works is an internal tap at SDLAudioDriver::SubmitFrame, and that needs a Canary rebuild whose cost they have measured: the build root build-canary targets does not exist in that container, the warm tree is configured against the same missing path, so it is a full reconfigure and compile on a box with ~700 MB free and a history of parallel builds OOM-killing the host.

A whole session for one probe. That is a human's call and neither agent should start it mid-loop. Until then the port keeps authoring with the known recorded: one stream of three, 🔴 in the manifest, the console line and authored/audio.json.

The settle run carries an unmeasured real-time factor — and the numbers it touches were already unauthored

The Decoder has withdrawn one of the two arguments propping up its settle-time run. It had claimed the plate pulse period was an internal clock proving the run was not slowed; re-examined, that estimate rests on one interval at a 125 ms sample interval (±6.7 %), and re-running the trough-picking gives 2.628 s rather than the 2.369 quoted — an adjacent local minimum had been counted as a separate trough. Against the corpus's 2.24 s that is +17.3 %. It is too weak to show anything and cannot resolve a real-time factor below ~7 %.

Nothing in the port moves, and this is the second time in two iterations that the right call was to have authored nothing.

number from that run anchored by did the port take it?
title → plate, 2.247 s three prior readings (2.13 / 2.132 / 2.138) and the disc's declared 120 units it is what the port already draws
menu build-in, 0.531 s nothing not authored
Ⓑ → title, 0.482 s nothing not authored
Ⓐ → menu, 3.763 s contains a 1.53 s load stall explicitly refused

Checked rather than remembered: grep over authored/ and port/scripts/ finds no 0.531 and no 0.482. The only build-in reference in the tree is the plate arithmetic — t=118 → t=238, 120 units — which is the anchored leg.

I declined those two because they were one-run figures the Decoder had itself flagged, and because the port was already within ~0.1 s of both from the disc's own keyframes. That reasoning has now been joined by a second, independent one I did not have at the time: a few per cent of slowdown sits inside them undetected. A provisional measurement adopted over a decoded number would have imported an error nobody could see.

verify-dwell — the comparison that refuted my own 🔴, made repeatable

Last iteration I measured the port's visible spans against the oracle's dwells by hand, and it refuted a red flag I had filed myself: rest.t is the wrong settle landmark, but "everything the sequencer paces off it is therefore late" was false, and I nearly went and re-paced screens that already matched.

That check existed once, in a transcript. It is now tools/port/verify-dwell.

screen               port         oracle (3 cold boots)      verdict
publisher wordmark   4.25 s       4.297 / 4.604 / 4.370      agrees
developer logos      3.75 s       3.508 / 3.503 / 3.366      agrees

⚠️ The trap it exists to prevent is in its header, because it is the whole point: a port's transition timestamps and the oracle's visible spans are not the same quantity — they differ by the exit ramp plus the black hold, about 0.6 s, which was the entire discrepancy I was about to chase. This corpus has been bitten by the identical confusion before, on the plate delay, where the two readings differ by 0.48 s against 6 ms.

The bar is the oracle's own run-to-run spread, plus one film interval. Three cold boots of the real game differ by 0.3 s; agreeing more tightly than the oracle agrees with itself would not mean anything, and a tighter bar would be a number chosen to look impressive. The developer-logo span read 3.50 s on the hand-run and 3.75 s here — one film interval apart, both inside the bar, which is the tool reporting its own resolution honestly rather than hiding it.

The oracle's numbers are quoted in the script as a test fixture and labelled as the oracle's, with the RE document they come from. Nothing in the port derives them and nothing may.

The PRESS Ⓐ plate pulses — authored per element, because the census forbids a rule

The human listed pulsation as first-class and the port drew nothing at all: the plate's focus record ptbtn00f was never reached, because press_start has no buttons and nothing is focused.

That it loops is measured, not assumed. The corpus timed the pulse four times — 2.12 / 2.19 / 2.34 / 2.31 s — and you cannot measure a period unless the thing repeats.

The rule I was going to write, and the census that forbade it

The spinning ring is a rule in the renderer (spin_period_units) and it earns that: 16 of 212 elements match its shape and all 16 are focus rings, zero false positives. So I looked for the analogous shape for a pulse — a group whose keyframes vary only in alpha, whose first and last alpha are equal, a closed cycle. ptbtn00f fits it exactly: 0 → 6 → 74 → 80 → 80 → 74 → 6 → 0.

Censused before writing it: 82 of 212 elements match. ptcopyright, palogo_sqex, ptmsg, ptlogo_back2, and every _eff fade-in-hold-fade-out on every screen. A renderer rule on that shape would make the copyright notice pulse.

Narrowing to focus records leaves exactly one distinct element (plus its JP twin). A rule justified by n = 1 is a special case wearing a rule's clothes. So the pulse is a lookup in authored/timing.json, keyed <screen>/<element>, and the census is recorded beside it so nobody widens it later.

The period is the element's own group — and the alternative is stated

129 units: its last timed keyframe is t=105, and the final untimed keyframe is reached exit_ramp_units (24) later. No new constant — 24 is the same authored value every other element's exit already uses. That is 2.150 s at 60 units/s, or 2.295 s at the ~28.1 fps the emulator presents, against measurements of 2.122.34 s. It sits inside the spread at either rate.

⚠️ It is a choice, and the alternative cannot be ruled out: the cycle could restart at the group's first keyframe (t=6) rather than at 0, giving 123 units = 2.050 / 2.189 s — also inside the measured spread. Nothing available separates them. t=0 is taken because it is where every other group in this port starts, which is consistency and not evidence, and authored/timing.json says so.

A fifth reading is recorded and not averaged in: the Decoder re-picked its troughs and got 2.628 s having previously reported 2.369 from the same run, then withdrew the estimate as too weak to resolve better than ~7 %.

Verified the way the ring was — bit-identity one period apart

20 authored periods is 2.15 × 20 = 43.00 s = exactly 172 film frames, so frames N and N+172 must be the same frame:

max difference
f_055 vs f_227 (43.00 s) 0/255
f_060 vs f_232 (43.00 s) 1/255
f_070 vs f_242 (43.00 s) 1/255
f_079 vs f_251 (43.00 s) 0/255
control — f_070 vs f_243 (43.25 s) 58.7/255

The control is what makes the rest mean anything: a quarter-second off the period differs by 58.7, on the period by 01. Measured on the held boot title, where the glow-box mean swings 26.0 ↔ 37.7 — a real pulse, not a static glow.

The oracle finally speaks: the exported voice IS the game's centre channel

The Decoder's fourth capture is the first faithful one — --gpu=null takes the guest from 0.70× to 0.96× real time so Xenia stops padding, an ALSA file tee in front of a paced slave removes PulseAudio's wall clock entirely. 59.7 s, 0.35 % silence, one gap in the whole file, six distinct channel hashes. Checked here independently: it passes check-capture, and its header sizes verify exactly (RIFF 34 369 572 / data 34 369 536 against 34 369 580 actual).

The correlator was already calibrated, so the fit was a re-run rather than a rebuild. Controls first, on this instrument: known-present margin +0.248, known-absent +0.005.

Speech band, 3003000 Hz

FL FR FC LFE RL RR
stream 1 (leading) +0.013 +0.006 +0.012 +0.009 +0.012 +0.005
stream 2 — the one exported +0.238 +0.171 +0.305 +0.011 +0.035 +0.006
stream 3 +0.240 +0.173 +0.307 +0.009 +0.054 +0.006

r = 0.989 on FC, margin +0.305 — above the known-present control. And the bed, in the low band, is the mirror image: FL 0.763 / FR 0.838 / RL 0.805 / RR 0.817, all agreeing on the same lag, and FC 0.317.

What is established, and what is not

The capture contains ADV's audio, at lag ≈ +6.66.7 s — agreed by two independent bands and by six channels.

The dialogue is in the centre channel and the bed is in the four corners. FC carries the voice and not the bed; FL/FR/RL/RR carry the bed and not the voice. That is a textbook film mix, and it is measured rather than inferred from a header — which matters, because the header says ChannelMask = 0x0002 on all three streams and would never have told us.

The port's exported voice file is the material the game plays in FC. The loudest choice in authored/audio.json — which I recorded as an unjustified choice — selects the dialogue. A mono voice file played into the mix is the right approximation of a centre channel.

⚠️ Streams 2 and 3 are indistinguishable to this instrument, and that is expected: I measured months-of-iterations ago that stream 3 is 0.60 × stream 2 with the residual 26.8 dB down. They are the same take at two levels, so they correlate identically with everything. This does not say loudest picked the right one of the two — only that whichever it picked is the dialogue.

⚠️ Stream 1 is not detectable in this window, margins +0.005…+0.013. Consistent with it being the tail of stream 2 (measured earlier at r = 0.998) and with a 59.7 s window that starts before the tail.

🔴 The 1 of 3 streams [refuted] warning stays. Nothing here explains what the other two contribute to the game's output, and the export still ships one. What changed is its character: it is no longer "one of three, contents unknown" but "the centre-channel dialogue, plus two streams whose relationship to it is measured and whose role is not."

⚠️ And the reach: 59.7 s of a 137 s movie, one run, at 0.96× real time. The --gpu=null route costs video, so this capture has no screen provenance — its provenance is the XMA probe showing ADV's three streams decoding during the run, which for an audio question evidences the thing recorded rather than what was on screen.

The stripping control passes — S00A is obtainable, and the gate is cleared

The Decoder made this the gate on S00A, and it is the right call: ADV plays itself on boot so it can be captured with --gpu=null at 0.96× real time, but S00A starts ~4.5 s after Ⓐ on a save slot, which needs a driven run, which needs screens, which rules out --gpu=null. So S00A is necessarily the 0.70× rendered route with ~10 % additive padding — and is only worth a boot if stripping that padding is exact.

It is. A real music+SFX bed (137.37 s, carrying 454 genuine zero runs of its own) had 1 149 holes inserted at 8.37/s to +9.9 % length, matching the observed ALSA profile, then was stripped and correlated in the low band:

r lag margin
original vs itself — ceiling 1.000 0.0 s +0.141
padded vs original — what padding costs 0.436 12.2 s +0.006
stripped vs original — recovered 1.000 0.0 s +0.142
stripped vs original-also-stripped 1.000 0.0 s +0.143

Two things worth reading off that table.

First, padding at that profile destroys correlation completely — r 0.436, margin +0.006, which is the known-absent regime. That independently confirms, on a file whose contents I control, that the earlier captures were unusable for the reason claimed rather than for some other reason.

Second, recovery does not require stripping both sides. The stripped capture matches the unstripped source at the ceiling. That matters operationally: the port's reference assets never need touching.

⚠️ What the control does not license. Stripping removes genuine silence too and cannot tell the two apart. On this material the genuine runs total 0.71 s in 137 s and cost nothing measurable; on material that is mostly silence they would. And the whole thing rests on the substituted-versus-additive distinction — it is valid for Xenia's ALSA padding, which inserts, and it is vandalism on a PulseAudio monitor capture, which substitutes. tools/port/strip-padding says so in its header before it says anything else, because running it on the wrong artefact would look like it worked.

Its output is byte-identical to the control's own stripping, so the tool and the experiment are the same operation rather than two implementations that agree.

The correctness harness the docs promised for eight milestones did not exist

tools/port/verify-screen, line 20, since P1: "Use tools/port/verify-capture for the correctness question." There was no such file. The port has had a harness comparing itself to sylpheed-cli — two renderers sharing its assumptions — and none comparing it to the game, while its own documentation said otherwise.

docs/re/captures/ORACLE-CAPTURES.md is blunt about why that matters: two renderers agreeing proves nothing, and this corpus has been bitten three times — the dropped pteff05 background, the scale-0 rect, rest() — each invisible to a render-vs-render diff and obvious against a capture.

tools/port/verify-capture now exists. Five screens, against framebuffer captures of the real game:

screen RMSE differing note
main_menu 14.79 0.25 % focus state may differ
extras 15.29 0.46 % focus state may differ
title 21.07 1.82 % ptloop sweeps never stop
publisher_logo 10.77 1.00 %
developer_logos 9.37 0.39 %

No screen shows a large connected blob — the shape a missing or misplaced element makes, and the shape all three historical failures made. The differences are scattered, and the two largest have stated causes.

74 % of main_menu's difference is the oracle's own focus signature

The corpus ships live-main-menu.png and live-main-menu-options-focused.png — the same screen with a different button lit. Their difference is what focus changes, measured by the oracle against itself. Of the port's 2 159 differing pixels, 1 599 — 74.1 % — fall inside that signature. So the bulk of the disagreement is a state mismatch (the port focuses NEW GAME, authored, because HANDOFF Q5 measured initial focus as unstable), not a rendering defect.

Refutation attempt — the tone curve survives in its stated reach and not past it

ui-render-tone-curve.md models the relationship as capture = 255·(render/255)^γ, γ ≈ 1.341.49, measured on dark flat patches (render ~060), with "nothing constrains midtones or highlights" written into its own reach.

I tried to fit that γ and got contradictory answers three times, and the contradictions were mine. Binning every structurally matched pixel of main_menu by render level gives the relationship directly:

render capture implied γ pixels
8 4.04 1.20 183 026
16 7.89 1.26 227 630
24 15.57 1.18 100 945
32 26.15 1.10 87 474
40 38.07 1.03 86 094
48 53.96 0.93 85 255
64 78.52 0.85 6 509
96 130.44 0.69 1 682

The claim survives where it was measured. In the darks the capture really is darker than the render and γ > 1.

🔴 It is not a single power law. The implied exponent falls monotonically and crosses 1.0 near render ≈ 44 — above that the capture is brighter. One exponent cannot express a curve that crosses unity, which is precisely why my whole-frame fits kept returning γ = 1.00: the darks want more than 1 and the midtones want less, and they cancel.

So the corpus's stated reach was not a hedge, it was the finding. ⚠️ And the exponent in the darks measures 1.181.26 here against the page's 1.49 for this screen — a disagreement I am recording rather than resolving, since they fit selected flat patches and I binned every matched pixel.

The tool reports the curve, not a best exponent

Two earlier versions of verify-capture reported a best-fit γ and were wrong both times — once by fitting across a 74 % structural mismatch, once by extrapolating past a reach the measurement's own authors had written down. Extrapolating a measurement past its stated reach is how this tool got it wrong twice, and the answer was not a better fit but a different instrument: a table somebody can argue with.

Identifying the capture's focused button — and my harness was posing the port wrong

The Decoder attached an honest caveat to its reproduction of the tone-curve refutation: its bins included the focus-state mismatch, so it was "not a clean second opinion". That is removable, and removing it found a defect of mine.

The method, with a known-answer control

--menu=main_menu --script=down,down,down,down walks focus through all five buttons and shoots each. Compare every one against a capture; the minimum identifies the focused button. The control is the capture whose answer is in its own filename:

render focus vs live-main-menu-options-focused vs live-main-menu
ptbtn01 NEW GAME 6 351 531
ptbtn02 LOAD GAME 7 087 7 094
ptbtn03 TUTORIAL 6 230 6 237
ptbtn04 OPTIONS 1 292 6 364
ptbtn05 EXTRAS 6 073 6 080

The control picks OPTIONS, by 4.7× — the answer the filename gives. So the test is trustworthy, and live-main-menu.png has NEW GAME focused, by 11.5×.

Which is what the port already focuses. authored/flow.json's initial_focus: ptbtn01 was chosen because HANDOFF Q5 measured focus as unstable across boots — four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME — and it is one of the two observed states. It is now also the state of the committed capture. ⚠️ That is corroboration, not a decode: Q5's instability stands, and this identifies one frame rather than a rule.

The defect: verify-capture rendered menus with no focus at all

--screen= draws no focus record, so the harness had been comparing main_menu to the oracle in a state the oracle was never in. Rendered properly, with --menu=:

before after
main_menu 0.25 % differing, RMSE 14.79 0.06 %, RMSE 13.21
extras 0.46 %, RMSE 15.29 0.20 %, RMSE 13.38

A 4× improvement on main_menu that was entirely my harness posing the port wrong, not the port drawing wrong. Worth stating plainly: the first run of a new correctness harness reported a discrepancy, and three quarters of it was the harness.

And the Decoder's caveat resolves to nothing — measured, not assumed

Re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 / 1.18 / 1.10 / 1.03 / 0.94 at render 8…48 — unchanged from the mismatched run. So the focus-state contamination it flagged really did not move the trend. Its reproduction stands as a second opinion after all, and that is now a measurement rather than a hope.

tools/port/which-focus — the Decoder asked for a detector, and it carries its own control

S00A is blocked on knowing which button a screenshot has focused. newgame_path.sh assumed NEW GAME is focused at boot, drove on that assumption, and landed in a tutorial mission — because HANDOFF Q5 measured focus as unstable across boots. And counting presses cannot substitute: ⬆ from the first item wraps to the last, so no fixed number of presses lands on a known item from an unknown start.

The Decoder's own attempt — a per-row brightness statistic — failed the control, picking NEW GAME on the capture whose filename says OPTIONS. The render-difference method passes it, so it is now a script that agent can run.

It runs the control on every invocation, not once when it was written

control -- live-main-menu-options-focused.png (answer is in the filename):
    OPTIONS      1285      <- picked
    EXTRAS       6073
    ...
  -> OPTIONS, margin 4.7x     CONTROL PASSED

If that fails, the tool refuses to report a result at all. A control that does not execute is not a control, and this one cannot be skipped.

Three checks, and one of them independently reproduces a corpus measurement

input verdict margin
live-main-menu-options-focusedknown answer OPTIONS 4.7×
live-main-menu — the question NEW GAME 11.4×
live-extrasknown from the corpus MISSION SELECT 4.2×
live-title-press-ano menu at all refuses 1.0×

The extras row is a second known answer I did not plant: authored/flow.json already records "MEASURED: EXTRAS opens focused on MISSION SELECT (live-extras.png)", and the tool reaches it independently.

The title row is the negative control. A frame with no menu in it gives a margin of 1.0× and the tool says "this frame does not decide it. Do not act on this."

⚠️ And that refusal now carries a non-zero exit code. The first version printed the warning and exited 0 — so a caller scripting it, which is the entire point, would have read a refusal as an answer. That is the same defect as a checker claiming a check it skipped, and it is the fifth instance of that shape between the two of us this session.

What it is not: it identifies focus in one frame. It says nothing about what selects focus; Q5's instability stands.

The title's 1.82 % — three of my own explanations refuted, and the format has no blend mode

title is the port's largest disagreement with the oracle, and last iteration I attributed it to the moving ptloop sweeps without checking. That attribution is wrong, and so were the two hypotheses I formed after it.

Not the sweeps. ptloop01/ptloop02 are 399×180 at (441, 270) — small and central — and their exported keyframes hold pos, scale and rotation constant, varying only alpha. The difference peaks at x ≈ 1088, nowhere near them.

Not an over-held element. --screen holds every element at its own rest.t, so I added --no-hold to render the other answer. Playing the title's groups past their rest fades the whole screen to black by t = 5.2 s — 30.97 % differing against 1.82 % held. Holding at rest is right.

Not a timing offset. Sweeping the build-in: 24.05 % at t=1.6, falling monotonically to 1.68 % at t=4.18 and 1.82 % settled. The capture is at the settled end; there is no earlier moment that fits better than marginally.

What it actually looks like: a horizontal redistribution

Signed difference (port capture), by cell:

x=0 x=320 x=640 x=960
y=0 +1.1 13.1 6.6 +16.0
y=169 +2.1 8.3 +4.2 +9.9
y=338 +5.8 4.5 0.0 +3.2
y=507 +3.9 +3.3 +2.0 +2.3

The port is darker centre-left and brighter right, and it nearly cancels — whole-frame means 63.8 against 62.5. That is not a level error and not a tone ramp; it is brightness in the wrong place. And it falls in exactly the rows spanned by the two wide elements ptlogo_back2 (1118×262 at 71,126) and ptlogo_back2eff (1133×280 at 64,117), with the column profile falling off past x≈1152 against their right edges at 1189 and 1197.

🔴 The export carries no blend mode, so the port cannot draw one

ptlogo_back2eff's exported keys are declared, id, index, keyframes, kind_raw, layer, layer_source, pivot, rest, role, sprite. There is no blend field, in this element or in FORMAT.md at all. The port composites everything with normal alpha.

If the game draws _eff layers additively, a wide gradient sprite would produce precisely this signature — surplus where the sprite is bright, deficit where the underlying art is brighter than the sum — and nothing in the export would reveal it. That is a decoding question, not a port one, and it is asked rather than assumed. ⚠️ It is a hypothesis I have not tested; I am recording it because the three I could test are dead.

A separate rest.t casualty, recorded and not acted on

pteff02 is a full-frame primitive whose group runs 0x40000000 (25 % black) at t=46 → 0xd4000000 at 76 → 0xcc000000 at 118 → 0x00000000 at 236. Its rest.t is 46, so the port holds a 25 % black veil the screen's own timeline removes. This is the third instance of rest.t naming a hold that is not the settled state — after the loading screen's opaque quad and ptlogo1's creep. ⚠️ It does not explain the residual — removing a darkening veil would make the port brighter still, and it is already brighter where it disagrees — so it is recorded rather than fixed.

And a new diagnostic

--no-hold plays a screen past its rest instead of clamping each element at rest.t. Added because the question "is the held pose what the idle game shows" could not be asked otherwise. ⚠️ Its first version set the flag thirty lines before view exists and silently rendered nothing — caught because the comparison loop found no files, not because anything reported an error.

🔴 The exporter dropped nested .rat leaf geometry on 45 elements — and it is the title's 1.82 %

The Decoder overturned one of my three eliminations, and it was the one I was most confident about. I ruled out the ptloop sweeps because "399×180 at (441, 270), keyframes hold position constant"that is the parent's record. The geometry is in the leaf, and the exporter never opened it.

parent (what the export shipped) leaf ptloopNN.rat
ptloop01 scale (100, 100), rot 0, pos (441, 270) fixed scale (100, 600), rot +30°, x sweeping 639 → 39 → 1521
ptloop02 scale (100, 100), rot 0, pos (441, 270) fixed scale (100, 800), rot 45°, x sweeping 1721 → 1111 → 839

Two ~1080 and ~1440 px quads leaning opposite ways and sweeping across the frame, against two 400 px sprites drawn upright and static in the middle. That is exactly the signature I measured — darker centre-left, brighter right, nearly cancelling — and the Decoder's GPU capture puts their centres at x ≈ 467 and 992, which are the two cells where my signed difference peaked.

ui_layout's own doc comment had said so: "the rotated quads come from its two nested .rat leaf records, which the census never opened." Neither did this exporter. It opened a leaf in exactly one place — highlight_name, for focus records — and nowhere else.

It is not two elements. It is 45

screen elements with a dropped leaf
main_menu, extras, press_start (+ JP twins) every button — ptbtn01.ratptbtn13.rat, ptbtn00.rat
title, extras ptloop01.rat, ptloop02.rat
build_00/01/12/15 pgloading_loop1/3/4/5.rat
title_jp ptlogo_eff2.rattwo elements

⚠️ The buttons are the benign case and screen.rs already knew it: "a BASE record's leaf duplicates the parent's placement and the two can differ by a unit (ptbtn04: parent y=401, leaf y=402). There the parent wins." The ptloop case is the opposite — the parent carries no geometry at all and the leaf carries all of it. 🟡 And title_jp's ptlogo_eff2 is the element DECISIONS has recorded since P1 as the single largest render disagreement in the export, "the one drawn element at a scale that is not a whole multiple of 100 %". It has a two-element leaf. That is a lead, not a conclusion.

Emitted, deliberately not drawn

Element::leaf now ships the decoded leaf, and one read_leaf closure serves both it and the focus path — a second copy is how the case would go missing again.

🔴 ScreenView ignores it, and that is the honest state. Parent and leaf each carry their own alpha ramp on a different span — parent 0 → 255 over t=70…238, leaf 255 → 0x80 → 255 over t=150…600 — so how the two compose is a decoding question, and drawing the leaf on a guess would replace a visible 1.82 % gap with an invisible wrong one. verify-screen confirms nothing moved: title still max 6 / over3 790, main_menu max 4, title_jp max 155.

Additive blending is refuted — the Decoder tested T8aD +0x04 bit 0x02 as an additive selector and "every measure worsens". My blend-mode hypothesis from last iteration is dead, and the export carries no blend field because none has been found: the per-draw capture records primitive type, index count, shader hashes, texture bindings and vertex attribute 0, and no RB_BLENDCONTROL.

🔵 And this makes the port's biggest oracle gap the same item as the rotation question already standing with the human. sylpheed-cli screen render deliberately does not rotate, which is why both renderers show it — the Decoder measures its own residual as tiles running 38.6 then +33.8 and cancelling, the same shape as mine. It is a shared decode gap, not a defect in my compositor, and MISSION's "Needs a human decision — rotation" now has a number attached: 1.82 % of the title's pixels, in a signature that can be recognised.

The leaf composition is decoded and implemented — and it does not close the 1.82 %

The Decoder decoded the rule I refused to guess: draw the leaf on its own timeline; do not multiply the parent's alpha in. Multiplying is refuted, not merely unsupported — at the fitted time the parent has expired (its group returns to 0 at t=250 and holds), so leaf × parent / 255 predicts zero for both quads and the sweeps would be invisible. They are drawn.

The fit is worth repeating because of its shape: the game's own composed alpha is observable in the per-draw vertex colours (C3FFFFFF/B6FFFFFF = 195 and 182), fitting only those two numbers against the two leaf ramps gives one consistent time t = 355, and the same t then predicts quad centres at 981 and 478 against 992.0 and 467.2 measured. No x entered the fit.

Implemented: _draw_leaf runs the leaf unclamped — like the spinning ring, and for the same reason. Held at its own rest.t the leaf sits at x = 1521, entirely off the right edge, so holding would delete the sweeps rather than settle them.

🔴 And it changes nothing measurable. The title is still 1.82 %

t units differing
4.35 s 261 1.82 %
5.917 s 355 1.81 %
7.00 s 420 1.79 %

At t=355 my interpolation puts the leaf's top-left at x ≈ 324 — off-screen left — where the Decoder's model puts the quad's centre at 981. Those cannot both be right, and the gap is not something to tune away: it is a disagreement about how the leaf's keyframes become a placed quad, most likely in the pivot and the rotation about it. Handed back with both numbers.

So the honest state: the exporter no longer drops the data, the composition rule is implemented as decoded, and the port's largest oracle gap is exactly where it was. Fixing the export was necessary and was not sufficient.

Two flagged elements deliberately not drawn

authored/rendering.json narrows drawing to ptloop01/ptloop02 — what the decode covers — while the exporter keeps flagging all 15.

  • title_jp/ptlogo_eff2 — parent 125 % at (412,96), leaf 100 % at (402,66). The same shape, and the element DECISIONS has recorded since P1 as the largest render disagreement in the export. The Decoder said plainly "I have not tested it." Drawing it would extend a decode past the case it was fitted on — the failure this port has made four times this session.
  • pgloading_loop5 — leaf scale (0, 0). A zero scale is one of the three historical failures this corpus names, so it is exactly where a guess costs most.

⚠️ And neither can be adjudicated here. title_jp has no oracle capture, and verify-screen compares against sylpheed-cli, which draws no leaves at all — so any leaf drawing increases that divergence whether it is right or wrong. Its max went 155 → 232 when they were drawn, and that number is not evidence in either direction. Recognising it as uninformative is the reason it did not become a justification.

The 324 was the old keyframe association, and the corrected one is available today

The Decoder found it and it is not geometry: my leaf path pairs each pose with the next pose's time. Fed my pairing into its own interpolation, it reproduces 324 to the digit. Corrected, t=355 gives top-left 781 and centre 980.5 against 992.0 measured — so there was never a 1 300 px pivot mystery to look for, and I am glad I said the two numbers instead of hunting one.

🔴 Correcting BLOCKED: this was never gated on a branch landing

docs/port/BLOCKED.md has carried the keyframe-record-layout row as "NOT taken, and it cannot be taken yet … sylpheed-formats is a path dependency, so this reaches the port only when that branch lands on main."

That is wrong. The corrected association is in this checkout, behind an environment variable:

// Set `SYLPHEED_KF_TIME_SHIFT=1` to read `W[k-1]` as block `k`'s time
// ("the word is the time the NEXT pose is reached") instead of `W[k]`.

It has been switchable the whole time. I read that file twice this session — once for rotation_deg, once for the leaf note — and did not notice the switch.

The experiment, run: mixed, and not decisive for the reason that matters

Re-exported with SYLPHEED_KF_TIME_SHIFT=1 and asked the oracle:

screen default shifted
main_menu 13.21 / 0.06 % 13.81 / 0.10 % — worse
extras 13.38 / 0.20 % 13.95 / 0.24 % — worse
title 21.07 / 1.82 % 20.41 / 1.86 % — RMSE better, area worse
publisher_logo 10.77 / 1.00 % 9.05 / 0.75 % — better
developer_logos 9.37 / 0.39 % 8.86 / 0.33 % — better

⚠️ And it does not adjudicate the association, because the port's renderer is built for the other one. Under the shift the untimed keyframe moves from last to first — the leaf reads t=None at x=639, then t=150, t=540 — while pose_at is written around "the final keyframe carries no t, so it is given a synthetic time exit_ramp_units after the last timed frame." So this measures a renderer/association mismatch, not the association. BLOCKED said as much: the change touches pose_at, settle_units, spin_period_units, exit_ramp_units and the plate.

Export reverted to the default. Adopting the shift is a real piece of work — re-deriving the exit ramp, the settle, the spin period and the plate against a layout where the untimed frame is the first — and doing it hastily at the end of an iteration is how a 1.8 % gap becomes five wrong ones.

⚠️ The methodological point, which is the Decoder's and is the best thing here

Its rule matched because alpha at t=355 sits inside a long segment where a one-keyframe shift barely moves it, while x sweeps 1 560 px over the same span. It confirmed on the insensitive quantity; I was wrong on the sensitive one; and neither of us saw it until the two were compared.

Check a new interpretation against the fastest-moving field you have, not the one that happens to agree.

That is a different failure from the ones this session has collected — not an uncontrolled instrument, but a control chosen where it could not fail.

A residual 11.5 px (980.5 against 992.0) is left over and is not to be fitted. A rotation about a declared pivot rather than the centre would displace by roughly that; if it still matters once the association is adopted, it gets measured rather than derived.

Re-running the P5/P6 gate after eight iterations of changes

The leaf path, the plate pulse, the BGM level, the voice export, the focus fix and a new diagnostic flag have all landed since the gate was last actually run. The mission's test is "a human presses a d-pad and Ⓐ and moves through those screens", and I had been verifying pieces of that against captures without once re-running the whole walk.

It works. Nine steps, unattended:

step
×4 ptbtn01ptbtn05 focus moves, every frame drawn
EXTRAS → screen extras, focus ptbtn11
back to main_menu, focus restored to ptbtn05 HANDOFF Q5's rule, live
⬆, Ⓐ ptbtn04 OPTIONS, destination outside this archive prints the gap rather than pretending

Filmstrip shared as 1788027380-788b1faafc3e. Every shot is non-blank (frame means 31.533.1).

The sound is verified by a null control, not by a detector

I first tried an onset detector: count sharp level rises in the recording. It found one onset in the walk — and the same one in the music bed alone, which contains no cues. The 50 ms envelope cannot see a short cue over the bed. ⚠️ The right response to a detector that cannot separate its control from its subject is to stop using it, not to widen the window until the numbers look better. That is the shape this session has hit five times.

So the same technique that settled the voice: shadow the three cues with silence through data/mods/, run the identical walk, and compare.

peak RMS
walk, cues playing +0.0003 dBFS 18.36
walk, cues silenced 4.74 dBFS 20.78

Silencing three sound effects costs 4.74 dB of peak and 2.43 dB of RMS, and the walk's peak is set by a cue rather than by the music. The mod log confirms exactly three files shadowed. No detector, no threshold, and the control is the same run with one input changed.

⚠️ Recorded under the Dummy driver, as everything audio here is. It shows the cues reach the Master bus at the right moments; it does not show they are the cues the game plays — that is HANDOFF Q8, and authored/audio.json still carries the offsets as measured rather than decoded.

Pinned formats-pin-2026-08-29c — and the knob I tested last iteration was retired

🔴 I tested the wrong switch. SYLPHEED_KF_TIME_SHIFT is a superseded partial fix: it got the association right but left pose 0 untimed, which is exactly why the untimed keyframe appeared to "move from last to first". It does not exist in the current parser. The real correction is the default in the tagged crate, with the old reading behind SYLPHEED_KF_TIME_LEGACY=1 — the opposite polarity from what I had.

So last iteration's five rows measured a renderer/association mismatch against a knob nobody should use. I suspected they were not decisive; I did not suspect the knob itself was retired.

The consequence is smaller than I budgeted for: there is no untimed keyframe

A placement group is an 8-byte header then frames × {u32 time; 36-byte pose}, so pose 0's time is the group's lead-in word and every pose is timed, including the last. Measured on the re-export: 866 keyframes, 0 untimed.

pose_at's premise — "the final keyframe carries no t, so it is given a synthetic time exit_ramp_units after the last timed frame" — does not invert, it disappears. The branch is now dead code rather than wrong code, which is why nothing needed re-deriving to adopt this.

And the leaf reads as the Decoder's table says: t=0 x=639, t=150 x=39, t=540 x=1521. At t=355 that interpolates to x = 781 — the top-left it predicted, and the 1 300 px discrepancy is gone.

Pinned by tag, which is what MISSION §2 is for

"The RE agent tags when it lands something you need and tells you over the message channel — that is how you stay current without floating."

That is precisely what happened, so crates/sylpheed-export/Cargo.toml now pins formats-pin-2026-08-29c by tag. ⚠️ BLOCKED.md was wrong in both directions — it said the change "cannot be taken yet" and that it arrives only when the branch lands on main. It arrives when the tag is pinned.

🔴 The cost, stated rather than discovered later: sylpheed-cli builds from the workspace crate, so until this reaches main the exporter and the reference renderer read different decoders, and verify-screen is comparing two eras rather than detecting drift. verify-capture is unaffected — it compares the port against oracle captures and never touches the CLI — and it is the check that matters. Revert to the path dependency the day the tag is an ancestor of main.

What the oracle says

screen before after
publisher_logo 1.00 % 0.75 %
developer_logos 0.39 % 0.33 %
extras 0.20 %, region 736×525 0.19 %, region 398×295 at (441,230)
main_menu 0.06 % 0.06 %
title 1.82 % 1.82 %

The splashes improve outright. ⚠️ extras is the interesting row: the area barely moved but its differing region collapsed onto the sweep position (441, 270) — the residual is now localised to the one element still in question rather than spread over the screen.

The title does not move. Its row is now posed at t=355, the Decoder's fitted sweep time, because the leaf group ends at t=600 with the quads parked off-screen at x=1521 — posing at the settle simply omits them. ⚠️ t=355 is not the time that minimises the difference: t=390 measures 1.65 %. Picking that would be fitting the pose to the score, which is what this harness exists not to do.

Refuted — my own "the single non-whole-multiple scale in the export"

DECISIONS has said since P1 that ptlogo_eff2 is "the single drawn element in the whole export at a scale that is not a whole multiple of 100 % (125 %)". That census was parents-only. Opening the 45 leaves finds thirteen distinct non-whole-multiple scales — 75, 96, 99, 101, 103, 112, 125, 150, 204×208, 210×220, 250 — and 125 % is among the rarest at two occurrences. ptlogo1/ptlogo2 carry 101/103/112 on the English title.

The claim's real content was "the only one the port draws" — a fact about my element set, not about the disc. Corrected.

🔴 And ptlogo_eff2 stays withheld, now for a stronger reason than caution. Its 125 % is a pop, not a steady scale: scale-0 → 125 % → scale-0 between t=50 and t=107, ≈0.95 s. The leaf draws at 100 %, as two superimposed copies at alpha 160 and 80, each rotating 360° over 960 units — 16 s per revolution. If parent scale gates the leaf it is a 0.95 s flash; if the leaf runs free it spins for 16 s. Nothing on the disc chooses, title_jp has no oracle capture, and the Japanese-locale capture MISSION has parked is what would settle it.

The 11.5 px was the fit's resolution, and the lesson inverts

The Decoder closed it by adding observables, not by tuning — the vertex buffer carries positions and colours at the same instant, so all four quantities must agree on one t:

observable solved t precision
quad A x 357.88 ±0.12 units
quad B x 357.58 ±0.12 units
quad A alpha 355.75 ±1.54 units
quad B alpha 354.09 ±1.89 units

Alpha moves only 0.270.33 levels per keyframe unit, so one byte of quantisation is worth 1.51.9 units, which at 4 px/unit is 68 px of sweep. That is the whole of the 11.5 px. At t = 357.7 the centres land within 0.70 px and both alphas inside one level.

⚠️ The lesson is the earlier one inverted, and this is the half worth keeping. Checking a wrong rule against alpha made it look confirmed. Here the same insensitivity manufactured a residual that did not exist. So an insensitive quantity does not merely fail to falsify — it invents error. Solve on the fastest-moving field; check the slow one; never the reverse.

I was already looking for a pivot rule to explain 11.5 px when they wrote. There was nothing to find.

Refutation attempt — the pivot claim, checked here and survived with a nuance

They state the leaf pivot is (200, 90) on a 399×180 sprite, "the pivot is the centre, so rotation displaces it by nothing." Checked against my own export: pivot [200, 90], sprite 399×180, true centre 199.5, 90.

Survives. ⚠️ With one correction of no consequence: the sprite is odd-width, so the pivot is the centre to within half a pixel, not exactly. Against their 0.70/0.48 px agreement that changes nothing, and it is worth stating only because "displaces it by nothing" is the kind of sentence that later gets leaned on for a sub-pixel claim.

verify-capture now poses the title at t=357.7 rather than 355: RMSE 21.07 → 20.92, differing 1.82 % → 1.81 %. Marginal, and it is the right pose for a stated reason rather than a better number.

ptlogo_eff2 is withheld for a better reason than mine

I was withholding it out of caution about untested generalisation. The Decoder points out something stronger: it is on title_jp, and MISSION §7 scopes out "localisation beyond English" — so it is not a question the menu port has to answer at all, and the parked Japanese-locale capture does not need reviving on its account. authored/rendering.json now says that first and the undecidability second. Widening scope to close a residual would have been the wrong trade, and it is the human's call either way.

🔴 The focus ring had silently stopped, and BLOCKED had listed it

docs/port/BLOCKED.md said the record-layout change touches five things: pose_at, settle_units, spin_period_units, exit_ramp_units and the plate. I checked pose_at and deleted exit_ramp_units, reported that, and did not work the rest of the list.

spin_period_units required "the first timed and the second untimed". Under the corrected layout the ring reads t=0 rot=0 and t=120 rot=360both timed — so the rule returned 0 and the focus ring stopped spinning. Nothing reported it: a period of 0 is a legal "this element does not spin".

Rewritten to take the span between the two poses. On the ring that is 120 0 = 120 units, the same number the old rule produced, which is a small piece of evidence that the corrected layout is self-consistent rather than merely different.

Verified the way P5 verified it — bit-identity one period apart, on the ring's own 60×60 box so the ptloop sweeps cannot confound it:

separation mean difference
+120 units (one period) 0
+120 units again 0
+30 units (quarter) 8.61
+60 units (half) 8.88

⚠️ Getting there took three wrong instruments, and the sequence is the lesson. A whole-frame max saturates on one rotating edge — adjacent frames scored 131 while their mean was 0.022. A live --menu filmstrip jitters by up to a frame, which is ~3° of ring, and its cadence cannot be pinned. And a whole-frame comparison is dominated by the sweeps, which move 480 px over one ring period. --focus=<id> was added so a --screen run can draw a focus record deterministically, which is what made the check reproducible at all.

The plate's period is now the disc's 105, and it disagrees with the measurement

Under the corrected layout ptbtn00f runs t=0 (alpha 0) → t=105 (alpha 0) — a closed cycle with every pose timed.

The ambiguity this entry carried is gone. It used to say the cycle might restart at t=6 rather than 0, giving 123 units, and that nothing separated the two. There is now one reading.

🔴 And the number is worse against the oracle, which is stated rather than avoided. 105 units is 1.750 s; scaled by the factor the ring shows between its declared 120 units and its measured 2.177 s (×1.089), 1.906 s — about 17 % below every one of the corpus's four timings (2.12 / 2.19 / 2.34 / 2.31 s). The old 129 gave 2.34 s, at the top of that range, which is exactly why it looked right.

129 was the last timed keyframe plus exit_ramp_units, and that constant is deleted. A period built from a constant that no longer exists cannot stay, even though it fitted better. So the port ships the disc's number and says it is wrong, rather than keeping a number that agreed with the measurement for a reason that has evaporated.

Verified: the plate is bit-identical 105 units apart (mean diff 0) and differs at 30 units (0.83). What would resolve the disagreement: whether the group loops from its start at all, or holds at alpha 0 between cycles. Asked.

The plate's period is 120, decoded — and it was falsified with my own ring number

The Decoder found it in the format: a nested record is itself a RATC bundle, and its header's +0x08 is the loop length — the same field ui_header_time_disc already tests at the top level. A record's keyframes need not fill it, and the slack is a hold at the final pose. ptbtn00f is 105 units of ramp inside a 120-unit cycle, so the glow rests dark for 15 units. The five ptbtn0Nf records fill their 120 exactly, which is what shows the slack belongs to that record rather than to the format.

Disc-wide over 1 781 timed nested records: 92.3 % declare exactly their last keyframe time, 7.7 % declare more, 0 declare less. That last row is the falsifier — a cycle cannot restart before its own last pose — and it never fires.

The decisive test used this port's number, not theirs

Both candidates need the same emulator pacing factor, and the ring measures it independently: declared 120 units → 2.177 s → 1.0885.

plate period nominal factor needed for the measured 2.122.34 s
105 units 1.750 s 1.211 … 1.337 🔴 excludes 1.0885
120 units 2.000 s 1.060 … 1.170 contains it

105 cannot reach the measured range under any pacing the ring also satisfies. Two different elements in different bundles, measured in separate runs, tied only by both declaring 120.

⚠️ My three readings of this number, in order, are the useful record: 129 (105 + exit_ramp_units) fitted the measurement for a reason that later evaporated; 105 (the group length) I shipped knowing it was 17 % short; 120 is decoded. And the 123-vs-129 pair I once called unseparable straddled the right answer without containing it — which is the sharpest argument I have seen against treating "two candidates, nothing separates them" as if the truth must be one of the two.

Verified: bit-identical 120 units apart (mean diff 0), 0.061 at a quarter and 0.888 at half. Still authored, because the pinned tag does not expose +0x08 yet — delete the entry the day a tag does.

Their rest() flag, checked rather than assumed

They warned that a focus record is exactly the kind of element rest() mishandles, since a pulse's last hold is not its resting state. Censused: 34 focus-record elements in the export, and only 2 have a varying alpha — both ptbtn00f, EN and JP. Their rest.alpha is 80, identical to their peak, which is precisely the pathology described. The port does not hit it because the plate is drawn through the loop path, and the other 32 are constant-alpha, so rest() is safe for them. Bounded, not hoped.

A settled screen is ONE instant, and it collapsed three residuals at once

The Decoder's finding, applied: rest() returns each element's last hold keyframe chosen independently of every other element. That is right for anything that ends the screen settled and exactly wrong for a transient — the title's ptlogo_back2eff1 is a two-frame flash (0 until t52, 255 at t5456, 0 by t58), so its last hold is the flash peak and rest() left it burning. There are five of them, and rest() drew all five at once.

The settled instant is the longest interval containing no keyframe time, over a bundle's top-level elements. Reproduced independently here before adopting: title [160, 236], midpoint 198 — the Decoder's number to the unit. ⚠️ The top-level restriction is what makes it match: including the ptloop leaves gives [269, 540] instead.

Against the oracle

screen before after
title 20.92 RMSE, 1.81 % 14.61 RMSE, 0.26 %
publisher_logo 9.05, 0.75 % 2.17, 0.01 %
developer_logos 8.86, 0.33 % 3.05, 0.01 %
main_menu 0.08 % 0.08 % — unchanged, window too narrow
extras 0.19 % 0.19 % — unchanged, window too narrow

Seven times fewer differing pixels on the title, seventy-five times fewer on the publisher splash, whose differing region is now a 13×18 box. This is the largest correctness gain the port has had, and none of it is mine: it is a decode, computed from the keyframe table with no reference to any capture.

⚠️ It is applied only where the window is wide, and that bar is not invented

The widths in this export split with nothing in between: press_start 214, publisher_logo 190, developer_logos 145, title 76 — then main_menu 12, extras 12, the loading screens 8 and 4. A 12-unit "settle" on a menu that builds in until t=70 is a gap between staggered ramps, not a settled pose.

The bar is 30 units: the Decoder's disc-wide census puts the knee there (30 % of bundles ≥ 30, 42 % under 10, the latter mostly loop* fragments meant to be in motion), and this export's own screens sit 4× either side of it with nothing between 12 and 46. Two independent populations agreeing on where to cut is what makes it a bar rather than a preference.

Checked unbroken: the boot pacing is unmoved (developer_logos@4.26, title@7.91, developer agrees) and the scripted walk still runs end to end with focus restored.

Their census, and a framing of mine they sharpened

I reported "34 focus-record elements in the export, only 2 with a varying alpha". Disc-wide it is 210 varying, 202 with rest() at the peak, across 1 130 focus records — 116 in GP_DEBRIEFING_PILOTLOG, 54 in GP_MOVIE_THEATER, 30 in GP_HANGAR_ARSENAL, 8 in GP_LEADERBOARD, and 2 in GP_TITLE.

My 2 is right because GP_TITLE has 2. ⚠️ But "only 2 have a varying alpha" reads as a fact about the format and is a fact about one pak — and the pathology sits in exactly the screens a wider port reaches next. The sentence was true as measured and false as remembered, which is the failure this corpus keeps finding, and it was mine this time.

⚠️ And they corrected a framing I had: I called rest.alpha == peak "the pathology". It is worse than that — a pulsing element has no resting pose at all. The question rest() answers is malformed rather than mis-answered, because the element's state is a phase, not a value. pose_at(t) with t inside the record's own declared cycle is the only well-formed query on one.

🔴 Worth carrying for whenever this port grows: GP_LEADERBOARD's py_ranking_btn01f swings 255 → 127 → 255 with no two adjacent keyframes equal, so rest() falls through to its longest-dwell rule and returns 244 — neither peak nor trough. A glow stuck at its peak is visibly wrong; one stuck at 244 of a 127255 range looks entirely plausible, and nothing reports it.

And a free second instance of the loop-length decode from a pak neither of us was looking at: py_ranking_btn01f's ramp ends at t=90 inside a declared 120 — 30 units of hold, the same shape as the plate's 105-in-120.

Their "do not draw all five flashes" flag — checked, and it does not apply here

The Decoder armed a draw capture before the title exists and caught the build-in on the console: ptlogo_back2eff1 in frames 130131, eff2 at 133, eff4 at 133135 — and eff3 never drawn at all. Not a miss: a flash's peak is 2 keyframe units, which at that run's pacing is 0.85 of a presented frame, so which flashes get sampled is a matter of phase. The console shows a subset on any given play, and the warning was that drawing all five would read heavier than the real thing.

Checked rather than reasoned about. Sweeping the port's build-in with --no-hold:

t flashes drawn
54, 56 eff1
58 eff2
60 eff2, eff3
62 eff3, eff4
64 eff4, eff5
66, 68 eff5

The port draws them sequentially, never more than two at once, and the two only overlap where their declared windows abut. That is the stagger the disc declares, not a pile-up — the pile-up was the rest() bug, and it is fixed.

⚠️ So the difference from the console is presentation rate, not content. At 60 fps each 2-unit flash gets ~2 frames; at the console's pacing it gets 0.85, so some are skipped. A frame-by-frame comparison of the build-in against a console capture will therefore show flashes the console missed, and that is a fact to know rather than a bug to fix. It is also why the settled-frame comparison — the one verify-capture makes — is unaffected: at t=198 none of the five is drawn.

Three things of theirs worth recording

My top-level restriction was verified, not merely accepted: top-level [160, 236] width 76, versus [269, 540] width 271 with the ptloop leaves included — an instant after every top-level element has exited. Worth having on their page, because the rule as described permits the wrong reading.

The 120-unit loop is confirmed from the guest's own vertex data, not inferred from pixels: the glow quad's per-vertex colour alpha is the element's fade alpha, giving an observed range of 0…80 against a decoded peak of 80, exact and unfitted, over 20 cycle starts. Fitting the decoded ramp gives RMS 13.16 alpha levels against 38.18 for the same ramp reversed — if the shape carried no information those would be equal.

📌 A trap noted for whenever this port grows draw-stream tooling: a 2D draw's identity is its vertex geometry, not its bound texture, because these sprites sample large shared pages. Matching on texture dimensions told them first that no flash is ever drawn, then that ptbase2 and pteff04 are drawn in frames 75105 — which are the intro movie, whose YUV planes are 640×360 targeting 1280×720. Two errors, opposite directions, one pass, neither loud.

The publisher_logo residual was a missing black hold, and we had both dismissed it

I had carried this as "0.03 s outside a composite bound, probably a property of the bound rather than the game", and the Decoder agreed. We were both wrong, and the way it was settled is the point: I stopped reasoning about the bound and filmed the transition.

At 0.05 s the port fell straight out of the publisher's fade into the developer logos — mean 5.06 → 0.32 at t=4.20, then 5.65 at t=4.25. There was no black frame at all, where the oracle measures a 0.170.23 s pure-black plateau (HANDOFF Q7). The bound was fine. The port was missing a fifth of a second of black, and had been since P3.

Authored at 12 units, because on the boot path there is nothing to read it from: publisher_logo and developer_logos each carry a single palogo_eff0 — a 1280×720 primitive with one keyframe at t=0, static, not a transition ramp. The menus' quad declares black for 12 units and 12/60 = 0.200 s sits in the middle of the measured range, so the number is the disc's where a screen has one.

Filmed after: t=4.25, 4.30, 4.35, 4.40 all at mean 0 — four black frames, 0.20 s — then the developer logos at 4.45.

before after
publisher interval 4.26 s, DIFFERS 4.47 s, agrees
developer interval 3.62 s, agrees 3.73 s, agrees

The settled-frame comparisons are untouched, as they should be — this is pacing, not pixels.

⚠️ The lesson is about the shape of the dismissal, not the number. "A 0.03 s miss against a bound composed from two measured ranges plus jitter slack is more likely a property of the bound" is a plausible explanation, it was offered and accepted by both of us, and it was wrong. The composite bound was the reason the miss looked small — the underlying gap was 0.2 s — and a plausible explanation for a small number is exactly how a real defect stays hidden. The film cost one command.

ptlogo_back2eff3 — recorded, deliberately not acted on

The Decoder has reproduced across two independent build-ins that the console never draws eff3: 0 draws against ~5 expected, while eff1 gets 4, eff2 3 and eff4 6. Three explanations are ruled out — sampling phase (eff3 is non-zero over six units against a 2.23-unit step, and frames at t=60.1 and 62.3 sit inside it drawing eff2 and eff4 instead), a draw the log cannot see, and a bad position guess (no quad anywhere is within ±30 of the expected 408 width; the spectrum jumps 262 → 748).

But why is not established — nothing in eff3's record differs from its neighbours: same kind 0x0, same keyframe shape, same u4/u8, same scale.

🔴 So the port keeps drawing it, and that is a decision rather than an oversight. Dropping an element the disc declares, on a measurement with no mechanism behind it, is authoring a behaviour neither agent can derive — and nothing this port gates on would notice either way: the flashes live only in the build-in, and verify-capture compares the settled frame at t=198 where none of the five is drawn. Acting would buy no measurable fidelity and cost an unexplained exception in authored/.

What would change it: a mechanism in the record, or a gate that measures the build-in against a capture. Until then the port is visibly wrong for two frames during a build-in nobody compares, which is the cheaper of the two wrongs.

The narrow settle windows are harmless, and I can now say why

Adopting the settle instant left main_menu and extras on per-element rest() — their windows are 12 units, below the 30-unit bar — and I recorded that as a gap. It is not one. rest() is malformed only for a transient, and the transients are precisely on the screens whose windows are wide:

screen window transients
title 76 ptlogo1/ptlogo2 ×4, pteff01, ptlogo_back2eff1…5, ptlogoall_eff
publisher_logo 190 palogo_sqex_eff
developer_logos 145 palogo_*_eff ×3
main_menu 12 none
extras 12 none

The discriminator is returning to dark far before the screen's own endptlogo_back2eff1 is dark again at t=58 while the title runs to t=269 — as opposed to the ordinary fade-in-hold-fade-out that every menu element has, where rest at 255 is the settled pose and the final 0 is the exit.

⚠️ I am not claiming that as a rule. Two screens having narrow windows and no transients could be coincidence; it is n = 2, and n = 2 is where I have gone wrong before. What it does mean is that nothing measurable is being left on the table by the 30-unit bar today.

Refuted, mine — "the menu residual is localised on the ptloop sweeps"

I have written that twice. It came from reading the bounding box of the differing pixels, which sat at (441, 230) — the sweep position. Tested by sweeping the leaf's phase against the live-menu captures:

main_menu, sweep phase differing
t=60 (barely on screen) 0.063 %
t=350 (mid-screen) 0.183 %
t≥600 (parked off-screen) 0.061 %

Two things fall out. The capture shows no sweeps: the port matches best when they are off-screen and three times worse when they cross the middle. And with them parked the residual's box is 834×358 at (445, 167) — the button column, which is where the focus signature lives, not the sweep position.

On extras the same test moves the box (398×295 at the sweep position at t=70, 736×525 spread at t=700) while the magnitude barely changes: 0.192 % against 0.200 %.

🔴 So the box moves with the sweeps and the residual does not. A bounding box over scattered pixels tells you where the outermost differing pixels are, not where the difference is — and I had been quoting it as if it localised a cause. verify-capture prints that box, so this is a caution about reading my own tool.

And a first piece of evidence on whether the sweeps loop

The disc gives one pass, t=0…600, ending parked off-screen at x=1521. ORACLE-CAPTURES.md says the title's sweeps "move continuously", which I had taken as implying a loop. The idle main-menu capture matches best with them off-screen, which is evidence they run once and park.

⚠️ One capture, one screen, and "best match" is a weak instrument for an absence — but it is the first evidence either way, and it points against looping. The loop-length field the Decoder decoded (+0x08 of a nested record's header) would settle it outright; it is not in the pinned tag.

Refuted — "the developer splash is one composited quad, the bounding box of the three logos"

The Decoder observed the game submitting one 525×259 quad at (378, 155) on the developer splash and read it as the bounding box of the three logos, warning that drawing three sprites there draws something the console does not. The arithmetic does not support it, and the port keeps drawing three.

bounding box
the three logos 500×421 at (390, 164)
gamearts_eff + seta_eff 521×261 at (379, 154)
the observed quad 525×259 at (378, 155)

A 259-tall quad cannot contain the three logos, which span y 164…585: palogo_anima alone starts at y = 449, thirty-five pixels below that quad's bottom edge. The observed quad matches the union of the two _eff glows to about four pixels in every dimension.

⚠️ And those two are transients — my own census flagged them, dark again by t=45 — so a frame containing that quad is a build-in frame, not the settled screen. Consistent with a draw capture that starts early, which is exactly what theirs does.

I cannot see their draw stream, so I have sent the arithmetic rather than a verdict. What I will not do is stop drawing an element on a claim whose stated identification excludes that element from its own bounding box.

The black hold is 9 units, not 12 — measured in draws rather than luminance

I authored 12 from HANDOFF Q7's luminance plateau of 0.170.23 s, supported by the menus' transition quad declaring black for 12. The Decoder counted submitted quads instead, which is the better instrument: luminance cannot separate the outgoing fade's tail from true black.

Frames 21125 submit palogo_sqex fading to alpha 7; frames 126129 submit no sprite quad at all; 130153 fade the developer splash in from alpha 34. Four presented frames at 2.284 units/frame — a rate derived from the disc as its own clock, because that run ran at 13.1 fps against 28 elsewhere — gives 9.1 units = 0.152 s, ±1 frame 6.911.4.

⚠️ It disagrees with the luminance figure and the disagreement is the point. 0.1140.190 s against 0.170.23 s overlaps only at the top, and the true black is shorter than 9 even so: both boundary frames still carry picture. My 12 was also supported by analogy — a different screen's quad on a different path, where the boot splashes carry no quad at all. A number that fits by analogy loses to one measured in place.

verify-dwell's bound moved with it. Both screens still agree: publisher 4.42 s, developer 3.78 s.

The title's sweeps loop — measured, and the field could not have told us

The disc gives one pass (ptloop01 t=0…600, ptloop02 t=0…720), each ending parked off-screen, and the port ran them once. The oracle says they loop: across two title dwells the sweep quad oscillates over its whole x range and resets hard to the same start — one reset in the first dwell, two in the second.

🔴 And the loop-length field could not have settled it, which corrects a hope I had stated. Both records declare exactly their last keyframe time — slack zero — and "loops at 600" and "runs once for 600 and stops" write the identical header. 92.3 % of records on the disc are in that state, so the field discriminates only where there is slack, as the plate's 105-in-120 had.

Implemented and verified on the two sweeps' least common multiple, since they have different periods: 600 and 720 realign at 3600 units = 60 s.

separation mean difference
+3600 units (LCM) 0
+1800 units 0.438
+600 units (ptloop01 only) 0.100

⚠️ Scoped to the title, because that is where it is measured. The menus declare the same 600/720 and nothing on the disc distinguishes them, but my own weak evidence points the other way there — sweeping the phase against live-main-menu.png, the port matches best with the sweeps off-screen (0.061 %) and three times worse mid-screen (0.183 %), and if they looped the sweep is on screen for roughly 73 % of the cycle. Two weak signals in opposite directions is a reason to scope, not to pick.

The menus' residual is the tone floor, not structure — and extras is not really 3× worse

extras sits at 0.19 % differing against main_menu's 0.06 %, on two screens of the same family, and that gap wanted explaining.

Signed difference (port capture), by cell:

x=0 x=320 x=640 x=960
extras y=169 +12.13 3.64 +2.43 +10.60
extras y=338 +12.29 +1.36 +1.42 +9.05
main_menu y=169 +11.63 0.68 +3.01 +10.24
main_menu y=338 +11.03 +3.93 +2.92 +8.84

The two screens are nearly identical, and the port is uniformly +9 to +12 brighter in the dark outer columns — which is exactly the transfer curve I measured earlier: γ > 1 in the darks, capture darker than render. There is no dipole, no displacement, no missing element.

So the 0.06 % / 0.19 % gap is not a difference in fidelity. The thresholded count only sees pixels differing by more than 64 levels, which are text and sprite edges; the two screens simply have different amounts of high-contrast edge. The level disagreement, which is what a tone term produces, is the same on both.

⚠️ This is the bounding-box lesson again in a different costume. I had two numbers, 0.06 and 0.19, and took the ratio as meaningful. It is a count of threshold crossings, and a count of threshold crossings is not a measure of how wrong a screen is.

A diagnostic trap of my own, worth writing down

My first pass at this reported 10 of 18 elements "transparent at rest" on extras — the buttons, the title, the frames — and looked exactly like a missing-element bug. It was not. --screen=NAME without --time renders at t = 0, and pose_at clamps t to minf(t, settle_units), so t=0 stays t=0 and every element is still at its first keyframe. Passing --time=2.0 draws 18 of 18.

The tool was right and my invocation was wrong, and the failure looked like a serious defect rather than an empty argument. Same family as the instrument traps this session has collected — and mine was the one that reported a worse problem than existed, which is the direction that wastes an iteration rather than hiding one.

Refutation attempt — their 239.8-unit figure, checked from my export

The Decoder converted the boot's black gap using the disc as its own clock: "palogo_sqex declares alpha ≥ 1 for 239.8 units and is drawn in 105 frames → 2.284 units/frame." That 239.8 comes from their reading of the record; I have the same element in my export and can compute it independently.

palogo_sqex ramps 0 → 255 over t=15…30 and 32 → 0 over t=251…255. Under the linear ramp the port already uses, alpha first reaches 1 at t = 15.0588 and last exceeds it at t = 254.8750:

239.816 units.

Survives, to four significant figures. It matters more than a spot-check: that number is the denominator of the units-per-frame conversion behind the 9-unit black hold I just authored, so an error in it would have propagated straight into a constant I ship. Two derivations from different sides of the same record agreeing to 0.02 % is what makes that constant safe to hold.

🔴 The loading screens are black at every instant — which proves the layer rule wrong for a layerless element

build_12 and build_15 have rendered blank since P3, and I had filed it as a rest() casualty: pgloading_eff00 is a 1280×720 quad whose rest is opaque black. It is not that, and the corrected keyframe association makes it provable rather than suspected.

Every element's declared alpha window on build_12:

element opaque / visible gone by
pgloading_eff00 (black quad) t=0 … 38 at alpha 255 clears at t=48
pgloading_loop4 t=8…32 38
pgloading_loop1 t=16…32 40
pgloading_line t=18…26 32
pgloading_str t=22…28 34
pgloading_loop3 t=24…26 34
baseeff, eff01, eff02, loop5 t≈16…32 3240

The quad is fully opaque across the entire span in which any content is up, and it only clears at t=48 — by which time every other element has faded to zero. Rendered at t = 20, 30, 36, 40, 42, 44, 46, 50 units with the timeline playing, the frame is mean 0 at every one.

So this is not a bad choice of pose. Under the port's current layer rule there is no instant at which this screen shows anything, and a loading screen that is black for its whole life is not what the game does. That is a proof by contradiction, not a preference.

The rule under suspicion is mine, and it is narrow

pgloading_eff00 carries layer_source: "none" — no layer key at all — and the exporter sorts a layerless element last, i.e. on top. Its paint_order is [6, 7, 8, 9, 1, 5, 4, 2, 3, 0] with element 0, the quad, drawn last.

⚠️ Every other full-frame primitive in the export has a layer keymain_menu, extras and title all give their pteff00 0x00008030 — so this rule only ever bites here, which is exactly why it survived eight milestones behind two screens nobody draws.

Where a layerless element sorts is a decoding question and I am not answering it. If the game sorts it first — behind everything — the screen renders and the quad becomes a backdrop rather than a cover, which is what a 1280×720 black rectangle at the bottom of a loading screen would sensibly be. That reading is consistent with the contradiction above, which is not the same as being established, and I have asked rather than flipped the sort.

What this does settle: verify-screen's BLANK verdict on those two rows was the right call. It reports that both renderers drew nothing and that the row proves nothing — and sylpheed-cli agrees with the port here precisely because it shares the assumption under suspicion. Two renderers agreeing, again.

Their eff3 retraction — my refusal was right, and my refutation found the same bug

The Decoder has withdrawn "the game never draws eff3". It draws all five, in the declared stagger, in both title entries. And the mechanism was the one my developer-splash refutation had already caught one layer down: a draw batches several quads and the log dumps only the first 8 vertices, so min/max over a line's vertex list merges them. eff3 (788…1196) lies entirely inside eff4 (447…1196), so the union is exactly eff4's extent and eff3 vanished with nothing anomalous to see. My 525×259 was gamearts_eff merged with seta_eff by the identical mechanism.

⚠️ The part worth carrying is theirs: three explanations were reported "ruled out", and all three were aimed at the wrong failure — the "a draw the log cannot see" check counted draws with no geometry when the hiding place was draws with partial geometry. Refuting three wrong hypotheses is not evidence for a fourth, and a list of failure modes written by whoever built the instrument is the least likely to contain that instrument's blind spot.

Nothing in the port changes: eff3 was never dropped, and the developer splash still draws three sprites.

The forced backdrop: two of sixteen screens were black for their whole life

build_12 and build_15 — the two dressed loading screens — rendered as pure black at every instant of their declared timeline. Not at rest, where a wrong rest.t could explain it: at t = 20, 30, 36, 40, 42, 44, 46 and 50 units with the timeline playing, mean 0 in every frame.

That is not a defect you can attribute to a pose. A screen that is black for its entire life is impossible on its face, and it is the kind of impossibility that survives a render-vs-render diff: verify-screen scored those two rows max 0 mean 0 over3 0 OKthe strongest verdict that script has, awarded for comparing nothing against nothing. Both renderers were black because both share implied_layer_key. The blank guard now in verify-screen was written after that, and it is what turned the pass into a row that says it proves nothing.

The rule, and whose it is

It is the Decoder's, decoded from the file rather than inferred from the render:

An element that covers the screen and is fully opaque at some instant cannot paint above anything visible at that instant. Where the elements visible during its opaque span are all of them, its position is forced to first.

pgloading_eff00 is a full-screen quad at alpha 255 from t=0 to t=38, clearing at t=48; every other element on those screens peaks around t=832 and is gone by t=3240 — entirely inside the opaque span. Under a layer-key sort it painted over all nine of them, at every instant they existed. Hence black.

What is implemented, and the two limits that are not negotiable

forced_backdrop_first in crates/sylpheed-export/src/screen.rs, as a post-pass over ui_layout::derived_paint_order. Two restrictions are copied from the Decoder verbatim because each one was found by a test that failed:

  • 🔴 Elements with no sprite only. Applied to sprites, the rule claimed 22 .t32 textures must sort first against their own layer keys. An element's alpha says nothing about whether its texture covers the screen — most of a sprite may be transparent. The assertion that caught this was one the Decoder had nearly deleted as over-strict.
  • 🔴 Not a name heuristic. *base* first / *eff* last matches 77 of 80 and fails on exactly the three families that cross it: palogo_eff0, pgloading_eff00, pzeff00. palogo_eff0.prm is named like an overlay and is measured painting first. The name is not the rule; occlusion is.

⚠️ Reach: it assumes straight alpha-over. Blend mode is undecoded, and an additive quad at alpha 255 would not occlude. It is a lower bound on one element's position, not an ordering — 80 elements are forced, 50 are constrained but not forced, and this says nothing about those 50.

The controls

Both are the Decoder's prior measurements off the running game. No new oracle run was made for this change, by either agent.

primitive measured our opaque instants outcome
palogo_eff0.prm first 256 (they measured 211) forced first
pteff00.prm last 2 (they measured 2) still last

pteff00 is the one that would break if this were implemented as "push every layerless element down". It is the fade cover: opaque at t=0 and again at t=269, its screen's entry and exit, and transparent for the 253 instants between. The constraint never binds it, and it remains last on all four title-family screens.

The palogo_eff0 count differs — 256 against 211 — because we take the opaque span to the screen's last keyframe (255) and they stop at 210. It changes no verdict here, since the element is opaque across the whole span either way, but the two spans are not the same span and only one of them can be the screen's. Filed in BLOCKED.

An element holds its final pose to the end of the screen; it does not vanish at its own last keyframe. Reading palogo_eff0's span as 0..=0 — it declares a single keyframe — would make the splash's backdrop a one-instant event rather than the thing on screen for the whole splash. Rendering build_12 confirms the hold directly: the frame is constant from t=30 to t=60 with the timeline running.

What changed, measured

  • build_12/build_15: mean 0 at every instant → ramps in over t=0…30 and holds (mean 1.95, max 214.5). The two BLANK rows are gone from verify-screen.
  • The splashes are unmoved against the oracle: publisher_logo 0.01 %, developer_logos 0.01 % differing region, unchanged before and after. ⚠️ That is non-regression, not confirmationverify-capture poses at the settle instant, and the ordering does not necessarily bind there. The evidence for the rule is the Decoder's two controls and the impossibility of a permanently black screen, not this row.
  • Six verify-screen rows now DIFFER: the six screens the rule touches. The reference sylpheed-cli builds from the workspace sylpheed-formats, which does not have the rule. That disagreement is expected and must not be tuned away — it ends when a pinned tag carries the Decoder's change, at which point this post-pass is deleted rather than kept in two places.

It also explains 36 builds the Decoder had filed as "coming out one colour": pzeff00.prm is forced first in 32 of 32 of them. Those were wiped by our own sort.

Refutation attempt — the forced-backdrop rule's quantifier, and whether it misses a case

The rule fires only when all other elements are visible during the opaque span. That is a strict quantifier, and a strict quantifier fails quietly: an element that is a full-screen opaque backdrop but misses the bar by one dark element would keep its layer-key position and go on hiding the screen, exactly the defect the rule was written to fix. So the question worth asking of somebody else's rule is not "is it right" but "is it enough" — and that one I can test without an oracle, over every layerless full-screen element in GP_TITLE.

screen element opaque instants others visible forced
build_12, build_15 pgloading_eff00 39 9 / 9
developer_logos ×2 palogo_eff0 211 6 / 6
publisher_logo ×2 palogo_eff0 256 2 / 2
title, title_jp pteff00 2 3 / 23
main_menu ×2 pteff00 2 7 / 15
extras ×2 pteff00 2 5 / 17
every screen pteff02 0

The rule survives, and the margin is the reason. Nothing sits near the boundary. Every element that fires does so at 100 % of the others; every element that does not is at 1347 %, and pteff02 never reaches alpha 255 at all, so no quantifier could fire on it. There is no borderline case in this archive for a stricter or looser reading to disagree about — which is the answer I could not have gotten by re-checking the six screens where it already fired.

It also reproduces the Decoder's second control number exactly: they report pteff00.prm forced below 3 of 23, and title measures 3 of 23 here. With their opaque-instant count of 2 already matched, two of their three published figures for that control now come out of an independent implementation unchanged. The third — palogo_eff0's 211 against our 256 — remains the span disagreement filed in BLOCKED.md, and note that the 211 is the number our developer_logos row does reproduce. That is worth saying plainly: the disagreement is not a constant offset, so "they stop 45 instants early" is not the explanation, and whatever it is differs per screen.

⚠️ What this does not test: whether the rule is right about the 50 elements it calls constrained-but-not-forced, and whether alpha-over is the blend mode. Both are the Decoder's to settle. It tests completeness within one archive, which is the half I can reach.

The 256/211 was never a disagreement — and my own census had already said so

The Decoder answered the span question, and the answer is that both numbers are right. palogo_eff0.prm appears on both splashes: the publisher pair (entries 10, 13) runs to t=255 and gives 256 instants, the developer pair (11, 14) runs to t=210 and gives 211. I computed the publisher; their page quoted the developer.

Worth noting how that came out, because it is the one part I can claim: the refutation census in the section above reported 256 on publisher_logo and 211 on developer_logos, in the same table, and concluded "the disagreement is not a constant offset, so 'they stop 45 instants early' is not the explanation". The resolution was already sitting in my own output before their reply arrived. What I got wrong was filing it as a disagreement at all — I compared one of my two numbers against one of theirs and did not check the other row of my own table against it. A per-screen quantity needs the screen named next to it, and my BLOCKED.md row named neither.

The span convention, confirmed rather than assumed

The span is 0 ..= max keyframe time over EVERY element in the build, and an element holds its final pose past its own last keyframe.

That is exactly what forced_backdrop_first implements, so nothing changes in the port. Two things they add that are worth having in writing:

  • the hold is not a convenience: a group holds at its last keyframe rather than looping, and the header's +0x08 never falls short of the last keyframe — the slack is that hold;
  • +0x08 and the elements' maximum are interchangeable, zero disagreements disc-wide. We use the elements' maximum. It stays, and this paragraph is the note that the two were checked equivalent rather than assumed so.

The hold decides 55 % of verdicts, and the oracle picks it

Reported by them over 130 keyless full-screen primitives (their measurement, not reproduced here — their page is docs/re/structures/ui-forced-backdrop.md, ⚠️ not yet on main as of this commit):

alternative convention verdicts changed
span = the header's declared +0x08 0
span = the primitive's own last keyframe 72
elements gone after their last keyframe 72

So the reading I very nearly shipped — span = the element's own last keyframe — would have changed 55 % of the verdicts disc-wide. My first implementation did exactly that, and palogo_eff0 is the case that catches it: a single keyframe at t=0, opaque for one instant, nothing else up yet, so the rule calls it free — against a game measured painting it first. The convention is not a matter of taste; the oracle rules one out.

None of our six verdicts rests on it. pgloading_eff00 is first under all four conventions and pteff00 is free under all four; only palogo_eff0 moves, and only under the convention its own measured order excludes.

The sharper form of the verify-screen failure

Theirs, and it is better than how I wrote it: those two solid-black frames were not two witnesses. Both renderers read implied_layer_key, so their agreement carried no information — a correlated failure is indistinguishable from a confirmation. What caught it was not a second opinion but that the agreed answer was impossible on its face. "Is this result even possible?" beats "do two implementations agree?" whenever the two share an ancestor, and in this project they nearly always do.

The boot gate still holds

Re-run after the reorder, since the rule changed the paint order of the first two screens in the boot path: publisher_logodeveloper_logosADV (skipped at 8.12 s) → title + press_start overlay, plate at full alpha at t=236, complete at 10.83 s holding on the title. No script errors.

The clock freezes at settle — the port's settle window, seen from the other side

The Decoder measured GP_TITLE build 4 in the draw stream and found the top-level clock advances through the build-in, stops inside the settle window, and holds. The exit ramp is not on a timer; it plays when something makes the screen leave.

Their interval is [160, 236]. The exporter computes title's settle window as [160, 236, 198]. Those are the same two numbers, and they were not obtained the same way: mine is the longest keyframe-free interval over top-level elements, read out of the file with no game running; theirs is where a captured clock stops advancing. A heuristic I adopted because it collapsed three pixel residuals at once turns out to name the exact interval the game holds in. That is the first evidence for the settle instant that does not come from the port's own renderer.

The file agrees from a third direction: ptcopyright sits at alpha 255 from t=160 to t=238 — it reaches full opacity precisely as the window opens.

Refutation attempt — their declared spans, checked against the file

Their draw-stream argument cites what the file declares, which I can check without a capture:

their claim the file
ptlogo1 declares an exit at t=264 last keyframe t=264, alpha 0 exact
ptcopyright alpha ≥ 1 for 106 units, t=138…244 keyframes at t=138 and t=244, alpha 0 at both endpoints exact

Both survive. One quibble, and it is only that: the instants with alpha ≥ 1 number 105, t=139…243, not 106 — the endpoints they bracket with are themselves alpha 0, so 106 is the keyframe span rather than the visible one. It changes nothing in their argument, whose force comes from 1 050 frames against either number.

What this costs the port, and it is not nothing

authored/timing.json and authored/flow.json both said "a screen's dwell is its OWN keyframe group" and "the pacing is the disc's own". 🔴 That is refuted. Build 4 declares about 120 presented frames and dwelled ~1 100 — nine times its own timeline. The group is not the dwell.

The decision is unchanged and still right: hold zero extra rather than invent a number. What was wrong is what the port claimed for it. Leaving when the group ends is not reproducing the disc's pacing — it is leaving at the moment the game starts waiting. Both files now say so, and so does boot.gd, where the comment had inherited the same claim.

The structure was already right, and this is the part the measurement confirms: _advance is caused by the next screen arriving, never scheduled off a timer, and the port's own comment already read "a screen plays itself out because something is taking its place". exit_ramp_units was deleted for an unrelated reason and its absence is now doubly justified.

⚠️ Only build 4 is measured, and build 4 is the one screen where the port is unaffected — it is the boot's end state and holds indefinitely. The two screens this actually governs, publisher_logo and developer_logos, have no measured dwell at all. The port's boot is known too fast [refuted] on both, by an unmeasured amount. Nothing here is a number for them.

🔴 WITHDRAWN, and this paragraph stood for days after the withdrawal was written. "Known too fast on both" is false: the splash dwells are declared on the disc — publisher t=0…255, developer t=0…210 — corroborated over three cold boots to 1.1 %, and the port was already emitting each declared value plus the 9-unit black hold, exactly. See Withdrawn — "the boot is known too fast [refuted]".

Found 2026-08-30 by the Decoder's rule: grep the corpus for the claim, not for the file you were working in. I wrote the withdrawal as a new section and left the original assertion untouched, so a reader arriving here first got the dead answer with nothing to warn them — the same failure as a correction that never reaches the manifest, one layer up.

🔴 Withdrawn — "the boot is known too fast [refuted]". The splash dwells are declared, and the port was already playing them

Last iteration I took the Decoder's build-4 measurement — declared ~120 presented frames, dwelled ~1 100 — and wrote into three files that the port's boot is "known too fast [refuted] on both splashes, by an unmeasured amount". That is withdrawn. It was wrong, and the way it was wrong is the interesting part.

They then measured the splashes directly, over 3 cold boots:

splash declared at 60 units/s corpus wall clock
publisher (entries 10, 13) t = 0…255 4.250 s 4.30 / 4.60 / 4.37
developer (entries 11, 14) t = 0…210 3.500 s 3.51 / 3.50 / 3.37

The developer agrees to 1.1 %, two of its three runs to 0.3 %. And the port emits 4.400 s and 3.650 s — each declared value plus the 9-unit black hold, exactly. The pacing was right the whole time and no code changes.

What I actually did wrong

Not the arithmetic — the generalisation. Build 4 is the title, whose exit is caused by something outside its timeline, so it holds. A splash's exit is caused by nothing, so it plays its declared timeline and leaves. The title is the exception, not the rule. I had one screen, it was the one screen in the boot the port is structurally unaffected by, and I used it to overturn the two it governs.

I wrote at the time that a ratio from one screen is not a unit of pacing, and declined to scale the splashes by nine. That refusal was right and is the only reason this cost nothing but documentation. But refusing to apply the number while adopting the conclusion it implied was half a caution: I still let one screen's behaviour rewrite what the port claimed about two others. The correct move was to file build 4 as measured and leave the splashes alone, which is what the file now says.

And the unit stays units

🔴 The Decoder's own container timed those same two dwells 1520 % longer than both the declared values and the corpus — same disc, same declared timeline — and three independent readings of that container's frame rate disagree with each other. A seconds figure is one emulator's pacing on one run. The declared units are on the disc. authored/flow.json dwell therefore takes units, and only for a screen measured to wait beyond its group.

This also retires the "two timestamps would settle it" ask I filed: timestamps were the wrong thing to author, and the measurement's own result says so.

Refutation attempt — their two splash boundaries are not anchored the same way

Their draw stream reports "publisher wordmark frames 6119" and "developer glows 123, wordmarks 140209". Taking those spans against the declared groups:

splash declared units their frames units / frame
publisher 255 6…119 = 114 2.237
developer 210 123…209 = 87 2.414

Within one continuous boot, on one guest, those should be the same number. They differ by 7.9 %, and the discrepancy runs the same direction as the error split they have open — publisher +4.1 % where the developer is 1.1 %.

The file suggests why, and it is not the guest's clock: the two boundaries are anchored on different elements. The developer span starts at its glows; the publisher span is reported as starting at its wordmark. Those are 15 units apart — every wordmark on both splashes is alpha > 0 from t=16, every glow from t=1 — and the publisher has a glow, palogo_sqex_eff, visible t=1…44, structurally identical to the developer's three. So either frame 6 is the publisher's glow and is mislabelled, or the publisher span genuinely starts 15 units later than the developer's and the two are not comparable quantities.

⚠️ I cannot tell which from here — it is their log, and the answer is one grep for palogo_sqex_eff in it. Recording it because a 7.9 % internal inconsistency and an unexplained 4.1 % error on the same screen are more likely one problem than two. ⚠️ It also does not touch the corpus comparison, which is a separate instrument (3 cold boots, not this draw log); their 4.1 % may still be real.

Their corrected boundaries check out against the file — all six, exactly

The Decoder found the cause of the 7.9 % I reported, and it was worse than the anchor mismatch I proposed: the developer splash batches six quads into one draw and their log dumps only the first two. While the three glows are alive they occupy that prefix, so the three wordmarks are invisible to the log until the glows stop at t=45. "Developer wordmarks first drawn at frame 140" was the logging prefix shifting, not the game. The anchor difference I found was a symptom; the truncation was the cause. It is also what hid palogo_anima.

Their fix is to count indices / 4, which the 8-vertex dump cap cannot touch. Every one of the six resulting calibration points matches this export:

splash their transition their t export
publisher 1→2 quads 15 palogo_sqex joins at t=16
publisher 2→1 45 palogo_sqex_eff ends t=44
publisher last drawn 255 group ends t=255
developer 3→6 quads 15 three wordmarks join at t=16
developer 6→3 45 three glows end t=44
developer last drawn 210 group ends t=210

The two 15-vs-16 rows are a half-open boundary, not a disagreement: they name the last frame at the old count, the export names the first instant at the new one.

A second thing falls out that neither of us was looking for: their quad counts are 1 and 2 on the publisher against 3 and 6 on the developer, and a count restricted to sprite-bearing elements reproduces exactly that. So palogo_eff0 — the layerless forced backdrop — is not in the batch they log, confirmed from the file. Their instrument and this export agree on which element is the odd one out, having disagreed about it in every earlier iteration.

Refutation attempt — does the drift actually explain the corpus's 4.1 %?

Their four segment rates recompute exactly (1.765 / 2.165 / 2.308 / 2.357; the developer's two agreeing to 2.1 %, the run rising 33.5 %). The explanation is that the publisher runs in the first seconds where the rate is furthest from its later value. Testing what that predicts for the corpus:

publisher ÷ developer ratio
declared (255 ÷ 210) 1.214
their drift predicts 1.369
corpus, 3 cold boots 1.278

Sign confirmed, magnitude not. The corpus ratio does sit above declared, which is what the drift predicts and is real evidence. But their container's drift would inflate it by 12.8 % where the corpus shows 5.3 % — roughly 2.4× too strong. So drift of some size is doing the work; drift of their size is not.

⚠️ And the reason to be careful here is that the move is the one I just got wrong: the 4.1 % is a property of the corpus, a different instrument (3 cold boots, elsewhere), and the drift was measured in their container. Transferring it is exactly what I did carrying build 4 onto the splashes. A general warm-up is plausible for any emulator, so this is not baseless — but it is unconfirmed for the corpus, and the magnitude gap is the evidence that the corpus's drift is not theirs. It cannot be closed without the corpus's own frame log.

Untouched by any of it: the declared 255 and 210, and the port's 4.400 s / 3.650 s. Neither uses their draw log.

The guard this puts on keyframe_units_per_second

⚠️ "No single units-per-frame figure describes a run" is a statement about emulator presentation pacing, and a later reader could easily take it as grounds to revisit the port's 60. It is not. 60 is the game's logical unit rate (HANDOFF Q1, measured), the port renders at its own frame rate and converts through it, and guest pacing cannot reach it. authored/timing.json now says so at the constant itself, where someone about to change it will read it.

The n=1 disclosure, and the one port constant that rests on a single run

The Decoder disclosed that their ARM=early capture silently loses its trigger ~40 % of the time — two of five runs logged ARMED EARLY and produced no draw log at all, indistinguishable in the session log from a run that armed. So every draw-stream figure of theirs is n = 1.

That is worth more than the number it was attached to, because the port authors constants from those runs. Auditing which: exactly one, black_hold_units = 9. Everything else comes from the disc (the declared dwells 255/210, the settle window, the plate period), from the exporter, or from multi-sample measurements (Q1's unit rate over seven frames, Q5's navigation).

9 is not wrong, and three of its supports have moved. Its conversion used a 105-frame count their own truncation fix has revised to 114; its second corroboration (2.231) is the figure behind their retracted plate period; and a run-average units-per-frame is the wrong shape for a 34 frame event now that the rate is known to rise 33 % across a boot. Redone on their corrected local segments, their two runs give 8.95 and 6.71 units.

⚠️ Those two were reconciled as replicating "within the ±1 both are quantised to". Overlapping error bars are not agreeing central values — one frame is a third of this quantity. The range is ~6.59.2 and the port sits at the top of it.

The value stays. Changing it would be my arithmetic on their instrument, and this port does not author a number the corpus has not given; it is filed as a proposal in BLOCKED.md with the one run that would settle it. And what is not in doubt is that the hold is real: until this was implemented the port had no black frame at all where the oracle measures a plateau.

Their statistics, checked

Their per-boot excesses reproduce exactly: +0.89 %, +8.24 %, +6.79 %, spread 7.35 pp, wider than the 5.30 pp gap under test — so boot 1 alone essentially is the declared ratio. Their concession is right and my 2.4× cannot carry the weight I gave it.

One refinement, which cuts against their concession rather than for it: their 2.3 σ uses the population SD (3.178). At n = 3 the sample SD (3.892) is the right estimator, giving 1.89 σ — their run is less of an outlier than they credited themselves with. Testing the other question, whether the corpus mean differs from their prediction, gives t = 3.27 on 2 df, p ≈ 0.08. Neither framing reaches significance, which is where both of us landed anyway.

📌 Their sharpening of the instrument point is the keeper: a truncated log and a --screen=NAME render at t=0 both return a complete, well-formed answer to a different question. That is why neither looks like an error — nothing inside either view can tell you it is not the view you asked for.

P6 gate — sound on the P5 walk, verified, and the tool I nearly shipped instead

tools/port/verify-menu-audio. Until now the evidence that P6's gate was met was that audio.play("move") appears in boot.gd — evidence that a call is written, not that a sound reaches the bus. Those differ, and this project has the case to prove it: the black hold was implemented, called, and emitted nothing for five milestones.

It needs no sound card. Godot records the Master bus to a WAV under the Dummy driver. Three runs on main_menu: the walk (down,down,accept,cancel,up) and two controls — wait×5 for the bed alone, and left×5 for presses that reach _unhandled_input and are bound to nothing (Q5: left/right do nothing).

check result
a dead press is silent bit-identical to the bed alone, 114 688 samples
move on the bus r=0.201 at 1.85 s, bed-only 0.016, margin +0.185
confirm r=0.945 at 2.14 s, bed-only 0.371, margin +0.574
back r=0.660 at 2.42 s, bed-only 0.195, margin +0.465
cue order vs script order consistent

The order is the strongest line and it is free: the correlator is never told where to look, so three different templates landing in script order is three independent searches agreeing with the log. move's absolute r is low because it is the quietest cue under the loudest part of the bed; the margin over the same template against the bed is what carries it.

🔴 What it cannot conclude, and must never be read as: that these are the cues the game plays. That binding is HANDOFF Q8, the Decoder's, and nothing here re-measures it. This tool cannot tell a correct cue from a confidently wrong one.

The instrument I nearly shipped

The first version counted envelope bursts above a multiple of the bed level. It reported 4 cues on one run and 0 on the next, from the same script — its answer was set by two hand-picked constants, the multiple and a minimum run length, and the bed level is not constant across a run. I caught it only because I ran it twice.

The replacement has no such constant: the exported cue file is its own template, the search covers the whole recording, and the verdict is a margin over that same template matched against the bed-only control.

⚠️ Cue length is deliberately not asserted. The audible part of a cue is far shorter than its wave — 0.120.38 s against authored 0.3441.016 s — because the bed masks the tail. "Elevated for 0.13 s" is a fact about the bed, not the cue, and I came close to filing that gap as a defect.

A check that could not be made independent

I tried to verify Q8's cue durations against the exported waves. They agree exactly — 0.533 / 1.016 / 0.344 — and the agreement is worthless: the exporter decodes from Q8's own offsets and packet counts, so the duration is determined by the claim under test. It confirms the export is faithful to the authored table, nothing more. Recording it because "I checked and it matched" is exactly what a correlated instrument feels like from the inside.

Their .tbm self-refutation does not reach this archive — and it fixes my guard anyway

The Decoder downgraded 38 of the forced-backdrop rule's 80 verdicts from decoded to inferred: those elements are .tbm, declaring fade ffffffff, and a solid white quad painted first at alpha 255 would make the screen white. No screen is white, so a .tbm's white is a modulation on a texture and its element alpha says nothing about coverage — the .t32 mistake one file extension further out.

No verdict the port ships is affected. All six forced elements here are palogo_eff0.prm and pgloading_eff00.prm, role: primitive, kind_raw 0x10, fade 0xff000000. They fall in the 42 that stay decoded. And no layerless full-screen element anywhere in GP_TITLE has a non-black fade — checked across all 16 screens, so the downgrade cannot touch this archive.

But their diagnosis applies to my code regardless: an element's alpha is not its texture's opacity, and only an untextured primitive makes the two the same fact. My guard was sprite.is_none() — a symptom test, the same shape as the one they say fixed their symptom and not their cause. It would keep admitting a .tbm that this exporter happens not to emit a sprite for.

The guard is now the positive test, role == "primitive". It changes no verdict today — the six are identical before and after, 16 screens still validate — and it is correct by construction if the corpus grows.

⚠️ Not adopted from their message: their reading that the blend question now narrows to pbafc.prm. That is theirs to settle and the port draws no additive quad either way.

Coverage is now tested per instant, because scale animates

The Decoder found that forced_backdrop judged screen coverage from the declared size alone, ignoring scale — and the disc carries its own counterexample. pbafc.prm declares 844×600 at alpha ff, which reads as a screen-filling cyan wash; it is scaled 2 % × 3 % and draws about 17×18 px, strobing and travelling x=178→291. A moving glint. A rule reading its declared size would call it screen-covering.

The port had the same gap and it is closed. scale_at interpolates scale on the same linear ramp as the fade, and coverage is folded into the opaque-instant test rather than checked once: an instant counts only where the element is both alpha 255 and covering. That is the rule's own wording — "covers the screen and is fully opaque at some instant" — where the previous code tested the two halves at different times.

The static size prefilter is now deliberately not a rejection: an element scaled above 100 could cover the screen from a smaller declared size, so rejecting on declared size would have replaced one version of the bug with its mirror.

No verdict moves. Six forced elements before and after; 16 screens validate; the oracle figures are identical to the digit (publisher_logo 0.01 %, developer_logos 0.01 %, main_menu 0.07 %, extras 0.19 %, title 0.26 %). Their claim that all 80 forced instances sit at scale 100 reproduces on the GP_TITLE subset, and more strongly: no layerless full-screen element anywhere in this archive has a non-100 scale on any keyframe.

It is in for the reason they gave, which is the right one: the data that would break it demonstrably exists on this disc. That is a better argument than a failure would have been, because it does not require the bug to happen first.

Their blend-robustness argument, checked

They classify the blend mode undecodable with reach and argue the rule does not depend on it, for a black quad:

drawn first drawn last
alpha-over, α=255 correct blanks the screen
additive, α=255 correct — adds nothing correct

The table holds. An additive black quad contributes nothing at any position, so both orders are correct under it; only alpha-over distinguishes them, and it picks first. "First" is right under both hypotheses, "last" under one — so forced_backdrop's verdict is robust to a question neither of us can close.

It also explains a detail of the original bug that I had not accounted for: "layerless sorts last" was wrong under alpha-over and merely pointless under additive, which is why those screens came out solid black rather than empty. The symptom was diagnostic of the blend mode all along.

⚠️ Not evidence that the blend is alpha-over, and I am not recording it as such. It is the reason the port can stop waiting on it. pbafc.prm remains the sole additive candidate and is outside the rule at 17×18 px; the port draws no additive quad either way.

P7 gate — the new-game intro plays and returns, and a defect I nearly invented

--menu=main_menu --script=accept walks the P7 path: Ⓐ on NEW GAME announces the two measured screens this export skips, opens S00A, plays it to its natural end at 93.33 s against the manifest's 93.9, and returns to the title. Nine film frames across the movie are distinct and non-black (mean 10 → 140). The gate asks for "plays, then returns to a defined state"; that is both halves.

The near-miss, which is the part worth keeping

Checking that the movie's audio actually reached the Master bus, I correlated the recording against the exported S00A.ogv audio and got r = 0.0068 at the movie's known start, with the correlator passing its own positive control at r = 1.0000. A working instrument returning zero at the right place. I was one step from filing "the movie's own audio never reaches the bus" — which would have been a serious P4/P7 defect, and the inverse of the human's original report that the intro "plays music but no voice".

It is false. Re-run on the boot path, where no menu bed is playing:

against the bed-free recording r at
ADV voice 0.8855 movie start 7.6 s
ADV movie audio 0.4178 movie start 7.6 s

Both present, at the same start. The movie's audio is simply quieter than the voice mixed on top of it, and in the P7 run the menu bed masked it below detection while the louder voice survived. The bed carries across into the movie — play_bed is documented as carrying across submenus — so the P7 run was never a clean measurement.

🔴 The lesson is about the control, not the bed. I validated the correlator on clean data and then ran it on masked data. A positive control proves the instrument works on the material it was given; it says nothing about whether the instrument can see through an interferer that was not in the control. A negative result needs its own positive control under the same masking, and this is the third time in this corpus that an unvalidated negative nearly became a finding — after "10 of 18 elements transparent at rest" and the burst counter that read 4 cues on one run and 0 on the next.

tools/port/verify-video-audio's header already warned that a fidelity comparison needs cross-correlation alignment and an agreed downmix. It was right, and I would add the third condition it did not know to state.

⚠️ What is not settled: whether the bed should carry into the movie and on to the title after it. It does, it is authored that way, and nobody has watched the game do either. That is a separate question from this one and is not filed as a defect — only as unmeasured.

Also checked, from their compose finding: GP_TITLE declares 115 .t32, 45 .rat, 18 .prm and no .tbm at all, and every non-primitive element has a resolvable sprite. The "draws no pixels for an unresolvable element" hazard — the shape that hid pteff05 from both renderers — does not reach this archive.

ScreenView.skipped was correct and unread since P1 — now it says so itself

The draw loop has always tracked what it could not draw, with the comment "a silently missing element looks like art". Nothing ever printed it. For eight milestones the port could drop an element on every frame and report it to nobody.

That is the same shape as the black hold — implemented, called, emitting nothing until somebody filmed it — and as verify-screen scoring two blank frames OK. A fact that needs someone to remember to look at it is a fact that goes unnoticed, so _note_structural prints from inside ScreenView rather than returning a value for a caller to surface. Routing it through a caller is exactly what did not happen.

Only structural skips are reported — (no sprite in the export) and (sprite failed to load). (transparent at rest) is ordinary animation, true of every element at some instant, and reporting it would bury the two that mean something under the one that never does.

Nothing is being skipped today: 0 across every screen, on the boot path and per-screen. This is a guard, not a fix. The export corroborates it — no missing sprite PNG, no element at alpha 0 on every keyframe, none at scale 0.

🔴 And the first version of that scan was a false pass

My first run reported "0 structural skips" on ten screens. screen_view.gd did not parse. I had inserted a line at three tabs inside a four-tab block — the Python assert old in s passed because a three-tab string is a substring of a four-tab line — which orphaned a continue. Godot loaded nothing, printed nothing, and grep -c faithfully counted zero.

A count of zero from a dead script is indistinguishable from a count of zero from a clean one, and I had already written the sentence claiming the clean reading. The scan now counts the screen summary line as a positive control: if the script did not run, summary-lines=0 says so, and the zero cannot be read as a pass. That is the third time this session that a well-formed answer to a different question nearly became a finding.

⚠️ Note the mechanism, because it will recur: matching indented code by substring is unanchored, and it silently matched a shallower indent than the one in the file.

Refutation attempt — "the element declared first paints first"

All six of the port's forced-backdrop elements sit at element index 0. So on those six screens the rule's verdict is indistinguishable from a far simpler hypothesis I had not tested: the first-declared element is painted first. If that held, forced_backdrop would be an elaborate way to reproduce the file's own ordering, and my six verdicts would be no evidence for it at all.

It is refuted, on 8 of 16 screens. Index 0 is not painted first on build_00/build_01 (position 2), extras (7), title (13), title_jp (18) and — decisively — main_menu, where index 0 is pteff00, painted last, position 15 of 16. pteff00 is the Decoder's own measured control: the game puts the first-declared element on top of that screen.

So declaration order is not paint order, the six coincidences are coincidences, and the rule is not redundant.

⚠️ What survives as a real limitation: those six screens, taken alone, cannot distinguish the two hypotheses. The evidence separating them comes from elsewhere in the archive. Worth stating because it is the exact weakness in the Decoder's pfbase.tbm upgrade — "element 0 of the save/load frame, and the measured order starts [0, 1, 2, …]". An order that equals the trivial order is weak evidence for any rule, since every rule preserving declaration order agrees with it. Their claim survives, but on evidence they did not cite: it is main_menu's pteff00 that rules out the trivial reading, not the save/load frame itself.

The menu bed plays under the cutscene, nobody decided that, and it stays

MenuAudio.stop_bed() exists and is called from nowhere. So the music that starts when the main menu goes live runs through the cutscene and on past it — and since authored/audio.json sets loop: "restart", it then loops. Both follow from the source and the authored data alone; no measurement is needed to establish them.

The port therefore emits two unrelated music tracks at once during S00A: the movie carries its own music and effects, and the menu bed is underneath it.

It is not being fixed, and that is the decision

PORT-MISSION's rule is to leave an unmeasured detail plainly wrong rather than plausibly invented, and this is the textbook case for it. Music over a cutscene is wrong in a way any listener catches in one second. Ducking or stopping the bed would sound entirely right — and would be a guess about a game nobody has watched. The audible version gets fixed; the plausible version ships forever.

So _play_video now announces it, the way skipped_chain already announces the two screens NEW GAME jumps over:

  -> video S00A at 1.13 s (…)
     🔴 the menu bed is STILL PLAYING under this movie -- unmeasured,
        left audible on purpose (BLOCKED.md: does menu music duck?)

It fires on the menu path and correctly stays silent on the boot path, where no bed has started. stop_bed is kept, not deleted: the day a capture says whether the game's menu music ducks under a movie, it is the one line to change.

⚠️ This is the mirror of ScreenView.skipped from the previous iteration — a fact recorded and never surfaced, against a capability provided and never used. Both were invisible for the same reason: nothing fails when they are missed.

🔴 And my correlator is not trustworthy on music under music

Chasing this I ran the envelope correlator over the P7 recording repeatedly and got answers that moved with the window and the template: the bed at r=0.42 with one template and no peak at all above 0.4 with another; a post-movie window search whose range excluded the correct answer and duly reported the bed's own loop as unidentifiable. I was drafting "46 s of unexplained audio after the movie" when the explanation was the authored loop: "restart" sitting in a file I had not re-read.

The instrument is sound where it was validated — discrete SFX against a bed, with margins of +0.5 over a negative control. It is not sound for music under music at comparable level, where every candidate scores 0.150.42 and nothing separates. ⚠️ A margin is only meaningful against a control at the same SNR, and I did not have one here. That is the fourth near-miss of this kind, and the first where I would have invented an anomaly rather than a defect.

Nothing in this section rests on that correlator. The finding is stop_bed having no caller, which is a fact about the source.

wait:<seconds>, and the bed's loop seam is 3.4 seconds of silence

The port could not be asked to run for a stated duration. A bare wait script step is pass — it returns as soon as the screen settles — so nothing happening after the settle point was observable from a harness at all. The music bed made that concrete: an 87.7 s track whose restart nobody had watched, on a harness whose longest menu run was under seven seconds.

--script=wait:105 fixes that, and the first thing it found was the answer.

The bed loops, exactly where it should

Recording the Master bus over 132 s with nothing but the menu playing — no movie, no voice, a clean signal — the bed's t=2…17 s template matches twice:

r pass begins
first pass 0.947 0.0 s
second pass 0.885 87.8 s

The track is 87.7 s. So loop: "restart" does what authored/audio.json says: replays from sample 0 at the track's end, no trimming, no loop point. First end-to-end observation of P6's looping behaviour.

And the seam is measurably as bad as it was authored to be

loop_why predicted "the listener hears the track's own fade-out and the silence after it before the music comes back". Measured off the bus:

window around the seam RMS
8 … 4 s 2057
4 … 2 s 714
2 … 0.3 s 431
+0.3 … +2 s 2164

and 36 consecutive 50 ms windows below peak 300, from 84.40 s to 87.80 s — about 3.4 seconds of near-silence before the music returns. That is long enough to read as the music stopped, not as a loop.

The claim was right and is now a number. ⚠️ It does not license trimming to the fade: that would still invent a loop point, and an invented one is indistinguishable from a decoded one a month later. The measurement is recorded to make the cost of the missing loop point concrete, in authored/audio.json.

🔴 My first wait: was wrong by 39 %, and the way it was wrong matters

It used create_timer, which counts down on the frame delta. In an idle scene this container throttles and the delta it reports is not the time that passed: a requested 30 s took 41.7 s of wall clock while the port reported 30. Measured against date either side of the process, with a no-wait control to subtract 1.21 s of startup.

Now polled on Time.get_ticks_msec(): 30 s requested, 31.38 s wall, +4.6 %.

⚠️ This is idle-specific and is not a general clock fault, which I checked before writing any of it down. Over a whole boot, where things are animating, the port's clock tracks wall clock to within 4 % — 10.43 s wall against 10.82 s reported. So the port's animation timing is sound and the earlier splash-dwell agreement (4.400 s and 3.650 s against declared 255 and 210 units) stands; I had briefly believed it did not.

What is genuinely unsound is _elapsed while idle: it reported 23.21 s across 30 real seconds of waiting. Every timing the port prints during animation is fine; a timing that spans a wait is not.

📌 The reason to care: the only reason to hold a screen is to observe something on a real clock — an audio loop, a timeout, an idle return. A timer that silently ran 39 % long would have put every such observation at the wrong instant, and the bed-loop result above would have been the first casualty. It survived only because the seam was read off the recording's clock, which the bed's own known length calibrates.

Two harness bugs, and the defect the second one was hiding

1. --capture with --script photographed the frame before the script

--capture fired in _ready and quit. With --script that is before the first press: at t=0.133 s, with 10 of 16 elements still transparent. Two runs differing by two down presses came out bit-identical, and I read that as "runtime focus never changes" — a confident wrong finding that --shots contradicted within a minute.

Fixed: with a script, the capture defers to the end of the run, through the same _capture_to member the boot path already used. Verified — the two runs now differ at max 235, and the capture lands at t=82 units instead of t=8.

2. --boot --capture= wrote no file at all — FIXED, see below

_finish_boot() is reachable only from the overlay-quit branch, but the boot quit first: line 412 exits when _film == "" and _overlay_spec.is_empty(), and _overlay_spec is cleared when the overlay is raised. So a plain --boot ended at 10.99 s, 1.2 s before its own scheduled 12.21 s, and the capture never happened.

Confirmed pre-existing by stashing my changes and re-running. Fixed by also requiring _overlay_quit_at < 0.0. The boot now runs to 12.19 s, prints "boot ends on title + press_start", and writes the file.

⚠️ The flag has a doc comment explaining that it exists so the boot has an artifact of its own instead of a 600-PNG filmstrip. It has been producing nothing. A flag that silently writes no file is the same failure as ScreenView.skipped and stop_bed: provided, plausible, and never exercised.

3. 🔴 And the artifact it now produces shows the plate is missing

The boot's end frame is bit-identical to --screen=title at the same instant — max difference 0. The PRESS Ⓐ plate is not in the port's end state.

ptbtn00's own fade explains it exactly:

t 0 214 236 238 244
alpha 0 0 255 255 0

The plate is visible for 8 units — 0.133 s — and the boot captures at t=246.54, two and a half units after it has gone.

That is not an accident of frame timing; the code chose it. _overlay_quit_at takes max(view.settle_time(), overlay.settle_time()), and its comment says why: "the plate arrives at t=238 and build 4 is still fading up from black until t=261 … quitting when the plate lands photographs a title that has not finished presenting." Both halves are true, and together they mean the two states cannot both be in one frame. The port picked the title, and the consequence — that the plate is in no artifact at all — was never written down.

⚠️ I am not moving the trigger. The earlier reasoning is sound and the measurement it protects is real; picking the other instant would trade a missing plate for a visibly dark title, which is the swap that was already made once and regretted. What settles it is what the game does with the plate after t=244 — filed.

📌 Worth naming: defect 3 was invisible while defect 2 existed. A capture flag that writes nothing cannot show you a missing element. The broken tool was hiding the thing the tool was built to find, and neither was noticed because the absence of a file looks exactly like not having run the command.

What was not wrong, and how I nearly recorded that it was

Runtime focus works. Per-step --shots across down,down differ at max 232233, with the differing boxes tracking down the button column. My contrary reading came from analysing 410 f_NNN files after asking for --shots=…/sthe filenames did not match the flag I passed, and that was visible in my first ls. A verbatim re-run produces six correctly-named per-step shots. I do not know where those 410 files came from, which is itself the point: I drew a conclusion from a file set whose provenance I never checked.

The PRESS Ⓐ plate: four bugs in a row, and a number I have been misquoting

Last iteration I filed that the plate was visible for 8 units and simply missed by the boot's capture instant. That was wrong in the direction that matters: the plate could not be drawn at any instant at all, and three separate faults had to be removed before it appeared. The fourth is a correction to figures I have quoted to the Decoder repeatedly.

1. --time= was silently ignored on half the screens

pose_at did if holding: t = settle_instant. The requested instant was discarded on every screen with a settle window ≥ 30 units — title, press_start. The flag parsed, the log printed the time asked for, and the pose came from somewhere else entirely.

ScreenView.frozen now marks an explicitly pinned instant and skips both clamps.

2. The settle window picked an interval where nothing is visible

press_start's keyframes are 0, 214, 236, 238, 244. The widest keyframe-free gap is 0…214 — the dead stretch before the plate exists, where ptbtn00 is alpha 0 throughout — so its settle instant was t=107, and every question about that screen was answered there.

🔴 A gap in which nothing is visible is not a settled state. The exporter now rejects those intervals. press_start becomes [214, 236] — 22 units, under the runtime's 30-unit bar — so it falls back to each element's own hold, which is the plate, opaque, as the disc declares it.

⚠️ It disturbs no window the settle instant was measured on: title keeps [160, 236], the interval the Decoder's draw stream independently found the game's clock freezing in.

3. An authored entry of mine was suppressing the decode

Even then the plate stayed dark, because authored/timing.json's looping_focus_records entry for press_start/ptbtn00 made _draw take the focus path — which draws the focus record instead of the base sprite:

with the entry without
press_start t=236 max 0 max 252.5
t=240 max 0 max 252.5
t=250 max 15.3 max 252.5

I authored that entry to give the plate a glow. It substituted a dim glow at the wrong phase for the element's own bright sprite, on the screen whose entire content is that sprite. Deleted — an authored guess that overrides decoded data with a worse answer is removed, not tuned. The glow is not claimed either way; drawing both would be a rendering rule nobody has measured.

The boot's end artifact now contains the plate: mean 95.7 in its region against 33.6 for the title art alone, and the overlay reports drew 1: ptbtn00 where it reported drew 0.

4. 🔴 verify-capture has been measuring a different pose than it reported

It passed --time=5.9617 for the title — t=357.7 units, the Decoder's refined sweep fit — and that value was never applied, because of fault 1. Every title figure this tool has printed, including the 0.26 % I have quoted to the Decoder more than once, was measured at the settle instant, t=198, under a note claiming t=357.7.

Honouring the flag made it visible: t=357.7 is past the title's own group, which ends at t=269, so the whole screen posed at its faded-out final keyframes and the disagreement went to 30.97 %. The instant was only ever meant for the ptloop leaf, which runs to t=600 and is looped separately by loop_leaf. Applying it to the whole screen was always wrong and was harmless only while it was ignored.

The splashes had the same shape: --time=99 was an idiom for "settled" that worked only because it was discarded. Both rows now pose by omission, and the tool's note says what it does.

before now
title 0.26 % (labelled t=357.7, actually t=198) 0.21 % at t=198, labelled t=198
publisher_logo 0.01 % 0.01 %
developer_logos 0.01 % 0.01 %
main_menu 0.07 % 0.08 %
extras 0.19 % 0.19 %

The agreements were real; the stated pose was not. Corrected with the Decoder, since they have those numbers.

The flag audit that started it

All 16 flags boot.gd parses were exercised for an observable effect after last iteration's two silent ones. --pose=rest (max 111 against the timeline), --play (enters the menu), --no-hold (max 255 on two screens) all pass. --time was the one that did not, and it took a screen whose content is a single late spike to make the failure visible.

The title's residual is the sweep phase, and the sweeps fit at ~400 units, not 357.7

Last iteration I found verify-capture was passing the Decoder's refined sweep fit as --time=5.9617 and having it silently discarded, so the value had never been tested. Asking for it also destroyed the frame — t=357.7 is past the title's own group end at t=269 — which is why nobody noticed.

--leaf-time=<seconds> separates the two clocks: the screen sits at its settled pose, the ptloop leaf is placed at whatever phase is under test. That makes the fit testable for the first time.

Controls first

  • The renderer is deterministic — three runs at one leaf phase are bit-identical, max difference 0. So variation across phases is signal, not noise.
  • The sweeps are detectable — two phases differ over 0.3953 % of the frame at a 10 % threshold. A comparison at this scale can see them.

The fit

Sweeping the leaf across its full 600-unit span against live-title-build4-no-plate.png, structural disagreement at a 25 % threshold:

leaf phase differing
240 units 0.1410 %
320 units 0.3395 %
357.7 — the Decoder's fit 0.2532 %
390 0.0129 %
395402 0.0124 %
405 0.0127 %
440 0.2033 %

A sharp basin at 390415 units, an order of magnitude below everything around it, and 20× better than t=357.7.

What that means, and what it does not

The title's 0.21 % residual is the sweep phase, not structure. At the fitted phase the disagreement falls to 0.0124 % — the same order as the splashes' 0.01 %. The port's title rendering is structurally right; the sweeps were simply somewhere else in their loop.

🔴 The port does not adopt 400 units, and verify-capture is not re-posed to it. That would be tuning until they match, which this repository's own tooling header warns against. The port loops the leaf freely — there is no phase parameter to set — and 400 units is a property of that capture's instant, not of the game.

⚠️ Reach: this assumes the port's leaf geometry and sprite are otherwise correct. A systematic error in how the sweeps are drawn could be absorbed by shifting the phase, and one capture cannot separate those. What makes the result worth having anyway is the sharpness — a 20× drop over 40 units is not something a geometry error would produce at a wrong phase.

⚠️ And it does not tell the Decoder their 357.7 is wrong as a measurement of whatever they measured it on. It says the phase that matches this capture is ~400. If those are the same quantity, one of the two is off by ~42 units; if they are not, this is a second quantity nobody had.

A second capture closes the sweep-geometry question, and the plate matches at 0.00093 %

Last iteration's leaf-phase fit came with a caveat I could not close: a systematic error in how the port draws the sweeps could be absorbed by shifting the phase, and one capture cannot separate those. A second capture can, and live-title-press-a.png — the title with the plate — was sitting in the corpus unused.

The second capture fits at a different phase, and better

capture pose differing
live-title-build4-no-plate.png settled, leaf at ~400 units 0.0124 %
live-title-press-a.png t=237, everything 0.00093 %

Two independent captures, two different sweep phases, both fitting to 0.01 % or better. A geometry error in how the sweeps are drawn would leave a floor in both, and at a phase-independent level. Neither has one. The caveat is closed and the port's sweep rendering is not systematically wrong.

The two phases are also consistent with each other rather than merely different: the plate is opaque only at t=236238, so a frame containing it is early (t≈237) and a frame without it is either earlier or later. The no-plate capture fits at leaf ~400 — 6.7 s in, well past the plate's window — and its filename says it is build 4 alone. Both readings agree on where each frame sits.

⚠️ I nearly drew a further inference — that the no-plate capture dates the plate's disappearance and therefore answers the BLOCKED question about whether the plate stays up. It does not: the filename says build4-no-plate, so the capture was taken of build 4 alone, deliberately without the overlay. It carries no information about how long a plate lasts. That row stays open.

Capture 1 is not a whole-screen instant, which corroborates loop_leaf

Sweeping the whole screen's time against the no-plate capture, the best is 0.1483 % at t=230, degrading sharply past 240 as the group fades out — an order of magnitude worse than the leaf-only fit's 0.0124 % at phase ~400, which the main timeline cannot reach without fading everything.

So that capture is not "the screen at instant t". It is the screen settled with the sweeps still looping — which is exactly what authored/rendering.json's loop_leaf_on_screens: ["title"] models. That decision was authored from the leaf's zero slack; this is the first independent evidence for it.

🔴 The 1 % floor was the plate not being drawn at all

Before any of that, every sweep phase against capture 2 gave a flat ~1.0 %, with the residual a row of glyph-sized blobs on the plate's own position.

--screen=X --overlay=Y pushed the raw elapsed clock into the overlay — 9 units at the moment --capture fires. press_start is transparent until t=214. So the one flag whose entire purpose is put the plate on the title drew nothing and reported drew 0, and the frame read as a title with no plate.

A static overlay now poses at its own arrival. The --boot path is untouched: there the shared clock is the finding — the 120 units between build 4's last ramp and the plate's a=255 is a fixed interval on one timeline.

⚠️ My first patch for this was wrong and I nearly committed the comment for it: I wrote that "nothing outside a boot sequence drives the overlay's clock". It is driven — from view.time_units, every frame. The symptom was identical either way, and only re-reading the log after the fix failed showed the cause was the opposite of what I had written down.

The new row

title_plate joins verify-capture at 0.00% — two orders below every other row, which makes it the most sensitive regression detector in the harness.

⚠️ Its instant is fitted, not measured. t=237 is where this capture's content places it, found by sweeping. Choosing which frame to compare against is what every row here does, but the 0.00093 % is a floor for that pose, not a general statement of accuracy.

--focus= did nothing on the menu path, and the corpus had an untested focus capture

Two unused live captures were sitting in docs/re/captures/title-builds/. live-main-menu-options-focused.png is the menu with OPTIONS focused — the only capture of a known focus state — and it was untestable, because --focus= silently did nothing on the --menu path.

The flag parsed, was stored in _force_focus, and was applied to view.focused_id at startup — and then _menu_enter overwrote it with the authored initial focus on every entry. Every run logged focus ptbtn01 whatever was asked for, and all five buttons produced the same frame. It is now pushed into the menu model, not just the view, so navigation continues from where it was forced rather than jumping back on the first press.

The port's focus rendering is right, measured against the oracle

Rendering each of the five buttons focused, against each capture:

focused vs live-main-menu-options-focused vs live-main-menu
ptbtn01 NEW GAME 0.7352 % 0.0705 %
ptbtn02 LOAD GAME 0.8204 % 0.8378 %
ptbtn03 TUTORIAL 0.7220 % 0.7365 %
ptbtn04 OPTIONS 0.1355 % 0.7449 %
ptbtn05 EXTRAS 0.7029 % 0.7236 %

Each capture picks out exactly one button, by 5× and 10×. This is the first time the port's focus rendering has been checked against the game at all — the harness's own main_menu row uses an authored focus, so it could never have caught a focus error.

What that settles, and what it does not

The port draws focus on the right button, distinguishably. live-main-menu.png shows NEW GAME focused, so the authored initial_focus: ptbtn01 matches the one frame it can be checked against.

⚠️ It does not overturn HANDOFF Q5, which measured initial focus as unstable boot to boot across four runs. One capture showing ptbtn01 is consistent with instability, not evidence against it. The value stays authored, with the agreement recorded beside it. Reading this as "initial focus is settled" would be exactly the over-generalisation from a single observation that this corpus keeps having to withdraw.

main_menu_options joins verify-capture at 0.13 %.

⚠️ Still unused: live-attract-title-press-a-band.png, a 1279×120 strip rather than a full frame. It needs a banded comparison the harness does not do, so it is named here rather than quietly left out.

The last unused capture, placed — and its residual is the oracle's, not the port's

live-attract-title-press-a-band.png was the one live capture nothing consumed. Following last iteration's rule — an unused capture is a signal about the harness — it turned out the same way as the previous two: nothing about the capture was unusable, the harness simply could not compare a band. It is 1279×120, not a full frame.

Placing it

Sliding it down the render, structural difference against the port:

y differing
300 51.99 %
500 19.61 %
515 9.14 %
520 0.354 %
525 8.87 %
555 24.02 %

y = 520, a 25× drop over five pixels. Measured, not guessed. Sweeping the instant at that offset puts it at t = 236238 — the plate's own opaque window, the same instant the full-frame title_plate row fits.

🔴 The 0.354 % is not the port's error

Three comparisons separate it:

differing
port's band vs live-title-press-a's same band 0.000 %
live-title-press-a's band vs the attract band (oracle vs oracle) 0.301 %
port's band vs the attract band 0.354 %

The port reproduces one capture's band exactly. The two captures differ from each other by 0.301 %, which is nearly the whole residual.

And that oracle-to-oracle difference is two thin horizontal strips — 248×5 px and 206×1 px — which is the shape of a sub-pixel edge difference or capture noise, not of a state difference.

⚠️ I had started writing that the attract-returned title differs from the boot title, which would have been a finding about the game inferred from 0.3 % of a band. It is two hairlines. The connected-component breakdown is what stopped it, and I would not have run it if the number alone had looked small enough to dismiss or large enough to report — it was in the range where you have to look.

So title_band joins the harness at 0.35 %, and its job is to stay near the oracle-to-oracle gap, not to reach zero. A row whose target is not zero has to say so, or the next person tunes toward it.

All eight live captures in the corpus are now used. Three were sitting idle and all three were blocked by the harness, not by the capture: an overlay posed at t=9 that drew nothing, a --focus= overwritten on every menu entry, and a banded comparison that did not exist.

MODDING.md had five rules and no check. Now it has one, and all five pass

MODDING.md opens by calling modding a constraint on the exporter today, not a later feature. Nothing verified it. That is the shape this port keeps finding — a rule stated, believed and unexercised: the black hold implemented and never called, ScreenView.skipped written and never read, stop_bed provided and never used, --focus parsed and overwritten on every menu entry.

tools/port/check-modding covers all five. Every one passes today, so it is a guard, not a fix: its value is that the next thing to break one says so.

rule check result
1 — one asset, one file every referenced sprite present, none orphaned, no split names 174 / 174, exact
2 — recognisable names no hex or hash-shaped filenames none
3 — modern formats extensions confined to json/png/ogg/ogv (+ sidecars) clean
4 — base and overrides data/mods gitignored and read by the exporter both
5 — provenance every generated JSON carries a source 17 / 17

It is proved to fail

A check that has never failed has not been shown to work — the lesson from check-capture, which once passed a file with 36 % holes punched through it. Three controls, each failing correctly with a non-zero exit:

  • a .cmd sidecar with its header stripped → rule 3;
  • a bogus.bmp in the sprite tree → rule 3;
  • one orphaned PNG → rule 1, "174 referenced, 175 present".

The one thing it found: an unlabelled generated file in the asset tree

The two .cmd encode-cache sidecars sat beside the .ogvs in the modder-facing tree with no line saying what they were — a bare ffmpeg command next to a video reads as something to edit or delete. They now carry a header stating that they are generated, are not assets, and that the way to change a video is an override under data/mods/.

Two details worth keeping:

  • the header is excluded from the cache key, so rewording it does not re-encode four minutes of video. A cache that punishes documentation gets documented once and never again.
  • the sidecar is now refreshed whenever its text differs, not only when a re-encode happens. It used to be written inside the !fresh branch — which meant a header change could never reach an existing export, because nothing that reads the header triggers the write that updates it. The explanation would have been correct in the source and absent on disc. Confirmed: two consecutive exports, 20 s and 19 s, header present, no re-encode.

And a question I asked the Decoder that I could partly answer myself

Last iteration I asked whether the 0.301 % between two of their captures implies a capture-path floor on every comparison in the corpus. It does not, and I had the evidence already: the port matches live-title-press-a.png at 0.00093 % full-frame and 0.000 % across the band. A general floor could not coexist with either number. So the 0.301 % is specific to the attract band capture, and my 0.010.2 % rows are not sitting on a hidden floor. ⚠️ What that does not settle is why those two frames differ — still theirs, and still worth an answer.

Five authored values had no reader — including the one I asked for measurements into

Applying the prior from the last six findings — a rule or capability nothing exercises turns out broken or inert when someone looks — to authored/ itself. Grepping every authored key for a reader in the exporter or the runtime:

key file status
dwell flow.json 🔴 no reader — now wired
ramp timing.json no reader — now asserted
left_right flow.json no reader — now asserted
input_during_transition flow.json no reader — now asserted
stems audio.json no reader (stems_why is carried; the sum is hardcoded)

Everything else — se, bgm, voice, boot, screens, navigation.wrap, draw_leaf_for, loop_leaf_on_screens, keyframe_units_per_second, black_hold_units, archives, also_export, presentation, loop_mode, initial_focus, skippable, then_video, after_video — is read.

🔴 dwell is the one that mattered

Its own text says "when a capture times the real boot, the extra hold per screen goes here." A number placed there did nothing. Two iterations ago I asked the Decoder for measurements destined for exactly that slot; had they arrived, they would have been filed into a value with no reader and the boot would have been unchanged, silently — and I would have reported the boot as matching.

It is wired now, and stays empty. Nothing is authored into it: the splash dwells are declared on the disc and measured to agree. Wiring the slot so that a future number has an effect is the opposite of adopting one.

⚠️ I wired it to the wrong branch first, and it did nothing — silently. Holding longer after settle changes nothing, because the screen still leaves when exit_time() + black_hold arrives and the extra hold is absorbed. A dwell has to delay the departure. I found it only because I tested the control: +120 units moved the transition 4.46 s → 6.43 s, +1.97 s, with the video following by the same amount. Reproducing the exact defect I was removing, inside the fix for it, is the strongest argument I have for testing that a wire carries current rather than that it exists.

The other three are asserted, not implemented

ramp, left_right and input_during_transition describe behaviour the port hardcodes. That is fine for a record and dangerous for a switch, and they are written like switches — setting left_right to "move" would change nothing and warn nobody.

Rather than invent the missing implementations, _check_authored_invariants asserts the value the port was built against, naming the file. Changing one now produces an error instead of silence.

That is precisely the distinction left_right's own why claims to be making — "written out rather than left unhandled so that 'the game ignores it' and 'we never wired it' are different lines of code" — and which was not in fact being made, because nothing read the value that was supposed to make it.

The validator is called, not merely defined. A validator nobody invokes is the same defect it exists to catch, and this file now documents six other instances of exactly that.

Verified: clean boot with no invariant errors and unchanged timings; setting left_right: "move" produces the error; all five MODDING rules still pass; the oracle rows are unmoved.

FORMAT.md declared the port's own export invalid, and a failed export is not atomic

Continuing the audit that has now found seven unexercised rules: FORMAT.md is the open format spec — written for a stranger reading the tree with no access to the disc or this exporter. So the question is whether what it promises is what sylpheed-export check enforces.

Five documented requirements, each broken in a copy of the tree:

broken caught
unresolved removed from a screen
peak_dbfs removed
peak_dbfs = 120 (silence)
duration_s removed
peak_dbfs = 0.0 on an se passes

The last row is the doc's error, not the code's

FORMAT.md said flatly that check "refuses a tree whose peak is ≤ 90 dBFS or ≥ 0 dBFS". The implementation is kind-dependent and deliberate: a bgm is a sum we produced, so a peak at or above full scale is our arithmetic and is refused outright; an se or voice is a single wave off the disc, mastered near full scale, whose lossy decode overshoots by a fraction of a dB, and those are allowed to +1.0.

🔴 And the doc was wrong about the port's own export. It ships confirm at +0.18 dBFS and the ADV voice at +0.31 — both above 0. A consumer implementing a validator from FORMAT.md would have rejected a valid tree, and the file that exists to let someone check our work without trusting us would have been the thing that misled them. Corrected, with the +1.0 marked as the judgement it is.

Verified both directions: a bgm forced to 0.0 is refused with "a SUM we produced clips"; an se at 0.0 passes.

🔴 A failed export leaves a tree that is not an export tree

Found by accident, and worth more than the way it was found. Testing the new stems assertion, the exporter bail!ed part-way — and left export/ with no manifest.json at all. Every subsequent tool then reported "has no manifest.json — is that an export tree?", which reads as a broken harness rather than as the aftermath of a deliberate failure.

⚠️ It cost me a wrong reading immediately: the first run of the requirement audit above reported every case as "no manifest", and I nearly recorded that the validator was checking nothing. It was checking a tree that had been half-written.

The exporter writes the manifest last, which is the right order — a manifest is a claim about a tree, and a manifest for a tree that was never finished would be worse. So this is filed rather than fixed: the behaviour is defensible and the message is not, since "is that an export tree?" describes the symptom and hides the cause. What a stranger needs to be told is the last export failed; re-run it.

check-all, a verdict that ignored its own statistic, and a claim of mine that was wrong

Eleven tools under tools/port/ and nothing ran them together, so each had to be remembered individually. That is the ninth instance of this port's recurring shape — correct, documented, unexercised — one level up: the checks were the thing nobody was running.

tools/port/check-all runs the four that assert (check, check-modding, check-capture-controls, verify-menu-audio), prints the oracle table, and handles verify-screen specially. All eleven were exercised first and none had rotted; which-focus independently picks NEW_GAME at a 93.8× margin, which is a second instrument agreeing with the capture fit's 10×.

Two things it is careful about:

  • the six exploratory tools are not listed as passes. They produce artifacts for a person to look at and have no verdict; counting them would invent six.
  • verify-capture is reported, not asserted — it always exits 0. Its header is right that the numbers are not a target, but not a target is not not a regression detector, and nothing would notice title_plate moving off 0.00 %. Named as a gap rather than papered over; a real fix needs stored baselines, and what a baseline means when the pose is fitted is a decision, not a chore.
  • the verify-screen allowance expires on its own condition. It is allowed to fail only while formats-pin-2026-08-29d is not an ancestor of origin/main; the day it lands, check-all fails instead. A suppression with no expiry is just a hidden failure.

🔴 The verdict ignored the statistic added to inform it

verify-screen computes over3 — how many pixels exceed the bar — because "a single max cannot tell 2 pixels from 25 444", its own words. The verdict was then decided on max alone. So main_menu (max 4, over3 0) read DIFFERS while extras (max 3, over3 0) read OK: one unit on one pixel separating two frames that are equivalent at the bar.

⚠️ Not fixed by raising the bar, which this file rightly forbids. The bar is still 3. A frame with no pixel over it now gets its own verdict, ROUNDING, instead of being lumped in with a real disagreement. Tenth instance: the fix was implemented, documented, and never wired to the thing it was for.

🔴 And "six expected DIFFERS [refuted]" — which I have told the Decoder more than once — was wrong

The true count was ten, now eight after the rounding fix:

screens count explained
the forced-backdrop six 6 the pin: two decoder eras
main_menu, main_menu_jp 2 now ROUNDING, not a disagreement
title, title_jp 2 🔴 not explained

title differs on 790 pixels and title_jp on 20 498, and neither is the forced-backdrop rule — those screens have no forced element. I had a blanket allowance covering two disagreements I had never accounted for.

My hypothesis for them is refuted. authored/rendering.json notes that the consistency harness compares against a renderer that draws no .rat leaves, so the port's ptloop sweeps looked like the obvious cause. Emptying draw_leaf_for and loop_leaf_on_screens changes the numbers not at all — 790 and 20 498 either way. verify-screen poses at rest, where the leaves evidently do not draw. Filed as open.

⚠️ title_jp is a localisation screen and out of scope (MISSION §7). title is on the boot path and is not.

The title disagreement, localised — and the question I filed for it was the wrong one

Two iterations ago I filed title's 790-pixel disagreement with sylpheed-cli as needing the Decoder: "which elements does sylpheed-cli draw on build 4 at rest". That was a hypothesis dressed as a question, and it is wrong. I could answer it myself, and did.

What is established

  • The pixels cluster in one place: x ≈ 9381162, y ≈ 172310, as blobs of 2066 px. That is ptlogo_back2eff1's position, pos=[938, 194] — one of the six ptlogo_back2eff* glows, all of which carry a sprite and no declared size, so the texture supplies it.
  • 🔴 Both renderers draw it. The region reads mean 95.60 in the port and 95.08 in the CLI. So the premise of my filed question — that one draws an element the other does not — is refuted. A set difference of element lists would have answered nothing.
  • It is not a placement offset. Rolling the port's frame by every combination of ±1 px makes it worse by two orders of magnitude — 790 pixels aligned against ≥ 175 406 for the best shift. The images are registered; they differ in content on 0.086 % of the frame.

What is not established, and why I am not guessing

The mechanism. My next hypothesis was edge antialiasing, and the test failed its own control: the edge mask classified 92 % of the frame as edge, so the 25.2 % of differing pixels landing on it is below chance and the instrument is dead. A number from it would have been noise wearing a decimal point.

The ask is downgraded rather than left standing

BLOCKED.md's row asked for the wrong thing on a refuted premise, which is worse than no row: it would have spent someone else's time confirming a difference that is not there. Corrected.

⚠️ And the residual value is genuinely low. This is two of our own renderers disagreeing on 0.086 % of one frame, on a screen where the port matches the oracle at 0.21 % and its plate variant at 0.00093 %. verify-screen's own header says a DIFFERS means "we moved apart, go find out which of us moved" — here neither moved from the game. It stays visible as a DIFFERS rather than being allowed, because an allowance is how the two title rows hid inside "six expected" in the first place.

Auditing BLOCKED.md found three stale rows, and the undated ones were all three

BLOCKED.md opens by warning that it goes stale "within the hour. It has." Nothing audits it. So I read every live row against a running port instead of against my memory of it.

Three were wrong

P3/P5 — the title screen was the worst, and had been for weeks. Every factual claim in it is false: the boot does not end on a plateless build 4, press_start is not unused, and the port has drawn two builds at once since the plate-delay work. Verified this iteration — boot ends on title + press_start, overlay press_start … drew 1: ptbtn00, plate region mean 95.70 against 33.6 for the bare title.

🔴 And a row four lines below it was already marked answered and TAKEN for the same question. Two rows on one question, one struck and one live claiming the opposite — and the live one was the stale one. A reader taking the page at face value would have believed the struck row was the old news.

P4 — is an attract movie skippable led with "the port could not tell which bug that is". It can and it did: DECISIONS.md records does skip the intro in this build, and every boot run since prints video skipped at …. Corrected to 🟡 — (a) answered, (b) still needs a capture.

P6 looping was not wrong, only thin. It now carries the measured cost: the bed loops at 87.8 s against the track's 87.7, and the seam is 3.4 seconds of near-silence. The missing loop point now has a price expressed as a number rather than as the adjective "ugly".

The undated rows are the ones that rotted

Checking which sections record the HANDOFF commit they derive from — the standing instruction, and the thing this page's own header demands — splits the file exactly:

sha
every prose section added since 2026-08-29 all
every row in the two tables 🔴 none

And all three stale rows were in those tables. That is about as close to a controlled experiment as this page will produce: the rows with no date are the rows nobody could tell had aged.

⚠️ I have not back-dated them. Nobody knows when most were written and an invented sha is worse than an admitted absence — it would make an undated claim look checked. Both tables now say so at the top, and carry the date they were last audited against a running port.

📌 The general form, which is the eleventh of these: a document that warns about its own failure mode is not thereby protected from it. This page has said "it goes stale, it has" since P1, in a header everyone reads and nobody acts on. The warning was doing the work of a check.

The record already answered last iteration's question, under headings that name it

Last iteration I investigated title's and title_jp's disagreement with sylpheed-cli, ruled out leaves and placement offsets, watched an edge test fail its own control, and filed the mechanism as unknown — to the Decoder as well as here.

Both were already explained in this file, under title: a tie in the paint order and title_jp: nearest-neighbour sampling phase. Headings that name the two screens in question.

Both still hold, checked rather than assumed

  • title — the CLI uses a paint order measured off the running game; the port derives one. Every disagreement is inside a tie. Verified against the current export: title still ties on 0x8083 (the back2 glow group, 5 elements), 0x80a0 (7) and 0x8010 (2), and the export declares paint_order_ties in unresolved. The old entry costs it at 904 px in the glow band, all 46/255; I measured 790 px at x≈9381162, y≈172310, max 6 — same band, same magnitude, count moved with the export.
  • title_jpptlogo_eff2 at 125 %, where the CLI samples the source at the destination pixel's top-left and a GPU samples at its centre. The entry claims it is the only drawn element in the export at a non-integer scale ratio. 🟡 A whole-export census finds 26 such keyframes — until it is restricted to elements visible at rest, which is the pose verify-screen uses. Then there is exactly one: title_jp/ptlogo_eff2. The claim survives, and it survives narrowly enough to be worth the check.

🔴 The failure is navigability, not memory

6 502 lines, 111 ## sections, 243 ###, and no index. So "has this been decided already?" had no cheap answer, and the expensive answer — re-deriving it — looked like diligence. It also cost the Decoder a message reporting an open question that was closed.

tools/port/index-decisions generates a contents block, and check-all runs --check. ⚠️ Two things about the design, both learned the hard way in the same half-hour:

  • it carries no line numbers. The first version did, and writing the index shifted every line below it — a fixpoint that failed its own --check on the first run. Worse, appending a section would silently invalidate all of them.
  • it is checked, because a stale index is worse than none: it would answer "is this already decided?" with a confident no. That is exactly the failure it was built to prevent, and building an unchecked one would have reproduced it one level up — which this port has now done often enough that I looked for it deliberately.

📌 The twelfth instance, and a new species. The first eleven were "correct, documented, unexercised." This one is correct, documented, unfindable — and it is worse, because it does not merely fail to catch an error, it manufactures work and then exports a wrong status to somebody else.

🔴 Twenty-one messages to a dead address, each one warning me it was dead

Every SendMessage I sent the Decoder from 2026-08-29 onward came back with:

accepted by the server for that session, but delivery is not confirmed: it has not reported that it can receive cross-session messages

Twenty-one times. I recorded it in my reply every time — "⚠️ delivery unconfirmed; everything is in the repo regardless" — and treated writing it down as having dealt with it. It was not a caveat. It was the tool telling me the address was dead, in the same words, on every attempt.

The cause

I addressed bridge:session_01QsEPXW…, copied from the from= of an inbound message. That is a session ID, and it died when the Decoder restarted. PROTOCOL.md says plainly that both agents "register under those names at startup" and shows SendMessage(to: "sylpheed-agent", …). Names survive a restart; session IDs do not. The protocol had the answer; replying to the from= field felt more precise and was strictly worse.

Why this one is the worst of the thirteen

The other twelve were things nobody was looking at. This one had an alarm, and the alarm fired every single time. I did not miss it — I read it, wrote it into my own status line, and carried on. Reporting a warning is not acting on it, and phrasing it as a footnote to the user made it look handled.

⚠️ It also silently inverted a habit I had been congratulating myself on. I kept saying "everything is in the repo regardless, which is the point of the rule" — true, and it let me treat a broken channel as a non-event for a fortnight. The repo did carry the findings. What it could not carry were the asks: four questions sat unasked while I believed they were queued, including one that can delete an authored entry.

What changed

Re-addressed by name after ListAgents, and the send came back without the warning — which is what a working channel looks like, and what twenty-one unworking ones did not.

📌 The general form: an unacknowledged warning is worse than a missing one, because it converts into evidence that the situation is understood. The fix is not "read the warnings" — I read them all. It is that a warning repeating unchanged across attempts is a state, not a footnote, and the second identical one should have been treated as a failure rather than a fact about the world.

The forced-backdrop pass is load-bearing on two screens, not six

I have said "six forced elements" since implementing the rule, and checked after every change that no verdict moved. That measured the pass's stability, never its necessity. Removing it entirely answers the other question:

screens order without the pass
publisher_logo, publisher_logo_r, developer_logos, developer_logos_r byte-identical
build_12, build_15 first element becomes pgloading_loop5 — the black screen returns

Four of the six are redundant. palogo_eff0's layer key is 0x00000000, layer_source: implied — lower than the lowest sprite key on those screens (0x0000a100) — so the crate already sorts it first and the occlusion rule merely agrees.

🔴 The two that matter have no key at all. On build_12/build_15, pgloading_eff00 carries layer: null, layer_source: none — the only two elements in the export with neither a read nor an implied key. Their position rests entirely on the occlusion constraint, with nothing to fall back on. That is the port's single strongest dependency on a rule it did not decode, and it was hiding inside a count of six.

Which is also what makes the rule worth having

An agreement on four screens where a key already existed is not evidence — it is the rule reproducing the crate. The two screens where it is load-bearing are the two the rule was found on, and the argument for it there is not the key (there is none) but that a permanently black screen is impossible on its face.

⚠️ So the honest statement, replacing the one I have been repeating: the rule decides two screens and confirms four. I will not describe it as six again.

The layer-key census this came out of

layer_source count which
sprite — read from the file 160 everything with a texture
implied — the crate's, measured per name 16 pteff00, pteff02, palogo_eff0, pgloading_eff00
none — no key exists 2 pgloading_eff00 on the two loading screens

Every layerless primitive's position is implied or absent, never read — and FORMAT.md requires a consumer to be able to tell, which layer_source delivers. The Decoder's own page says pteff00's place on top "is still a measured per-name entry, not a decoded one"; the port inherits that through layer: 0xfffffffe, layer_source: implied and declares it. Nothing is being passed off as decoded that is not.

Re-running the Decoder's necessity census: every figure reproduces, and what that is worth

They took the stability-vs-necessity correction disc-wide and published the instrument with it — "so you can re-run it rather than trust it". I did, from a worktree at their branch, across all 33 archives their census names:

theirs my re-run
forced instances 80 80
decided by the rule 62 62
merely agreed 18 18
decided, by extension 38 .prm / 24 .tbm 38 / 24
decided rows listing a keyless element all all 62

Exact, on every figure. ⚠️ The example defaults to GP_TITLE and takes an archive path, so a bare run reports 6 instances, not 80 — the disc-wide number needs the loop. Worth saying because "I ran their instrument" would otherwise be true and mean a thirteenth of what it sounds like.

🔴 What this verification is not

I ran their code. A fault in the instrument reproduces identically for me, so this is not two independent measurements — it is one measurement executed twice. That is the same correlated-instrument trap as verify-screen scoring two blank frames OK, and I would rather name it than let a table of matching numbers imply more than it has.

The genuinely independent evidence is narrower and came first: I removed my own post-pass in the exporter and diffed the export. Different code, different language, different layer of the stack — and it agreed on the six GP_TITLE instances. Their crate-side run agrees with that. So the GP_TITLE result has two witnesses; the other 74 have one, executed twice.

The consequence for the port

Both of the port's decided screens are inside the 62, and every one of the 62 is keyless — so the impossibility argument ("a screen black at every instant of its own timeline cannot be right") is carrying all of them alone, with no key beneath it anywhere. The exposure is theirs to hold disc-wide; the port's share is build_12 and build_15.

⚠️ 24 of the 62 are .tbm, which their page still records as "correct or inert" because the corpus cannot find a .tbm's pixels. None are in GP_TITLE — checked again: 115 .t32, 45 .rat, 18 .prm, no .tbm — so nothing the port ships depends on that half. If the alpha-over assumption ever fails, those 24 go with it and the port's two do not.

A second witness for the pixel-cost claim, from a different renderer

The Decoder moved the necessity question to a new layer rather than re-running the sort — they rendered each of the 62 deciding builds twice and diffed the pixels, finding that on all 38 .prm deciders the changed pixels equal the composite's entire ink, 38 of 38, and putting build_12/build_15 at 49 771 px = 5.40 % each.

That is checkable in Godot, which is a genuinely different renderer — unlike last iteration, where re-running their instrument gave one measurement twice.

build_12, --pose=rest ink
with the rule, threshold > 0 59 530 px (6.46 %)
with the rule, threshold > 1 48 368 px (5.25 %)
without the rule 0 px at both

The strong form holds. Removing the rule does not dim the screen or shift it — it takes the ink to exactly zero. So "the changed pixels are the whole composite" is not a way of saying "a large difference"; it is the screen ceasing to exist, measured in a renderer that shares no code with theirs.

Their 49 771 sits between my two thresholds, 2.9 % above the > 1 count. That is a threshold convention on a mostly-dark frame, not a disagreement, and it is worth recording as such so nobody later reconciles two ink figures that were never counting the same pixels.

⚠️ Method note: the no-rule order was produced by applying their fallback — sprite key, else implied, else u32::MAX — to the export's own element list and swapping only paint_order on one screen file. That keeps the port's renderer and every other input identical, so the only variable is the order. pgloading_eff00 duly sorts to first-drawn under the rule and last-drawn without it.

Their point 4 is the pattern catching one of their own

They report that the 24 .tbm deciders all measured zero pixel cost — and that this is not the rule being free: compose draws no pixels for a .tbm at all, so their position cannot change a pixel by construction. The control asked whether the composite had ink, which it always does. A control that could not fail.

That is the fourteenth instance of this project's recurring shape and the first found by the other agent using the frame rather than by me. It leaves the .tbm half exactly where it was — "correct or inert", still indistinguishable — rather than falsely cleared, which is the outcome the bad control would have produced.

Nothing the port ships is affected: no .tbm in GP_TITLE, and the port's two deciders are .prm.

Reconciling the two ink figures, and what "has its own key" is resting on

The Decoder reconciled our build_12 ink counts and corrected a mistake I would otherwise have made in their favour.

theirs mine gap
RGB > 0 49 771 59 530 9 759 px
RGB > 1 48 043 48 368 325 px — 0.67 %

🔴 Their 49 771 was never a threshold figure. It is exact RGBA inequality between the two paint orders, which over a black backdrop coincides with ink>0 — so it belongs against my 59 530, not against my 48 368. Matching it to the closer number would have made us agree for the wrong reason, and they said so before I could do it.

Checked on my side: my value-exactly-1 population is 11 162 px, and the gap above >1 is 325 px. So 9 434 of the 9 759-px gap — 96.7 % — lives in pixels whose value is exactly 1, and their renderer produces that many fewer of them. Their claim holds structurally, not just in headline.

Two conventions worth carrying, both theirs:

  • >0 is not portable between renderers on a mostly-dark frame; >1 is. Any cross-renderer ink figure needs its threshold named. This corpus has spent real effort on residuals at the 0.1 % scale, and a 16 % artefact hiding at 1 LSB is exactly the size of thing that would have been argued about.
  • the without-the-rule zero is threshold-insensitive in both renderers, which is the first double-witnessed result on this rule.

One refinement on the 18 confirmations

They sharpened my summary: the 18 are worse than "the crate agreeing with itself" — 14 have their own key so the rule never fires decisively, and 4 are builds where every element is forced, so the tie-break gives the same order regardless. None is evidence in either direction. Agreed.

⚠️ But for the port's four, "has its own key" is doing quieter work than it sounds. palogo_eff0's key is 0x00000000 with layer_source: implied — the crate's measured per-name entry, not a value read from the file. So those four confirmations are not the file already settles it; they are another measurement already settles it, in the same category as pteff00's place on top, which their own page is careful to call measured rather than decoded.

That does not change the verdict — a confirmation is not evidence either way. It changes what the confirmation is made of, and the distinction is one their page already insists on elsewhere.

Not one of the 80 has a decoded key — and the port's four are the rule's oracle check

The Decoder took the key-source question disc-wide and the answer is stronger than either of us had it: 0 of the 80 forced instances carry a key read from the T8aD header. 14 have an implied_layer_key, 66 have nothing.

So the rule has never been checked against a decoded field, because there is no instance where both can speak. That is what a keyless-element fallback necessarily looks like — but it removes a check a reader would assume exists.

The port's six are the same pattern exactly, verified from the export:

count
read from the file 0
implied — measured in the running game 4 palogo_eff0 on the four splashes
nothing at all 2 pgloading_eff00 on build_12/build_15

They retracted something in the direction that costs them

Last round they wrote that "none of the 18 is evidence for the rule in any direction", and I agreed and repeated it. They have since separated two questions I had let run together:

  • does the rule change the composite there? No — the sort already had a key. That is the necessity count and it stands.
  • does the rule get the right answer there? Yes — and this is the only place it can be asked. implied_layer_key is a position measured in the running game, so the rule forcing those elements first is the rule agreeing with the oracle, not with their crate.

So the 14 are not non-events; they are the rule's only external corroboration, and four of them are the port's splash screens. My own framing — "the port's four rest on an implied key rather than a read one" — was right about the provenance and wrong about its weight: I filed an oracle measurement under "not the file", which reads as weaker when it is stronger.

⚠️ The revised state, which I expect to be stable: 38 .prm deciders with a measured pixel cost, two double-witnessed · 24 .tbm deciders untestable by either agent · 4 inert · 14 confirmations against measured positions, the rule's only external check · 0 against a decoded field, anywhere. The impossibility argument carries all 62.

📌 Closing this thread on my side. It has run four iterations and produced: the necessity correction, a second witness in a different renderer, a reconciled ink convention, a retracted control of theirs, and this. It has also stopped being about the port — nothing in build_12/build_15 has moved since the rule landed, and the remaining questions are the Decoder's to hold. Continuing would be refining someone else's page.

A withholding reason that was false, and the measurement beside it that was not

authored/rendering.json names three leaf records the exporter flags and the port does not draw. Auditing the reasons rather than the behaviour:

🔴 build_12,15/pgloading_loop5 — the reason was wrong

It read "leaf scale (0,0). A zero scale is one of the three historical failures this corpus names." That describes t=0 and t=30 and nothing after them.

What the leaf actually holds, read from the export: one element, pgloading_ring, with a sprite, whose scale ramps 0 → 250 → 800 → 1000 while its alpha rises to full at t=55 and falls to nothing by t=130. An expanding, fading ring — a loading pulse, not a degenerate record.

⚠️ And it is visible at the instant this port poses. build_12's settle window is [40, 48], so the pose lands near t=44, where the ring interpolates to scale 140 at alpha 143. Withholding it is not declining to draw nothing; it is declining to draw something, and the one-line reason concealed which.

It stays withheld, on the reason that always applied and is already written below it in the same file: there is no way to adjudicate it here. The loading screens have no oracle capture — the Decoder records them as unreachable from the title path — and verify-screen compares against a renderer that draws no leaves. Drawing it would put unadjudicable content on a screen, which is the test ptlogo_eff2 also fails. Behaviour unchanged: build_12 still renders 59 530 px of ink.

📌 The two entries in that list were written to different standards. The title_jp/ptlogo_eff2 reason is four paragraphs, states the scale as a pop (0 → 125 % → 0 over 0.95 s), works through both readings and says why neither can be chosen. The pgloading_loop5 reason was one line and false. Same file, same author, same day — the difference is that one was interesting and one was a loading screen.

The measurement in the same paragraph checks out

That paragraph also claims "its max went 155 → 232 when they were drawn" for title_jp. Reproduced by adding ptlogo_eff2 to draw_leaf_for and re-running: 155 → 231, with the differing pixels going 20 498 → 58 313. One off a number recorded days ago, which is rounding rather than drift.

So the file's measurements were sound and its reasoning was not, in adjacent sentences. That is worth naming: I have been auditing whether numbers are right far more often than whether the sentences around them are.

The sweep discriminator resolves: different frames, and a sweep position cannot date one

The Decoder answered ask #2 — t=357.7 was never fitted against a PNG. It comes from title-draw-capture-vertex-colours.log, a GPU per-draw capture of the vertex buffer the game submitted: two quad centres and two vertex alphas, four observables solved at once, nothing rendered by either of us.

They then handed me the discriminator rather than running it, because the fit is against my renderer: if your ~400 is pteff03 and your frame is inside the first cycle, pteff03a in that same frame must sit at centre 295.

Run from the export, with their published t=355 centres as the control:

leaf phase pteff03 centre pteff03a centre
t=355 — their control 980.5 (published 981) 477.7 (published 478)
t=400 — my fit 1160.5 294.9

294.9 against a predicted 295. The discriminator is satisfied: the two numbers describe different frames, and neither of us is wrong. My computation also reproduces their control to half a pixel on both quads, which is what makes the 295 worth anything.

Why the two numbers could never have matched

The sweeps are nested records on a free-running loop with cycles of 600 and 720 units — read from my own export as each leaf's last keyframe, matching their header +0x08 — while the top-level clock stops at settle. So two captures of one settled title share a screen time and not a sweep phase, by construction.

🔴 The rule to carry: a sweep position cannot date a frame. I had been treating my ~400 as a property of the capture — it is a property of how long that title had been up, which is exactly what the settle window makes unmeasurable.

And the two are not comparable in kind. Their 357.7 is a joint fit where both leaves agree; mine poses one phase. With 600 and 720 the phases coincide only every 3 600 units — 60 s — and their draw capture caught both inside their first cycle, which is the only window where one number can cover both.

My --leaf-time is right for this by accident rather than design: it sets one phase, and loop_leaf takes fposmod against each leaf's own span, so the two diverge correctly past 600. The sweep I ran covered 0600, inside the first cycle, where a single value is unambiguous.

Their refutation attempt on my number, which failed

Worth recording because they published it: they hypothesised my fit was minimised by the quad leaving the screen — "best fit" meaning "draws least", the same shape as their .tbm control that could not fail. At t=400 quad B is fully on screen and quad A is 319 of 400 px wide. The number is fitting something present, and it survives.

Their trap, run against my tree — and I found its mirror instead

The Decoder's Ⓐ blocker turned out to be the sign-in dialog, already described in canary-scripted-input-traps.md §3 and in boot_menu.sh's header. Their lesson: a measurement whose only record is a script comment is invisible to the document that needs it — and they asked whether I have findings living in exporter comments.

I ran it: every measurement-shaped token inside a comment in crates/sylpheed-export/src/, port/scripts/ and tools/port/, checked against everything in docs/.

Seven candidates, and six were my matcher's fault — thousands separators (1 950 px), a range written 0.70-0.82 % in one place and 0.1355 % in the other, precision differing between 9.1 and 9.14. The findings themselves are all in DECISIONS.md, including the one that looked most alarming: the leaf comment's "981 and 478 against 992.0 and 467.2 measured off the capture" is recorded, and the 11.5 px residual has its own paragraph saying it is not to be fitted.

🔴 The one real defect is the opposite trap

check-capture's control table and AUDIO-VERIFICATION.md both record the voice control, and they had drifted: 53.3 % in the tool, 53.2 % in the doc, twice each.

Neither can be re-measured — that control file was transient and is gone — so there is no way to say which copy aged. The Decoder lost a finding because it had one record, in a script comment. I lost a digit because a finding had two records and nothing kept them equal, and both copies look authoritative.

Fixed by removing the duplicate rather than picking a winner: the doc is the record and the tool cites it. The commented explanation keeps both numbers, since naming the drift is the only part that could not be reconstructed later.

⚠️ And it corrects something I sent the Decoder

I told them my computation "reproduces your published centres to half a pixel" — 980.5/477.7 against 981/478. True, and model against model: their published figures are their fit's centres, not the capture's. Against the capture this corpus already records 992.0 and 467.2, an 11.5 px residual, in a paragraph that says it is not to be fitted.

So the half-pixel agreement is two derivations of one model agreeing, which is the correlated-instrument shape I have been careful about all week and did not apply to my own message. The discriminator result is unaffected — 294.9 against a predicted 295 is a different quantity — but the strength I attached to the control was overstated, and I have told them so.

The plate pulses — measured, and the port was wrong on the boot's end state

Ask #1 came back the other way. Held at the title with no input, the plate oscillates continuously — two windows in one boot, ~23 cycles each, no decay, no settling — and 🔴 it never goes off: the plate-absent floor is 159 thresholded green pixels and the pulse bottoms at 714, four and a half times that.

That reading is what makes the mechanism recoverable. A glow alone cannot do it: ptbtn00f ramps 0 → 80 → 0. A steady base plus a pulsing glow can, and 714 and 1520 are exactly base-only and base-plus-glow.

And the port already had the base right. ptbtn00's fade to 0 at t=244 is its exit ramp, which plays when the screen leaves; while the screen is held the base sits at its own hold, alpha 255 at t=238. What was missing was the glow.

The defect was in the renderer, not in the deleted entry

_draw treated a looping record like a focused one — drawing it instead of the base sprite. For a focused button that is right and measured (the focused sprite covers the base at 100 % of base-visible pixels). For the plate it substituted a dim glow for a bright sprite, which is why the plate vanished and why I deleted the authored entry on 2026-08-29.

I deleted the right thing for the wrong reason. The entry was correct; the branch it landed in was not. A looping record now draws over the base, and the entry is restored with the measurement behind it.

plate region, base only 95.68
base + glow at peak 115.52
measured period over 10 cycles 1.980 s
declared 120 units at 60 u/s 2.000 s — ratio 0.990

The pulse appears on the boot path, where the overlay runs on the shared clock. It does not appear under --screen=title --overlay=press_start, because a static overlay poses at its own arrival — my own earlier fix — so its clock never advances. That is correct for a still frame and worth knowing before anyone reads a flat plate there as a regression.

What is authored, and what is refused

120 units, not seconds, on the Decoder's own instruction: their run measured 2.530 and 2.540 s and an earlier corpus run 2.24 s — one declared number through two emulator pacings (×1.27 and ×1.12 against a nominal 2.000 s). Hardcoding 2.5 s would author a loaded container's clock.

⚠️ Their three limits are carried into authored/timing.json verbatim: one boot, two windows inside it are not two boots; it does not distinguish the boot title from an attract-loop title, since run 1 opens at t≈255 s against Q9's ~193 s baseline; and 🔴 714/1520 is not an alpha ratio — the counter is thresholded pixels, so no duty cycle or ramp shape may be read from it. The port draws the record's own declared ramp and infers nothing about its shape.

Oracle rows unmoved; every asserting check passes.

A static overlay now advances, and a refutation attempt on the pulse floor

The static overlay was frozen at its arrival, which was my fix overshooting

--screen=X animates X. --screen=X --overlay=Y froze Y, because the fix for the original defect — the raw elapsed clock pushed in, 9 units at capture, plate drawing nothing — replaced a frozen-too-early overlay with a frozen-at-arrival one. One command animating one build and freezing the other is an inconsistency, and the plate pulse is what made it visible: the plate oscillated on the boot path and sat flat here, which reads as a regression and is not one.

Now offset, not pinned: the overlay begins at its own settle and takes the same delta the main view takes. Verified — the static path pulses over the same range as the boot path, 95.85 → 115.41 against 95.68 → 115.52. Still frames are unaffected (--time freezes both) and title_plate holds at 0.00 %.

📌 Both halves of this were mine, a week apart: the original bug, then the over-correction for it. The over-correction was invisible until a third change — the pulse — gave it something to be wrong about. A fix that overshoots leaves no symptom until something else needs the part it disabled.

🟡 Refutation attempt — their pulse floor of 159 green pixels

Their measurement gives a plate-absent floor of 159 thresholded green pixels, a pulse minimum of 714 and a peak of 1520, the floor measured on live-title-build4-no-plate.png — a capture I hold. So it is checkable.

It is not reproducible from the published description. Across the plate region (513×48 at +383+551) my counts are 35× theirs at every threshold:

green > no-plate press-a ratio
100 676 9 369 1 : 13.9
150 528 5 732 1 : 10.9
180 462 4 908 1 : 10.6
200 411 4 272 1 : 10.4
theirs 159 1 520 1 : 9.6

No threshold produces both numbers, so their region must be a tighter crop than mine. Neither the region nor the threshold is stated on the page.

The finding survives in the part that matters. The ratio is stable at 1 : 10.410.9 across a wide threshold band and brackets their 1 : 9.6, so "the plate-present frame carries about ten times the green of the plate-absent one" is robust to how it is counted. What cannot be checked is the absolute figures — and those are what the "never goes off" conclusion is stated in.

⚠️ This is the convention we agreed two iterations ago, applied to their own new page: a cross-renderer pixel figure needs its region and its threshold named. I raised it there about my numbers drifting between two files; the same rule makes a published measurement unverifiable when a reader has the capture and not the crop. Raised as a documentation gap, not a defect — nothing in the port's implementation depends on the absolute counts, only on "steady base plus pulsing glow", which the ratio supports.

Their pulse floor reproduces exactly once the predicate is named — 159, to the pixel

I challenged their 159 / 714 / 1520 as unreproducible: my counts over the plate region were 35× theirs at every threshold. They published the method — whole 1280×720 frame, and a three-channel predicate (g>130) & (gr>45) & (gb>45), not green>N.

Applied to the capture I hold:

theirs mine
floor, live-title-build4-no-plate.png 159 159 — exact
pulse minimum 714 live-title-press-a.png gives 753, 5.5 % apart
"never goes off" ratio 714 / 154 = 4.6× 753 / 159 = 4.7×

The floor reproduces to the pixel, and the load-bearing claim now has an independent witness: a capture from a different session, counted by me, lands at 753 against their pulse minimum of 714. My region-and-single-channel counts were simply the wrong measurement — the method statement was the whole difference.

📌 The challenge was worth making and the outcome was not the one I expected. I raised it as "your figures are unverifiable"; the answer was "here is the predicate", and then they verified exactly. What the challenge actually bought was their own geometry bug: naming the method exposed that the 159 floor came from a 1279×675 capture while the pulse frames are 1280×720 — two crops silently compared. They have replaced it with a same-run, same-geometry floor of 154.

So a demand for reproducibility found a defect that was not the one being demanded. ⚠️ And note which direction it cut: my counts were the wrong measurement and theirs had a real flaw, at the same time. "One of us must be right" was never the shape of it.

Their retraction, and whether the port banked it

They retract citing a faulting run's dumped logged_profile_slot_0_xuid = "" as evidence no profile was signed in: Xenia dumps the config file, before command-line overrides — a run launched with --mute=true dumps mute=false, four for four. So that dump cannot say what any run did.

Checked: the port's docs never cite it. The Ⓐ diagnosis is recorded here as retrodicted, not demonstrated, and nothing in the port depends on it. Their A/B now supplies the demonstration — 3 811 swallow lines against 0, and a main menu at the documented 327 glyphs — with the limits they state: one run per leg, and leg A shows the swallow, not the crash.

⚠️ Also worth carrying: their first A/B pair was void. The detector fired on a single frame over threshold and caught the intro movie's green flashes ~6 s before the title, in both legs. The presses were real — each skipped the rest of the movie, which is Q9's behaviour — but the pair tested nothing. A detector that can fire on one frame will fire on the wrong one.

My rendered pulse, counted in their units — and #4 refutes the voice value without fixing it

The pulse lands in the right range, and I am not tuning the depth

Their predicate makes my render comparable to their measurement for the first time. Counting the boot's held title with (g>130) & (gr>45) & (gb>45) over the whole frame:

theirs mine
plate-absent floor 154
pulse minimum 714 805
pulse peak 1 520 1 420

Same range, and both ends far above the floor — so "never goes off" holds in the port's own render, not just in their capture. The trace oscillates cleanly: 1413, 1359, 930, 857, 805, 830, 1182, 1413, …

⚠️ My pulse is 24 % shallower (615 against 806). I am not tuning to close that, and their own limit says why: "714/1520 is not an alpha ratio — the counter is thresholded pixels, so dim pixels drop out first." A depth measured this way is a fact about the threshold as much as about the glow, and matching it would be fitting my alpha ramp to a counter neither of us claims is linear. The port draws the record's declared ramp.

#4 refutes the voice value from the output side

They recorded 148 s of the game's own output over the boot intro — ALSA tee, --gpu=null, 0.15 % silence, with provenance from the XMA probe rather than a screenshot. Five of six channels carry distinct content, no channel is a copy of another, and the largest pairwise correlation is 0.70 between FL and FR.

🔴 So presentation: "loudest" — keeping one stream — cannot be right. It was already labelled known-wrong here because the game decodes all three concurrently; it is now refuted by what the game plays.

And it stays unfixed, on their instruction. The stream→channel mapping is not run — that correlation is their next iteration — and changing the mapping now would swap one authored guess for another. A guess that is labelled is a better position than a guess that is fresh, because the label is what stops it being believed later. authored/audio.json records the refutation and keeps the value.

⚠️ Two of their limits are the kind I would otherwise have skated past: this says nothing about which stream lands where, so it does not make summing right; and "6 channels" is not evidence the game is 5.1 — that count is Xenia's hardcoded kFrameChannelsDefault. The evidence is that five of them differ, which a stereo guest cannot produce. The number of channels in a capture is a property of the capture.

The voice export now carries every qualifying stream — and a unity sum was refused by our own check

#4 is answered and it reframes the question the port had been asking. ADV.wmv carries one audio stream and it is WMA Pro 5.1, not XMA — so "which of three voice streams to ship" was missing the bed entirely. The movie's own track is the bed; the three streams are additional.

Solving capture = 0.600 × movie + residual per channel, the gain is 0.600 uniformly (4.44 dB), and the residual is three signals at three positions — front pair (r 0.918), rear pair (r 0.929), and a centre whose partner LFE is empty to 115 dB. 🔴 The load-bearing number is LFE reproducing to 115.73 dBFS: where nothing is added the two decoders agree essentially exactly, so the other residuals are added content, not codec mismatch.

presentation: "all" now keeps every equal-length non-silent survivor: ADV 2 of 3, S00A 1 of 3. The third ADV chunk is the leading one this port had already measured to be the tail of another (r=0.998, lag flush against its end) — correctly dropped — and S00A's others are digitally silent. The top-level warning now keys on kept < present rather than on "more than one stream exists", so it still fires and says what is absent.

🔴 A unity sum was tried and check refused it

First attempt summed at unity, on the precedent of stems: "sum" for a BGM bank. ADV came out at +2.62 dBFS, over the +1.0 bound, and the validator rejected the tree.

It was right, and the precedent did not transfer. A BGM bank's two waves are stems of one signal; these three are positions in a 5.1 field. A stereo downmix weights them 0.4142, 0.2929 and 0.2929 — which sum to one whatever the assignment. So the total is fixed even though the distribution is unknown, and dividing by the input count preserves that total while claiming nothing about which stream sits where. ADV now lands at 3.1 dBFS.

⚠️ Note this is the opposite of the two divisor bugs this file already carries. Those were wrong because an input contributing nothing sat in the divisor — a silent chunk summed, a silent channel averaged. Here every input carries signal and the weights genuinely sum to one. "Divide by N" is not right or wrong in itself; it depends on whether the inputs are parts of one signal or parts of one field, and I reached for the wrong precedent first.

What is still not claimed

⚠️ Which stream sits at which position is not determined — their assignment is by position, not content — so the port builds no 5.1 and applies no positional downmix. ⚠️ Their correction to the earlier census page is carried too: the ALSA permutation [0,1,4,5,2,3] does not apply to that capture; recomputing with no assumed order gives the identity, so the "BR is 82 % silent" channel was really LFE, which reconciles with the movie's own 80.64 % silent LFE. I had recorded the census's channel labels; they are corrected here rather than left standing.

Every asserting check passes.

Their stream assignment does not fit my region — weights NOT applied

The assignment is settled on their side by byte_size: ctx0 (1 294 336) → FL/FR, ctx1 (1 118 208) → FC with LFE silent, ctx2 (1 171 456) → BL/BR, giving the port weights 0.4142 / 0.2929 / 0.2929. Applying them needs their contexts to be my chunks. They are not.

bytes
their three contexts, summed 3 584 000
my whole resolved voice region for ADV 3 114 352
difference +469 648 — 15 % larger than the region they must fit in

My region yields three chunks, one of which is an 84.553 s / 806 972 B leading chunk this port measured to be the tail of another (r=0.998, lag flush against its end) — and 806 972 is none of their three sizes. That leaves two real streams totalling 2 307 380 B, and no pair of their contexts matches it: the closest is ctx1+ctx2 at 2 289 664, out by 17 716 B.

🔴 So the weights are not applied. Assigning positions on a byte-size match that does not hold would be worse than the current divide-by-count, and their own figures say how much worse: a swapped assignment is wrong by 11.76 dB. The export keeps presentation: "all" with the count divisor, which claims nothing about placement.

⚠️ What I am not claiming: that their assignment is wrong. It is derived from a probe of the running decoder; mine is resolve_movie_voice_region's byte range off the disc. One of the two spans is not what the other thinks it is, and that is a decode question in their lane, not a mixing question in mine. The numbers above are the whole of what I can say.

📌 This is the first time in this exchange that taking a settled result would have been the wrong move. Every previous round ended with me adopting something — sometimes after checking it, once after they retracted it. The discipline that mattered here was checking whether the identifiers resolved before using the conclusion they index, and the identifier was a byte count I happened to be able to test.

What I did take

One mixer gain, not two — the same 0.600 scales the movie bed and the voice — is worth having independently of the assignment, and is recorded. ⚠️ Not baked in as a constant: whether 0.600 is a fixed mix constant or a volume setting is unknown, and the port applies no gain of its own either way.

And their two failed instruments, which look like results and are not: envelope correlation returns 0.860.95 for every stream against every channel, because all six residual channels share the dialogue's activity timing — a matrix of 0.9s reads as a strong finding and is the opposite. Sample-level correlation returns ≈ 0, because the XMA decode's framing offset is unknown. I have used envelope correlation as my main audio instrument all fortnight; that it saturates where the content shares timing is a limit I did not know it had.

The resolver starts late, and my "duplicate tail" was a real stream all along

My refusal to apply their weights found a defect in their decoder: resolve_movie_voice_region starts inside the first stream. For ADV, ctx0 declares 632 packets (1 294 336 B) and the resolver's leading chunk is 394 (806 912 B) — late by exactly 238 packets, 487 424 B. A whole number of packets, so an offset rather than corruption; extending by 238 makes to_xma_riffs yield all three declared sizes. Disc-wide, 24/24 single-chunk regions are fine and 8 of 10 three-chunk regions start mid-stream.

🔴 Which re-reads a measurement of mine, without touching the measurement

This port measured the leading chunk as "the TAIL of the kept stream [refuted]" — envelope correlation r=0.998, lag flush against that stream's end — and dropped it as an understood duplicate.

The correlation was right and the conclusion was wrong. If the three streams are simultaneous and the region starts 238 packets into ctx0, the surviving fragment is ctx0's later 62 %, which aligns with the later part of the others — flush against the end. That is precisely what I measured. Same number, and it means the opposite of what I read into it: not a duplicate tail to discard, but a start-truncated simultaneous stream. The port has been dropping a real stream.

⚠️ I first suspected the instrument, because they had just shown envelope correlation saturating at 0.860.95 across every stream/channel pair. Tested on the actual dialogue: a 30 s window against other windows of the same take gives r = 1.0000 at zero offset and 0.08 … +0.08 everywhere else. It localises sharply. Their saturation is a different regime — concurrent streams sharing timing at zero lag — and does not reach a lag search over one track.

So the instrument was sound, its control was adequate, and the error was entirely in the inference. That is a less comfortable finding than a broken tool: there was nothing to fix, only something I concluded.

Not fixed here, deliberately

The port still drops chunk 0. Including it correctly needs one of two things I do not have:

  • the corrected span — theirs, and they explicitly say not to extend blind: ADV is start 238×2048, but for the other seven affected regions the exact clip is unknown, their audit's figure being an upper bound (243 for ADV where the truth is 238);
  • or an alignment decision — chunk 0 is missing its head, so summing it from sample 0 would be wrong by 84.5 s against 137.3 s. Aligning it flush to the end is what the measurement implies, and implementing that on my own authority is the invention the last twenty rounds have been about not making.

The assignment itself still stands — their ratio test was chosen to be immune to the clipping — but ⚠️ chunk 0's absolute level was measured over 62 % of its stream, so its 0.05 dB agreement is luckier than it reads.

The export knew the voice was incomplete; the runtime did not say so

The manifest has carried a full account of the voice export's gap for weeks, and the runtime printed + voice ADV and nothing else. That asymmetry is the dangerous one for audio specifically: a reader of manifest.json gets a paragraph, and a person listening gets clean dialogue with no way to learn that a stream is absent from it.

This port already governs the same situation elsewhere — NEW GAME announces the two measured screens it jumps over rather than skipping them silently. Audio had no equivalent, so:

  • ManifestAudio gains incomplete, one line naming what an asset is known to be missing. Absent means nothing is known to be missing — never that the asset was checked and found complete.
  • MenuAudio carries it alongside the stream, and _play_video prints it at the moment the voice starts.
     + voice ADV
       🔴 KNOWN INCOMPLETE: 2 of 3 streams. The running game decodes all 3 …

Verified on both paths — the boot's ADV and P7's S00A.

🔴 And the first version of the message was false for one of the two assets

It read "one is a start-truncated stream this export drops". That is ADV's story: its dropped chunk is the start-clipped remainder of ctx0. S00A's dropped chunks are digitally silent — a different reason entirely — and the message would have told anyone running the new-game intro something untrue about their own asset.

Caught by reading the output for both, which took one command and which I nearly skipped because the ADV line was obviously correct. The wording now states the counts and points at the entry's why, because which streams are dropped and why is not the same story twice and a single sentence cannot carry both.

⚠️ Worth naming as its own shape: a message generated once from a template but true only for the case it was written against. It is the failure mode of every generic warning, and it is harder to see than a wrong number because the sentence is well-formed and confident in both places.

The voice export is complete — new pin, and the cause was a "within one bank" cap

formats-pin-2026-08-30. The cause of the late start was a second condition on the start filter: end - s < 1_500_000, "only within one bank". ADV's predecessor trailer sits 3 618 816 B before end, so it was rejected and start fell back to anchora TOC offset, not a stream boundary. That is exactly why it hit regions over 1.5 MB (the multichannel three-stream ones) and never the single-stream ones. 17 of 95 movies took the fallback.

before after
ADV region 3 114 352 B 3 618 816 B
ADV streams kept 2 of 3 3 of 3 — complete
S00A streams kept 1 of 3 2 of 3, the third digitally silent
ADV peak 3.1 dBFS 2.84 dBFS

The voice export is now complete for both movies, closing a defect that was open for this entire session — and my re-reading of the "duplicate tail" as a start-truncated simultaneous stream is what the fix confirms. ADV drops zero chunks; the runtime no longer prints its incomplete line.

🔴 And the incompleteness warning was crying wolf on S00A

With ADV fixed, S00A still read KNOWN INCOMPLETE at 2 of 3 — because its third chunk is 93.694 s of exact zeroes. A dropped silent stream is not missing content, and a warning that fires on it teaches a reader to ignore the one case that means something.

Exported gains content_waves — how many sub-waves carry signal — and the warning, the console line and the manifest's incomplete all key on kept < content rather than kept < present. Both movies now report no gap, correctly.

⚠️ Second time in two iterations that this warning was wrong in the cautious direction: first a template message true only for ADV, now a gap claimed over silence. Over-warning is not the safe failure it looks like — it is the failure that makes the next real warning unreadable.

A second movie, in the predicted direction

They note only ADV has external ground truth; the other 16 rest on their own sweep. S00A is a second data point from a different movie: its kept count went 1 → 2 because a chunk that was previously a different duration now matches the others at 93.694 s — exactly what restoring a truncated first stream predicts.

⚠️ It is not independent ground truth — it is my exporter reading their fixed crate — but it is a different asset than the one the fix was derived on, and the outcome was predicted before it was observed. Recorded as that and nothing more.

Oracle rows unmoved; MODDING rules pass.

The positional weights are applied — keyed by byte size, so the key is a check

With the span fixed, ADV's three chunks map onto the Decoder's contexts exactly — each is a declared byte_size plus the 60-byte RIFF header to_xma_riffs prepends:

chunk bytes 60 context position weight
0 1 294 396 1 294 336 ctx0 FL/FR 0.4142
1 1 118 268 1 118 208 ctx1 FC, LFE silent 0.2929
2 1 171 516 1 171 456 ctx2 BL/BR 0.2929

authored/audio.json gains voice.stream_weights, keyed by declared byte size, and the exporter applies positional weights only when every kept stream's size is in the table — otherwise it falls back to the count divisor.

🔴 The key is the check. Two weeks ago these same sizes did not fit the region the resolver returned, and that is how a 238-packet late start was found. Applied by position instead, the weights would have gone onto the wrong streams in silence. S00A matches nothing here and keeps the divisor: extending by position would assume the ordering generalises from one movie, which is exactly the inference the byte-size key exists to prevent.

ADV now mixes at 0.4142 / 0.2929 / 0.2929 and lands at 2.87 dBFS.

An unlooked-for structural confirmation

The generated filter folds chunks 0 and 2 from two live channels (0.5*c0+0.5*c1) and chunk 1 from one (c0). live_channels found that independently, by measuring which channels carry signal — and it matches the Decoder's structural claim that ctx1 is the only stream with a digitally silent channel, and LFE the only channel with an empty residual. Their evidence is a decomposition of the game's output; mine is a peak measurement on the disc's own chunks. Different sides, same structure.

🔴 Unexplained: verify-menu-audio's dead-press check has started failing

Its first assertion — five presses bound to nothing produce a Master bus bit-identical to five waits — now reports DIFFER, reproducibly across three runs. The two recordings diverge at 0.085 s, differ on 92 % of samples, and have different durations (1.300 s against 1.207 s) where they were previously identical.

⚠️ I have not identified the cause and am not guessing at one. It is not the voice change — that touches only the voice export, and neither control run plays a voice. The candidates I can name and have not separated are the new pin, the plate-pulse draw path, and the static-overlay clock.

📌 What the failure does expose is a weakness in the test I wrote: it compares two separate process runs and assumes bit-determinism across them. That premise held for weeks, which is why it looked like a strong assertion — no thresholds, no tuning. It is strong only while startup is deterministic, and nothing in the test checks that it still is. A comparison within one run, or an explicit determinism control, would not have this failure mode.

Filed rather than patched: silencing it would remove the only check that a dead press stays silent, and I would rather have a failing check than a passing one whose premise I have stopped believing.

External ground truth for every three-chunk region — the movies' own durations

They have no external check on the 15 regions beyond ADV, and no route to one that does not go through the port or the emulator. There is one reference in reach that owes nothing to their crate: each movie's own duration, read from its WMV. A start-truncated stream must decode shorter than its movieADV's was 84.55 s against 137.71, a 38 % deficit.

Dumping every region with their adv_voice_dump and decoding each chunk:

three-chunk regions found 25
chunks decoded 75
chunks more than 5 % short of their movie 0
largest deviation of any chunk 1.78 %

No region is still truncated. The reference is external — the durations come from /disc/dat/movie/*.wmv, which their resolver never touches — so this is the independent confirmation S00A alone could not be. What it does not cover is whether a region's start is byte-exact; it catches truncation, which is the defect that existed.

🟡 A population discrepancy worth their attention. Their page says "8 of 10 three-chunk regions start mid-stream". I find 25 three-chunk regions on this disc, not 10. Both numbers cannot describe the same population, and I cannot tell from here which is the different one — a filter of theirs, or a difference in what adv_voice_dump returns after the fix. Reported, not resolved.

🔴 My first run silently covered half of them and said it was clean

It reported "0 of 13 three-chunk regions have a short chunk". There were 25. cargo run inside a while read loop consumes stdin, so every second line of the movie list was eaten — the covered set was positions 1, 3, 5, 7… exactly.

The result was correct for what it measured and the population was half what the sentence implied. I caught it only because 13 did not match the 25 I had counted one command earlier, and I nearly did not compare them — the finding I wanted was "0 short chunks", and it was there.

⚠️ This is the shape I have been cataloguing all fortnight arriving in my own shell loop: a silently reduced sample presenting as a complete one. It is the truncated-log trap, the --screen default at t=0, and the checker whose input was smaller-but-valid. Redirecting the loop's input to fd 3 fixes it; noticing it at all was luck, and the general defence is to state the population and the coverage in the same breath, which the second run does.

The menu bed loops at 61.93 s — and my 3.4 s "ugly seam" was mine, not the game's

The Decoder captured 240 s parked on the menu, reached in 26.8 s via the XMA-log oracle they wrote down rather than a screenshot. Two findings, and both cut against what this port had authored:

  • 🔴 No seam. Zero runs ≥0.3 s below median18 dB in 232 s of the real menu.
  • 🔴 Not the wave's length. r = 0.009 at 87.750 s; top lag 61.909 s. A second instrument agrees — 30 s slices located inside the decoded waves show playback advancing exactly +5.00 s per 5 s and wrapping at 61.93 s, three times, against a control that finds slices cut at 10/45/70 s at 10.00/45.00/70.00.

The loop is [≈0, 61.93) of an 87.744 s wave, so the final ~25.8 s — the fade-out and trailing silence — is never played. The game loops before the fade.

What this port had recorded, and how confidently

loop: "restart" replayed from sample 0 at the wave's end, and I measured the resulting seam off my own Master bus: 36 near-silent 50 ms windows spanning 84.4087.80 s, about 3.4 seconds. I wrote that up as the price of a missing loop point, put it in BLOCKED.md to raise Q10's priority, and sent the Decoder a message describing it as the cost of the field nobody had found.

It was our seam. The measurement was correct and the attribution was wrong — a defect in the port's own loop, reported as a property of the disc.

⚠️ That is the second time this fortnight I have measured something real and assigned it to the wrong side. The first was reading a start-truncated stream as a duplicate tail. Both were cases where the number was solid and the sentence around it named the wrong cause, which is a failure mode no amount of instrument control catches.

The fix, and why it trims the file

Godot loops a whole file, so a loop region has to be the file. BgmSpec gains loop_end_s and the exporter trims: the bed is now 61.930 s, and the runtime's existing whole-file loop is then correct by construction rather than carrying a loop point nothing could honour.

Verified on the port's own bus over 131 s: 5 near-silent 50 ms windows, no run ≥0.2 s — against 36 windows and 3.4 s before. The seam is gone.

⚠️ Recorded limits: the loop start is inferred, since [0.0, 61.93) and [0.25, 62.18) are not separated at their resolution — the port takes 0 because a bank's data begins there, and says the choice was not measured. And a modder replacing main_menu.ogg is now replacing the loop region, not the whole bank; MODDING rule 1 still holds (one logical asset, one file) because the logical asset is what the game plays.

🔴 And their "8 of 10" was a truncated file, not a count

They have withdrawn it: the audit run was cut short, the committed file ends mid-list at S11A with no summary line, and they read a partial file as a complete one. So "the defect is specific to multichannel regions" is now unsupported — possibly true, not shown, and my 25 stands unopposed.

📌 Their tell and mine were the same on the same day, from opposite directions: their table had no summary line and mine had a population that didn't match a count from one command earlier. The defence that covers both is to state the population and the coverage together — and theirs adds a second: refuse to read a table whose summary line is missing.

The dead-press check was passing by luck, and the luck ran out

Two iterations ago verify-menu-audio's first assertion — five presses bound to nothing produce a Master bus bit-identical to five waits — began failing. I filed it undiagnosed and named three candidates: the new pin, the plate-pulse draw path, the static-overlay clock.

It is none of them. Three identical invocations of the same command give two outcomes:

run duration
1 1.207438 s
2 1.300317 s
3 1.207438 s

The difference is 0.092879 s = exactly 4096 samples, one mixing buffer. The recording quantises to whole buffers, and a one-buffer shift moves both the length and the alignment of everything inside it. So a byte-for-byte comparison of two separate runs cannot hold.

🔴 The premise was never guaranteed — it was luck. It held while the run's timing sat away from a buffer boundary, and a larger export (three voice streams where there had been one) moved it onto one. A test that passes by luck reports the luck running out as a regression in the code, which is exactly what it did: I spent two iterations listing suspects in the port, and the port was never involved.

⚠️ It also passed for weeks looking like the strongest assertion in the harness — exact equality, no threshold, nothing to tune. That was true and it was resting on an assumption nothing checked. Strength of the assertion said nothing about soundness of the premise.

The fix keeps what mattered

Still exact equality and still no threshold; the comparison may now slide by whole buffers, which is the one degree of freedom the recorder actually has. In practice it finds +0 or +1.

And it can still fail, which is the part worth proving: ctrl against walk — a run that really does contain cues — differs at every alignment.

📌 The general form, and it is not the same as the earlier entries: those were checks nobody ran, or that ran and answered a different question. This one ran, answered the right question, and rested on a property of the environment that was never verified and had no reason to be stable. The thing to state alongside an assertion is not only what it checks, but what it assumes about the machine.

Independent confirmation of the 1.5 MB cap — the mechanism, not just the conclusion

Their census settles the population — 104 movies, 95 resolved, 25 three-chunk, confirming my count — and corrects their own claim twice: "specific to multichannel regions" holds (17 of 17 changed regions are three-chunk, 0 are one-chunk), while "all three-chunk regions were broken" is false, since 8 of 25 were already fine.

The 8 they name are the checkable part, because the cause predicts them. A region trips the end - s < 1_500_000 filter only if its span exceeds the cap. Measuring every three-chunk region's span myself:

set count span range
never affected — their 8 8 71 680 … 1 400 832 B
affected — their 17 17 2 023 424 … 6 516 736 B

The cap separates the two sets exactly, with no violations, and it is not marginal: there is a 622 592 B gap between the largest unaffected region and the smallest affected one, with 1 500 000 sitting inside it. That confirms the mechanism — a byte-size threshold — and not merely the list of names, which a coincidence could reproduce.

🔴 My first run of this reported seventeen contradictions

It printed 🔴 CONTRADICTS against all 17 affected regions and 0 for the unaffected — a clean, consistent, entirely wrong pattern. Every span read 0 B, because awk '{print $NF+0}' took the trailing B of = 3618816 B rather than the number.

Had I sent that, I would have told them their causal account was refuted 17 out of 17 — and it would have looked strong, because the failure was uniform and fell exactly along the line under test. A broken extractor produces a pattern shaped by the question, not by the data, and the more structured the question, the more convincing the artefact.

⚠️ What saved it was the 8 unaffected regions reading 0 B too. A span of zero is impossible for a region that resolves, and the "confirming" half of the table was as broken as the refuting half — which is only visible if you read the half that agrees with you as carefully as the half that does not.

📌 Their note about my dead 8 of 10 sharing a digit with the 8 genuinely unaffected regions is the same hazard from the other side: a wrong number that resembles the right answer is the one most likely to survive into a later document. They wrote the coincidence down rather than quietly replacing the figure, which is what makes it safe.

The loop is a runtime field, the two readings conflict, and the port keeps what it shipped

The loop point is decodable — loop_start/loop_end in the XMA decoder context, set by XMASetLoopData, logged by Xenia without a patch. But the values imply a cycle of roughly [10 s, 72 s] against the [0.25, 57.18 s] their audio tracking gave, and neither reading is withdrawn.

Two predictions of theirs were refuted by their own data, which is the part that makes the conflict credible rather than a slip: loop_start is not ~0 (it is 11.6 % into the stream), and a linear bits→seconds conversion yields 62.34 s and 63.29 s for two stems that must stay sample-synchronous — 0.95 s apart is impossible, so the data refutes the linear assumption on its own. XMA frames are variable-length in bits.

The port keeps loop_end_s: 61.93, on their instruction and because the length survives better than the placement: 61.93 rests on an autocorrelation that used no wave at all.

The one check the port could add, and what it is worth

Neither of their instruments asked whether the trim joins smoothly. Over 126.5 s of the port's own bus, the wrap at 61.93 s and again at 123.86 s shows a maximum adjacent-sample step of 212 and 208, against a whole-file median of 132 and a 99.9th percentile of 3 737. The join is not a click.

⚠️ It does not discriminate the two readings, and saying so is the point: a smooth join means the waveform does not jump, not that the loop is musically right, and a cut landing near a zero crossing is smooth wherever it falls. I recorded it as evidence that nothing is audibly broken and explicitly not as support for 61.93 over [10, 72].

🔴 What the conflict costs if the runtime fields win: this export is about ten seconds short, since [61.93, 72] would be content the game plays and we omit. Filed with that number rather than as "the loop point may move", because the former is weighable and the latter is not.

Their diagnosis of their own locator is the entry to keep

"A control easier than the measurement does not bound the measurement's error." Their locator's control matched slices cut from the wave itself — exact copies — where the real task was matching a capture differing by decoder, gain and mix. The clean +5.00 s stepping showed it was self-consistent, not that it had locked to the right phrase, and music with repeated sections is exactly where a locator aliases.

📌 That is the same shape as my awk '{print $NF+0}' reading every span as 0 B: in both cases the output was internally consistent and structured, and in both the tell was in the rows that agreed — my confirming half read impossibly too, and their control was passing a problem it never had to solve.

The duration is confirmed and the window is wrong — and the start is now a visible field

They stopped converting the runtime fields and timed them instead: a probe tailing the Apu debug log, stamping read_offset on arrival, watching three wraps — each from its own loop_end to its own loop_start, with both contexts wrapping at the same instant every time.

observed cycle 61.56 s, 62.06 s → 61.81 s
authored here 61.93 s
difference 0.2 %

The length is settled, and by instruments sharing nothing: a wall clock between decoder events against an autocorrelation that never touched the wave. Both contexts wrapping together is the sample-synchrony the linear bit conversion could not produce — the same conversion that gave 62.34 and 63.29 s for two stems that must be synchronous, and so refuted itself.

🔴 The window is wrong. loop_start is at 3.6 M bits — 11.6 % of the stream, about ten seconds — not the 0.25 s their earlier tracking gave. So this export has the right duration over the wrong window: it replays the bank's intro every cycle and omits the tail the game plays.

📌 My smooth-join check has a second use I could not have anticipated. It said the wrap is not a click, and explicitly not that the loop is musically right. That distinction is now load-bearing: it explains why a wrong ten-second window went unheard. A cut near a zero crossing is smooth wherever it falls — including on the wrong ten seconds. A check whose limits are written down keeps working after the thing it was checking turns out to be wrong.

Not re-cut — and the assumption is now a field

Their instruction is to wait: the exact start is not measured. Linear back-extrapolation says ~913 s, and linearity is refuted by the same run, where the bit rate varies 4.4 % within one stream.

But loop_end_s alone silently asserted a start of zero, and that start is now known to be wrong. So the entry gains loop_start_s, authored as 0.0 and flagged as wrong, with -ss applied before -t so the pair is (start, duration) and moving the start cannot silently change how much is kept.

⚠️ An assumption a reader has to infer from a missing field is not one they can weigh. This is the same move as layer_source — the export must let a consumer tell a measured value from an assumed one — applied to a value I had been carrying implicitly for two days.

The new path is proved before it is needed: with loop_start_s = 10.0 the command carries -ss 10 -t 61.93 and the output stays 61.930 s — a window, not a truncation. Restored to 0.0; the export is byte-unchanged. When the start is measured this is a one-value edit, not a code change.

The loop window is measured — -ss 9.44 -t 61.87 — and the near-silence count tracked the error

The region is [9.44 s, 71.31 s] of an 87.744 s wave: the first 9.44 s is an intro played once, the last 16.4 s a fade-out never played. Two derivations on both stems, neither converting bits to seconds — the conversion that had refuted itself by giving two sample-synchronous stems 62.34 and 63.29 s.

61.87 replaces 61.93, 0.1 % apart. The measured value is taken because it has the loop's own endpoints under it; the autocorrelation that produced 61.93 never touched the wave and agreed to a tenth of a percent, which is what makes both worth having.

The port's own near-silence count tracked the window's correctness

window near-silent 50 ms frames in ~127 s
no trim, restart at the wave's end 36, spanning 3.4 s
[0, 61.93] — right length, wrong window 5, no run ≥0.2 s
[9.44, 71.31] — measured 0

That is a real corroboration from this side and it was not designed as one. The old window kept part of the bank's quiet intro; the measured one excludes both the intro and the fade, so no quiet stretch survives anywhere in the loop. The count fell monotonically as the window got closer to right.

⚠️ Wrap continuity is unchanged and still not evidence: max adjacent-sample step 287 and 354 at the two wraps against a 99.9th percentile of 3 812. As before, a cut near a zero crossing is smooth wherever it falls — the silence count discriminated where the step count could not.

🔴 A stale why reached the manifest for two days

Correcting loop_end_why and loop_start_why left loop_whythe field the exporter concatenates into manifest.json — still asserting that the loop would be "AUDIBLY WRONG AT THE SEAM [refuted]", that "no loop-point field has been identified [refuted] anywhere", and that trimming "would INVENT a loop point". All three refuted; all three shipped to any consumer reading the export.

A correction that does not reach the artifact a consumer reads has not been made. The corrections existed, were accurate, and were in the wrong fields.

⚠️ And my first check of the fix reported the stale text still present — because the replacement quotes the refuted sentences in order to name them, so a substring search finds them inside the paragraph saying they are false. I had to read the context to see it. That is the "check the rows that agree" lesson landing on a grep: the match was real and its meaning was the opposite of what the search implied.

Why the wait for 9.44 was cheap

Their note is worth keeping: it was not that the field predicted the value, but that loop_end_s alone was asserting a start of zero in a form no reader could weigh or find — and that proving 10.0 produced a window rather than a truncation before the real value existed meant arriving at 9.44 was a one-value edit with a clean baseline behind it.

Applying "grep the corpus for the claim" to my own corpus

The Decoder found that a claim they refuted in a new page was still standing in bgm-two-stems.md and in HANDOFF.md — the page a reader is told to consult instead of the rest. Their rule: grep the corpus for the claim, not for the file you were working in. Run against mine, on four claims I refuted this fortnight:

claim where state
"the leading chunk is the TAIL of the kept stream" audio.rsmanifest.json 🔴 still shipping
"six expected DIFFERS [refuted]" BLOCKED, DECISIONS marked
"the boot is known too fast [refuted] on both" DECISIONS 🔴 standing, unmarked
"the only thing making the plate reappear" BLOCKED marked

The one that shipped

The dropped-chunk explanation in the exported why still told readers the leading chunk "IS understood: the TAIL of the kept stream [refuted]". That interpretation was refuted — the correlation was sound, but what matched end-flush was a start-truncated simultaneous stream, because the resolver began 238 packets inside it.

⚠️ And it was wrong twice over, in the shape I had already fixed once: S00A's dropped chunk is the silent one, not a leading chunk, so the sentence described a case that was not present. A template message true only for the case it was written against — the second instance of that exact defect in this file, which suggests the first fix taught me nothing general.

Replaced with a per-case account that names the refutation rather than deleting it.

The one that was standing

"The port's boot is known too fast [refuted] on both, by an unmeasured amount" — withdrawn days later, when the splash dwells turned out to be declared on the disc and the port already exact. I wrote the withdrawal as a new section and left the original untouched, so a reader arriving at the older paragraph got the dead answer. Annotated in place, pointing at the withdrawal.

And a false positive that is its own lesson

BLOCKED.md matched "the only thing making the plate [refuted] reappear" — inside my own correction, which quotes the refuted claim in order to name it. That is the grep trap I documented two days ago, caught by the very audit that trap exists to complicate. Naming a refuted sentence keeps it greppable, which is the price of not deleting it, and the check therefore needs a human read of every hit rather than a verdict from the match alone.

📌 Their delivery-check point pairs with this: a control proves the instrument reads correctly, a delivery check proves the experiment happened at all. Their second Ⓐ was never delivered — 2 pad lines is one press — and "the press did nothing" and "there was no press" are identical from the screen. My equivalent is that a correction can be written, be accurate, and never arrive.

A refuted-claim register, because the audit found what the audit found

The Decoder ran my corpus audit against theirs and found four refuted claims still standing — including one they had corrected in a message to me, agreed with, and written a METHOD entry about, without landing the correction for a full iteration. Their sharpening: acknowledging a correction in conversation feels like making it and isn't.

A hand audit finds the instances present on the day it runs. It does not stop the next one. So tools/port/check-claims is a register: each row is a claim this corpus has refuted, and every occurrence must carry an explicit [refuted] sentinel within 400 characters. check-all runs it.

🔴 It found four more than my hand audit did

My manual pass checked four claims and found two problems. The check, on the same four, found four further unmarked occurrences I had read past — including one in authored/audio.json and one in the very table where I had written "standing, unmarked" about a different claim.

The marker is a sentinel, not a keyword, and that mattered

The first version matched a per-claim keyword near the hit — "refuted", "WITHDRAWN". Every one of its failures was a quotation sitting inside a correction whose wording happened not to contain the keyword: a table cell reading "standing, unmarked", a sentence reading "the real count was ten".

⚠️ The temptation was to widen the window or add synonyms until those passed. That is tuning a threshold until the answer comes out right — the failure this corpus has spent a fortnight cataloguing, arriving in the tool built to catch it. So the marker became a token the author must place. It cannot be satisfied by phrasing, and its absence means exactly one thing.

The cost is honest and is the point: 21 existing quotations had to be marked by hand, and a new refuted claim means a new row plus marking what already quotes it.

Proved it fails: removing one sentinel makes the run report that claim unmarked and exit non-zero.

What the register cannot do

⚠️ It only knows claims someone has entered. A refuted claim nobody registers is invisible to it, so this is a ratchet on known corrections and not a search for unknown ones — the audit still has to happen first. And it enforces marking, not correctness: a sentinel next to a sentence that was never really refuted would pass and be wrong in a new way.

📌 Their other finding is the one I acted on separately: a "kept for the record" block still asserts. BLOCKED.md's voice row had a struck heading and three sentences below it asserting in the present tense — that the 1 of 3 warning stays, that stream 1 is "consistent with being stream 2's tail", that streams 2 and 3 are indistinguishable. All three resolved days earlier. Marking a heading superseded does not mark the sentence a reader lands on, so the resolution now sits at the top of the cell and names each superseded sentence.

State of the port, and a claim I built on for a week without checking

Every asserting check passes: the format validator (16 screens against sylpheed.screen/3), all five MODDING rules, the capture-control sweep, the refuted-claim register, the decisions index. The oracle rows sit at the tone floor — title_plate 0.00 %, both splashes 0.01 %, main_menu 0.06 %, main_menu_options 0.15 %, extras 0.19 %, title 0.21 %, title_band 0.35 % against its own oracle-to-oracle gap. The P5 walk runs and ends on the title.

🔴 The refutation attempt this iteration was of something I had already used

The claim that reframed the entire voice question — ADV.wmv carries one audio stream and it is WMA Pro 5.1, not XMA, so the movie's own track is the bed and the three streams are additional — is checkable in one command against the disc:

index=0  codec_name=wmapro  channels=6  channel_layout=5.1  sample_rate=48000
1 audio stream

Exactly confirmed.

⚠️ And I had built on it for a week without running it. The positional weights, the presentation: "all" change, the refusal to apply the assignment when the byte sizes did not fit — all of it rests on that reframing, and the verification cost one ffprobe against a file I have had all along. I checked the byte sizes scrupulously because they were the identifier I could test, and never checked the sentence the identifier was serving.

📌 That is a different failure from the ones this file catalogues. Not an unexercised rule, not a correction that never landed, not a control easier than the measurement: a premise so foundational that everything downstream got audited and the premise itself did not. The scrutiny went to the parts that moved.

What is still authored rather than measured

Four values, and the file says so at each:

value state
flow.screens.main_menu.on_cancel authored — likely but UNPROVEN; Ⓑ returning to the title is stated in HANDOFF with no capture behind it
ptbtn01.after_video authored; the game goes into Mission 1, which MISSION §7 scopes out, so "return to the title" is a chosen end state
flow.navigation.input_during_transition authored, not measured — nobody has watched a press mid-fade; ignoring invents least
authored/rendering.json's withheld leaves two leaf records deliberately not drawn, each with its reason

Everything else in authored/ now carries kind: measured — the BGM bank and its loop window, the plate's pulse period, the keyframe unit, the black hold, the navigation wrap, the cue bindings, the voice streams and their positional weights.

⚠️ The black hold is measured but sits at the top of its range (~6.59.2 units, authored 9), and that is recorded at the value rather than in a footnote.

Identifying their submenu capture: edges where intensity could not

They reached and captured a submenu but could not identify it. Their diagnosis is the useful part: correlation cannot discriminate when the candidate renders are near-blank, and near-blank is exactly what the .tbm hypothesis predicts — all 19 GP_SAVE_LOAD builds scored 0.004…0.010, a ranking with no information in it. The instrument is disabled by the thing it was brought in to detect.

That diagnosis implies its own fix. Their capture is 99.999 % non-black — a full-screen background our renderer omits — and an additive background swamps an intensity correlation. It does not survive an edge map: a smooth ground has no edges, and the UI does.

The control first, because a ranking is worthless without one

Edge correlation against my own title capture, over seven GP_TITLE builds whose answer I know:

build r
4 — the right answer +0.2792
6 (extras, its nearest sibling) +0.1936
everything else ≤ +0.037

Right answer on top, 1.4× over second and 7.6× over third. Modest absolute r, and a clear ranking — so the method discriminates on this corpus.

The result

Their capture against all 22 candidate builds:

build r
GP_TUTORIAL build 0 +0.4962
GP_TUTORIAL build 1 +0.3137
best GP_SAVE_LOAD (17) +0.0713
worst 0.0331

🟢 The submenu is a GP_TUTORIAL build. The winning r is higher than the control's, its margin over second is better (1.58× against 1.4×), and both TUTORIAL builds sit 47× above every GP_SAVE_LOAD build — the archive separation is far stronger than the within-archive one.

It is independently plausible: authored/flow.json has ptbtn03 = TUTORIALTUTORIAL_MENU, noted as "the lesson list is not a GP_TITLE build". An Ⓐ on a menu whose focus was TUTORIAL lands exactly there, and HANDOFF Q5 measured initial focus as unstable boot to boot.

⚠️ What this does and does not settle. The archive is identified with a large margin. Which build within it is not: 1.58× is the same order as my control's 1.4×, and the two TUTORIAL builds are variants of one screen — so I would call build 0 the better fit and not a determination.

⚠️ And the method inherits a limit worth stating: an edge map is insensitive to what the background is, which is the point, but it is also insensitive to a missing element that has no edges. It answers "which screen", not "is our render complete".

Refutation attempt: "screen render omits every .tbm background, but none of your screens has one"

Their branch, HEAD d92a962. The first half is theirs to prove and they proved it against a capture. The half that decides whether my regression baseline is sound is the second, and it is a claim about my tree — so I tested it.

screen info --all, grepped for .tbm, across all 16 builds in my manifest: zero references. Their claim holds, and holds wider than they stated — they said "none of your five screens", it is none of sixteen.

Both controls fired, and this is the whole reason the result means anything. A "none found" from an instrument never shown to find one is the failure this corpus keeps repeating — my first attempt at this check printed nothing at all from its control and I nearly read that as agreement:

.tbm mentions
positive — GP_TUTORIAL build 0 1 (pubase.tbm, the element they named)
negative — GP_TITLE build 5, main_menu 0

The guard, and why a passing check still needed one

So tools/port/verify-screen cannot be misled today. ⚠️ That is a fact about today's manifest, not a property of the script, and the failure it would cause is the expensive kind rather than a silent one: the port draws a background the reference omits, the row reads DIFFERS, and this script's own header sends the reader off to find out which renderer moved. Neither did. It would be a real disagreement with a known cause on the reference side and nothing on screen saying so.

The row now says so. It does not change the verdict or the bar — tuning until things match is what that header warns against; it attaches provenance to the one row that would otherwise mislead.

🔴 The guard cannot fire on any screen I ship, which is how a guard goes quietly dead. Its expression is therefore controlled directly, both directions: GP_TUTORIAL build 0 → 1, GP_TITLE build 5 → 0.

Regression unchanged after the edit: title max 6 / over3 790, main_menu max 4 / over3 0 — the committed baseline exactly.

Their identification and mine agree, from unshared assumptions

They identified the screen by reading the word TUTORIAL off the framebuffer. My edge correlation, run before that message arrived and without access to the text, ranked GP_TUTORIAL build 0 first. Two methods with no assumption in common, one answer.

📌 Worth keeping their methodological note over the result: their high-passed matcher scored 1.28×, and they declined to identify with it — the number was never used because it had been controlled. My 1.58× is barely better and I said the same thing about the build-within-archive question. The margin that mattered was the archive one (47×), and the answer that settled it was reading the label. Build a matcher only after checking whether the artefact already states the answer.

on_cancel: one half measured, and a MEASURED stamp removed from the other

The Decoder measured Ⓑ on the main menu → the title (their 86a8ce7, docs/re/data/b-on-main-menu.txt): delivery-confirmed, 73.5 % of pixels changed, both captures naming themselves, ≤ 0.4 s, and no loading screen on the path despite the disc carrying four.

authored/flow.json main_menu/on_cancel moves from "likely but UNPROVEN" to MEASURED. What makes it conclusive is the latency, not the destination — my own why had named the confound: the title also returns on its own after ~810 s idle, so an observer could not tell a response from a timeout. ≤ 0.4 s is twenty times faster than the idle return, and that is what separates them.

🔴 The other half: my tree stamped MEASURED on a claim with no evidence

title/on_cancel_why read "MEASURED, HANDOFF Q5: Ⓑ on the title does nothing." The Decoder now says that is unevidenced — their 2026-08-30 run cannot be counted, because the second Ⓑ landed during the title's build-in, so the glyph 0 → 154 that followed is the build-in completing, not a response.

I did not invent the stamp, and that is the point worth recording. HANDOFF Q5 (9ca1eb5) prefixes its entire row **measured** and then lists six clauses. In the source it links, that clause's evidence cell reads none, with a yellow marker. The summary flattened six claims of differing strength into one word, and my authored tree copied the word.

⚠️ The value does not change — null either way. Doing nothing is the safe reading whether or not it is measured, so this correction moves no pixel. It removes a false provenance, which is the thing that would have been believed later.

The same row has a second empty cell, which nobody flagged

Auditing the rest of Q5 rather than only the clause I was handed: the up / down row — "one item per press, no auto-repeat at the durations tried" — also has an empty evidence cell, and my navigation block cites that same row.

It splits cleanly, and only one half is exposed:

  • one item per press is evidenced indirectly and well — the wrap montage's count only comes out if each press moves one (4 presses from EXTRAS landing on OPTIONS). Keep it.
  • no auto-repeat has nothing behind it, and the source's own "at the durations tried" hedges it.

🔴 Worse, the port already behaved this way without stating it: boot.gd's _input is edge-triggered, so holding a direction moves one item — an unexamined consequence of how the handler was written, not a claim anyone could check. navigation.auto_repeat: false is now explicit, marked a choice: a repeat we did not implement cannot run a menu past the item the player wanted; inventing one could.

Audit of every MEASURED stamp in authored/

34 stamps. Six cite a HANDOFF row and nothing else — the laundering path above. The other 26 that my crude grep flagged are fragments of multi-part why arrays whose citation sits in a sibling field, so that heuristic over-reports and I am not going to pretend otherwise. Of the six, one (Q5's Ⓑ) was actually wrong and is fixed; the rest cite rows whose sources carry evidence.

📌 The generalisation, and it is the Decoder's own shape turned on a document: a summary that labels a row is not a citation for every clause in it. A bundled **measured** is exactly as strong as its weakest cell.

BLOCKED.md's five "blocking" rows were all answered, some days ago

The standing instruction says this file rots, and it had. Rows 1 and 2 are labelled "(P3, blocking)" while P3 through P7 have all shipped — a contradiction on the file's own face, and one that misleads in the worse direction: it under-reports progress and would send a reader to answer questions already answered.

Audited every row against HANDOFF 9ca1eb5, and — the part that makes this more than bookkeeping — checked whether the port actually acted on each answer:

row answer did the port act?
1 splash predicate no content rule exists; take the entry index addressed by entry index; publisher_logo 10/13 now exported
2 fade-out (a), play the group to its end and see below — the prescribed constant was deleted
3 focus over vs instead my choice was fine; the miss was the ring ptbtneff01 exported and drawn
4 rotation human's call; pivot anchor measured drawn about pos + pivot
5 gamma captures are not gamma-neutral, RMSE has a floor in verify-capture's header

🔴 HANDOFF ask 2's prescribed action is stale, and following it would double-count

Ask 2 says: "write one authored constant (~0.4 s / ~24 units) and play the group to its end." Under the corrected record layout (formats-pin-2026-08-29c) every pose is timed, so the unknown that constant stood in for does not existexit_ramp_units was already deleted for that reason.

Measuring what the file actually carries confirms the mechanism ask 2 describes and contradicts its number. On main_menu, the final alpha ramps are:

  • pteff00 — the black quad — 0 → 255 over 10 units (0.17 s)
  • ptmsg, pteff10, pteff12255 → 0 over 68 units

"the quad goes a=255 while the buttons, ptmsg and the glows go a=0" is in the file, exactly as described. ⚠️ But the ramp is 10 units, not 24. Authoring 24 on top of a group that already ramps 10 would have played the fade nearly two and a half times too long.

A decomposition that fits both numbers — offered as a hypothesis, not a finding

HANDOFF Q7 measures two quantities off the game: the fade-out ~0.4 s and the black-hold plateau 0.170.23 s. The file gives the ramp as 10 units (0.17 s).

in-file ramp 10 units  +  measured hold 1014 units  =  2024 units = 0.330.40 s

🟡 The measured ~0.4 s sits at the top of that range. So the ~0.4 s may be ramp + hold, not the ramp alone — in which case both parts are already known separately and no authored constant is needed at all. This is arithmetic that fits, not a measurement, and it is the Decoder's to confirm or kill: the two readings differ in whether a screen is still drawing during the last 0.2 s.

🔴 And it exposes a disagreement in my own tree

authored/timing.json holds black_hold_units: 9 = 0.15 s, measured in the draw stream. HANDOFF's plateau is 0.170.23 s, measured off the game. Mine sits below their floor, by 15 units.

I am not changing it. Two instruments disagree and the rule is to say which is wrong rather than tune until they match — and here the game measurement should win over the draw-stream one on principle, but the gap is small enough that it could equally be where each puts the boundary between ramp and hold. It goes to BLOCKED.md as an ask, at the value it was measured at.

The plate came back in the game and not in the port

The Decoder's Ⓑ run answered both my asks and threw in a third finding: after Ⓑ from the menu the PRESS Ⓐ plate is re-drawn — pressed 351.2 s, pulse back 358.5 s (daf8f47).

🔴 The port did not do that. Ⓑ landed on a bare title. _menu_arrive() calls _drop_overlay() — correct, the plate goes with the screen it was measured on — but nothing ever put it back: _overlay_spec is cleared the instant the overlay is raised, and only the boot sequence ever set it. Confirmed by running it, not by reading: the drawn list was the ten title elements with no ptbtn00.

Fixed. _rearm_overlay_for(name) looks the declaration up in authored/flow.json's boot step for that screen rather than naming press_start, so the plate returns by the same code path and the same shared clock as on boot, and a screen that gains an overlay later gets it on both paths with no edit here. No new constant — the delay is not authored, it is whatever the boot already does.

Controlled both ways: Ⓑ → overlay press_start raised, drawing ptbtn00, ptbtn00f; entering EXTRAS, which declares no overlay, raises nothing.

An independent agreement I did not tune for

The script log had no press timestamp, so the port's own latency could only be guessed from surrounding lines. Added one. With it:

Ⓑ pressed 1.01 s
title arrives, overlay armed 1.37 s
port's press → title 0.36 s
their measured Ⓑ latency ≤ 0.4 s

That agreement is worth something because nothing here was fitted to it — the port's transition timing comes from the screens' own fade keyframes, and this is the first time the two numbers have been put beside each other.

🟡 The plate's return time does not agree, and I am not adjusting it

The plate is raised on arrival and its own group takes it opaque at t=238 (3.97 s), so the port's press → plate visible ≈ 4.33 s. Theirs is 7.3 s to the pulse. The pulse has a 120-unit (2 s) period, so pulse detection can lag first paint by up to 2 s — which closes it to ~6.3 s at most and leaves roughly a second unexplained.

⚠️ It would be easy to author a delay that makes 4.33 into 7.3. That is exactly the tuning this corpus keeps warning about, and the previous authored delay in this very block (after_settle_seconds: 2.13) was already refuted once by arithmetic. Left alone; recorded as an ask.

Two stamps upgraded, both now measured for real

  • navigation.auto_repeat — a 2.0 s held ⬇ moves the cursor once, their counter passing its control first. Was a consequence of edge-triggered _input; now a measurement.
  • title/on_cancel — Ⓑ on a settled title does nothing, twenty seconds confirmed. This cell has now been MEASURED (wrongly), AUTHORED (honestly), and MEASURED (truly), with the value null the whole way through.

🔴 verify-screen was nondeterministic, and it looked fine most of the time

Running the full set after the plate fix, two rows had moved off the committed baseline. One of them was not a regression at all — it was the harness.

press_start returned over3 5021, 8919, 5021 on three identical runs. The plate's looping focus record takes its phase from time_units, which free-runs, so the captured frame lands wherever the grab happened to fall — while the reference renderer cannot pulse at all. A detector that answers differently each run is worse than one that fails: it teaches its reader to ignore it.

⚠️ The port is not the thing that was wrong. A thing that pulses does not stop because the screen has arrived, and the pulse is measured. What was wrong was comparing a moving frame against a static one and calling the difference a regression. So ScreenView.loop_phase_units pins the phase, negative means free-running, that stays the default everywhere, and only the harness passes --loop-phase=0.

Controlled, and the control is what makes the fix trustworthy:

pinned, 3 runs identical md5
free-running, 4 runs 3 identical, 1 different

🟡 That 3-of-4 is the finding worth keeping. It is usually stable, which is exactly why it survived — a flake that fires one run in four reads as a real regression that "went away", and a --loop-phase that changed nothing would have been indistinguishable from a fix without that negative control.

With the phase pinned, press_start reads max 1 / over3 0 OKthe recorded baseline exactly, not some new number. Fifteen of sixteen rows now match the committed baseline.

The sixteenth: title_jp has genuinely drifted, and I cannot say which side

max over3
committed baseline 155 20 498
now 233 61 208

What is established:

  • deterministic — 233 / 61 208 twice, so not the phase.
  • not the reference — the Decoder reports screen render is byte-identical across the stale and rebuilt binaries (max per-channel 0), so the reference is stable and the movement is on the Godot side.
  • localized — the differing region is a single 350×396 block at (405, 74), the logo stack. title is untouched at max 6 / over3 790, which rules out anything shared by both title screens (the forced-backdrop rule among them).
  • The port draws ptlogo_jp, ptlogo3a/b/c and the five ptlogo_back2eff* layers here that are transparent at rest on the English title.

🔴 What I cannot do is say which renderer is right. There is no capture of the Japanese title in the corpus, and this script's own header is explicit that agreement with the reference is not correctness and a DIFFERS is not automatically the port's fault. Guessing a direction here is precisely the move the mission forbids. Asked, not resolved.

🔴 WITHDRAWN — the JP capture does NOT go against the port; I scored the wrong frame

*(This heading read: "The JP title capture adjudicates title_jp — and it goes

against the port." Withdrawn in full below. I scored verify-screen's

--pose=rest frame, which the port does not ship; posed as it runs, the port

beats the reference +0.9994 to +0.8727. The heading asserted the opposite of

the finding for as long as it stood.)*

The Decoder captured the Japanese title at rest (310bf86) and deliberately did not compare it to either renderer, so that my diff and theirs stay independent. This is the oracle for the block I could not adjudicate.

Aligning it, because the last capture's geometry did not transfer

Their submenu capture had the game surface at y=45 in a 1280×720 frame. I did not assume that here — I recovered the alignment by row/column profile correlation, with the English pair as a control:

dy dx
control — English capture (1279×675) vs port 0 (r 0.994) 0 (r 0.977)
JP capture (1280×720) vs port 45 (r 0.927) 1

The control lands on (0,0) as it must, and the JP offset comes out at their stated 45 as a measurement rather than an inheritance. My first look at the frame said "no letterbox, content spans all 720 rows" — true, and irrelevant: the surface is offset inside content that extends past it.

The instrument is fair, and then the verdict

Comparing the capture against both renderers in the disputed 350×396 block at (405,74), and against a control strip where the two renderers agree:

region vs port vs reference closer
control strip r +0.9751 r +0.9756 tie — instrument is fair
disputed block r +0.7462 r +0.8727 REFERENCE

🔴 The port moved, and it moved away from the game. The verdict is stable under gamma compensation at both measured title gammas (raw / 1.34 / 1.49 → reference every time), so it is not an artefact of the known capture gamma floor. The port puts light on 25.6 % of the block that the capture does not have, against the reference's 15.9 % — it is drawing too much, not too little.

⚠️ This is the opposite of what I expected. The Decoder's description — a crystalline burst behind the wordmark, the ptlogo3a/b/c + ptlogo_back2eff* stack that English holds transparent at rest — reads as confirmation that the port's extra layers are right. They are not: the burst is there, and the port draws more of it than the game does. A qualitative match on "is the effect present" was about to stand in for a measurement of how much.

What is not settled: which change did it

Both renderers draw this screen at rest, t=10 units, so the settle-window logic is not in play — the two decoders disagree about the rest pose of the JP effect stack itself. Four commits this session touched that path (the forced-backdrop rule, per-instant coverage, the looping record, the sweep/hold work) and I have not bisected them. Naming one now would be a guess dressed as a cause.

📌 What this does settle: title_jp's DIFFERS is the port's, not the reference's. That reverses this script's usual presumption, and it is the first row in the baseline whose direction has ever been established against a capture.

🔴 CORRECTION: the port did not move away from the game — I scored the wrong frame

The previous entry concluded, from the JP title capture, that "the port moved, and it moved away from the game". That conclusion is withdrawn. It is wrong, and the way it was wrong is worth more than the answer.

I scored verify-screen's title_jp frame against the oracle. That frame is posed --pose=rest, which this port does not ship. Posed as it actually runs:

disputed block whole surface
port, --pose=rest (the frame I scored) +0.7462
port, as shipped +0.9994 +0.9652
reference +0.8727 +0.9200

Holds under gamma compensation (+0.9928 at γ=1.34) and on the English control, same method: port +0.9946 against the reference's +0.9560. The port is closer to the game than the reference on both title screens.

Why rest produces a frame the game never shows

ptlogo_back2eff1 on the JP title is (t, alpha) = (0,0) (98,0) (100,255) (102,255) (104,0) — a 4-unit sparkle, and its rest.t is 100: the peak of its own flash. Six of these stagger across the logo. Posing at rest fires every sparkle simultaneously at full brightness, which is exactly the "port puts light on 25.6 % of the block the capture does not have" I reported as a defect. The excess light was real; it was in a frame nobody sees.

⚠️ verify-screen is not at fault — it poses rest deliberately, because both renderers read rest through one decoder and that is what makes it a consistency check. Its header said so. I used a consistency-check frame to answer a correctness question, and the tool now says in its own header that its frames must never be scored against a capture.

A second, smaller thing in that entry was also wrong

It said the port draws layers "that are transparent at rest on the English title". Both screens draw them under --pose=rest. I had compared a --menu run's log (timeline pose) against a verify-screen log (rest pose) and read the difference as a property of the screens rather than of the two modes.

What actually stands from that entry

The alignment work survives intact — the measured dy=45 with the English control at (0,0), and the observation that the instrument is fair on a control strip. So does the arithmetic. What failed was choosing which frame to feed it, and no amount of control on the comparison could have caught that: every control I ran was a control on the metric, and the error was upstream in the input.

📌 The generalisation: a control proves the instrument, not the sample. Both of my last two iterations' errors were of that shape — a live reader pointed at the wrong field name, and a fair metric pointed at the wrong frame.

Wired so it cannot recur

tools/port/verify-capture takes a fifth per-row field, a capture crop, because this capture is a full 1280×720 display frame with the surface at +0+45 while every other capture in that directory is pre-cropped to 1279×675 — comparing it whole would score the port against a 45 px shift. With it, title_jp reads RMSE 20.91, differing region 1.04 %, beside title's 14.16 / 0.21 %.

⚠️ The row prints no capture until the Decoder's branch merges. Their capture is theirs to commit; it was staged locally to test the row and removed.

The rest() flash defect reaches four screens I ship — and the port already survives it

The Decoder censused it from the file side while I was looking at one instance: of 13 991 elements with ≥2 keyframes, 2 305 have no plateau so the dwell fallback decides, and 1 697 (74 %) of those get a visible pose. In GP_TITLE, 5 fires and 4 are visible — all four on the splash screens this port ships.

Confirmed in my own export, and it is exactly the JP-title shape on different screens:

element keyframes rest
palogo_sqex_eff 0:a0 15:a255 30:a212 45:a0 t=30, a=212
palogo_anima_eff 0:a0 15:a255 30:a212 45:a0 t=30, a=212
palogo_gamearts_eff 0:a0 15:a255 **30:a255** 45:a0 t=15, a=255
palogo_seta_eff 0:a0 15:a255 **30:a255** 45:a0 t=15, a=255

📌 A refinement to their description, which named the 212 shape: two of the four hold 255 through t=30, so their fallback lands on the flash's peak rather than its decay. Same defect, worse pose — full brightness, not four-fifths. The logos themselves (palogo_sqex holds 255 from t=30 to t=235) have a real plateau and are unaffected.

The port ships the right frame, and now there is a number for it

Both poses of the publisher splash against the committed oracle capture:

pose RMSE differing
timeline — what the port ships 2.17 0.01 %
--pose=rest — the harness frame 9.05 0.75 %

🔴 75× the differing area on a screen this port ships. So the rule I wrote into verify-screen's header after getting it wrong on title_jp is not a special case — it generalises, and here it is demonstrated against an oracle rather than argued.

The port's settled pose evaluates pose_at(hold), not rest, so it skips the flashes and agrees with the capture at 0.01 %. The defect is confined to the harness pose. Nothing shipped is wrong; nothing needed fixing in the render.

What did need fixing: the port said "at rest" about a pose it never looked at

ScreenView logged "%s (transparent at rest)" for every skipped element, whatever instant it had posed. On the timeline path the pose is pose_at(time_units) — so it reported palogo_sqex_eff (transparent at rest) about an element whose resting alpha is 212.

⚠️ That is not cosmetic. The rest-versus-posed-instant confusion is precisely what made me score a --pose=rest frame against a capture and write up a drift that did not exist. A log line that erases the distinction is that error pre-printed, waiting to be believed. It now names the instant: transparent at t=6.

Controlled both ways on one screen: timeline → transparent at t=6 and the flash skipped; --pose=rest → still at rest, and the flash drawn.

Correction: those two are the sound path, which makes the rule stronger

The Decoder refuted my refinement, and it is a correction I would rather have than the credit. I wrote that palogo_gamearts_eff / palogo_seta_eff show "the same defect, worse pose — their fallback lands on the flash's peak". Wrong on the mechanism. They hold a=255 at identical x, y and scale from t=15 to t=30 — a genuine plateau at pair index 1, which rest_plateau() handles, and t=15 is the correct answer for that path. They are not among their census's four.

🔴 And the consequence runs the other way from a retraction. My rest pose for them really is the flash's peak, reached by the sound path. So "a rest render is not a frame to score against a capture" does not depend on the fallback being unsound: a plateau can itself be the held peak of a transient. The 2 305 / 1 697 census understates the exposure rather than bounding it.

Censusing my own tree — and the first answer was wrong

I asked how many elements I ship whose rest is visible but whose visibility is transient. First pass keyed "transient" on the element's own visible span, and returned 28 across 12 of 16 screens — a plausible-looking number.

🔴 It was wrong, and what caught it was the check the Decoder and I just agreed on: say what the number means physically. The list included ptmsg — the main menu's own ⊙ Select Ⓐ OK footer — as "visible 2 of 64 units", and ptbtn00, the PRESS Ⓐ plate. Those are on screen the whole time the game sits there. The story collapses on contact.

The cause: ptmsg is [0:a0 44:a0 56:a255 58:a255 64:a0], and that final zero is the screen's exit ramp, which every element has. I had counted the exit as the end of visibility, so every normal element looked like a flash. No control would have caught this — the arithmetic was right.

Re-keyed on the screen's span rather than the element's: a transient is gone while the screen is still up.

elements whose rest shows what the settled screen does not 31
screens affected 8 of 16

Every entry now has a coherent story — *eff* and *loop* sparkles, plus the two loading screens — and ptmsg/ptbtn00 fall out on their own, which is the check passing rather than being applied by hand.

📌 My exposure is twice what the splash finding suggested. Not four screens, eight — both titles, both splashes and their region twins, and both loading screens. None of it reaches shipped output: the port poses pose_at(hold) and agrees with every capture it has. The number bounds what would break the day anything scored a rest frame against an oracle.

The two loading screens are no longer black, and it was the paint order

verify-screen's header has carried, since P1, that build_12 and build_15 "render as pure black in BOTH renderers, mean 0 and max 0", with an open question: "whether that is the port's bug or the decoders' reading of rest".

Both halves are now settled, and the answer is neither. Measured today:

max mean
port 214.5 1.949
reference 214.5 1.918

Not blank, on either side, and the two agree — the rows read OK on a real comparison rather than on nothing-against-nothing.

🔴 It was the paint order, not rest. My own earlier measurement had already answered it and I had not connected the two: removing the forced-backdrop pass makes these screens' first element pgloading_loop5, "and the black screen returns". pgloading_eff00 is the full-frame opaque untextured quad, and it carries layer: null, layer_source: none — the only elements in the export with neither a read nor an implied key. Its position rests entirely on the occlusion constraint. The rule that fixed it is the one the Decoder supplied and I implemented this session.

⚠️ The guard stays and the stale paragraph stays with it, marked as history. It was correct when written — two of sixteen rows were reporting this script's strongest verdict for comparing nothing against nothing — and a guard that stops firing is exactly the kind that rots out of a tool. A reader who hits a blank pair tomorrow needs the reasoning, not just the verdict.

Refutation attempt: does the Decoder's census miss my title screens?

Their GP_TITLE census is "5 fires, 4 visible, all four on the splash screens". My own census found six transient ptlogo_back2eff* elements on title and seven on title_jp — also GP_TITLE builds. If those were fallback fires, their count of four would be wrong.

Their claim survives. All six reach rest by the plateau path — alpha 255→255 with identical pos and scale across the pair — so rest_plateau() handles them and the dwell fallback never runs. They are not fires.

📌 Which is their own point back at them, now with my screens as evidence: my census counts a superset spanning both paths, and the difference between the two numbers is not disagreement but scope. A plateau that happens to sit on a transient's peak is invisible to a fallback census and still produces a rest frame the game never shows.

A proposed sharpening of the census, tested and rejected

The Decoder's surviving number rests on a structural fact: the dwell fallback runs only when no two adjacent poses are equal, so every pose it returns is un-held by construction, and no threshold is needed. That is clean, and the obvious move was to borrow it — replace my "gone before 60 % of the screen" cutoff with how long the rest pose is held, which would drop the arbitrary threshold.

🔴 It fails my own control.

element held of screen flagged?
ptmsg — the main menu's footer 2 units 80 (2.5 %) yes
ptbtn00 — the PRESS Ⓐ plate 2 units 244 (0.8 %) yes
ptlogo_back2eff1 — a real sparkle 2 units 269 (0.7 %) yes

All three sit on a 2-unit plateau. Hold duration cannot separate them, and the two it gets wrong are the exact pair whose absurdity caught my first census.

Why the criterion does not transfer. On the fallback path nothing is held, so "un-held" is the defect. On the plateau path the plateau is real — what distinguishes a footer from a sparkle is where it sits relative to the screen's end: ptmsg's 2-unit plateau is the last pose before the exit ramp, so the port holds it past the end and the game shows it throughout; the sparkle's identical 2-unit plateau is followed by a return to zero while the screen is still up.

📌 So the screen-span criterion stays, threshold and all. A cleaner definition that fails a control is worse than an ugly one that passes — and I would have adopted this on its elegance if the control pair had not already been sitting there from the earlier mistake.

Adjudicating the Decoder's rest() replacement against the game

They proposed posing every element at the screen's settle instant instead of asking each element for its own resting pose, found their own control could not validate it — "a candidate cannot be adjudicated against the incumbent it is meant to replace" — and said the oracle number is what decides. It is, and I had only ever run it on one screen. Running it on every capture-backed screen:

screen candidate (settled) incumbent (rest)
title 0.21 % 1.82 % candidate
publisher_logo 0.01 % 0.75 % candidate
developer_logos 0.01 % 0.33 % candidate
main_menu 0.07 % 0.25 % ⚠️ confounded
extras 0.19 % 0.46 % ⚠️ confounded

Three screens adjudicate cleanly and all three favour the candidate, by 9× to 75×. The settled figures are corroborated: they match verify-capture's independently recorded numbers to the digit.

🔴 Two of the five rows are not evidence, and my first table said they were

My first run had main_menu at 3.29 % for the candidate — losing to the incumbent by 13×, the opposite direction from everything else. That had no plausible story, which is what made me look.

--screen= shoots the frame immediately: the "settled" main_menu drew 6 of 16 elements and skipped ptframe1, ptframe2 and ptmsg as "transparent at t=9". It was a mid-build-in frame. The same wrong-frame error as title_jp, caught before publishing this time and only because the number's direction made no sense. Properly posed via --menu --script=wait, it is 0.07 %.

⚠️ But that fix introduces a confound, and it is fatal to those two rows. The only way to pose these screens settled is --menu, which also draws the focus record; the rest column is rendered by --screen, which draws none. This tool's own header records that difference: main_menu without focus is 2 159 differing pixels — 0.234 % — against 531 with it. My incumbent figure is 0.25 %. The entire gap on those two rows is the focus record, not the pose.

So they stay in the table marked confounded rather than counted. A 50 result was available by not looking.

What this does and does not settle

The candidate is better on every screen where the question can be asked cleanly, against the game rather than against the incumbent — which is the adjudication their failed control could not provide.

⚠️ It does not validate their implementation. I tested the port's settled pose, not UiBuild::settle_time(); the two agree in direction, and whether they agree in value is unmeasured. And three screens are three screens.

📌 They are right not to change rest() on this. I pin their crate, nothing I ship reads rest, and a proposal whose evidence comes entirely from the consumer has no business landing in the dependency on that basis alone.

The boot's own end frame, scored against the game for the first time

--boot --capture= used to write no file: _finish_boot() was reachable only from the overlay-quit branch, and the boot quit first because that branch fires when _overlay_spec.is_empty() — which it is the instant the overlay is raised. Fixed by the _overlay_quit_at < 0.0 guard added earlier this session. The defect entry above is left standing with a pointer here, because the reasoning is what makes the guard legible.

⚠️ I fixed it and never went back to check what it made possible. The whole P3/P7 artifact — the boot running unattended and photographing its own end state — has been available for hours and unused.

What it shows

RMSE differing
boot's own end frame, real sequence, unattended 12.80 0.00 %
title_plate, synthetically posed at --time=3.95 12.83 0.00 %

Zero pixels over the threshold against the game. The residual RMSE is the known capture gamma floor, which every row on this corpus carries and which is not a target.

📌 And the two agree to 0.03 RMSE. That is the more useful half. verify-capture reaches this frame by a shortcut--screen=title --overlay=press_start --time=3.95 — rather than by booting. The shortcut has been trusted since it was written and never tested against the thing it stands in for. It is faithful: posing the composite directly and arriving there through publisher logo → developer logos → ADV.wmv → title → plate land on the same frame.

⚠️ What this does not show is that the intervening sequence is right. It is one frame, the last one; the boot could take a wrong path and still end correctly. --shots walks the sequence, and comparing those against captures needs captures of the intermediate states, which the corpus does not have for the video handover.

Refutation attempt: the settle-instant candidate is not uniformly better

Their symmetry-breaking property attributes every rest() disagreement to the chosen plateau not covering the settle instant. That is testable from my side on the two screens my earlier table had to mark confounded — and it turns up a screen class where their candidate is worse.

🔴 At main_menu's settle instant the footer is half-drawn.

screen settle window settle instant ptmsg alpha there
main_menu [44, 56] 50 127.5 of 255
extras [38, 50] 44 127.5 of 255

ptmsg is [0:a0 44:a0 56:a255 58:a255 64:a0] — it finishes arriving at t=56, after the settle window has closed at 56 and well after its midpoint at 50. The settle instant is the midpoint of the longest keyframe-free interval, and on both menus that interval ends exactly as the footer starts to arrive.

Measured against the game, with focus absent from both renders so the confound that voided my earlier rows cancels:

pose RMSE differing
t=50 — the screen's settle instant 14.98 0.30 %
t=58 — the footer's own hold 14.83 0.25 %

⚠️ Small, and real. This does not overturn their proposal — it wins by 9× to 75× on title and both splashes, and by 4.6× on their own title measurement. What it shows is that "pose everything at the screen's settle instant" has a failure mode of its own: an element that arrives after the settle window closes is caught mid-fade. The port's hold-based pose does not have it, because it parks each element at its own hold.

📌 So the honest summary of this whole thread is narrower than either of us has been writing: the screen-settle pose beats per-element rest() decisively where rest() returns a transient's peak, and loses slightly where an element arrives late. Both are approximations to a settled screen; neither is the settled screen.

And a unit bug, caught by two identical numbers

My first run of this asked for --time=50 meaning 50 units and got 50 seconds — 3000 units, past the end of everything. Both poses returned RMSE 56.02 / 9.15 %, identical to two decimal places, which is what made me look. Two different inputs producing exactly the same output is not a result; it is the instrument saying it ignored the input. Same tell as the two --script runs that came out bit-identical because the capture fired before the first press.

My own predictor holds — and the evidence that made it better than width does not

The Decoder censused my ptmsg failure mode disc-wide (25.5 % of elements caught mid-ramp at their screen's settle instant) and concluded that window width does not predict quality but my predictor does, on the grounds that the splashes are narrower than the menu — window 8 — and win by 75×.

The predictor itself holds, 5 of 5 on my capture-backed screens:

screen mid-ramp at settle measured outcome
title 0 settle wins 9×
publisher_logo 0 settle wins 75×
developer_logos 0 settle wins 33×
main_menu 2 (ptmsg, pteff10) settle loses
extras 2 (ptmsg2, pteff20) settle loses

🔴 But their window figure for the splashes disagrees with my export by 20×, and that figure is the whole of the argument.

screen their window mine
title 76 76
main_menu 12 12
publisher_logo 8 190
developer_logos 8 145

Recomputed independently from the raw top-level keyframe times rather than read off my own settle_window field: publisher_logo's times are [0, 15, 30, 45, 235, 239, 251, 255], whose widest keyframe-free gap is 45 → 235 = 190. developer_logos gives 45 → 190 = 145. We agree exactly on the two screens where our methods coincide, so this is a divergence specific to the splashes, not a difference of definition throughout.

Why this matters more than a corrected number

The splashes are the widest of my five, not the narrowest. With that, the data reads:

screen window mid-ramp outcome
publisher_logo 190 0 wins 75×
developer_logos 145 0 wins 33×
title 76 0 wins 9×
main_menu 12 2 loses
extras 12 2 loses

🔴 Width and mid-ramp now predict identically and are perfectly confounded. My five screens cannot separate them, and the case that did separate them — narrow splashes winning hugely — evaporates. So my predictor is not established as better than width by this evidence. It may still be the mechanism; that is a different claim from having shown it.

And my numbers make their own census coherent, which is the strongest thing I can say for them. Their buckets run 40.9 % mid-ramp on windows under 10 and 11.7 % on wide ones. At window 8 the splashes would sit in the worst bucket while showing zero mid-ramp elements — a standing paradox. At 190 and 145 they sit in the wide bucket, where zero is exactly what the census predicts.

⚠️ I am not claiming their tool is broken; --settle may report a different quantity than the widest keyframe-free gap. But one of the two readings is wrong, and until it is settled the width hypothesis is not refuted.

Checking my own tree for the ordinal foot-gun that just voided three of theirs

The Decoder retracted three claims: screen render --build N takes a build ordinal, screen list maps [10] → entry 12 and [11] → entry 15, and the splashes are entries 10 and 11 — so their splash rows had rendered the loading screens against splash captures. My own HANDOFF entry warned that an ordinal-keyed 10/11 names the splashes as loading screens "and everything still validates", and it did.

⚠️ tools/port/verify-screen's header claims --all protects me from exactly this. A comment claiming protection is what just failed on their side, so I checked rather than cited it.

RMSE
my CLI reference for build 10 vs the publisher splash capture 8.97
my CLI reference for build 11 vs the developer splash capture 8.77
cross-control — publisher reference vs developer capture 48.17

Both references are the screens they claim to be, and the cross-control is 5.4× worse, so the discriminator has teeth rather than passing everything. My --all addressing is correct, and now measured rather than asserted.

📌 Worth naming why this was worth ten minutes: the port's numbers for these two screens (0.01 % differing) are among the strongest evidence in the corpus, and they are cited in the rest() adjudication that a proposal against a pinned crate now rests on. Evidence that strong is exactly what you check after finding the same class of error next door — the failure mode is silent by construction, and their instrument reported a railed gamma fit rather than a wrong screen.

What survives of the settle-window disagreement

Their retraction confirms my reading: 190 and 145, matching my recomputation from raw keyframe times exactly. Their library was never wrong, only the invocation.

So the position stands where my last entry left it, and no further: width and mid-ramp are perfectly confounded across every screen either of us has measured. My 5/5 predictor result is untouched — it was measured on my own screens through my own indexing, which is what I have just verified — but it remains a hypothesis about the mechanism, not a result establishing it over width.

Looking for a case that separates width from mid-ramp — there is none, and I nearly invented one

Width and mid-ramp predicted identically across my five capture-backed screens, so the useful question was whether any of my sixteen breaks the tie: a wide window with a mid-ramp element, or a narrow one without. Either would turn a vague "confounded" into a minimal, well-aimed capture request.

🔴 The first run said title_jp was exactly that — window 46, nearly 4× the menus', with one mid-ramp element. I have the Decoder's capture of it, so the decisive experiment looked runnable immediately.

It was wrong. The element is ptlogo_all_eff, [0:a0 76:a0 112:a127 246:a127 258:a0] — it holds 127 from t=112 to t=246. That is its plateau, not a transition. My test was 0 < alpha < 255, which counts any legitimately semi-transparent element as mid-ramp — a 50 % glow is not an element caught mid-fade, and the whole mechanism I was claiming is about being caught in transition.

Corrected to: t falls strictly inside a segment whose endpoints differ.

screen window old test corrected
title_jp 46 1 0
main_menu 12 2 1
extras 12 2 1
title, both splashes 76190 0 0

My 5/5 result survives — the menus keep a non-zero count and the winners stay at zero, so mid-ramp is still present exactly where the settle pose loses. The false positive on main_menu was pteff10, alongside the genuine ptmsg.

🔴 And there is no separating case anywhere in the export. Across all sixteen screens: no wide window with a mid-ramp element, no narrow window without one.

What that settles, which is a limit rather than an answer

The confound is structural across my whole corpus, not an artifact of choosing five screens. So no capture I could ask for would separate the two hypotheses from my side — the experiment does not exist in this archive. Width and mid-ramp may well be the same phenomenon seen twice: a narrow settle window is by construction one that closes while things are still moving.

📌 The near-miss is the part worth keeping. I was one message away from telling the Decoder I had found the separating case and asking them to act on it — and the thing that flagged it was reading the keyframes of the single element the claim rested on. The screen most useful to me was the one I checked least.

Auditing my tree for the disc-wide ordinal foot-gun

The Decoder found the ordinal/entry divergence is disc-wide — 21 of 24 build-bearing archives, 18 diverging at ordinal 0 — and that GP_TITLE is the mildest case on the disc, the only archive whose first ten ordinals are the identity. That is the whole reason this corpus survived, and it is luck, not design.

No exposure in my tree, checked rather than assumed.

  • The four archives they flag as exposed — GP_READY_ROOM, GP_HANGAR_ARSENAL, GP_MISSION_SELECT, GP_OPTIONS — appear in authored/flow.json with no numbers at all, only as "not in this export". Nothing to misread.
  • authored/screen_names.json already says "LOCATED BY ENTRY INDEX, not by a rule" — the exact disambiguation their second warning asks for.
  • Their second point (--all swaps the predicate, so --build N and --build N --all are different objects) is what verify-screen's header already relies on, and every tool of mine passes --all.

Verifying the high ordinals, where GP_TITLE's luck would run out first

The identity holds for the first ten ordinals. My export addresses 13 and 14, past that point, so the interesting test is up there:

RMSE
publisher_logo (10) vs publisher_logo_r (13) 3.06 region twins — near-identical
developer_logos (11) vs developer_logos_r (14) 4.33 region twins — near-identical
publisher_logo (10) vs developer_logos (11) 47.91 different screens — control, 1116× worse

--build N --all lands exactly where HANDOFF says entries 10/13 (publisher) and 11/14 (developer) are, across the full range where divergence could begin, and the control shows the test would have caught a mismatch.

⚠️ The constraint is recorded for whoever exports those four archives, which is not this port today: --build 0 is not entry 0 in any of them. The current absence of exposure is a fact about what I have exported, not a property of the tooling.

A precision correction to my own wording

They tried to refute my ptlogo_all_eff correction and could not — the quote is exact and a=127 holds flat across 134 units with position and scale constant.

⚠️ But they flag something I should not have said. I called it a "50 % glow". What is measured is the plateau; that it is a glow rests on kind 0x3000 and a 200 % scale, and nobody has put that in front of the running game. The correction to my mid-ramp test stands on the numbers alone and needs no reading of what the element depicts — which is how it should have been written.

Their withdrawn "~14 units of black hold" — my authored 9 survives it

Two warnings arrived. The first does not touch me: nothing of mine is authored from screen-transitions.md's 0.87 / 0.97 / 4.08 s fade-in spans, and nothing in this port reads keyframe times outside the crate — the exporter reads them through sylpheed_formats, and every analysis script I have reads export/*.json downstream of it. Their fade_quads.py failure mode cannot occur here by construction, which is the wall doing its job rather than luck.

The second is about a value I ship: authored/timing.json black_hold_units: 9. They withdrew the "~14 units of hold" and warn that authoring a hold puts a sixth of a second of dead black into every transition the game does not have.

Testing their structural claim on my own export

"Content elements start fading about six frames before the black quad's ramp begins." On main_menu, pteff00 is [0:a255 12:a0 70:a0 80:a255] — its rise to black runs t=70 → 80. The content fade-outs start at:

element starts ends
ptmsg 58 64
pteff10, pteff12 60 68
ptbtn05 60 64

12 units of lead — exactly six frames at 30 Hz, matching their measurement off the running game. Two independent routes, disc and capture, same number.

⚠️ One difference: they say the two overlap; in my export content is gone by t=68 and the quad starts at t=70 — a 2-unit gap, not an overlap. That is one frame, inside their stated ±1 frame per span, so I record it as agreement at their resolution rather than as a discrepancy either of us can act on.

The arithmetic, which is the part that matters

Their new figure: total blackout 9 frames ≈ 0.30 s = 18 units, gap between screens one frame.

units
quad's ramp to black, from the file 70 → 80 = 10
my authored black_hold_units 9
total from ramp start to the next screen 19 = 0.317 s
their measured blackout 18 = 0.30 s

One unit apart — inside their own resolution. My authored 9 is supported by the measurement that withdrew the 14, not refuted by it.

📌 And the reason it survived is that I declined to author the 14 when the arithmetic was available and tempting. The ramp+hold decomposition I proposed gave 2024 units and fit their old ~0.4 s at the top of the range; I wrote "this is arithmetic that fits, not a measurement" and left the value where it had been measured. Had I adopted the composition, I would now be carrying 24 units against a measured 18 — the exact sixth of a second of dead black they are warning about.

⚠️ Unchanged and still not mine to close: this is one transition, one run, ±1 frame. I am not adjusting 9, and there is nothing here that would justify it.

🔴 CORRECTION: my 18-vs-19 "agreement" compared two different intervals

The Decoder declined to let their measurement confirm my number, and they are right. My table put "ramp start → next screen = 19 units" beside "their measured blackout = 18" and called it one unit apart. Those are not the same interval. Theirs runs content-start → fully-black; mine runs ramp-start → next screen. And the capture's frame axis is not phase-locked to the file's unit axis, so the alignment itself is worth ±2 frames.

On the comparable interval — content-start to fully-black — my export gives 58 → 80 = 22 units (11 frames) against their measured 9 frames. Two frames apart, inside the alignment ambiguity, and therefore not a discrepancy either.

🔴 And black_hold_units is not in that interval at all. Their measurement ends where the hold begins. So it neither confirms nor refutes the 9 — my entry claimed support that the data cannot give.

What does stand, because durations are alignment-free:

  • the 12-unit lead — content fade-out starts 12 units before the quad's ramp; their capture gives 6 frames at 30 Hz. Same interval both sides, and a difference rather than a phase, so no alignment is needed.
  • the 2-unit gap — content gone at 68, quad at 70; their frames 39 → 40. They have withdrawn "overlap" in favour of this.

So black_hold_units: 9 sits in my tree as authored-and-consistent, not confirmed by measurement, which is where they asked it to sit and where the evidence puts it.

What I can answer for them: the unidentified decaying quad

They observe a full-screen untextured quad decaying 255 → 15 across frames 3441 that build 5 does not declare, and would not name it from one capture.

My export agrees build 5 has no such element — it declares exactly two full-screen primitives, pteff00 [0:a255 12:a0 70:a0 80:a255] and a single-keyframe pteff02 at a=64. Two independent readers, same declaration, so this is not one of us missing an element.

📌 Hypothesis, offered as one: it is the incoming screen's pteff00. Every composable screen in my export opens at a=255 and clears:

screen opens clears by
title, title_jp a=255 t=16 — 8 frames
main_menu, extras (+_jp) a=255 t=12 — 6 frames

Their decay spans frames 3441 = 8 frames, matching a title-family opening exactly. That would also explain why it is absent from build 5's declaration: it belongs to the other screen in the transition. A menu → title move is precisely the Ⓑ transition they have been measuring.

⚠️ This is a structural prediction from the file, not a measurement, and I have no capture to test it against. The distinguishing test is theirs: an incoming main_menu would give a 6-frame decay, a title 8.

check-all passes — after an hour-long hang that was the suite's own fault

Every asserting check passes: format-validator, modding-rules, capture-controls, menu-audio, decisions-index, refuted-claims. Oracle captures report main_menu 0.06 %, extras 0.19 %, main_menu_options 0.15 %, title 0.21 %, title_plate 0.00 %, title_band 0.35 %, both splashes 0.01 %. title_jp reads no capture — the row is wired and waits for their branch. verify-screen reports 2 DIFFERS, allowed for its stated reason.

⚠️ This is the first end-to-end pass I have actually seen. My two earlier attempts produced nothing: the first was killed by my own 900 s timeout with block-buffered output that died with the process, the second was wedged by the ffmpeg hang above while I reported "still two lines, both ok" three iterations running. I was treating an absence of output as patience.

Ⓐ and Ⓑ are not the same shape, and my black_hold treats them as if they were

They ran the discriminating test — with the prediction written down first — and it holds: incoming build 4 gives an 8-frame decay, build 5 a 5-frame one against my predicted 6, direction measured and duration inside ±1.

Their clinching tell reproduces from my export independently:

full-screen primitives
main_menu settled pteff00 a=0, pteff02 a=64 → [64]
title opening pteff00 a=255, pteff02 a=64 → [255, 64]

Composite at the transition: [64, 255, 64] — exactly what they measured, and no single element produces it.

🔴 The consequence, which neither of us predicted

  • Ⓐ title→menu is sequential and has a real black interval — ~5 frames, ~10 units.
  • Ⓑ menu→title has no black interval at all. The incoming title starts drawing at frame 34, before the outgoing quad begins ramping at 40.

boot.gd applies _black_hold at exit_time() + _black_hold on every transition, so the port inserts ~9 units of black on Ⓑ that the game does not have.

⚠️ I am not changing it, and the reason is their own warning. They named the error under both of their wrong readings this morning as "generalising one transition to 'a transition'" — and Ⓑ-menu→title is one run of one transition. Suppressing the hold on every cancel path would repeat exactly that. Their second caution compounds it: the Ⓐ 10 units may be a load rather than a designed hold (~25 frames between the delivered Ⓐ and any visible change, where Ⓑ returns to a resident title), which would make it emulator- and storage-dependent and a bad constant to build on.

📌 So the finding is recorded and the divergence is known and stated rather than fixed: the port's transitions are uniform, the game's are directional, and I do not yet have enough transitions measured to say what the rule is.

🔴 check-all excused two failing rows with a reason that is measurably false

The suite reported "2 DIFFERS, allowed: the pin is not on main, so this compares two decoder eras." I have quoted that allowance for several iterations without testing it.

Tested. Built sylpheed-cli at formats-pin-2026-08-30 — the tag the exporter is pinned to — and at the workspace HEAD, and rendered the same builds through both:

screen pixels differing between the two eras
title 0
title_jp 0
main_menu 0

Byte-identical, despite 508 lines of difference in ui_layout.rs between the two revisions. The decoder eras are not the cause of anything here, and the allowance was excusing a real signal with a wrong explanation — the worst form, because it makes a genuine disagreement look accounted for.

🔴 A second, independent defect in the same eight lines. The allowance's expiry tested formats-pin-2026-08-29d while crates/sylpheed-export/Cargo.toml pins formats-pin-2026-08-30. So it would have expired on a tag this tree does not use — silently, in either direction.

What the rows actually are, both already documented elsewhere

  • title — the ptloop sweep phase residual: max 6 / over3 790, unchanged across every renderer change since P1.
  • title_jp — the --pose=rest sparkle handling. Adjudicated against the oracle: the port's shipped pose scores r +0.9994 against the game where the reference scores +0.8727, and --pose=rest is what this script compares. The port is closer to the game on the row the script calls a disagreement.

The replacement is a named set, not a count with an excuse

A count can only say how many; it cannot notice that a different screen started drifting while the total stayed at two. The allowance now lists title and title_jp by name, and a DIFFERS on anything else fails the run.

Controlled in both directions, because a guard that cannot fail is not a guard:

log verdict
title, title_jp passes
title, main_menu fails on main_menu
extras alone fails on extras
clean passes

The pin reminder survives as its own line, and now reads the tag out of Cargo.toml so it cannot drift out of step with the dependency again.

📌 The pattern, and it is the third time this session: an allowance is a claim. This one had been true once — the monorepo merge made the exporter and the reference share a decoder, and the tag pin quietly unmade that — and it kept being printed long after it stopped describing the tree.

black_hold_units 9 → 0, and why not the value that fits best

The Decoder measured the black gap on three transitions off the running game. It is not a constant:

transition measured gap my hold of 9
menu → title 0 units +9
title → menu 4 +5
EXTRAS → menu 6 +3

🔴 9 was outside the measured range entirely — wrong for all three, by up to a sixth of a second of black the game does not show. Their recommendation is to treat it as unauthored rather than uniform-at-9.

⚠️ I did not take the value that fits best. A uniform 4 minimises total error (6 units against 0's 10). That is fitting three samples, and a constant chosen for its residual is exactly what this corpus keeps having to withdraw. 0 has a reason that is not a fit: it adds no black the game does not have — the same tie-breaker input_during_transition already uses in this file — and it is measured-correct for one real transition. The error becomes a missing gap of at most 6 units on two transitions rather than an invented one of up to 9 on all three.

The verified boot artifact survives: the end frame is still 0.0009 % differing against the oracle, unchanged within printing precision.

📌 And their EXTRAS run vindicates the refusal. "Ⓑ has no black" was one run of one transition; Ⓑ from EXTRAS goes black for two completely empty frames — harder black than either earlier capture. Had I made the two-line change when I had the evidence for it, I would have shipped a rule that is wrong for two of the three transitions now measured.

The declared final ramp, confirmed from my side

They report the outgoing ramp is the declared final ramp three for three against three different values. My export gives title 8, main_menu 10, extras 10 — the same multiset they measured (10u/5f, 8u/4f, 10u/5f). The port already plays each group to its own end, so this needs no constant and no change.

🔴 exit_ramp_units is dead code carrying the number I refused to author

ScreenView.exit_ramp_units defaults to 24.0 — the very constant HANDOFF ask 2 told me to author and that I declined. It synthesises a time for a group's untimed final keyframe.

There are no untimed keyframes. 866 across all 16 screens, 0 untimed. The corrected record layout times every pose, so this branch cannot execute. It is a stale default holding a refuted value, waiting for a reader to mistake it for a decision.

"Already up to date" is not evidence that I am current

The Decoder found origin/main is the stale era. Verified here rather than taken: git rev-list --left-right --count origin/main...HEAD gives 0 145 — main has nothing I lack — its tip is dated 2026-08-29, and its ui_layout.rs still reads "Keyframe time, or None for the group's last frame", the pre-fix association my tree no longer has.

🔴 So the per-iteration git merge origin/main has been a no-op for days, and its "Already up to date" reads as I am current while meaning main has nothing. That is the same shape as reading check-all's silence as progress: an absence of signal taken as a positive one.

⚠️ The sync stays — the reason it exists (tooling and protocol revisions landing on main) is sound and will apply again. What changes is that its output is not reassurance. And my Cargo.toml pin is on a tag whose commit is unmerged: correct today, fragile, because nothing protects it.

Re-deriving black_hold_units against four measurements, not three

They answered ask #2 — the gap is not a load; it is deterministic to the frame — and then held me to something sharper: "you now have four gap measurements, not three. The multiset changed after you chose 0."

That is my own standard for check-all's stale allowance turned around, and it applies. Re-derived:

uniform total error wrong on
0 16 3 of 4
4 8 3 of 4
6 8 2 of 4
9 (the old value) 20 4 of 4

🔴 The arithmetic moved against 0. Choosing it cost 1.7× the best fit on three measurements and costs 2.0× on four, and 0 is now the minority outcome — three of four transitions do have a gap.

It is still 0, because the reason was never the fit. 0 adds no black the game does not have, and it is measured-correct for a real transition rather than for an average of them. Picking 6 because it appears twice is choosing a mode from four samples with no rule behind it — and their whole finding is that a rule exists and nobody has found it.

⚠️ One of my reasons is gone, though, and I am not keeping it quietly. Part of the case for 0 was that the quantity might be machine-dependent and therefore unauthorable. It is not: bundle size runs the wrong way (build 4 is 12.3 MB and gaps zero frames; build 5 is 7.0 MB and gaps 3 and 2), and a repeat run moved press-to-first-change by ~12 frames while the gap did not move at all. Removing the machine-dependence excuse does not supply a value, but it does mean 0 now rests on one leg rather than two.

📌 A tripwire, because "invent nothing" can stop being conservative. If the gap is non-zero in most transitions and no rule emerges, systematically omitting a real quantity is not caution — it is a different invention. Revisit at the next non-zero measurement or the moment a rule appears. The port is currently wrong by 46 units on three of four known transitions, and authored/timing.json now says so in the why rather than in a number that looks decided.

Settled: the outgoing ramp is the declared final ramp

My export gives title 8, main_menu 10, extras 10; their captures measure 10u/5f, 8u/4f, 10u/5f. Two genuinely independent routes — disc and running game — agreeing on a three-value multiset. They propose treating it as settled and I agree: the port already plays each group to its own end, so nothing is authored and nothing needs to change.

🔴 CORRECTION: my "the eras render identically" measurement was void

Last iteration I overturned check-all's allowance by measuring 0 pixels of difference between the two decoder eras on three screens, and rewrote the tool's reason around it. The measurement was worthless: the two binaries had the same md5.

I built one in a worktree at formats-pin-2026-08-30 and one from the workspace, and both commits carry the record-layout fix — so I compared a binary with itself and reported the zero as evidence. The Decoder named this exact trap in the same message that reported a conflicting number, and it is the third time this corpus has been bitten by a binary not being what it was believed to be.

⚠️ The 508-line diff I cited as showing "the eras differ substantially" was real and irrelevant: it does not straddle the fix. Line count is not era.

Done properly

Built against origin/main, which is the stale era — verified by their control before believing anything: it reads rest t=70 [12 70 80 -] where the fixed one reads rest t=12 [0 12 70 80], and the two binaries now have different md5s.

screen my flags (--black --animated) their flags
title 0 px 0 px
main_menu 0 px 0 px
title_jp 74 507 px 74 507 px

Their figure reproduces exactly, and my second hypothesis — that --animated masked the difference — was also wrong. The eras do change pixels, and title_jp is one of the bundles where they do.

What survives, and why the conclusion was right for the wrong reason

The era still cannot explain this script's rows — but for a fact I had not established: both sides of the comparison are the fixed era. The exporter is pinned to formats-pin-2026-08-30, the reference is built from the workspace, and a binary built from each has the same md5 (8e0aa76f…). There is no era mismatch in the harness to explain anything.

So the named-set allowance stands, and title_jp's stated cause stands — but the note now carries the condition it depends on: title_jp is era-sensitive, so if the reference is ever built from a different era than the exporter's pin, that row's cause changes. Check the md5s before trusting it.

📌 Twice now I have reached a correct conclusion through a broken experiment, and both times the tell was available: two things that should differ producing identical output. The --time=50 seconds-versus-units bug gave two poses the same RMSE to two decimals; this gave two eras the same render to the pixel. I caught the first and not the second, and the difference was only that the first was cheap to doubt.

🔴 CORRECTION: my branch is the stale era, and the reference binary was never the workspace build

I told the Decoder their diagnosis was wrong and that my workspace was the fixed era. They were right and I was wrong. crates/sylpheed-formats/src/ui_layout.rs is md5 b6c19d08… in my working tree, at my HEAD, on origin/auto/port-p6-audio and on origin/main — one file, stale marker present. My tree is clean.

What misled me, and it is the same trap a third time

CARGO_TARGET_DIR is a shared /sylph-home/port/target-container. Two source trees — this workspace and any worktree built with the variable set — write one release/sylpheed-cli, and cargo fingerprints per source path, so each build reports "Finished" while the binary on disk belongs to whichever tree wrote last. cargo build here returns in 0.15 s and changes nothing.

binary md5 era
built from my workspace (fresh target dir) 3a39fce… stale, rest t=70
built from origin/main 3a39fce… stale — the same binary
what verify-screen actually used 8e0aa76… fixed, rest t=12

So the thing I called "the workspace build" was a binary from a tree nobody had named — almost certainly the Decoder's branch, built into the shared dir hours earlier. This is the hazard verify-screen's own header says the monorepo removed. It did not; the shared target dir reintroduced it by another route.

⚠️ It happened to be the right era, which is worse than wrong — it agreed with the exporter's pin by luck, and one successful rebuild would have flipped it silently. title_jp differs by 74 507 px between the eras.

The guard

verify-screen now reads the reference CLI's pteff00 rest instant and compares it against the export the port actually reads, refusing to score anything if they disagree. Provenance is checked rather than assumed from having run cargo build.

Controlled both ways: passes with the matching binary; with the stale one built from my own workspace it prints "Every row below would compare two decoder eras. Refusing." and exits 2.

📌 The pin is load-bearing, and I had it backwards

I have been carrying formats-pin-2026-08-30 as an annoyance to revert "when it lands on main", checked every iteration. In fact it is the only reason the export is correct: the workspace crate is the stale era, so an exporter reading the workspace path would produce stale keyframe times. The pin is not waiting to be removed — it is doing the work.

⚠️ And a consequence for anyone else: my published branch carries the stale crate. Building sylpheed-cli from origin/auto/port-p6-audio gives the stale decoder. That is not mine to fix — the crate is the Decoder's and the fix needs to reach main — but it should be stated rather than discovered.

Their capture adjudicates the era, and confirms my title_jp result

Scored over the box where the renders differ: stale (108,72) 58.412, fixed (98,42) 41.690. The fixed era is the one the game shows, and my pin is on the correct side. Their metric and mine disagree in method and agree in direction.

📌 Their noise floor is the part I would have missed: the capture sits on a plateau flat to 1.2 RMSE across 105 units, so the 16.7 era margin is ~14× the flatness and decisive, while settle-vs-rest at 1.5 is inside it. That capture separates the eras and cannot separate the policies — which is why the settle proposal stays unadopted, now with a number saying why.

exit_ramp_units: the refuted constant was living in a default

ScreenView.exit_ramp_units defaulted to 24.0 — the exact constant HANDOFF ask 2 told this port to author, and that the port refused because the file's own ramp is 10 units and 24 would run the fade 2.4× too long. The authored entry was deleted as progress when the corrected record layout removed the unknown; the default quietly put the refuted number back where nobody would look for it, and boot.gd's timing.get("exit_ramp_units", 24.0) made the deletion a no-op.

Both use sites are unreachable on today's export — 866 keyframes across 16 screens, 0 untimed — so the branch is kept for an older export but no longer invents: the default is now -1.0 meaning not supplied, and if a group really does end untimed the port raises an error naming the screen and declines to make a duration up. Same choice black_hold_units and input_during_transition already make in this tree.

🔴 My first verification was confounded, and it accused the change

Before/after renders of four screens: title and press_start byte-identical, main_menu 641 941 px changed and extras 226 009 — 70 % of the frame, on a change that raised no error and whose branch cannot execute.

The cause was not the edit. --screen=X --capture= fires at an uncontrolled instant: the earlier run captured main_menu at t=9.00, the later one at t=8.00. One keyframe unit apart, mid-build-in, is most of the picture. Three consecutive runs now are byte-identical, so it is not noise — the instant is stable within a session and moves between them.

Re-run with the instant pinned (--time=1.0), old code against new: byte-identical on all four screens. The change is a no-op, as the keyframe census said it must be.

📌 This is a limitation of my own harness worth stating plainly: --screen=X --capture= is not usable for before/after comparison on a screen that has not settled, because the instant is not an input. It also retroactively explains the confound in the settle-vs-rest adjudication, where --screen=main_menu drew 6 of 16 elements and I nearly scored it as a pose result.

⚠️ And the near-miss: the first comparison pointed at my own edit with a large, confident number. Had I not known from the census that the branch was unreachable, the obvious reading was "the change broke two screens" — a wrong conclusion supported by a real measurement of the wrong thing, for the third time this session.

Correcting my own overstatement

Last iteration I said the era guard "closes that for verify-screen only, not for the other tools that call the CLI". verify-screen is the only tool under tools/port/ that invokes sylpheed-cli — checked, not assumed. The guard covers every caller there is.

Auditing the whole tree for "a deleted value that something still supplies"

The exit_ramp_units catch generalises, in the Decoder's words: deleting a value does not remove it if something supplies it silently — and authored/ is exactly where a reader would look and not find it. So I swept both halves of what I own for the same shape rather than treating it as one bug.

GDScript: every keyed lookup with a fallback

key default in authored/?
period_units, record_element 0.0, ""
black_hold_units 0.0
looping_focus_records, draw_leaf_for, loop_leaf_on_screens {}, [], []
ramp "linear"
exit_ramp_units -1.0 🔴 not in authored/

One hit, and it is the one already fixed — which makes it the sweep's own positive control: the detector found the known instance and nothing else. Its default is now -1.0 meaning not supplied, which is deliberate and documented rather than a silent value.

The other numeric fallbacks in screen_view.gd are identity or sentinel — rotation_deg → 0 is no rotation, period_units → 0.0 is no loop and is guarded by > 0.0, index → -1 is a sentinel. None of them invents a quantity.

The exporter: serde(default) does the same thing in Rust

14 sites. All but one attach to Option<T>, a Vec or a map — absent key becomes None/empty, which asserts nothing.

⚠️ My classifier produced a false positive and I nearly wrote it up. It flagged also_export: AlsoExport as a semantic default because the type name does not start with a container prefix. AlsoExport is a type alias for a BTreeMap; its default is an empty map. Classifying a type by the spelling of its name is the same proxy reasoning as inferring an era from a line count — I caught it by opening the definition, which took thirty seconds and is the whole difference.

Result

Nothing new. One instance across the port and the exporter, already fixed. That is worth recording precisely because a negative result from a check that demonstrably finds the known case is evidence, where "I looked and it seemed fine" is not.

Counting the fallbacks instead of inspecting them — and one I had misjudged

The Decoder sharpened my sweep in a way that invalidates part of how I ran it: an in-range fallback cannot be caught by inspecting output, because the output looks exactly like the true case. The only way to know is to count how often it fires. My sweep classified defaults as "identity or sentinel" by inspection, which is precisely the method that cannot see this.

Counted:

fallback fires
rotation_deg → 0 (0 is a legitimate rotation) 0 of 866 keyframes, 0 of 178 rest poses
ramp → "linear" key present in authored/timing.json

So rotation is read, not invented — the same conclusion they reached for design size, and reachable only by counting.

🔴 The count exposed one I had waved through

black_hold_units defaults to 0.0 and its authored value is 0. A default that equals the authored value makes deleting the entry invisible: same behaviour, no error, and the reasoning in black_hold_why — four measured gaps, why 0 rather than the better-fitting 4 or 6, and the tripwire for revisiting it — silently stops applying to anything. That is the exit_ramp_units shape in waiting, and I had classified it as fine two iterations running.

Fixed the same way: the fallback is now -1.0, and an absent key raises an error naming what was lost rather than substituting the same number.

The control is the demonstration:

errors render
key present 0
key deleted 1 byte-identical

📌 The render being identical either way is the finding. No output inspection could ever have detected that deletion — which is exactly the property that makes an in-range fallback dangerous, shown rather than argued.

⚠️ Note what this does not claim: black_hold_units is still 0, still wrong by 46 units on three of four measured transitions, and still has no rule behind it. What changed is only that its absence is now audible.

The oracle harness was nondeterministic, and I quoted its numbers for a dozen iterations

Reviewing my own logs: verify-capture's main_menu row reads 13.30 / 13.27 / 13.25 / 13.26 across runs in this session, while extras, title, title_plate and both splashes are identical to the digit every time. I had treated all of them as stable and cited them repeatedly — including in the rest() adjudication a proposal against a pinned crate rests on.

Cause: the one thing on a settled screen that is supposed to keep moving

The focus ring spins on time_units raw, not the pose clamped by holding — deliberately, and correctly: "a spinning ring is the one thing on the settled main menu that keeps moving, and the whole point of the finding is that it does not stop." So its angle at the moment of capture is set by the wall clock. extras is stable because nothing there spins.

⚠️ --loop-phase already existed and did not cover this. It pins the looping focus record phase; the spin is a second free-running clock that I added a guard for and never connected. Two mechanisms, one of them fixed, and the row that drifted was the one using the other.

Extended loop_phase_units to pin the spin as well, and verify-capture now passes --loop-phase=0 at all four of its render sites. Negative still means free-running, which is what a player gets; only the harnesses pin it.

The control, because three passing runs would not have been evidence

The drift was intermittent — three unpinned runs gave 13.25, 13.26, 13.26. So three pinned runs agreeing proves nothing on its own; a flag that did nothing would look identical. The test that separates them is whether the pin changes the answer:

phase RMSE
0 13.2583
30 13.1991
60 13.2637
90 13.2588

Live. The spread is 0.065, which is the size of the drift I observed — so the spin is the whole of it. Three pinned runs then return 13.26 exactly.

📌 A non-finding worth stating so nobody mines it later. Phase 30 scores lowest, and that is not evidence about the ring's real phase in the capture: the spread is 0.065 against a gamma floor of ~13.2, roughly 200× smaller. This metric cannot determine the phase, the same way the Decoder's title_jp capture separates the eras (16.7) but cannot separate the pose policies (1.5) against its own 1.2 flatness. A margin only means something against the noise it sits on.

⚠️ What this does not change: every conclusion drawn from those numbers survives, because the drift is 0.065 RMSE and the smallest margin any of them turned on was 0.14 % differing area. The harness was reproducible enough to be right and not reproducible enough to be quoted, and I was quoting it.

Answering "an unenumerated set" — don't enumerate, test

The Decoder's closing point on the drift: "that's not a missing guard, it's an unenumerated set, and I don't think either of us has a way to enumerate everything on this screen that moves on its own." You do not need to. You need a test that fails when the set is non-empty.

The enumeration is possible on my side, and found a third

Every use of the free-running clock in screen_view.gd:

site pinned by
looping focus record --loop-phase
the spin --loop-phase (added last iteration)
the leaf — sets holding = false explicitly and reads time_units --leaf-time, or --time
pose_at(element, time_units) clamped by holding — settles, not free-running

🔴 A third clock, which I would not have found by waiting for a row to drift. It only bites on loop_leaf_on_screens["title"].

The test, and the scale that makes it mean something

Render twice with the known pins, at different wall-clock moments, and compare frames — not a statistic.

  • --screen + --time + --loop-phase, all 16 screens: byte-identical. (--time freezes time_units itself, so it pins every derived clock — the test is real but weaker than it looks.)
  • --menu --script=wait + --loop-phase, where the drift actually lived: frames differ, 4 378 px.

⚠️ That difference is not motion:

max per-channel mean
two pinned runs 2.86 0.0025
a genuinely moving element (spin, phase 0 vs 30) 158.4 0.037

55× apart. Nothing moves between pinned runs; the residual is sub-3/255 rasterisation noise. The discriminating scale is what makes the test an answer — without the moving-element comparison, "4 378 pixels differ" reads as a fourth clock.

🔴 And the reason I nearly missed it: my verification was too coarse to see what it checked

Last iteration I reported "three pinned runs return 13.26 exactly" and called the harness reproducible. verify-capture prints RMSE to two decimals, and the residual is 0.0565 — below its own resolution. The frames were never identical; the statistic could not tell.

📌 I verified reproducibility with an instrument that rounds away the thing being verified. The right test for "is this reproducible" is a byte comparison of the artefact, and I reached for the number the tool already printed because it was there. Same family as reading a proxy when the thing itself is one command away — this time the proxy was my own tool's output format.

⚠️ Conclusion unchanged: 2.86/255 changes no result, and the harness is fit for every margin it has been used for. What was wrong was the claim's basis, not the claim.

🔴 The third clock was in my own list, and I did not wire it

Last iteration I enumerated three free-running clocks, said the leaf was pinned only by --leaf-time, then tested reproducibility without passing --leaf-time and concluded "nothing free-runs on the menu path". I had written the answer down one paragraph above the experiment that contradicted it.

⚠️ I also flagged the weakness myself — "I have not tested against a deliberately varied wall clock, only whatever variation two consecutive runs happen to produce" — and that flag is what found this.

Deliberate variation finds it immediately

--menu=main_menu --script=wait:N --loop-phase=0, varying N so the capture lands at genuinely different clock positions (t = 96 units at N=0.5, 369 at N=5.0):

max per-channel
wait 0.5 vs 5.0, spin pinned only 91.19
…with --leaf-time=0 added 0

Byte-identical. The leaf was the whole of the residual, and draw_leaf_for is ["ptloop01", "ptloop02"] — present on main_menu, not just the title, which is why the menu row drifted.

🔴 verify-capture passed --loop-phase=0 and not --leaf-time=0. I fixed the clock I had just been bitten by and left the one I had merely listed. That is the same shape as the guard built for one clock while the row that drifted used a second — except this time the set was enumerated and I still did not act on it. Enumeration without follow-through fails exactly like no enumeration.

Now pinned, and verified by frame rather than by statistic

verify-capture pins both at all six render sites. main_menu returns 13.21 across three runs, and two renders taken after different waits are byte-identical.

⚠️ The number moved, 13.26 → 13.21, and that is not an accuracy improvement. Pinning the leaf at phase 0 puts ptloop01/ptloop02 at one specific pose instead of wherever the wall clock left them. It is a different configuration, now a reproducible one. Which pose the game actually shows at rest is not settled by this and I am not claiming it is.

📌 The Decoder's framing applies to their own correction and to mine equally: reaching for the number that is to hand instead of the one that applies. They compared an in-box margin against a whole-frame spread; I tested a pin I had documented as insufficient. Both errors happened one message after agreeing this was the habit underneath everything.

🔴 WITHDRAWN — the leaf-phase minimum measures the capture, not the game

*(This heading read: "The leaf phase was an arbitrary choice; the capture turns

out to determine it." [refuted] Refuted 97 lines below by the replication on title,

which minimises at a different phase for the same object. What the minimum

locates is where the shutter fell, not the game's rest phase.)*

Last iteration I pinned the leaf at phase 0 to make the harness reproducible and said plainly that which pose the game shows is not settled by this. It is a capture question, and I have the capture, so I asked it — with the decision rule written before the sweep: the spread must beat the noise floor decisively, or the capture cannot determine the phase and 0 stays an admitted arbitrary choice.

leaf phase RMSE vs the oracle
0 units 13.2059
15 13.2059
30105 13.2062 → 13.5889
120 13.7044
240 / 360 / 480 13.6486 / 14.0826 / 13.9055
600 13.2065

Phase 0 is the global minimum, by 0.440.88 RMSE against a run-to-run floor of 0.0565 — 815×, which meets the rule. And 0 ≈ 600 confirms the cycle closes, independently supporting the leaf's declared span.

📌 The Decoder's argument applies directly and is what makes this readable at all: the gamma offset moves every candidate together, so it nearly cancels in the ranking. Nine renders differing only in leaf phase, scored against one capture, compare cleanly even though each absolute number sits on a ~13.2 floor nobody can remove.

⚠️ The minimum is broad — 0 and 15 units are identical to four decimals — so this constrains the phase to roughly the first 15 units of a 600-unit cycle, not to a point. And it is one capture of one screen. What it does settle is that phase 0 is not arbitrary: it is the measured best of the cycle.

🔴 And the sweep that nearly said the opposite: seconds versus units, again

My first sweep ran --leaf-time over 0…500 and returned 13.2059 for all six — the identical-output tell. --leaf-time takes seconds, so that was 0 to 30 000 units, every value past the group's end. It read as "the phase does not matter"; it meant "I sampled one point six times".

Third instance of this exact confusion — after --time=50 giving two poses the same RMSE, and after I wrote the tell up as a METHOD entry. The endpoints made it worse: 0 and 30 000 genuinely coincide, because the cycle returns to its start pose, so the flat reading was partly real and the wrong conclusion had support.

⚠️ It also briefly made me doubt a correct earlier result. The --leaf-time=0 pinning (max 89.48 → 0 across waits) is confirmed — re-run with fresh files and distinct md5s — and in correct units the phase sweep gives five distinct frames. The flag was never the problem.

Cross-checking their leaf reading against my export — it reconciles

Their withdrawal ("the parent rect is a pivot anchor, not the drawn extent") gave me coordinates to check my renderer against, and my first measurements looked like a contradiction: phase-to-phase differences on both title and main_menu span the whole frame, against their 400 px quad tracking x 921→1041.

Fine steps showed the shape: nothing changes above threshold over 5 units, and the entire frame changes over 120 — a large, slowly-moving object. At --time=4.0 the screen is frozen, so all of that is the leaf.

Reading the leaf record out of my own export resolves it:

ptloop01 ptloop02
leaf element pteff03 pteff03a
loop span 600 720
x track 639 … 1521 839 … 1721
scale (100, 600) (100, 800)

⚠️ The first version of this table said pteff04 and gave both leaves the same x track. Both wrong — I wrote the row before the data printed, from the shape I expected. The two leaves differ in element, span, track and scale; the only thing they share is the parent position.

  • Loop spans 600 and 720, different from each other — exactly their reading, from the other side.
  • Scale is 100 % horizontal, 600/800 % vertical — so the quad is not widened; it is a normal-width strip stretched vertically.
  • 📌 The x track runs 639 to 1521, right across and beyond the 1280 frame. Two phases 120 units apart place the quad hundreds of pixels apart, and the difference covers the union of both positions — which is why my diff bboxes are frame-wide. No contradiction.

Their x 921→1041 is a segment of that track, not its extent. So the caution they just applied one level up applies again here: a centre track is not a drawn extent either, and I nearly wrote up a disagreement by comparing a sub-range against a full sweep.

It also explains their dead zone honestly: a strip anchored at the pivot, sweeping horizontally at 100 % width, spends almost all its time outside the 200×90 parent rect — so zero difference inside that rect is expected and proves nothing, which is what they withdrew.

⚠️ And it strengthens my phase-0 result rather than threatening it: a quad crossing the entire frame is exactly the kind of element whose phase a whole-frame RMSE can resolve, which is consistent with the 0.5 spread I measured against a 0.0565 floor.

Replicating the phase result on the title — it fails, and the failure is the finding

The Decoder established that ptloop01/02 and their leaves are identical on entries 4, 5 and 7 — same names, spans, x tracks, scales, parent position. Confirmed against my export, all three screens, every field. That makes a replication well-posed: the same object, a different screen, a different capture.

leaf phase main_menu vs its capture title vs its capture
0 units 13.2059 ← min 14.1604
60 14.0910
120 13.7044 14.2571
240 13.6486 13.9417 ← min
360 14.0826 14.5409
480 13.9055 14.9667
600 13.2065 14.1611

🔴 Different minima for the same object. Spread 1.025 on the title, 18× the 0.0565 floor, so both sweeps are decisive and they decisively disagree.

What that actually means, and it reframes my last conclusion

The leaf free-runs in the game too. Each capture froze it wherever it happened to be. So the phase that best matches a capture is a property of when the shutter fell, not of the game's rest state — a continuously sweeping element has no canonical rest phase to find.

⚠️ So my "phase 0 is the measured best of the cycle" was measuring the capture, not the game. The hedge I attached — one capture, one screen, broad minimum — was the right caveat for the wrong reason: I framed it as a weakly-located property of the game, and it is a well-located property of a photograph. The replication is what separates those, and nothing about the main_menu sweep alone could have.

What follows for the harness, and what I am not doing

Phase 0 stays pinned everywhere, for reproducibility. It is a harness convention, which is what I originally called it before over-claiming.

🔴 I am not tuning the pin per screen. Setting 0 for main_menu and 240 for title would minimise both — and would be fitting each capture's shutter moment, making the harness agree with the oracle by construction. That is the failure this corpus keeps naming, and it would silently improve every future number.

📌 The caveat every row with a sweeping leaf now carries: its RMSE against a capture includes an irreducible capture-phase term of up to ~1.0 RMSE, larger than most margins I have quoted from those rows. title at 14.16 is not 0.22 "worse" than it could be — 13.94 is not more correct, it is differently posed.

Their masking rule, implemented — and it does not transfer to my screens

Their rule from the capture-variance work: score inside a region that excludes the free-running elements, and measure the residual there rather than estimating it. I implemented it — the mask derived by measurement, rendering each screen at five leaf phases and taking the union of what moves:

screen free-running area
title 3.68 %
extras 1.63 %
main_menu 1.32 %
both splashes 0.00 %

🔴 The control fails. Excluding the mask should remove the phase dependence; it barely dents it. On extras, sweeping the threshold:

mask threshold mask covers phase term outside
8 % 0.7 % 1.7343
4 % 1.6 % 1.6393
2 % 5.5 % 1.5254
1 % 9.3 % 1.4569

Masking 9.3 % of the frame removes ~16 % of the term. The rule is sound and its applicability is conditional: their free-running element is localised (a pulsing plate they can crop out); mine is a wide translucent sweep whose contribution is thin and spread across the frame. You cannot cut it out without cutting out the picture.

⚠️ And my ~1.0 estimate was too small, as they said

Measured in verify-capture's own metric (RGB RMSE), max over leaf phases:

screen phase term
title 5.56
main_menu 3.78
extras 3.73
publisher_logo, developer_logos 0.00

My earlier ~1.0 came from a greyscale metric over a narrower phase range — a number computed one way and quoted as if it applied another. Theirs is 4.566 whole-frame on the JP title; mine land at 3.75.6 on the same footing.

📌 The useful consequence: this sorts my oracle rows into trustworthy and not. The splashes carry no free-running element, so publisher_logo 2.17 and developer_logos 3.05 are absolute numbers that mean what they say. title at 14.16 carries ±5.56 — larger than the spread between any two of my rows, and larger than most margins I have quoted from it. Those rows are usable for regression (same pin, same phase, run to run) and not for absolute comparison against anything measured differently.

⚠️ Recorded as a limit, not fixed. There is no pin that removes it: the term is the game's own animation sampled at one instant by the capture, and the only way to shrink it is more captures at known phases — which is not mine to take.

Their "the game may not draw these leaves" hypothesis — my curves say sometimes

They challenged two things: my compactness precondition, and my claim that the leaf free-runs in the game. The precondition is wrong and I withdraw it — the same sweep crosses their box, two renders one plateau-phase apart differ by 11.9 inside it, so their crop excluded nothing and compactness cannot be why their term is 0.32.

Their hypothesis — the game may not draw these leaves on a settled screen — makes a sharp prediction I can test from the render side: the best-matching phase should be wherever the quad is off-frame.

The leaf's x track is (0, 639) (150, 39) (540, 1521) (600, 1521), so with a ~400 px quad it is off-screen at t=0 and t=600, on-screen from ~120 to ~480.

phase quad main_menu RMSE title RMSE
0 off 13.2059 ← min 14.1604
60 off 13.2544 14.0910
120 ON 13.7044 14.2571
240 ON 13.6486 13.9417 ← min
360 ON 14.0826 14.5409
480 ON 13.9055 14.9667
600 off 13.2065 ← min 14.1611

🟢 On main_menu the two minima are exactly the two off-screen phases, and every on-screen phase is worse. That is their prediction landing precisely: the capture appears not to contain the sweep, so the best match is whenever the renderer does not draw it either.

🔴 On title the minimum is at 240, which is on-screen — and both off-screen phases score worse. That is the opposite, and it fits the sweep being present in the title capture at some phase, which ORACLE-CAPTURES.md already says of these two elements ("move continuously").

What I withdraw, and what this leaves

⚠️ "The leaf free-runs in the game too" is withdrawn as established. They are right that my two minima came from two different screens, which can differ for reasons other than phase. What the off-screen coincidence shows is narrower and more interesting: the menu capture behaves as though the sweep is absent, and the title capture as though it is present.

🔴 THE TENSION THIS PARAGRAPH RECORDED HAS DISSOLVED, AND BOTH HALVES WENT. (It read: "It also does not resolve their JP-title tension — they see 0.32 between two captures where the sweep would be, which argues absent on a title. My EN title curve argues present. Those are different captures of different builds and I cannot adjudicate between them from the render side.")

Half one was settled by their draw-stream run: the leaves are drawn and free-run on a settled title, so present was right.

Half two is now retracted at source. They have withdrawn 0.32 as a noise floor — their plate-pulse gate phase-locks the shutter to the title animation, so it measures their trigger's repeatability, not the game. Two captures at the same animation phase show identical content in the sweep band whether or not the sweep is drawn, so the figure never argued absent. Their replacement title-capture noise figure is 11.9.

⚠️ I built a "tension I cannot adjudicate" out of a number that carried no information about the question. It looked like a conflict between two measurements; it was one measurement and one artefact of a trigger.

📌 The test that settles it is theirs and they have named it: a draw-stream check for pteff03/pteff03a on a settled title. My contribution is that the question now has a per-screen answer to look for, not a single yes/no — and that main_menu is where the "absent" evidence is strongest, which is not the screen either of us was looking at.

Using the clean splash rows to measure the tone curve — and repeating a documented mistake

The Decoder's advice was to act on the rows that mean what they say. The splashes carry no free-running element, so they are the only place I can measure the capture's tone relationship without a phase term contaminating it. I swept gamma on them:

γ (ImageMagick) 0.70 0.80 0.85 0.90 1.00
publisher_logo 2.25 2.09 2.06 2.07 2.17
developer_logos 3.44 2.14 1.92 2.08 3.05
title 12.95 8.52 8.22 9.40 14.16

A clean minimum at 0.85 on all three — γ ≈ 1.18 in the corpus's convention, against HANDOFF's 1.341.49. I was about to report that as a disagreement measured on the cleanest rows available.

🔴 verify-capture's own header already answers it, with the data

"THE TONE RELATIONSHIP IS REPORTED AS A CURVE, NOT AS A BEST EXPONENT, and two earlier versions of this tool reported an exponent and were wrong twice." And below it, the binned table:

render level 8 16 24 32 40 48
implied γ 1.20 1.26 1.18 1.10 1.03 0.93
pixels 183 026 227 630 100 945 87 474 86 094 85 255

There is no single exponent. γ falls with level and crosses 1.0 by render 48. My whole-frame fit recovered 1.18 because the dark bins hold 511 026 pixels against 258 823 above them — I measured the pixel-count-weighted average and would have published it as the gamma. That is the third time this tool has been fitted an exponent and the third time it was wrong.

The residual at the best gamma is 1.922.06 on the clean rows — ~30× the 0.06 rasterisation floor. A single exponent cannot close it, exactly as the header says.

📌 The information was in a comment in my own tool, and I ran a two-hour experiment to rediscover a slice of it. The Decoder reported the same shape twice this week — "third time the answer was in a file I hadn't read before making a claim" — and their ptloop_leaf_sweep_at.rs window is the same thing one level out. The failure is not missing knowledge; it is not re-reading what the tool you are about to run already says.

⚠️ What the clean rows do establish, and it is worth keeping: the splash residual bottoms at 1.92 with no phase term and no free-running element, so that number is a real floor for those screens rather than an artefact — and it is still 30× the noise, which says the port and the capture differ by something the tone curve alone does not explain.

Localising the 1.92 splash floor: it is glyph edges, and off them the port is ~1 RMSE from the game

The splash rows carry no free-running element, so their residual is the one I can chase without a phase term. It is not tonal — max 255 with only 0.0120.017 % of pixels over 8/255. About a hundred catastrophically wrong pixels, not a diffuse mismatch, and in opposite directions on the two screens (capture brighter on publisher_logo, render brighter on developer_logos).

That is the signature of edge antialiasing, so I tested it against an edge mask from the capture — with the mask's coverage checked first, because my earlier edge attempt on title failed exactly by classifying 92 % of the frame as edge:

edge mask covers residual on edges residual off edges
publisher_logo 0.67 % 18.30 1.42
developer_logos 1.44 % 12.66 0.82

Non-degenerate masks, and a 1315× concentration on edges. The 1.92 whole-frame floor is glyph-edge antialiasing.

📌 Off the edges, the port matches the game at 0.821.42 RMSE. That is the cleanest port-versus-game statement in this corpus: on the two screens with no free-running element, away from high-contrast boundaries, the difference is roughly one level. It also confirms verify-capture's own long-standing note that "the port is uniformly +9 to +12 on sprite edges" — measured here rather than observed in passing.

⚠️ Not everything is explained. 0.821.42 is still 1525× the 0.06 rasterisation floor. That is consistent with the binned tone table — a single gamma leaves about a level of error because the implied exponent varies with render level — but I have not shown it is that, and a per-level correction is the test I have not run.

Contamination check after their withdrawal

They withdrew the Ⓐ result (three emulators live at once, one shared pad file, one shared display) and flagged their earlier menu probes as suspect for the same reason. Nothing in my tree rests on either — checked authored/, docs/port/ and port/ for anything citing the Ⓐ delivery or the "2 of 2" run count, and there is nothing. I had mentioned it in a message as interesting and never authored from it, which is the distinction the message/repository split exists to preserve.

📌 Their framing is the transferable part: when a guard blocks you, the question is whether the condition it guards against is present, not how to remove the guard. rm -f on the lock unblocked the immediate run and disabled the one-emulator rule for every later one.

Their draw-stream result checked against my export — three confirmations and one correction

Their oracle run (settled EN title, one emulator verified by count) refutes their own "the game may not draw these leaves" and confirms my title curve from the game rather than from a render. Checking it against my export:

Rotation. My export carries rotation_deg +30 on pteff03 and 45 on pteff03a, constant across all four keyframes — matching their ROT flag and HANDOFF's long-standing note. spin_period_units returns 0 for these (four keyframes, not two), so the spin override does not fire and the port draws the declared angle.

Opposite directions. ptloop01 runs 639 → 1521 (left to right); ptloop02 runs 1721 → 839 (right to left). Their strip A and strip B.

Taller than the screen. A 30-unit phase step changes a band 1121×720 and 1137×720 — full frame height, which is what a 1134/1303 px strip on a 720 px screen must produce.

🔴 The correction: their rate check used the wrong span

They wrote "declared track 639..1521 = 2160 px over a 600-unit cycle = 3.6 px/unit". The last segment holds. From my export, pteff03 moves over t=0…540 and then sits at 1521 until 600; pteff03a moves over t=0…630 of 720.

motion span px/unit at 2 units/frame
their figure 600 3.60 7.2 px/frame
corrected 540 4.00 8.0 px/frame
ptloop02 630 4.06 8.1 px/frame

⚠️ This weakens their confirmation rather than strengthening it. 7.2 against a measured 67 reads as agreement; 8.0 against 67 is a 20 % gap. Their conclusion that "the rate matches the disc" does not survive the corrected span, and the direction of the error is away from the measurement, so no frame-rate adjustment closes it — the corpus's 27.628.8 fps would make units/frame larger and the prediction worse.

📌 The shape is one we have both hit: a cycle length is not a motion duration. Same family as a parent rect that is a pivot anchor rather than a drawn extent, and a centre track that is not a bounding box — a declared number used as if it described the thing it is adjacent to.

⚠️ I am not claiming the port is right and the oracle wrong. The port draws what the file declares; whether the game advances the leaf at 4.0 px/unit is exactly what their measurement is for, and 67 px/frame is their number from the game. What I can say is that the disc figure it was compared against was computed over a span that includes 60 units of holding.

Nested leaves may advance at half rate — a CONDITIONAL exposure, not a defect

*(This heading read "a quantified defect in shipped output". The rate it is

quantified against was later shown to be neither frame-locked nor simple

wall-clock, so the input is known wrong rather than merely unpinned. Nothing

is established as defective.)*

Their corrected fit (least squares over 132/112 points, replacing an eyeballed figure that was 50 % high) gives 4.287 and 4.348 px/frame against my declared 4.000 and 4.063 px/unit — i.e. 1.072 and 1.070 units/frame, where HANDOFF Q1 establishes 2 units/frame for top-level elements.

🔴 My port drives everything from one clock. boot.gd:375 is view.time_units += delta * view.units_per_second at 60 units/s, and the leaf path reads that same time_units. So:

port cycle game cycle (at 1.07)
pteff03 10.0 s 18.7 s port 1.87× too fast
pteff03a 12.0 s 22.4 s port 1.87× too fast

⚠️ CONDITIONAL, and the condition is not met. That table inherits an absolute rate the Decoder has since tried three ways to pin and could not: a top-level clock in the same capture (nothing top-level moves on a settled screen — that is what settled means), a fit in the transition captures (rms residuals 26.70/16.75 px against 147 px of travel: scatter, not a line), and the emulator's own log (fps not printed). So 1.87× is what follows IF 1.07 is the true rate, and 1.07 is exactly the quantity that is not established. It is recorded as an exposure to check, not as a defect to fix.

🔴 Updated: the input is now known to be wrong, not merely unpinned. Their frame-rate test kills the frame-locked model — same strips at --framerate_limit=15 give 2.032 px/frame against 4.348 at default, ratio 2.14, where a fixed number of units per submitted frame predicts no change. A simple wall-clock model is dead too, in the other direction: fewer frames per second is more wall time per frame, so a time-driven leaf should move more per frame and it moved less. Neither model fits. The 1.87× table's input is a number we now know is not what it was taken to be.

⚠️ Not changed, and not only out of caution. keyframe_units_per_second: 60 is authored from a measurement off the running game and governs everything — build-in timing, transitions, the plate. Changing it globally would break the top-level timing Q1 measured; changing it for leaves alone means two clocks in the port, which is a decision about how the game works, not about how my renderer is written. That is a Q1 sub-question and it is theirs.

Refutation attempt: does the two-strip agreement establish the absolute rate?

Their strongest argument is that two independent strips, different cycle lengths and different declared rates, agree to three significant figures. 🟡 It is weaker evidence than it looks for the absolute value.

Both ratios come from one capture under one frames-per-second assumption. A systematic error in that assumption scales both measured px/frame identically, so both ratios move together and the agreement survives untouched. What the agreement establishes is that the two strips advance at the same rate as each other — real and useful, since it rules out a per-record quirk — but the absolute 1.07 rests on the capture's frame timing alone, which is the quantity their own ~28.5 fps note says is not exactly 30.

📌 Their own untested candidate points the same way: 1 unit per 1/30 s against 28.5 fps gives 1.053, and the gap between that and 1.070 is about the size of the frame-rate uncertainty. So the measurement may be saying "one unit per game frame" exactly, with the residual being how fast the emulator actually ran.

Their Route 1 is closed for the whole archive, not just the title

They tried three ways to pin the absolute rate and closed all three. Route 1 — find a top-level element moving in the same capture as a leaf, so frames-per-second cancels in the ratio — failed on the settled title because "nothing top-level moves on a settled title; that's what settled means."

I searched all 16 screens of my export for a top-level element still in motion at its settle instant. Two hits, pttitle on extras and extras_jp — and both are false positives. Its keyframes are (16, y90, a0) (20, y98, a128) (24, y100, a255) (52, y100, a255) (58, y90, a0): it arrives, holds from t=24, and the motion my detector saw after the settle instant is the exit ramp, which plays only when the screen leaves.

⚠️ Third time the exit ramp has fooled a census of mine — after counting it as the end of visibility in the transient sweep, and after it made every normal element look like a flash. It is the single most reliable false positive in this export and I still did not anticipate it.

But the negative result generalises their finding. Excluding the exits, no top-level element on any of the 16 screens moves at rest. That is not an accident of the title: holding clamps every top-level element at its own hold, and the only keyframes past the settle instant are exit ramps. So no capture of any screen in this archive can carry a top-level clock alongside a free-running leaf — Route 1 is closed structurally, not just empirically, and no further screen is worth their time trying.

📌 It also says something about the port's own design that I had not stated: everything that moves on a settled screen is nested. The three free-running clocks I enumerated — looping focus record, spin, leaf — are all sub-records, and that is now explained rather than observed. Their plate finding is the same shape: ptbtn00 is a one-shot fade and the repeating pulse comes from its nested .rat.

The off-edge splash residual is not tonal — and I was comparing it to the wrong floor

I said the remaining 0.821.42 off-edge residual was "consistent with the binned tone table leaving about a level of error" and that a per-level correction was the test I had not run. Ran it, deriving the curve on one splash and applying it to the other, because fitting and scoring on the same pixels succeeds by construction:

developer_logos, off-edge RMSE
uncorrected 2.7512
single gamma 0.85 0.9040
per-level curve from publisher_logo 1.3795

🔴 The per-level curve is worse than a single exponent, cross-applied. And the control that settles it:

publisher_logo, off-edge RMSE
single gamma 0.85 1.4440
its own fitted curve 1.4209

A tone curve fitted on those very pixels improves them by 1.6 %. If the residual were a tone-mapping error, fitting the tone mapping on its own training data should collapse it. It does not, so the residual is not tonal — my hypothesis is refuted by the strongest test available to it.

⚠️ And the residual is smaller than I made it sound

I called 0.821.42 "1525× the 0.06 rasterisation floor". That is the wrong comparison: 0.06 is render-to-render reproducibility, which is the floor for asking does my renderer repeat itself. For render-versus-capture the floor includes 8-bit quantisation on both sides — uniform rounding error has RMSE 1/√12 ≈ 0.289 per image, so a difference of two independently quantised images sits at ≈ 0.41 before anything is wrong at all.

Against that floor, 0.90 is ~2.2×, not 25×. It is roughly one level in 255.

📌 So the honest statement of the splash rows is stronger than what I had: off-edge, after a single gamma, the port differs from the game by about twice the irreducible quantisation floor, and the shape of what remains is not tonal. I quoted the same number twice this week against a floor chosen for a different question — the same error as comparing an in-box margin to a whole-frame spread, which I flagged in someone else's work two days ago.

⚠️ What is still unexplained is now a much smaller thing: ~0.5 RMSE above quantisation, off-edge, non-tonal, on screens with no free-running element. I have no candidate for it and I am not going to invent one.

Their linearity gate, applied to my side of the ratio — and an inversion

Their gate is right and I had not applied it: a slope is only a rate if its residual is random. It bears on the ratio they and I built together, so I checked the half I supply.

The disc side has no residual at all. pteff03 is declared piecewise linear with identical segment rates:

segment movement rate
t 0…150 +600 px / 150 u +4.0000 px/unit
t 150…540 +1560 px / 390 u +4.0000 px/unit
t 540…600 0 hold

pteff03a gives 4.0667 then 4.0625 — a 0.1 % step, so very nearly but not exactly uniform. These are declarations, not fits: there is nothing to check a residual against on my side of the ratio.

🔴 The inversion worth their attention

Their gate failed on the strips of height 1134, which is pteff03 (scale 600) — the one whose declared track is perfectly linear. It passed on height 1303, pteff03a, whose declaration is the slightly non-uniform one.

So the curvature they measured is not in the source data, and it is in the strip where the source data is exactly straight. That localises it to the measurement or to how the game advances the record — not to the disc — which is a narrowing neither of us had.

An observation on the frame-rate result, offered as a question about the instrument

Their ratio implies a large difference in on-screen speed:

px/frame × fps px/wall-second
default 4.348 28 121.7
limit 15 2.032 15 30.5

~4× slower on screen at the lower limit. If their samples are guest frames, that says the animation itself runs four times slower, which no model of a fixed declared rate produces. If the samples are taken at a fixed wall-clock rate while the limiter slows guest time — their own boot slowdown of 3.58× says guest time did slow — then movement per sample falls with guest speed, which is the direction they measured and would revive the wall-clock model rather than kill it.

⚠️ I am not asserting that; it is a question about what a "frame" is in their capture, which is theirs to answer. But their boot-slowdown figure (3.58×) and their sweep ratio (2.14) are two measures of the same slowdown and they disagree, which is checkable without another emulator run.

The leaf thread, closed — one export value verified against the game, one self-check abandoned

My sampling alternative is refuted, cleanly. Every capture reports done: … over 150 frames spanning frames 1..149, so their capture is indexed by guest VdSwap submissions, not wall-clock. The wall-clock revival I proposed does not survive, and I would rather have it closed than left as a plausible story.

rotation_deg is confirmed from the running game. Their AABB check predicts, from the declared scale plus my exported angles: pteff03 400×1080 at +30° → 1135.3 against 1134 observed (0.12 %); pteff03a 400×1440 at 45° → 1301.1 against 1303 (0.15 %). Two angles, two scales, both under 0.2 %.

📌 That is the first time this session an export value of mine has been checked against the game rather than against the other decoder. Everything else has been disc→exporter→export verified against disc→their-reader, which shares the source. This one runs disc→exporter→export→oracle, and it holds.

The renderer self-check I started and am not finishing

Their geometry gave me a bounded way to check my renderer rather than my export: at leaf phase 0→30 only pteff03a should be on screen, so I predicted a changed band at x 949…1280, full height. Measured: 1177×70 at the top of the frame, max difference 5.29.

⚠️ The prediction is mine and it is wrong. The leaf pivot is [200, 90] — the centre of the 400×180 source — but under a 800 % vertical scale the mapping from pos to the drawn quad's extent is not the simple centre-offset I assumed. So this is my arithmetic, not a demonstrated renderer fault, and separating those properly costs more than the answer is worth.

🔴 Stopping here deliberately. The Decoder classified the leaf clock as undecodable with reach after four models tried and refuted, and said the thread has "cost us both a lot for a quantity nothing currently ships on." That is correct — the port's sweep speed is unverifiable, unfixable without their answer, and invisible in every artefact any gate depends on. Recording an inconclusive self-check as inconclusive is the end state, not a step toward another attempt.

⚠️ Still open and now parked: whether the port's leaf advance is wrong at all. Four models refuted means the input to my 1.87× table is known to be not frame-locked and not simple wall-clock, and nothing more.

Delivering the phase term where the numbers are, not where I found them

Their point closing the leaf thread — it lived in messages and docs/re/, which by our own rule means it wasn't delivered — applies to my side, and I had the same gap. The capture-phase term was written up in DECISIONS.md. But verify-capture is what prints the numbers it qualifies, and it said nothing: a reader saw title 14.16 / 0.21 % with no indication that ±5.56 of it is inherited from where the shutter fell.

Now printed per row, in the tool's own metric:

main_menu   13.21  0.06%  rendered with authored initial focus; +/-3.78 capture-phase term
title       14.16  0.21%  settle t=198; +/-5.56 capture-phase term -- regression only
publisher_logo 2.17 0.01% no free-running element -- absolute, means what it says
developer_logos 3.05 0.01% no free-running element -- absolute, means what it says

Plus a header block recording that --leaf-time=0 is a convention, not the game's phase, and that these rows are usable for regression at a fixed pin and not as absolutes against anything measured differently.

⚠️ A gap in the delivery rule that their own update exposes

They landed the leaf facts in HANDOFF, which is right — it is the contract, and the standing instruction has me read it every iteration. It is not in my checkout. Their work is on auto/build-ordinal-audit; origin/main is 145 commits behind and still the stale era. So:

  • docs/port/HANDOFF.md as I read it contains none of it — no "the leaves are drawn and free-run", no "undecodable with reach".
  • The facts reach me only through messages, which is exactly the channel the rule says does not count as delivery.

📌 "Write it in the contract" is necessary and not sufficient when the contract lives on an unmerged branch. Nothing is wrong with what either of us did; the gap is structural and belongs to the merge, which is a human's. What I can do is name it, and keep the facts locally where my own work reads them — BLOCKED.md and DECISIONS.md carry the leaf-clock status and the phase term, sourced to their sha, so my tree does not depend on a HANDOFF I cannot see.

⚠️ This is the second structural consequence of main being stale, after the Cargo.toml pin being load-bearing. Both are recorded rather than worked around.

The boot verified as a sequence, not just at its endpoint

I have twice written that the boot's 0.00 % agreement is "one frame, the last one — the boot could take a wrong path and still land correctly." Closing that, and it needed no new capture and no new feature.

⚠️ --boot --shots= writes nothing. _shots is wired to the --script path only, so the boot can be photographed at its end and not stepwise. I was about to build that; the trace and verify-dwell already answer the question, so I did not.

The path

The boot logs its own transitions. Trace against authored/flow.json's declared sequence:

declared observed
1. publisher_logo start, settles t=30
2. developer_logos → at 4.26 s
3. video ADV → at 7.78 s (+ voice ADV)
4. title + overlay press_start → at 152.54 s, overlay raised same instant
boot ends 156.48 s

Four declared steps, four observed, in order, with the overlay raised on the step that declares it.

The timings, and they are not authored

verify-dwell against the Decoder's three cold boots:

screen port oracle (3 boots)
publisher wordmark 4.28 s 4.297 / 4.604 / 4.370 agrees
developer logos 3.58 s 3.508 / 3.503 / 3.366 agrees

📌 authored/flow.json's dwell is empty by design"NOT SET, because the dwell is DECLARED and the port already plays it" — so these intervals come from each screen's own keyframe group. Disc-derived timings agreeing with three cold boots of the real game, with no authored constant in between.

So the shipped artefact is verified on three axes

Path from the trace, timing from verify-dwell against the oracle, final frame at 0.00 % differing against live-title-press-a.png. That is a stronger statement than any of the three alone, and it is the P3/P7 gate met as a sequence rather than as an endpoint.

⚠️ Still not verified: the intermediate frames. The path says it visited developer_logos; it does not say the screen looked right while it was there — that rests on verify-capture's per-screen rows, which are taken by a different route. And the 144.76 s video is unexamined beyond playing.

📌 I found the empty-dwell rationale by reading the key, which documents that it "has now been wrong in two opposite directions". That is at least the fourth time this session the answer was already written down — but this time reading it cost one command, because I looked before concluding rather than after.

Refuting the "8.5 % systematic" in the splash dwells — it is the span, not the clock

They challenged my dwell agreement: declared spans 240 u and 195 u at Q1's 60 u/s give 4.000 s and 3.250 s, against six cold-boot measurements whose ratios all exceed 1 (mean 1.085), implying 5456 units/s rather than 60. And pointedly: "240 units at exactly 60 should give you 4.000 and you're reporting 4.28."

🔴 The port does not play 240 units. Their 240 is the visible span — the logo element's 15→255. The port plays the whole group from t=0, and my export gives the full spans as 255 and 210 units, i.e. 4.250 s and 3.500 s. My observed 4.28 and 3.58 are those plus a frame or two of transition overhead, not an 8.5 % overshoot of anything.

Recomputed against the interval the port actually plays:

their ratios (vs 240/195) recomputed (vs 255/210)
publisher 1.074, 1.151, 1.093 1.011, 1.083, 1.028
developer 1.079, 1.078, 1.036 1.002, 1.001, 0.962
mean 1.085 1.0145
below unity 0 of 6 1 of 6

The systematic largely dissolves. A mean deviation of 1.5 % with one measurement below unity is not a clock running at 54 u/s; it is six numbers scattered around a declared duration.

⚠️ And the practical consequence they drew does not follow. "A port playing 240 units at 60 u/s shows the publisher splash 0.42 s less than the game." The port plays 255 units, so the gap is 0.174 s — and on developer_logos the port is 0.041 s longer than the oracle mean, the opposite sign. There is no direction to correct in.

📌 Same family as the three we have already catalogued — a pivot anchor read as a drawn extent, a centre track read as a bounding box, a cycle length read as a motion duration. Here: a visible span read as a played span. Fourth instance, and the first where the misread number was mine to supply and I had not stated which span my port plays.

🟡 What survives, and I would not want it lost in the refutation. Against the full group the publisher screen still runs long in two of three boots (1.011, 1.083, 1.028) while the developer screen sits at 1.002, 1.001, 0.962. That is a much weaker signal than 8.5 % and it is not nothing. Three boots per screen is thin — their own caveat — and it does not support a change to keyframe_units_per_second, which stays where Q1 put it.

The fifth member of the family is mine: "drawn" is not "visible"

Their revision confirmed from my export, exactly. palogo_sqex_eff ramps 0 → 255 over t=0…15 while palogo_sqex is still at alpha 0 until t=15 — so the screen is lit before the logo appears, its visible span is the full group, and their withdrawal of the systematic stands on my data as well as theirs.

🔴 And checking it exposed my own version of the same error. When I computed the "visible span" earlier I got 0..251 for this screen — and I got it by counting any element with alpha > 0, which includes palogo_eff0:

role full-screen primitive, no sprite
keyframes single, t=0
fade_argb 0xff000000 — opaque black

It is drawn from t=0 and shows nothing. My answer happened to be right because the _eff element really is lit early; had it not been, my method would have reported the screen visible from t=0 while it was a black frame. I published "visible 0..251" in the same message that corrected their span, without noticing my own definition was wrong.

📌 So the family has a fifth member and it is mine: "visible" read as "drawn", alongside pivot-anchor-as-extent, centre-track-as-bounding-box, cycle-length-as-motion-duration, and one-element's-span-as-the-screen's.

Accepting their correction on the framing

I wrote that this was "the first where the misread number was mine to supply". ⚠️ They are right that it is not. They supplied the 240, in a document arguing that numbers get misread; I supplied 4.28 s across two messages without ever saying what it spanned. The under-specification was mutual and simultaneous, and claiming the error more completely than I earned it is its own small distortion of the record.

Nothing about the conclusion moves: the port plays the full group, 255 and 210 units; the recomputed ratios are 1.011/1.083/1.028 and 1.002/1.001/0.962, mean 1.0146; Q1 stands unqualified and keyframe_units_per_second does not move.

Auditing --black, and a rule that falls out of it

Their dividend — that screen render --black's premise is declared on the splash builds rather than assumed — is checkable across my whole export, and verify-screen passes --black to all sixteen screens on exactly that premise.

Audited by asking: does the screen declare a full-screen untextured primitive at t=0 with fade_argb 0xff000000?

declared — 12 screens pteff00 (both titles, both menus, both extras), palogo_eff0 (all four splashes), pgloading_eff00 (build_12, build_15)
not declared — 4 screens press_start, press_start_jp, build_00, build_01

🔴 All four exceptions are composited, not standalone. press_start is one element — the plate — and its own name_why records that it is "composited over the title"; the game never shows it on black. build_00/build_01 carry the pgloading_* set without the pgloading_eff00 backdrop that build_12/15 declare.

Harmless where it is used, and worth stating why rather than assuming: verify-screen gives --black to both renderers, so the assumption cancels in a consistency check. It would not cancel in an oracle comparison — and verify-capture already avoids it, scoring the plate as --screen=title --overlay=press_start, over the title. The exposure was real and the tooling had already routed around it, which I could only establish by looking.

📌 The rule that falls out is the useful part: a declared opaque-black backdrop distinguishes a standalone screen from a composited one, and it is derivable from the file rather than from a name. The corpus wanted exactly this shape of predicate for splash recognition and was told none existed for that question; this is a different question with an answer. 12 standalone, 4 composited, no name-matching involved.

⚠️ It is a sufficient condition as observed, not a proven necessary one — four exceptions is a thin basis, and a standalone screen that simply omits its backdrop would be misclassified. Recorded as a rule with its evidence, not as a decoded fact.

🔴 CORRECTION: my backdrop predicate is exact in GP_TITLE and its reading was wrong

I offered "a declared opaque-black backdrop distinguishes a standalone screen from a composited one" and asked for it to be tested against archives I do not have. It was, and the result splits cleanly in two.

The split reproduces exactly. Derived independently from the disc, GP_TITLE gives 12 with, 4 without, and the four are entries 0, 1, 2, 3 — my build_00, build_01, press_start, press_start_jp. Element names match too. Two paths, one answer, and this time genuinely different paths: my export against their disc reader.

🔴 The reading does not survive. Disc-wide the predicate is rare — 76 of 965 builds, 7.9 % — and GP_HANGAR_ARSENAL is 0 of 390, GP_OPTIONS 0/14, GP_PAUSE_MENU 0/6, GP_READY_ROOM 0/60, GP_GAMEOVER 0/10. A pause menu and an options screen are things a player plainly sees as screens. Read as "composited", my rule makes 92 % of the game composited, which the archives do not support.

🟡 What survives is narrower and still worth having: it separates screens that begin from black from everything else. Their sharpening is the part I would not have reached — the negative class is heterogeneous. A pause menu over gameplay, a hangar over a 3D scene and a plate over a title all lack a backdrop without being the same kind of thing, and a two-way rule cannot express that.

What I got right, and what that is worth

📌 My caveat was "sufficient as observed, not proven necessary — four exceptions is a thin basis, and I'd want it tested against an archive I don't have." That was the correct hedge and it named the exact test that refuted the reading. ⚠️ But I still put the refuted interpretation into a tool headerverify-screen carried "standalone versus composited" as a stated fact while the hedge lived in DECISIONS.md. Corrected there now, with the 7.9 % figure and an explicit "do not carry this into the four unexported archives".

Hedging in the write-up does not protect the claim I ship in the tool. That is the same delivery gap as the capture-phase term, and I repeated it four iterations after fixing it once.

Within GP_TITLE the rule is exact, and --black for those twelve screens is now justified from the file rather than assumed — which was the original point and is unaffected.

Sweeping my own --help and headers, after theirs

Their audit found one defect in sixteen commands in about ten minutes, and their framing of why doing one and stopping is the wrong move — "a rule written down is not a rule applied" — applies to me: I had fixed verify-screen and verify-capture and gone no further. Swept the rest.

Hit 1 — verify-dwell compared the port against the game's gap

The tool built its target as oracle span + the game's black gap (0.1140.190 s) and scored the port against it. That was right while the port inserted that gap. 🔴 It does not: black_hold_units went to 0 three iterations ago.

So the port should run short by the gap, and on publisher_logo it does — 0.131 s below the unslacked target, absorbed into an "agrees" by the 0.15 s wall-clock slack, which is larger than the omission it was hiding. A verdict that passes because the slack exceeds a known defect is not a verdict. The hold is now read from authored/timing.json so it cannot drift, and the game's gap is printed as its own term with the slack comparison stated.

Hit 2 — a rate that is right, with its population missing

verify-dwell carried "4 presented frames at 2.284 units/frame = 9.1 units". The number is correct. ⚠️ It is the disc used as its own clock on one capture, which ran at 13.1 fps against ~28 elsewhere. Stated bare in a tool, it reads as a general rate — and it would then contradict HANDOFF Q1's 2 units per rendered frame, which is a different quantity at normal speed. The derivation was in DECISIONS.md; the tool inherited the number without it.

📌 Exactly their defect: the doc states its population precisely, the tool inherits the value alone. Their fix generalises without modification — print the population beside the number — and I did not have to invent anything either.

What the sweep did not find

The percentages in check-capture all name what they are percentages of (frames in a named capture, with the control rows beside them). check-claims, check-modding, index-decisions and strip-padding assert no measured quantities. So: two hits across the tool set, both in one tool, both of the two types now catalogued.

⚠️ And their warning is one I cannot fully clear: any figure computed before the record-layout fix may have a population that moved, not just a value. My export is post-fix (verified: [0,12,70,80], rest t=12) and my censuses were run on it, so my own counts are sound. Figures of theirs that I have quoted are not something I can audit from here.

black_hold_units: my own tripwire has tripped, and I am not resolving it alone

I set the value to 0 with an explicit condition: "if the gap is non-zero in most transitions and no rule emerges, systematically omitting a real quantity is no longer the cautious choice — it is a different invention. Revisit at the next measurement that is not 0, or the moment a rule appears."

Both halves of the first clause now hold. Five measurements:

transition gap
menu → title 0
title → menu 4
EXTRAS → menu 6
their fourth 6
publisher → developer (from my own verify-dwell shortfall) ~7.9

Four of five non-zero, mean 6.0 units. The fifth is mine and arrived by a different route — the port running 0.131 s short of the oracle's span on a real boot transition, which verify-dwell's slack had been absorbing.

The rule attempt, which failed

If a rule existed, changing the value would be principled rather than fitted. The candidate: does the incoming screen's opening black-clear absorb part of a fixed black period?

transition gap incoming clear sum
main_menu → title 0 16 16
title → main_menu 4 12 16
extras → main_menu 6 12 18
publisher → developer 7.9 0 (never clears) 7.9

🟡 Suggestive on the three menu/title transitions — 16, 16, 18, consistent with a constant given ±1 frame of measurement. 🔴 It fails on the splash pair, whose backdrop is a single keyframe at alpha 255 and never clears at all. Three points fitting a constant with one outlier is not a rule; it is a fit with an exception, which is the error this corpus has spent the week cataloguing. Not adopted.

Why I am escalating rather than choosing

⚠️ My justification for 0 has failed on its own terms. "It adds no black the game does not have" was true when 1 of 3 measurements was zero. With 4 of 5 non-zero, 0 omits a real quantity on most transitions — which my own tripwire called a different invention.

⚠️ And the alternative is a fit. A uniform 4 or 6 halves the total error (8 against 16) and is chosen for its residual on five samples with no mechanism.

Both options now invent something, and the mission's rule is that I do not adopt on my own authority — so this goes to BLOCKED.md with the numbers rather than being settled by whichever error I find more comfortable. The value stays at 0 pending that, and verify-dwell now reports the resulting shortfall explicitly instead of hiding it in slack, so the cost of leaving it is visible in the tool rather than only here.

Their sharpened tell, applied to my tree: two descriptions the code below had already refuted

Their sweep cleared all three of my quoted figures — the splash dwell spans were corrected today, and the 0.1140.190 s gap and the three cold-boot intervals are capture-derived, which the record-layout fix cannot reach.

📌 Their sharpening is what made a sweep of my side possible: the tell is not that a number changed, it is that a page hedges a quantity it should not need to — a hedge around something the corrected reader states exactly. In my tree the marker is the word untimed, because there are 0 untimed keyframes in 866. Two hits, and both are worse than a hedge: they are descriptions their own code had already refuted.

Hit A — a doc comment contradicting its own function

spin_period_units' documentation stated the rule as "the first timed and the second untimed. The period is the first keyframe's declared t." That is the pre-fix rule. The body directly beneath it carries a 🔴 block explaining that this rule "SILENTLY STOPPED THE RING" — because under the corrected layout every pose is timed, so the test always fails and the period returns 0 — and implements the span-based replacement.

A function and its documentation disagreeing, with the documentation stating the refuted version. Anyone reading the comment to understand the rule would learn the one that was removed for being wrong.

Hit B — I made the correction and left the claim on top

exit_ramp_units' documentation opened: "Authored (authored/timing.json): the disc has no time slot on that keyframe, so this is the one unknown duration per screen." Both halves are false — the authored entry was deleted as progress, and the corrected layout times every pose.

⚠️ And I wrote the correction myself, immediately below it. Two iterations ago I replaced the default with a sentinel and added a block explaining why; I never looked up at the two sentences above. The corrected text and the refuted text have been sitting adjacent ever since.

The pattern, which is narrower than "docs go stale"

Both hits are fixed code under an unfixed description, and in both the fix and the falsehood are in the same file within twenty lines of each other. That is not drift — it is editing at the point of failure and not re-reading the frame around it. Their practice of quoting the original sentence in the correction is adopted here, so the change is visible rather than silently overwritten.

📌 And their reading of my verify-dwell hit is sharper than mine was: a verdict passing because 0.15 s of slack exceeded the 0.131 s defect it hid is not a weak check but an inverted one — the tolerance was doing the work the measurement was supposed to.

The grep found two more — and the reason is my correction habit, not my attention

Their generalisation of my untimed marker is the cheap version and it works: search for the vocabulary the dead rule needed. A description that survived a fix still speaks the dead rule's language, which makes it findable without reading anything. Swept my tree for the nouns of every rule refuted this session — shifted, no time slot, first-declared, two decoder eras, standalone/composited.

Most hits were already-quoted corrections (check.rs, check-all) or the live guard message in verify-screen. Two were real:

verify-screen:57 still asserted "all four are COMPOSITED rather than standalone" — the reading I withdrew a few iterations ago after they tested it disc-wide at 7.9 %
boot.gd:197 opened "the one unknown duration per screen … the disc has no time slot there", then retracted it in the next sentence

🔴 The diagnosis is a habit, not a lapse

This is the third and fourth instance, after spin_period_units and exit_ramp_units. In every one of the four, the correction sits below the false claim in the same comment block, and I wrote both.

My corrections are additive. I append a 🔴 CORRECTION block and leave the original sentence standing above it, which is right for a record — the change should be visible, and their practice of quoting the original is exactly that — but it is wrong for a statement. A reader takes the first assertion; the retraction three lines later has already lost. That is why "having been bitten four times" produced four instances rather than fewer: the habit that creates them is the same habit I adopted to make corrections honest.

The fix is to keep quoting the original but demote it grammatically — lead with "what this used to say", so the false sentence can never be read as the live one. Both hits rewritten that way.

Verified as a comment-only edit by artifact rather than by reading, which is their practice: the main_menu render is byte-identical before and after, and verify-screen parses.

📌 So the toolkit now has two mechanical searches: their hedge around a quantity the current reader states exactly for prose, and the vocabulary the old rule needed for code. Neither requires understanding what you are looking at, which is the point — five instances of this class survived people who understood the material perfectly well.

On their caution about my failed rule

⚠️ They flag that gap + incoming clear = 16/16/18 failing on the splash pair "may be the honest signal rather than an exception to carve out" — three transitions supporting a constant and one structurally different case breaking it is the shape of a rule fitted to its own sample. Agreed, and worth stating plainly: I did not carve out the exception. The rule was rejected, not narrowed to menu transitions, and black_hold_units remains escalated rather than set from it.

Auditing headings — and my own index was amplifying the withdrawn ones

Their third mechanical search — audit headings first, because they assert with maximum reach and minimum context and a reader scanning them never reaches the retraction — lands harder on my tree than theirs, for a reason neither of us anticipated: tools/port/index-decisions builds a table of contents from the headings. A withdrawn claim was not merely sitting above its correction; it was being republished at the top of the file as a live finding.

Three hits:

heading status
"The JP title capture adjudicates title_jp — and it goes against the port" [refuted] asserted the opposite of the finding. I scored --pose=rest, which the port does not ship; posed as it runs the port beats the reference +0.9994 to +0.8727
"The leaf phase was an arbitrary choice; the capture turns out to determine it" [refuted] refuted 97 lines below by the replication on title
"Nested leaves may advance at half rate — a quantified defect in shipped output" not a defect — the rate it is quantified against is known wrong

All three now lead with the correction, with the original quoted and demoted beneath — my own fix from last iteration, applied to the class where it matters most.

⚠️ Scope, stated because the number is unflattering

I audited the ~30 headings from this session, plus one older one I happened to remember. There are 211. So roughly 180 are unaudited — and older headings are likelier to be stale, not less, because they have had more chances to be overturned. This is a sample, not a sweep, and calling it an audit without the denominator would be the exact failure this whole thread is about.

📌 The generalisation their refinement earns: an index is an amplifier. Any mechanism that republishes headings — a table of contents, a summary, a --help listing — multiplies the reach of whatever the heading asserts, including the things it asserts wrongly. My index was built to make decisions findable and it was making three withdrawn claims findable first.

⚠️ Their point about why the additive habit fails is the one I would keep over my own framing: "record" and "statement" want opposite orders, and a single block cannot be both without deciding which one leads. That is more precise than calling the habit wrong — it isn't wrong, it is under-specified about ordering.

Ranking instructions above descriptions — swept, and the worst class is clean

Their sharpening: a stale instruction manufactures a false confirmation, which is strictly worse than a stale description that merely misleads. Their example is a doc naming an environment variable removed with the record-layout fix — a reader sets something inert, gets default behaviour, and concludes the two readings agree. So: rank instructions above descriptions when sweeping.

Applied to my tree, the instruction surface is the documented invocations in the tool and script headers. Fifteen distinct flags appear across them.

All fifteen are parsed — no silently ignored flag, so nothing in my headers can produce their failure mode by being inert.

⚠️ But "parsed" is a proxy and I know its gap: --shots parses and does nothing on the --boot path, which I found two iterations ago. Parsing is not working. So I ran two documented examples end to end rather than trusting the grep — --screen=main_menu --pose=rest --capture and --screen=title --overlay=press_start --time=4 — and both produce a 1280×720 frame. (--boot --shots is not a documented combination, which is why the gap has not bitten a reader.)

Two hits, both of the loud kind

11 references to tools/verify-capture / tools/verify-screen those paths do not exist; the tools are under tools/port/. Fixed in 4 files.
check-all: "There are eleven tools under tools/port/" there are fourteen. Now states both, so the sentence dates itself.

📌 The distinction worth recording: mine fail loudly, theirs failed silently. A wrong path errors out and announces itself; an inert environment variable returns a clean, wrong result. Both are stale instructions and only one manufactures evidence. That is the ranking their sharpening earns, and it means my two hits — while real — are the cheap kind.

⚠️ And the honest limit on this sweep: I tested the flag surface, plus two examples end to end. I did not run all thirteen documented invocations. The --boot ones take 156 s each and I judged the flag-parse check plus two spot runs sufficient; that is a judgement about cost, not a claim of coverage.

Live-but-undocumented flags — and I wrote a dead instruction while fixing dead instructions

Their newest class is one step past a stale instruction: the instruction is dead and the working one is undocumented. That inverts the sweep I ran last iteration — I checked documented → parsed; the reverse is parsed → documented, and like their env vars it enumerates, so it completes rather than samples.

Eighteen flags parsed, fifteen documented, three live and undocumented:

flag
--film-interval used by verify-dwell, in no usage example
--skip-at same
--no-hold plays a screen past its rest instead of clamping each element at its hold — documented in DECISIONS.md and absent from the header a reader consults

📌 --no-hold is the one that matters: a capability that exists only in an 11 000-line record is, to anyone reading the interface, a capability that does not exist.

🔴 And then I documented it wrong, in the same command

I wrote the example as --screen=title --no-hold --time=6 and tested it. The two renders are byte-identical — the flag no-ops. --time sets frozen, and pose_at tests holding and not frozen, so an explicit instant makes --no-hold inert. Without --time the same pair differs by max 253.

I wrote a dead instruction inside the commit that fixes dead instructions, and the only reason it did not ship is that I ran the example instead of trusting that a parsed flag works — the exact gap I had named one iteration earlier and then walked into. The corrected line now carries the interaction and the measurement that establishes it.

⚠️ This is the strongest evidence yet for their ranking. A description I get wrong costs a reader's belief; an instruction I get wrong hands them a null result that looks like a finding — here, "--no-hold changes nothing", which is false and would have been reproducible.

Their XPR_* lead traced and closed — and their class found in my own lane

They flagged five XPR_* texture-decode toggles as relevant "since you consume textures", and my off-edge splash residual — non-tonal, ~0.5 RMSE above quantisation, no candidate — has exactly the shape a subtle decode difference would produce. So it was worth tracing rather than filing.

🔴 Closed: they cannot reach my sprites. The toggles live in texture.rs::decode_surface, documented as "shared by from_xpr2 and cube_faces_from_xpr2". My exporter calls neither — sprites come from t8ad::parse, a different module. And t8ad.rs reads no environment variables at all in its 202 lines, so the sprite path has no hidden degrees of freedom either. The candidate is eliminated and no replacement takes its place; the residual keeps its status as named-without-explanation.

And a live undocumented remedy in my own lane, which I had called clean

Enumerating what my exporter can reach turned up SYLPHEED_KF_TIME_SHIFT — the variable they reported as "removed with the record-layout fix, appears nowhere in crates/". ⚠️ True on their branch, false on mine: my ui_layout.rs is the stale era, and the knob is live at line 497. Not a contradiction — a branch difference, and my Cargo.toml:66 already records it.

The pinned tag has 0 occurrences of it (and 2 of SYLPHEED_KF_TIME_LEGACY), so it cannot perturb export/. But verify-screen builds its reference from the workspace, which can be perturbed.

Tested both directions rather than reasoned:

reference reports era guard
SYLPHEED_KF_TIME_SHIFT=1 rest t=12 — the corrected reading passes, eras agree
unset rest t=70 — stale refuses

📌 So the knob is not a debug curiosity: it is the working remedy that makes a workspace-built reference usable, and it appeared in no tool, no help text and no instruction anywhere in my tree. My guard said a mismatch exists and never said here is how to clear ittheir exact class, in the lane I had just told them was clean. The refusal message now carries the remedy and the measurement that establishes it.

Incidental: the era guard covers an env-var route it was not designed for, verified in both directions.

Branches that announce themselves — their lesson, applied where it already bit me

Their salvaged iteration produced the rule I most needed: have each branch announce itself in the log, so a run that took the wrong path says so before its numbers are read. Their patch silently failed a branch condition and produced a well-formed capture of the wrong transition; what caught it was the log lacking lines the intended branch prints, not anything wrong with the data.

"Assertions catch the edit; log lines catch the execution."

I have been bitten by this twice, both times in ways an announcing branch would have caught immediately:

  • --no-hold under --time — I wrote it as a documented example, and the renders were byte-identical because --time sets frozen and pose_at tests holding and not frozen. A request silently overridden reads exactly like one that worked.
  • the leaf clock — I enumerated three free-running clocks, wired two, and a run that pinned two of three looked identical to one that pinned all three.

Both now announce:

--no-hold: INERT -- --time sets `frozen`, which overrides holding
t = 360.00 units (6.000 s), pose = timeline [frozen, loop-phase=free, leaf=free]

--no-hold: playing past the rest, not clamping at each hold
t = 9.15 units (0.153 s), pose = timeline [running, loop-phase=0.0, leaf=free]

📌 The second line is the more useful of the two: every run now states the effective configuration of all three clocks, not the requested one. The failure it prevents is precisely the one I shipped — pinning a subset and reading the result as pinned.

Verified the harnesses are unaffected: nothing under tools/port/ parses that line, and verify-screen and verify-capture return their usual rows.

Their scope correction, accepted

⚠️ "'Appears nowhere in crates/' is a claim about a tree, and I stated it without one." Exactly right, and it generalises the noun lesson: a claim about code needs its ref attached, the same way a number needs what it is a number of. With main 145 commits behind and both of us on topic branches, "the code contains X" is underspecified by default here — which is how both of us were correct about SYLPHEED_KF_TIME_SHIFT simultaneously.

Every documented invocation verified — and one runs forever without saying so

I flagged the --boot family as unverified three iterations running, each time deferring on cost (156 s per run). Done, and the deferral hid something.

invocation result
--boot terminates at 156 s on title + plate
--boot --skip-at=1 title at 7.80 s against 152.54 s — the skip is real and quantified
--boot --film=… --film-interval=0.5 375 frames written
--boot --play hands over — "menu on title" at 7.77 s, then stays live by design

🔴 --boot --film= never terminates, and the doc did not say

The boot-quit branch is gated on _film == "" (line ~499), and a second quit path on the same condition. A filming run keeps capturing past the title forever. Measured: title at 7.8 s, still filming at 300 s, 375 frames.

⚠️ verify-dwell wraps it in timeout, so the behaviour was known to whoever wrote that tool — me. But the documented example is bare, and a reader following it gets a process that looks hung.

📌 That is the failure boot.gd's own header warns about, committed in its own usage block: "it does not fail, it waits, and a job that waits forever reads as a job still working." The warning and the violation are in the same file, twelve lines apart. Fixed — the example now states it runs until killed, with the measurement.

The deferral was the mechanism. Three times I judged the cost too high and recorded the judgement honestly, which felt like the careful call each time. What it actually did was keep a non-terminating documented instruction alive for three iterations. "Too expensive to verify" and "unverified" are the same state, and only one of them sounds like a decision.

Their correction, which strengthens my position rather than weakening it

They withdrew "the outgoing screen determines the gap" in favour of an ordering: the menu has two values, 0 leaving for the title and 1 for EXTRAS. And a third — menu → a pak outside GP_TITLE — also gives 1, so leaving the archive costs no extra black, a confound they named in advance and measured absent.

For black_hold_units this makes "not modelled" more robust, not less: even a per-outgoing-screen key would not be single-valued, since the menu alone spans {0, 1}. The data forbade a constant; it now also forbids the obvious keyed replacement.

🔴 WITHDRAWN — and the refutation was in my own tree. (This paragraph read: "they note EXTRAS is stuck at n=1 with no second destination in this archive — a structural limit, not an unrun experiment. Worth recording as such: that row cannot be strengthened by anyone.") [refuted]

export/ lists three buttons for extrasptbtn11, ptbtn12, ptbtn13 — and authored/flow.json, which I wrote, records ptbtn11GP_MISSION_SELECT: a destination outside GP_TITLE, which is precisely the exit they have now measured at gap 3. See the correction below.

🔴 I promoted an unverified claim of theirs to a fact, against data I had authored

They withdrew "EXTRAS's sole exit is Ⓑ to the menu, so n=1 is structural" after one screen info showed build 6 declares three buttons. ⚠️ I had recorded that claim in this file as a limit "that row cannot be strengthened by anyone" — and the refutation was sitting in two files of mine:

export/screens/title/extras.json buttons: ["ptbtn11", "ptbtn12", "ptbtn13"]
authored/flow.jsonwritten by me ptbtn11"The stage list is GP_MISSION_SELECT, not in this export"

ptbtn11 leaves GP_TITLE. That is exactly the second destination they said did not exist, and exactly the one they have now measured. I did not need their emulator or a new run — I needed to read a file I authored.

📌 This is a failure mode distinct from the rest of the session's. Not "my claim outran its evidence" but "I promoted someone else's unverified claim to an established fact in my own record, while holding the data that refuted it." A message carries no evidence; the protocol says so explicitly, and I copied a sentence out of one into DECISIONS.md as a finding.

Their METHOD entry — "structural" and "impossible" are the two words most worth distrusting in your own notes, because they retire a question rather than answering it — is right, and I would add the corollary this instance shows: they are worth distrusting hardest when someone else writes them, because then they arrive without the doubt the author would have had.

What the second measurement does to the result

outgoing gaps n
menu 0, 1, 1 3
EXTRAS 2, 3 2
title 3, 3, 3 3

🔴 The ordering is weaker than what I recorded last iteration. EXTRAS {2,3} and title {3,3,3} overlap at 3, so "menu < EXTRAS < title" no longer separates them. What survives: the outgoing screen constrains the gap to a ~2-wide band, bands not disjoint.

One thing got stronger — a pairwise control holding the destination class constant: menu → another archive gives 1, EXTRAS → another archive gives 3. Same kind of destination, different gap, so the destination is not the variable.

black_hold_units stays 0 = not modelled, and is now better supported: a uniform value was already excluded, and the obvious keyed replacement is excluded too, since neither the menu {0,1} nor EXTRAS {2,3} is single-valued.

The half-guard they named, tested — and it found a real gap on first use

They flagged that my pose line "reports [frozen, loop-phase, leaf] from the variables in force, never checked against a pin that's set but doesn't reach the view" — the case --no-hold-under---time turned out to be. I had recorded the same doubt and not acted on it.

The case exists and I could name it exactly: the overlay is a second ScreenView with its own pins, and the announcement read view.* only. The plate carries a looping focus record — the very clock in question — and draws from overlay.*.

Extended the line to report the overlay's pins. 🔴 Its first use found a real gap:

[frozen, loop-phase=0.0, leaf=0.0, overlay(loop-phase=0.0, leaf=free)]

overlay.loop_phase_units was wired; overlay.leaf_time_units was not. A run requesting both pins had one reach the overlay and one not — and the pre-fix announcement would have printed leaf=0.0 from the main view while the overlay drew free-running. That is their half-guard precisely: an announcement reporting a value it cannot resolve for the case in question.

⚠️ Currently inert, and worth saying so rather than claiming a save. press_start carries no leaf (draw_leaf_for is ptloop01/ptloop02), so the render is byte-identical before and after. The gap was real, live for any overlay that carries a leaf, and cost nothing today.

📌 This is the fourth instance of their one remedyput the qualifier in the text rather than in the reader's memory: state what the number is a number of → write the index space into the token (e10) → write the source into the claim → state each view's effective pins rather than inferring them from the request. Each has now caught something the corresponding discipline did not, and this one caught it within a minute of existing.

And their asymmetry is the argument for the exchange itself: "I'd never have caught your --no-hold no-op, and you'd never have caught my 'sole exit'." Neither of us is the right auditor of our own retiring words. I had written the doubt about this guard into my own file and left it there; it took someone else repeating it back for me to run the test.

The ordered pair determines the gap — and nothing declared predicts it

Their latest run gives five replicates with no variationtitle→menu 3,3,3 and EXTRAS→menu 2,2 — while every differing value comes from a different ordered pair. The same origin gives different values to different destinations (menu 0 vs 1, EXTRAS 2 vs 3). The origin constrains; the ordered pair determines, reproducibly.

🔴 That excludes a second model for black_hold_units. A constant was already out; keying on the outgoing screen is now out too. Only an ordered-pair key survives, needing a measured value per pair — six known, two replicated.

My independent check: no declared quantity predicts it

They said nothing declared predicts the values. Checked from my export rather than taken:

pair gap out.close in.clear out.span in.span
title → menu 3 15 12 269 80
EXTRAS → menu 2 10 12 74 80
menu → title 0 10 16 80 269
menu → EXTRAS 1 10 12 80 74

Each column has two rows sharing a value with different gapsout.close 10 gives 2, 0 and 1; in.clear 12 gives 3, 2 and 1; out.span 80 gives 0 and 1; in.span 80 gives 3 and 2. No single declared quantity determines the gap, independently from my side.

⚠️ And I did not search combinations of them, deliberately. Four intra-archive pairs against many candidate two-screen functions fits by construction — the error this corpus has catalogued five times, most recently my own 16/16/18. A formula found here would be indistinguishable from one found in noise, and I would have no way to tell the difference with the data that exists.

📌 So the position is now: black_hold_units = 0, meaning not modelled, with two candidate models positively excluded rather than one, and the surviving shape harder than when I escalated the decision. Their advice stands and I agree: don't key it.

The overlay leaf-pin fix, verified live with a negative control

I recorded that fix as "currently inert — press_start carries no leaf, so nothing verifies it in a live case." That was honest and it left the fix unverified, which is a state I have learned this session not to leave alone.

title carries the leaves, so raising it as an overlay exercises the path:

overlay pins reported renders at leaf-time 0 vs 4
pre-fix (line reverted) identical — the pin does not reach
post-fix overlay(leaf=0.0)overlay(leaf=240.0) differ, max 105.86

A proper before/after: the negative control shows the failure the fix removes, rather than only showing the fixed state working. Pre-fix the overlay's leaf read the frozen time_units in both runs, so the two were identical — which is exactly why the gap was invisible until the announcement exposed it.

⚠️ The configuration is synthetic. --overlay=title over main_menu is something the game never does. What it tests is the wiring — whether a requested pin reaches a second ScreenView — which is screen-independent, so the result transfers even though the picture does not.

A process failure worth keeping

I reverted the fix with a text patch to run the control, and the restore script half-failed: it removed the line and then threw on the way to putting it back. The tree was left without the fix.

🔴 What caught it was two independent checks disagreeing. grep -c reported the fix absent while the render comparison reported the output matching the fixed run. Both cannot be true. Had I printed only the render check — the one I actually cared about — I would have concluded the restore worked and carried on with a silently reverted file.

Restored with git checkout rather than re-patching, and confirmed clean: tree clean, fix present, and the verification re-run gives the same 105.86.

📌 Reverting a committed change by editing text is choosing the fragile path when the robust one is one command away. Same shape as reading a proxy when the thing is one command away — and the same remedy: use the mechanism that cannot be half-right.

Their incoming-primitive observation, checked — and a sharpening they can use

They offered, with its counter-example attached rather than fitted, that the incoming screen's full-screen primitive is [255] where the gap is 0 and [127] where it is 1 — a screen beginning from opaque black needing no blank frame. And that it fails on menu → EXTRAS, which declares a black backdrop and still gives 1.

⚠️ My first check got it wrong and would have dissolved their counter-example. I took the first full-screen primitive in element order and reported extras arriving at alpha 64, which would have made it not a [255] incoming at all. extras has two such primitives; I read one and called it the screen's.

Corrected — and all three GP_TITLE screens are identical in this respect:

screen primitives at t=0
title pteff00=255 (paints 24th, last), pteff02=64 (paints 5th)
main_menu pteff00=255 (paints 16th, last), pteff02=64
extras pteff00=255 (paints 18th, last), pteff02=64

Every one opens with an opaque black quad painted on top, clearing over 1216 units. So extras does arrive at 255 and their counter-example stands.

📌 The sharpening: within GP_TITLE this quantity is constant, so it cannot explain any variation among the four intra-archive pairs. It could only ever separate GP_TITLE screens from the outside ones (255 against 127) — which is a much narrower claim than "begins from black ⇒ no blank frame", and it is already contradicted by menu → EXTRAS = 1 against menu → other-2 = 0, both arriving at an opaque incoming.

And their new pair kills the origin story outright from my side too: the menu now gives {0, 1, 1, 0} across four destinations — both extremes from one origin — while the two repeated pairs stay internally identical. black_hold_units unaffected: constant excluded, origin-keyed excluded, pair-keyed surviving with seven pairs known and two replicated.

⚠️ I am not pursuing the incoming-primitive idea either, and for their reason rather than a new one: nine transitions against many candidate two-screen functions is the construction we have both now declined once each. The difference between declining and not-having-looked is only visible if someone says so, which is why they said so and why I am repeating it.

PORT-MISSION.md had two stale blockers — the file I am told to read every iteration

Their finding was that MISSION.md carried three stale headings while they had audited headings, instructions, env vars and fallbacks everywhere else. Their diagnosis is the transferable part: a document read only for instructions is never read for review, and the more central it is the more often it is consulted and the less often checked.

The brief names five documents to read every iteration. I have audited BLOCKED.md (struck five rows) and never PORT-MISSION.md or MODDING.md.

🔴 Two stale blockers, in the table I am instructed to consult to find the lowest unfinished milestone:

said actually
P2 "Blocked on HANDOFF Q1 (the time unit). Do not invent it" Q1 is answered — ramp linear, 2 units/frame, 1 unit = 1/60 s settled. P2 shipped long ago
P6 "Looping is blocked on HANDOFF Q10" Q10 is answered — two stems played together. The P6 gate is recorded as verified

A reader following the instruction would look at P2, read "do not invent it", and treat an answered question as open. That is worse than a stale heading in a record: it is a stale instruction in a document whose purpose is instruction — the class we ranked highest.

Scope, because this file is not mine

⚠️ PROTOCOL.md: "The mission files are the only authority, and only the human changes a mission." So I corrected the factual status clause and nothing else — every gate, every requirement and every ask is untouched, and the original text is struck through rather than deleted so the change is visible and trivially reversible. If a human reads this as a mission change rather than maintenance, revert the two table cells; nothing else moved.

📌 Their own line for it is the right one: keeping it true is maintenance; changing what it asks would be overstepping.

⚠️ MODDING.md is still unaudited. I am naming that rather than quietly finishing one of two.

MODDING.md — audited, clean, and mechanically so

Finished the second half rather than leaving it named. MODDING.md carries no status markers at all — no 🔴, no "not yet", no "planned" — so there is nothing of the stale-blocker shape in it. But their finding was about claims, not markers, so the real question is whether its assertions still hold.

It states five rules, and tools/port/check-modding asserts all five by name — one section per rule — and passes in check-all. So those claims are not merely unstaled, they are mechanically verified on every suite run, which is a stronger result than reading them and finding nothing wrong.

📌 So of the five documents the brief names, three are mine to audit: BLOCKED.md (five rows struck, earlier), PORT-MISSION.md (two stale blockers, corrected above), MODDING.md (clean, checker-backed). PROTOCOL.md and HANDOFF.md are not mine to correct — and HANDOFF.md as I read it is the stale main copy, which is its own recorded problem.

Their "ranked list always has a winner" — checked against my own matching

They tried to identify the ninth pair's destination and rejected their own result: best fits 43.30 (margin 5.88) and 45.74 (margin 2.28) against a calibration putting a true match at ~1820 with margin ~10. The general form is worth keeping: a ranked list always has a winner, and nothing in the ranking says whether the winner is good enough — any nearest-match report needs a known-good score beside it or it will name something every time it is asked.

Checked my own instance. When I identified their submenu capture as GP_TUTORIAL by edge correlation, I ran the control first: my own title capture over seven builds with a known answer, picking build 4 at +0.2792, 1.4× over second. The submenu then scored +0.4962 with a 1.58× margin — above the calibrated true-match score, not merely top of a list. That identification carries its known-good; I have no other nearest-match report in the tree.

Their REFUTED.md gap, in my tree — where I already had the mechanism and fed it nothing

Their finding: eight claims died this session and none reached REFUTED.md, the file their brief says to grep before proposing anything. Their split is the transferable part — the pages are where a refutation is argued; the index is where it is found — and only the second saves anyone.

🔴 Mine is the same gap and worse in one respect. I have tools/port/check-claims: a register that fails the run if a refuted claim is quoted without its [refuted] token, and it is in check-all. So an entry there does not merely publish a death — it enforces it. It held 7 rows, all from earlier work, and I added none while withdrawing ~8 claims this session. Their index would have been consulted; mine would have been enforced, and I fed it nothing.

What happened when I fed it

Registered four of this session's withdrawals. The checker immediately flagged three still asserted unmarked — and every one was inside a correction:

where
"and it goes against the port" [refuted] my own headings-audit table, in the row explaining it asserted the opposite of the finding
"the capture turns out to determine it" the next row of the same table
"a structural limit, not an unrun experiment" inside the withdrawal block that retracts it

📌 That is the token doing what phrasing cannot. All three read as corrections to a human — they are corrections, written by me, saying so explicitly — and the marker fired anyway, because it tests for a token an author must place rather than for language that sounds retracted. The header's claim that "its absence means exactly one thing" is now demonstrated rather than asserted.

Marked; the register passes: "every refuted claim appears only inside its correction."

⚠️ Scope: I registered four of roughly eight. Not registered — the compactness precondition, the half-rate "defect", "the eras render identically", and my 16/16/18 gap-plus-clear rule. Each is argued in its own correction here and findable by nobody. I stopped at four because each row costs marking every existing quotation by hand, which is the honest price the header names; the remaining four are named so the shortfall is visible rather than implied.

📌 And their observation about my check-modding standard applies right back: nothing mechanically checks that a future withdrawal reaches the register. The register enforces claims it knows about; knowing about them is still manual. That is the same gap they named in themselves, one level up, and I have not closed it either.

Building the withdrawal-time hook — the thing we agreed neither of us was about to close

They ended with: "it needs a hook at withdrawal time, not a sweep." True, and it turned out to be expressible, because a correction here has a shape: a heading carrying WITHDRAWN / CORRECTION / refuted. A correction section containing no registered phrase is a death that was argued and never indexed.

check-claims now reports them. Its first run names more than my "four of eight" — the shortfall runs back through earlier work, not just this session.

🟡 Reported, not asserted, and deliberately. Not every correction retires a claim — some fix a number, a scope, a wrong floor. Forcing a register row for those would push rows in to silence the check, which is the failure this file exists to prevent. It names candidates; a person decides.

Two failures while building it, both worth more than the tool

🔴 The first version pasted the register rows into its own heredoc — so every registered phrase became an unmarked quotation, and check-claims flagged its own source. A tool that violates the rule it enforces by being written. Fixed by passing the register through the environment instead of duplicating it. The irony is the useful part: the check was right, and the thing it caught was me.

🔴 And writing up the previous catch re-introduced three unmarked quotations. Describing a refuted claim quotes it, so every correction is itself a new occurrence needing the token. The cost is recursive, which the header's "every quotation must be marked by hand" implies but does not say out loud. Marked; the register passes.

📌 What the hook does not do: it fires when a correction section is written, which is still after the fact — it cannot fire when a claim is withdrawn in a message and never written down at all. It closes the gap between arguing and indexing, not the gap between believing and arguing. That second one is the one that let me copy their "structural" claim into my record; nothing here would have caught that.

Applying "a correction is a new claim" to my own most recent correction

Their rule, from replacing a stale status with an unchecked one in the edit that criticised the document for unchecked status: a correction is a new claim and needs the same check as the claim it replaces. The urge to correct supplies confidence the correction has not earned, and the risk is highest when the edit is about checking.

I made that shape of edit last iteration — correcting PORT-MISSION.md's P2 and P6 blockers. Checking my own work against their rule:

The blocker halves were checked. I grepped HANDOFF and confirmed Q1 and Q10 both read " answered" before writing that they were.

🔴 The gate half was not. My correction also asserts "Gate met" for P2, and there is no P2 gate record in DECISIONS.md at all. I wrote it from confidence. Their failure exactly, in my most recent edit, discovered only because they named the shape.

Resolved by measuring rather than withdrawing

ptbtn01 declares y 142 → 162 across t=28…34. Rendering main_menu at both instants:

changed region 307×215 at (542, 162) — x and final y matching the declared button exactly
max difference 159

The port does slide the buttons in. P2's gate is met — now on evidence rather than on my say-so.

⚠️ And being right is the dangerous part. My unchecked assertion happened to be true, which is precisely the case that does not announce itself: had it been false, the next reader would have inherited it from a document I had just corrected for carrying unchecked status. Correct-by-luck and correct-by-checking are indistinguishable in the text.

What the check turned up on its own

🔴 P0, P2 and P5 have no gate record in DECISIONS.md, while P1, P3, P4, P6 and P7 do. The mission states every milestone is gated by an artifact, "never by 'it compiles'" — three of the eight have no artifact written down.

⚠️ I have verified P2 here. P0 and P5 remain unrecorded and unverified, and I am naming that rather than fixing one and implying three. P5's gate is "a human clicks through it", which I cannot self-certify at all.

📌 Their boundary is the honest limit and it holds for this instance: my correction was written, indexed, and would have passed my own withdrawal-time hook cleanly. Neither mechanism tests whether a correction is true — only whether it is recorded. They enforce bookkeeping and cannot enforce accuracy.

P0 gate — recorded at last, and the gap it belongs to

They were right that the P0/P2/P5 finding matters more than the P2 fix. P0 is the one of the three I can close alone — its gate names no human and no emulator: "export/screens/title/main_menu.json validates against FORMAT.md and the PNGs open."

the named file exists, 51 011 bytes
validation 16 screens validate against sylpheed.screen/3, that file among them
sprites it references 20
open as PNG 20 — 0 missing, 0 unreadable

P0's gate is met, on an artifact, and now written down. It had been met for a very long time; what was missing was the record.

The shape this belongs to

📌 That is the argued-versus-indexed split one level up. The refutation register taught it about deaths: the page is where a refutation is argued, the index is where it is found, and I had eight arguments and no index entries. Here the milestone was completed and never indexed — the work existed, the artifact existed, the gate record did not. Same failure, different object.

⚠️ Remaining, and stated rather than quietly finished:

  • P2 — verified last iteration (buttons slide: changed region 307×215 at (542, 162), matching the declared button), recorded there.
  • P5"a human clicks through it." I cannot self-certify this and will not try. A gate written to require a person is not satisfied by me deciding it looks fine, and converting it into something I can check would be rewriting the gate to fit the checker — which is the mission's own warning about gating on "it compiles", in a more flattering costume.

📌 So: two of the three closed with artifacts, one left open by its own terms. Their line about P5 is the right one — the right kind of thing to leave standing rather than quietly satisfy.

Their sufficiency gap, run on authored/ — clean, after I nearly reported 35 false positives

Their audit found 48 citations resolving and 0 missing, with the caveat that matters: "it cannot see data a page should have cited and did not — a page citing nothing would have passed as 0 missing. Existence and substance, never sufficiency."

The port-side analogue is exact. My earlier audit checked what a MEASURED stamp cited; it could not see an authored value carrying no why at all, which passes every such check by being absent. The mission requires every authored entry carries a why, so that absence is the thing to look for.

🔴 First pass: 35 of 131 values flagged as bare. Inspecting before reporting — every sample was a false positive:

  • ptbtn01's label and goto have no why key, and the object carries five *_why siblings (skipped_chain_why, then_video_why, unobserved_why, skippable_why) plus a blocked explanation.
  • /voice/stream_weights/…/position is covered by a _ key one level above it, which my check only looked for in the same object.

Ancestor-aware, the real number: 126 values, 0 uncovered. Every authored scalar has a why, _ or *_why in its own object or an ancestor.

📌 35 was the instrument's resolution, not a finding — and this is the third instance in one exchange: their 9 raw hits → 2 real, my 33 hook candidates against a real shortfall of a few, and now 35 → 0. A first count from a new detector is a measurement of the detector. All three of us stopped and inspected rather than publishing the raw number, which is the only reason none of them became a claim.

⚠️ And their caveat transfers unchanged, so I will state it rather than enjoy the clean result: this tests that a why exists in scope, not that it explains that value. A parent _ covering twenty values may say nothing about any one of them. Existence and scope, never sufficiency — the same limit they named, and I have no better instrument for it either.

Their absence shape on my own citations — and the wording gap in my P0 closure

Their finding was about their own audit rather than their corpus: evidence exists in three forms — data files, inline tables, committed tests — and their check looked for one, so "48 citations, 0 missing" was a statement about the data-file form. They reported it in the wording rather than the scope.

The analogue I could run: do my own citations resolve? 32 distinct file paths cited in DECISIONS.md; 12 do not resolve. Inspected before publishing — the fifth instance of that habit in this exchange — and most are not findings:

kind example
relative fragments quoted mid-sentence title-builds/live-title-press-a.png (full path exists)
the Decoder's files, on their branch docs/re/data/b-on-main-menu.txt
a historical absolute path /reborn/docs/re/captures/…
a hypothetical modding example data/mods/sprites/…

Genuine: five stale citations from the docs/docs/port/ reorganisation — docs/BLOCKED.md ×4 and docs/FORMAT.md ×1. A reader following them fails. Rewritten.

🔴 And the one that reached a claim I made last iteration

P0's gate says the export "validates against FORMAT.md". My closure reported the validator saying "16 screens validate against sylpheed.screen/3". Those are different words, and I certified the gate on one while quoting the other — their exact failure, in the gate closure I published as verified.

Checked rather than assumed: docs/port/FORMAT.md is 405 lines and the string "format": "sylpheed.screen/3" appears in it as the specification. So the schema the validator enforces is the one FORMAT.md defines, and the closure stands.

⚠️ But it stood on an unstated identity. Had FORMAT.md described a different or superseded format, my P0 closure would have been a confident artifact-backed certification of the wrong thing — and nothing in the check I ran would have said so, because the validator's output never mentions FORMAT.md at all.

📌 The general form, now with both instances: verifying in the tool's vocabulary and certifying in the gate's vocabulary is a substitution nobody performs explicitly. It is the noun problem again — the number was right, the thing it was a number of went unstated — and this time the two nouns happened to denote the same object.

The off-edge splash residual, localised — three mechanisms ruled out, one honest description

The last open technical question I own: after excluding glyph edges, the splashes differ from the game by 0.821.42 RMSE — ~2× the double-quantisation floor, non-tonal, and with no candidate since the XPR_* texture toggles turned out not to reach t8ad::parse.

Tested the one signature left: is it positional? A sub-pixel or resampling difference makes the residual track the local gradient. Rule stated first: r > 0.5 to call it gradient-linked.

gradient brightness (control)
publisher_logo +0.109 +0.047
developer_logos +0.307 +0.471

🔴 Rejected. Neither meets the bar, they disagree by 3×, and the control settles it: on developer_logos brightness correlates more strongly than gradient, so the gradient signal is not distinguishable from "content is where things happen". Not positional.

Where the residual actually lives

The brightness correlation pointed somewhere better. Signed residual (render capture) by capture brightness, off-edge:

band 015 1647 4895 96159 160255
publisher_logo 0.00 +1.68 1.09 2.48 +0.45
n 843 025 542 951 312 12 743
developer_logos 0.02 +2.19 +0.74 1.57 0.96
n 812 111 26 196 4 498 5 232 2 832

📌 98 % of the off-edge area has a residual of essentially zero. The entire 0.821.42 RMSE comes from the ~2 % of pixels that are lit — the logo interiors.

⚠️ And within those, the sign is inconsistent across bands and across screens: publisher runs +1.68, 1.09, 2.48, +0.45 while developer runs +2.19, +0.74, 1.57, 0.96. Not a global gain, not a global curve — which is why fitting one never helped, and is consistent with the earlier finding that a per-level LUT fitted on its own pixels improved them by 1.6 %.

What this is worth

Three mechanisms are now ruled out with evidence: global tone (a curve fitted on its own data barely moves it), texture decode (the toggles do not reach the sprite path), positional (gradient loses to brightness).

And the description is far better than "0.5 RMSE, no candidate": the port matches the game exactly across 98 % of the off-edge area, and differs by 12.5 levels inside the lit logo, with no consistent direction.

⚠️ Still no mechanism. That is now a much smaller and better-posed question than it was — but naming what it is not four times over is not the same as finding what it is, and I am not going to invent a fifth candidate to close it.

Full regression after a session of edits — and the phase term moving two published rows

I had changed boot.gd, screen_view.gd, four tools and two authored files without a full suite run. Ran it.

Every asserting check passes — format-validator, modding-rules, capture-controls, menu-audio, decisions-index, refuted-claims. verify-screen's two DIFFERS are the named pair with their per-screen reasons.

🟡 Two oracle rows moved, and not as a regression

row before now
title_plate 12.83 / 0.00 % 13.04 / 0.09 %
title_band 15.31 / 0.35 % 12.86 / 0.00 %

They moved in opposite directions, which is the signature of a phase change rather than a regression — and the cause is mine: adding --leaf-time=0 to verify-capture's render sites pinned the sweeps to one pose, and the captures froze them at whatever pose the shutter caught.

📌 This makes the capture-phase term concrete rather than theoretical. I documented ±5.56 RMSE for title from a sweep; here it moved two published rows by 0.09 and 0.35 percentage points of differing area, in opposite directions, from a one-line harness change. The annotation was not decoration.

⚠️ And it touches a number I published. My boot-end-frame verification quoted 0.00 % differing, measured before the pin, with the boot's leaf free-running. The equivalent row now reads 0.09 %. Both are inside the stated term, and the right reading is that neither is "the" number — a row containing a sweeping leaf has a phase-dependent value, and quoting either without the term attached is the error the annotation exists to prevent.

Narrowing my own hook — 33 was a measurement of the regex

The withdrawal-time hook reported 33 correction sections registering nothing. I called that a detector measurement at the time and then left it standing, which is the same shape as everything else this session.

🔴 Its regex matched headings about corrections, not headings making them: "Resolved by measuring rather than withdrawing", "Their REFUTED.md gap", "Building the withdrawal-time hook". Narrowed to a leading WITHDRAWN/CORRECTION/Refuted or an explicit "is withdrawn".

33 → 10, and every one of the ten is a genuine retraction. The list is now actionable where it was noise.

⚠️ A residual limit worth naming: several of the ten are flagged because the registered phrase does not appear in that section — the corrected JP heading reads "does NOT go against the port", which does not contain the registered "goes against the port" [refuted]. The register wants the claim quoted; a good correction paraphrases it away. Those two pull against each other, and I do not think the tension resolves — it is the cost of a substring register, like the 0.32 collision that made that claim unregistrable.

Their Q10 correction checked, and the register's cost is per-mention, not per-correction

Their stale Q10 row does not reach me. My stems_why reads "a bank is exactly TWO waves of identical duration" — the corrected understanding, not the three-sub-waves row they withdrew — and the discrepancy is already recorded in this file as refuted. stems: "sum" is unchanged, which is what they said it should be.

Nor do I cite their coherence discriminator anywhere. They flagged it because its own control showed L-vs-R within a single wave reading only 0.220.50, so the test's premise fails in this material. Nothing of mine depends on which kind of second stem it is — only that both play, aligned at sample 0.

Their paraphrase resolution, adopted

The register-versus-paraphrase tension: "keep the dead phrase quoted verbatim in REFUTED.md and paraphrase freely everywhere else — they are different documents, so it costs the correction nothing." Right, and it resolves the prose half cleanly: the phrase always has one exact home without any correction having to carry it.

⚠️ It does not resolve my hook, and I have written that limit into the tool rather than chasing it: the hook detects "does this section contain a registered phrase", so it will always over-report on well-written corrections. Its candidate list mixes never registered with registered and paraphrased and cannot separate them — a prompt to check, never a defect count.

🔴 Fourth instance of the recursive cost, and it happened while I documented it

Writing that comment quoted a registered phrase, and check-claims failed. So did my previous entry, which quoted the phrase while explaining that the corrected heading no longer contains it. Both marked.

📌 So the honest statement of the cost is sharper than the header's: it is not per-correction, it is per-mention — and mentions multiply once the register becomes a subject of discussion. Every time I write about a dead claim I create a new occurrence needing the token, including in the sentence explaining that this happens. Four instances, each inside text about the mechanism. That is not a reason to drop the token — its absence still means exactly one thing — but the cost curve is steeper than "mark it once when you retire it".

The contract I read every iteration is 3 185 lines shorter than the contract

📌 docs/port/HANDOFF.md on main: 926 lines, last touched 9ca1eb5, 2026-08-29. The live one: 4 111 lines, 27938aa, today. 96 commits I have never read, +3 930/745. The mission tells me to read HANDOFF every iteration and I have. I have been reading main's copy. The Decoder writes it on origin/auto/no-disc-and-menu-captures, which main is a hundred-odd commits behind, so the contract and the copy of the contract I open have been diverging for two days.

Several of those commits are addressed to me by name — "handoff: deliver the concurrent-streams refutation to the page the port reads", "handoff: tell the port its refusal found a decoder defect". They were delivered to the page I read. The page I read is not the page they were delivered to.

🔴 The instruction that was supposed to prevent this cannot detect it

BLOCKED.md's own header says rows rot because they carry no derivation sha, and the standing rule is to record the HANDOFF commit each row derives from. I built tools/port/blocked-provenance to supply them from history rather than memory — git log -S on each row's key phrase gives the commit that introduced it — and the answer is that every one of the 27 open rows derives from 9ca1eb5, because HANDOFF-on-main has not moved since. A constant cannot discriminate.

So the sha the rule asks for is the one field guaranteed to be identical on a fresh row and a rotten one. The rot is not that rows are old. It is that the document they derive from is frozen while the thing it is a copy of moves.

⚠️ And my own BLOCKED.md asserts "HANDOFF has not moved in four milestones". That is withdrawn. HANDOFF has moved 96 times. It has not moved on main, and I wrote the observation up without the qualifier that carried all of its meaning.

What the tool measures instead, and the control that caught it lying

Counted against every ref rather than my own ancestry, each row has 196 unread docs/re/ commits behind it — again identical for every row, because none of that branch is my ancestor. A number that is the same everywhere is a property of the document, not of a row.

To make it per-row, the tool ranks the unread commits by word overlap with each row. The first version silently missed its own known positive. P6 looping asks where the menu loop restarts; 712cac8 measures it at 9.44 s and this port has shipped that value since. The pair scored zero: looping did not stem to loop, menu was stoplisted, and the ≥2 shared words threshold dropped what was left.

The threshold was the defect, not the constant. Two common words scored the same as two rare ones, in a corpus where nearly every subject says menu. Weighting each shared stem by log(N / subjects containing it) lets one rare word outrank two common ones and removes the cutoff altogether — the list is ranked and fixed-length, so nothing is decided by a number I could have tuned. The control then passes at rank 1 of 7, and it passed without touching the stoplist, which is the difference between fixing an instrument and fitting it.

📌 Every discard is now counted: struck rows not scanned, scoring pairs below the cut, stoplisted words that can never match. The Decoder reached the same rule from the opposite failure the same day — their checker's suppression path was silent and its clean runs were therefore unfalsifiable, while mine over-reports loudly. A detector that can drop a candidate without saying how many must not be believed when it reports zero.

It immediately found two open rows whose answers were already written

row unread commit
P3 — the plate's PULSE 07e93ce (score 14.0) the period is 120, not 105
P5 — Ⓑ on the main menu 9a10258 (score 10.6) the menu has no idle self-return — the row's own reasoning is refuted

Both had sat unread for a day. Neither needed an experiment; they needed the document to be looked at.

A refutation attempt on +0x08 is the loop length — it survives, and the port adopts it

The claim the port was about to build on, so the one to attack (PROTOCOL: refutation is cheapest where the other agent is most confident). HANDOFF 27938aa, delivered at 07e93ce: a nested record is itself a RATC bundle, its header's +0x08 is the loop length, and the plate's glow therefore cycles over 120 units while its keyframes end at 105 — "🔴 So stop shipping 105."

I re-ran their own two controls on my own reading of the disc rather than taking the census — cargo run -p sylpheed-export --example record_loop_control:

disc-wide
timed nested records 1 781
+0x08 == max t 1 643 92.3 %
+0x08 > max t (a hold) 138 7.7 %
+0x08 < max t 0 0.00 % — the falsifier never fires

Identical to their figures. The falsifier is the load-bearing one: an animation cannot restart before its own last pose, so a wrong reading of the field should produce violations, and none exist in 1 781 records. The non-triviality control holds too — a field that always equalled max t would carry nothing.

⚠️ And I added the control they could not run: the same two restricted to the eight records this port actually animates. A disc-wide 0.00 % says nothing about my six screens if all six sit in the exceptional tail.

   record        +0x08   max t   slack
   ptbtn00f        120     105      15
   ptbtn01f…05f    120     120       0
   ptloop01/02  600/720 600/720      0
   shipped: 7 exact, 1 hold, 0 falsified

Their table, cell for cell. ⚠️ Note how narrowly non-trivial it is here: across the disc 7.7 % of records hold, but on my shipped set exactly one of eight does. The claim survives on my data; it is not richly confirmed by it.

🔴 The instruction cannot be complied with using anything they published

"Stop shipping 105" presumes the port can read the field. loop_length_units is decoded in an example and a test and documented in docs/re/ — and exposed in sylpheed_formats' public API on no ref at all. I checked every ref that touches the crate's src/.

It is still reachable: parse_build publishes each record's (offset, size), so screen.rs reads four big-endian bytes at a documented offset inside a span whose magic it checks. That is consuming a delivered finding, not writing a second decoder — but it is the port holding a format detail it should not own, and the doc comment says to delete it the day the crate exposes it. Filed as an ask, not a complaint: one pub field would take it back where it belongs.

The value does not change. Its provenance does.

authored/timing.json already had period_units: 120 for the plate — from a wall-clock measurement of the running game, ≈2.37 s. The disc declares 120. So this port never shipped 105 for the plate, and the instruction was aimed at a state I had already left by a different route.

📌 That is the result worth keeping. An emulator stopwatch and a field on the disc, sharing no instrument, no code and no assumption, land on the same number. ScreenView._loop_period now prefers the derived value and keeps the authored one as the fallback and as that second witness — and a disagreement between them is announced with push_warning, never silently resolved, because preferring one number quietly is exactly how a measurement and a declaration drift apart for milestones without anybody finding out.

Not settled, and not mine: the export shows ptbtn11f/12f/13f on extras and ptbtn01f…05f on the main menu all declaring the same 120-unit cycle, while looping_focus_records names only the plate. Whether those records animate while focused is behavioural — an ask, not an inference from the header.

The contract is checked now, not read — tools/port/contract-check

Last iteration established that docs/port/HANDOFF.md on main is 926 lines frozen at 9ca1eb5 while the live document on the Decoder's branch is 4 111 lines at 27938aa — now 99 unread commits. The obvious response is "read the other one", and it is not good enough: there are 70 sections in it this port has never opened, more arrive daily, and the failure mode is not laziness but that nothing tells me which of them contradict what I ship.

So the contract's numbers are checked against the port's own tree instead. Each check pulls its expected value out of the live HANDOFF text by pattern — never restating it here, which would make this file a third copy to go stale — and compares it against export/ or authored/.

contract port
fade quad, title / menu / extras [0,16,261,269] [0,12,70,80] [0,12,64,74], α 255/0/0/255 identical
fade-out ramps 10, 10, 8 on the title 10, 10, 8
plate glow cycle 120 120 derived, 120 authored
menu BGM loop window -ss 9.44 -t 61.87 9.44 / 61.87
black hold between screens 0 0
menu BGM bank BGM_103 BGM_103.slb
boot splash dwells 190 and 145 190 and 145

Three outcomes, and the third is the point

ok, MISMATCH, and ANCHOR LOST — the pattern no longer matches the contract. That is reported as loudly as a mismatch, because a check whose anchor has drifted passes forever while measuring nothing, which is the exact shape of failure this tool exists to catch one level up.

The known negative, because a clean first run is not evidence

--control perturbs the contract by one token per check — 120, not 105 becomes 121, not 105, -ss 9.44 becomes -ss 9.45 — and requires every check to fail. All seven do. Without it I would be reporting seven passes from an instrument nobody had ever seen react to anything, which is the same unfalsifiable clean run the suppression counting fixed in check-claims yesterday. Both are in check-all now, the control as its own asserting step.

🔴 What a pass does not mean

Seven values out of a 4 111-line contract. The other 70 sections are still read by eye or not read at all, and the tool prints that line on every run so a green result cannot be quoted as "the port agrees with the contract".

A refutation attempt on the fade numbers — it survives, from a third reader

The claim to attack, per PROTOCOL's prefer what the port is about to build on: HANDOFF's "🔴 the transition is OVERLAP, not ramp-then-hold. And your menu fade-in is 5× too slow", which corrected screen-transitions.md from a 0.97 s menu fade-in to 12 units, 0.20 s. Their cause: fade_quads.py read each pose's time from blk+36, the next record's time word — the association the record-layout fix retired in the crate, never swept into the Python helper.

The port never held that number. There is no authored fade duration anywhere in authored/ or port/scripts/ScreenView animates pteff00 from its own exported keyframes, so the 5× error could not reach it. The instruction was aimed at a state I was not in, for the second consecutive iteration.

But it makes my export an independent check on their correction, and it holds exactly: [0,16,261,269], [0,12,70,80], [0,12,64,74] with α 255/0/0/255, and fade-outs 10/10/8. Their rebuilt tools and my pinned crate are different readers of the same bytes, so agreement means both got the record-layout fix — which is precisely what their helper had not had.

📌 And the same for the splash dwells: HANDOFF's 190 and 145 retraction was caused by my recomputation, and the export now re-derives 190 and 145 from the keyframe times a third time. A retraction confirmed by the party that provoked it is worth less than one confirmed by a third reading; this is the third reading.

The walk is checked too, and "only the ring moves" tested against my own renderer

docs/game/navigation.md — the screen-by-screen walk written from the committed oracle frames — is a second document unreachable from main, and authored/flow.json is its executable form. Nothing in the port fails when a label drifts from it, so three more checks join contract-check, anchored on the walk's own text: the five main-menu labels in order, EXTRAS' three items, and the cursor wrapping. All three agree; all three fail their known negative. Ten checks now, ten controls.

The manual audit that produced them found nothing else to fix: initial focus is already kind: "authored" citing Q5's instability, left_right is an explicit no-op, auto_repeat is measured, and every unexported destination is marked blocked with the reason rather than invented.

The refutation target: "it is the only thing moving on this screen"

The walk says the focus ring turns continuously and is the only thing moving on the settled main menu — labels, bracket and footer at temporal std exactly 0.000 over 20 s. I cannot test that against the game, but I can test whether my port obeys it, which is the direction that matters. Five renders across a full ring cycle (--loop-phase 0…96, --leaf-time pinned):

pixels varying by > 2 1 428 of 921 600 — 0.155 %
bounding box 46 × 44 at x 498543, y 158201
distinct clusters 1

One region, beside the focused item. The port animates one ring, not five — worth checking, because the export shows all five ptbtn01f…05f declaring the same 120-unit cycle and a renderer that ran them all would look identical to one that ran the right one until you diffed frames.

🔴 And I nearly filed a defect against myself off a debug pin

Sweeping the other free clock — --leaf-time 0…8 s with the ring pinned — moved 10.4 % of the frame, full-screen bounding box. On a screen the contract says has exactly one moving thing, that reads as a serious P5 defect.

It is not one. --leaf-time is a debugging pin, and 0…8 s is 0…480 units, which lands inside the build-in: ptloop01's sweep runs t=0→600 and ptloop02 t=0→720. At settle they are parked at x = 1521 and x = 839, both off-screen on a 1280-wide frame, and loop_leaf_on_screens scopes the replay to title alone. So the settled menu moves exactly the ring, and the 10.4 % was me driving an animation the settled screen has already finished.

📌 The general form is worth more than the incident: a pin that can address states the screen never occupies will manufacture defects on demand. The three pins exist precisely so a render is reproducible, and reading their output as if it were the shipped behaviour inverts what they are for. Same shape as the --leaf-time seconds-versus-units error, from the opposite side.

The +0x08 ask came back answered — and is not consumable yet

The Decoder exposed ui_layout::loop_length_units at b5df02a, and it is byte-for-byte the logic screen.rs holds: same RATC guard, same 0x08, same big-endian read. So the deletion my doc comment promises is a one-line switch.

⚠️ Not taken this iteration, and not for a reason about the code. crates/sylpheed-export/Cargo.toml pins tag = "formats-pin-2026-08-30" and there is no tag carrying b5df02a. Moving to a bare rev on an unmerged branch would replace a deliberate pin with an incidental one — and BLOCKED.md already records this pin as load-bearing. Asked for a tag; keeping the local read, which is guarded and controlled, until one exists.