diff --git a/docs/agents/PROTOCOL.md b/docs/agents/PROTOCOL.md index 92cc2052..f59fc5a7 100644 --- a/docs/agents/PROTOCOL.md +++ b/docs/agents/PROTOCOL.md @@ -133,6 +133,42 @@ Refutation is cheapest where the other agent is most confident. Prefer: centroid estimator that is 19.8° out on a known rotation cannot measure an unknown one. A filter that fails its own known-positive is dead, not tuneable. +⚠️ **And a control verifies CAPABILITY, not CONFIGURATION.** Both agents ran +controls and both were still wrong: one tested whether the method *can* detect a +blend difference, not whether *that run* had `blend_mode` set — it was left at +the engine default. The other tested whether NDC→pixel conversion is right, not +whether the dump captured all six quads; it captured two, with a well-formed line +and no ellipsis. Assert the run's configuration, not just the method's power. + +### R1 — a refutation is only as good as its instrument + +**A refutation whose instrument is one of our renderers is not a refutation.** It +is *"our renderer disagrees"* — 🟡, not ❌. + +Agreed by both agents 2026-08-31, applied to +[`../re/REFUTED.md`](../re/REFUTED.md) by the human on 2026-09-01. The register +is not yours to reclassify: it is the file you both read to decide what *not* to +try, and two agents agreeing is not the authority for changing it. Propose; +do not enact. + +What this asks of you, in practice: + +* **Every claim you retire names its instrument** — `⟨capture⟩`, `⟨disc⟩`, + `⟨image⟩`, `⟨render-vs-capture⟩`, `⟨harness⟩`… The register's reading guide + holds the vocabulary and says which tags are ours. +* **When you improve a renderer, a reader or the harness, run + `tools/stale-instrument `.** It lists what that instrument + killed. Those claims re-open. This is the mechanism the rule exists for: the + motivating failure was not that anyone was careless, it is that **nothing + re-opened a claim when the instrument that killed it improved**, and a real + disc field sat dead for weeks as a result. +* **A 🟡 carries what would settle it.** A re-opened claim with no next + experiment is an unanswered question wearing a colour. + +The exception, from R5: our tool is the right instrument for a question **about +our tool**. *"Can `screen render` draw the developer splash?"* is ours to answer. +*"What does the game draw?"* is not. + **Disagreements escalate to the human with both positions.** They are not resolved by seniority, by who wrote it down first, or by whoever is more certain. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 8c094167..1409a5e2 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -14,36 +14,131 @@ neighbourhood, not just the line. --- +## How to read this file + +**Reclassified 2026-09-01 under rule R1**, by the human, on the retro both agents +asked to have adjudicated ([`../agents/RETRO-2026-08-31-agreed.md`](../agents/RETRO-2026-08-31-agreed.md) §7). +Neither agent may reclassify this file; it is the file they both read to decide +what *not* to try, and two agents agreeing is not the authority for changing it. + +### The rule + +> **A refutation whose instrument is one of our renderers is not a refutation.** +> It is *"our renderer disagrees"* — 🟡, not ❌. + +Not because such work is careless. The motivating case was careful: *"blending +those sprites additively worsens every measure against the capture"* killed a +**real disc field** for weeks, and the renderer that produced that measure had a +stale keyframe association, no leaf geometry and no rotation. It read exactly +like a publishable negative. Nothing in the entry could have told you otherwise — +which is why the fix is structural rather than a warning to be more careful. + +### The verdicts + +| | meaning | may I re-open it? | +|---|---|---| +| ❌ (unmarked) | dead. The instrument was **not ours** — a capture, the disc, the executable, Canary's source, or the claim's own control. | no, unless you bring a new instrument | +| 🟡 | **our instrument disagrees.** The conclusion may be right; it has not been tested against the game. | **yes — and re-open it when that instrument improves** | +| ~~struck~~ | withdrawn by a later entry. Read the correction under it. | it is already open | +| ✅ | established, and kept here because it once sat in this file as dead | no | + +### The instruments + +Every entry ends with `⟨instrument⟩`. Ours are marked, and the mark is what +`--stale` queries. + +| ⟨tag⟩ | ours? | what it means | +|---|---|---| +| `capture` | no | the real game in Canary — a screenshot, a draw log, a GPU readback, an observed frame | +| `disc` | no | bytes on the disc: a parse, a census, a disc-wide scan | +| `image` | no | the executable: code, strings, tables, xrefs | +| `canary-source` | no | Xenia Canary's own source read directly | +| `control` | no | the method **failed its own positive control**. Valid whoever built it — a filter that cannot find a known-good is dead, not tuneable | +| `environment` | no | a fact about the container or host | +| `audio-analysis` | no | signal measurement on disc audio, with a control | +| `corpus` | no | a stale row found by reading our own documents | +| **`render-vs-capture`** | **yes** | a correlation, RMSE or box-mean between one of **our renders** and a capture | +| **`screen-render`** | **yes** | `ui_layout::blit`, `sylpheed-cli screen render`, the `rest()` heuristics | +| **`our-reader`** | **yes** | one of our decoders used as the yardstick, rather than disc bytes read directly | +| **`harness`** | **yes** | our capture harness: polling cadence, `x11grab`, frame classifiers | +| **`our-tool`** | **yes**, but see below | our own tool, where **the question is about that tool** | +| `reasoning` | — | argued from structure, not measured | +| `unrecorded` | — | **the entry states no evidence.** 83 of 222 are here | + +### Two things this rule does *not* say + +**1. `our-tool` is not disqualifying when the question is about our tool.** R5 +disqualifies renderer-derived labels *for disc-side questions*. *"Can `screen +render` draw the developer splash?"* and *"does `blit` apply the pivot here?"* are +questions about our code, and our code is the right instrument for them. Those +stay ❌. The tag is still there so `--stale our-tool` can find them if the tool is +rewritten. + +**2. `unrecorded` is not a verdict.** It means nobody wrote down how the claim +died. It is the single largest group in this file, and the honest reading is that +**a third of this register cannot be audited at all**. Do not treat those rows as +either safe or suspect. `--stale unrecorded` is the backfill queue. + +### What changed in the pass + +Ten entries moved ❌ → 🟡. Eight are `render-vs-capture`, one `our-reader`, one +`harness`. Each names **what would settle it**, because a re-opened claim with no +next experiment is just an unanswered question with a colour. + +Two of them are the same question pointed both ways: **the `rest()` pair.** *"rest += last keyframe"* was refuted by a sibling argument, and that refutation was then +itself refuted by correlating our render against captures. Both legs run through +our renderer, so under R1 neither survives — the question is **open**, not +settled, and it had been reading as settled in both directions depending on which +entry you found first. That is the clearest thing this pass turned up. + +One entry was already revived by the agents before the pass, on capture evidence: +the additive-blend bit. It is the archetype and is kept in full, below. + +### Querying it + +``` +tools/stale-instrument # everything, grouped by instrument +tools/stale-instrument render-vs-capture # what one instrument killed +tools/stale-instrument --ours # only instruments that are ours +``` + +Run it when you improve a renderer, a reader or the harness: it lists exactly +what that instrument killed, so those claims re-open instead of staying dead +because nobody remembered which ones rested on it. + +--- + ## Offsets, structs and the progress singleton -* `position = instance − 0x12c` → refuted. -* `+0x29d0` → refuted. +* `position = instance − 0x12c` → refuted. ⟨unrecorded⟩ +* `+0x29d0` → refuted. ⟨unrecorded⟩ * "an offset intersection can find a struct's consumer" → **only for LARGE or - unusual offsets.** Small ones have no power (`+184`: 301/351/115 hits). + unusual offsets.** Small ones have no power (`+184`: 301/351/115 hits). ⟨image⟩ * "a `+1956` store means a progress write" → writes go through the COPY, not - direct stores. 9 direct stores, none of them a progress write. + direct stores. 9 direct stores, none of them a progress write. ⟨image⟩ * "the singleton-global filter can find progress writers" → it fails its own - control. + control. ⟨unrecorded⟩ * "the progress copy destination is an `r1`-relative stack local" → it is a **frame register**. The `r1` assumption returned 0 for all 21 candidates - *including the known-good* — the filter was killed by its own control. -* "word B's writer also stores the Time/Points record" → it does not. + *including the known-good* — the filter was killed by its own control. ⟨control⟩ +* "word B's writer also stores the Time/Points record" → it does not. ⟨unrecorded⟩ * "the debriefing records the metric with the clear bit" → 44 calls, exactly two - strings (`DEBRIEFING`, `BASE_INFO`), no `Time`, no `Points`. + strings (`DEBRIEFING`, `BASE_INFO`), no `Time`, no `Points`. ⟨image⟩ * "`0x820AF030` holds live state" → all 384 words constant; it is a - spawned-entity record, not live state. + spawned-entity record, not live state. ⟨capture⟩ ## Screens, classes and RTTI * "the rotated draw in the title's capture is the `Z` swoosh (`ptlogo_back2*`)" → **mine, and refuted.** Its quads span y −209…925 and −292…1012 in screen space; the swoosh is a band at y 126…360. - [`ui-title-build-map.md`](ui-title-build-map.md) + [`ui-title-build-map.md`](ui-title-build-map.md) ⟨capture⟩ * "the keyframe words at `+4`/`+8`/`+12` are always zero" → they are non-zero in **4.76 %, 4.62 % and 14.50 % of 83 862** blocks disc-wide, reading as **degrees** (±180, ±90, 120, 360). The original note was a sample artefact. (Superseded figures: an earlier count of 72 287 blocks missed every nested - record — see the alignment entry below.) + record — see the alignment entry below.) ⟨disc⟩ * ~~"those angle fields are where the title's rotated quads come from" → **no** — every `GP_TITLE` build 4 element has all three at zero.~~ → **that refutation was itself wrong, and is withdrawn (2026-08-28).** `+12` *is* exactly where @@ -51,38 +146,43 @@ neighbourhood, not just the line. quads belong to its two **nested** leaf records, `ptloop01.rat` (`+12` = 30) and `ptloop02.rat` (`+12` = −45), which the census never opened. Measured off the GPU: **+30.26°** and **−45.28°**. - [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) + [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) ⟨disc⟩ * "the rotated draw's element cannot be named from the capture" → refuted; its quads' **edge lengths** name it. 400 × 1076 and 400 × 1444 match `pteff03` 399×180 at 600 % and `pteff03a` 399×180 at 800 % — two different heights, both - landing. [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) + landing. [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) ⟨capture⟩ * "a keyframe-block scanner may assume 4-byte alignment" → refuted; it found **0/3** of its own control blocks and under-counted the corpus by 16 341 - blocks. Nested `RATC` blobs start at odd offsets (`0xbb5966`). + blocks. Nested `RATC` blobs start at odd offsets (`0xbb5966`). ⟨disc⟩ * "keyframe rotation lives only in **nested** `.rat` leaf records" → **mine, and refuted within the hour by my own sweep.** It held for the three archives I had checked (`GP_TITLE`, `GP_BUNK`, `GP_CHALLENGE`) and failed on the next: `GP_DIALOG` and `GP_DEBRIEFING_PILOTLOG` rotate **top-level** elements, and those are the clearest examples on the disc. - [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) + [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) ⟨disc⟩ * "the pivot-anchored scale formula `kf.x − pivot·(scale−100)/100` is our renderer's reading, not a measurement" → **now measured.** At the `ptloop` pair's 600 %/800 % scale it predicts both quad centres at y = 360.0 against a captured 359.1/360.0, where top-left anchoring predicts 810/990. - [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) + [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) ⟨capture⟩ * "the game passes a pink per-vertex colour for the title swoosh" → **refuted by - draw capture.** Every vertex colour in the capture is `FFFFFF`, white RGB. + draw capture.** Every vertex colour in the capture is `FFFFFF`, white RGB. ⟨capture⟩ * "the swoosh discrepancy is undecodable" → **solved**: the game submits it as two **rotated parallelograms**; `ui_layout::blit` only does axis-aligned rectangles. - [`ui-title-build-map.md`](ui-title-build-map.md) + [`ui-title-build-map.md`](ui-title-build-map.md) ⟨capture⟩ * "the `--log_ui_draws` per-draw capture reads the guest's blend state" → **mine, and wrong.** It records primitive type, index count, index-buffer address, VS/PS ucode hashes, texture bindings and vertex attribute 0 — no `RB_BLENDCONTROL`. It can test vertex colour as-is; blend state needs a Canary - change. [`ui-title-build-map.md`](ui-title-build-map.md) -* "the plate-free title capture (t ≈ 4.0 s) may be too early to be settled" → - **mine, and refuted.** The swoosh band correlates 0.7342 at t = 4.0 s and - 0.7353 at t = 21.5 s — identical to 0.001 over 17.5 s. + change. [`ui-title-build-map.md`](ui-title-build-map.md) ⟨our-tool⟩ +* 🟡 "the plate-free title capture (t ≈ 4.0 s) may be too early to be settled" — + **our renderer disagrees** (was ❌ before the R1 pass). The swoosh band + correlates 0.7342 at t = 4.0 s and 0.7353 at t = 21.5 s — identical to 0.001 + over 17.5 s. Both numbers are *our render* against a capture, so the test is + blind to anything our renderer does not draw: an element missing from the + render cannot move the correlation whether or not it moved on screen. + **To settle:** compare the two captures *to each other* — no renderer in the + path. ⟨render-vs-capture⟩ * ~~"`T8aD +0x04` bit `0x02` selects an additive blend" → **mine, and refuted.** Blending those sprites additively worsens every measure against the capture.~~ 🔴 **REVIVED AND ESTABLISHED 2026-08-31 — the refutation was wrong, and it was @@ -105,175 +205,175 @@ neighbourhood, not just the line. and "the bit selects premultiplied alpha" (the flagged group violates `RGB ≤ A` *more* than the unflagged one). Those were readings of the bit's **meaning**; what is established here is its **effect**. - [`ui-title-build-map.md`](ui-title-build-map.md) + [`ui-title-build-map.md`](ui-title-build-map.md) ⟨capture⟩ * "the title logo elements' wrong pivots (off by up to 59 px, authored against the other language's sprite) explain the swoosh rendering too thick" → **mine, and refuted.** `blit` sizes from the texture and applies the pivot only as `pivot·(scale−100)/100`; all seven swoosh elements are scale `(100,100)` at every keyframe, so the term is zero. The mismatch is real but inert here — it would bite `ptlogo1`/`ptlogo2`, which scale to 150 during the build-in. - [`ui-title-build-map.md`](ui-title-build-map.md) + [`ui-title-build-map.md`](ui-title-build-map.md) ⟨our-tool⟩ * "the title screen loops at ≈ 2.2 s" → **mine, and doubly wrong.** The title *art* is near-static (wordmark sd 0.06); the 2.3 s pulse is the **`PRESS Ⓐ BUTTON` plate**, which is a *different build* (2, not 4). Localised by a per-tile amplitude map and decoded in `ptbtn00f.rat`. - [`ui-title-build-map.md`](ui-title-build-map.md) + [`ui-title-build-map.md`](ui-title-build-map.md) ⟨capture⟩ * ~~"a `.rat` leaf record's keyframes use the build's 40-byte layout" → they do not~~ — **withdrawn.** They do. The scan that "found nothing" required 29 increasing times; the records hold **three**. Decoded in - [`ui-title-build-map.md`](ui-title-build-map.md). + [`ui-title-build-map.md`](ui-title-build-map.md). ⟨disc⟩ * "the title's measured ≈ 2.2 s oscillation is the `ptloop01`/`ptloop02` elements" → **mine, and refuted by decoding them.** Their sweeps run 7.5 s and - 9.5 s; a 22 s capture showed 8 peaks, not 3. The period's source is unidentified. + 9.5 s; a 22 s capture showed 8 peaks, not 3. The period's source is unidentified. ⟨disc⟩ * "the developer splash cannot be rendered by `screen render` at all" → it can, with **`--all`**. It is only invisible to the *default* listing, which filters on - `is_build`. [`ui-title-build-map.md`](ui-title-build-map.md) + `is_build`. [`ui-title-build-map.md`](ui-title-build-map.md) ⟨our-tool⟩ * "the four `GP_TITLE` entries that are not screen builds are unidentified" → they - are the **splash**: 10/13 the `SQUARE ENIX` logo, 11/14 the developer logos. + are the **splash**: 10/13 the `SQUARE ENIX` logo, 11/14 the developer logos. ⟨disc⟩ * ~~"the boot title is phase 2 and the attract title is phase 4 state 0, and only phase 2 handles Ⓐ" → refuted~~ — **the REFUTATION is withdrawn.** The test assumed Ⓑ lands in phase 4 state 0; the event-0 block actually sets **phase = 2** and state = 0 together, so it probed phase 2. The hypothesis is untested, not dead. - [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) + [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) ⟨image⟩ * "any title after the first one refuses input" → too broad. The **Ⓑ-returned title accepts Ⓐ**; only the **attract**-returned title is inert. - [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) + [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) ⟨capture⟩ * "`sub_821C6458` is `GamePart_Title`'s state machine" → **mine, imprecise.** It is the machine for **phase 4** of a five-way outer dispatch at `this+132`; phase 0 is the splash. The ten states and eighteen edges are phase 4 only. - [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) + [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) ⟨image⟩ * "`sub_821CC860` is the game's by-name screen factory" → **mine, and wrong.** Its decoded arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`, - `GAMMA_RGB` — it is a **generic name-keyed lookup**, mostly config. + `GAMMA_RGB` — it is a **generic name-keyed lookup**, mostly config. ⟨image⟩ * "`DIFFICULTY` and `EXTRA_MENU` are corroborated screen names" → **mine, and wrong.** Neither appears in `r5` at any of the 48 call sites; they were strings merely referenced by the same functions. Only `TUTORIAL_MENU` survives. - [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) + [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) ⟨image⟩ * "the `{func, func, ptr}` triples at `0x820a3b48` are a GamePart state table" → **static-initialiser records trailing the `RegisterToFactory` strings.** The bytes before them are the tail of a diagnostic string and the data column is zero-filled descriptors. - [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) + [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) ⟨image⟩ * "the boot sequence is driven by a table the game reads" → it is **not data-driven**; four search spaces closed, transitions are calls with an id - argument. [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) + argument. [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) ⟨image⟩ * "`config.ini` is a GamePart settings table, so the boot order is in it" → its `[SYSTEM]` section is **empty**; the only populated section is `[LANGUAGE]`. - [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) + [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) ⟨disc⟩ * "the attract loop is `GP_ADVERTISE_DEMO` (GamePart 1)" → 🟡 id 1 has **no registration site** in the shipped build; the attract is the title replaying - `ADV.wmv`. + `ADV.wmv`. ⟨image⟩ * "the 29 id-table names are 29 distinct GameParts" → 24 register, and `3`/`4` - are the **same class** (`GamePart_SaveLoad`). + are the **same class** (`GamePart_SaveLoad`). ⟨image⟩ * "Ⓐ on `NEW GAME` leads to a standing black-screen hang" → it opens **`DIFFICULTY`**, then **`SELECT DATA`**. What looked like a hang was a menu waiting for input that nobody pressed; the crash that follows is the already documented `sub_823070B0` cache throw. - [`menu-navigation-semantics.md`](menu-navigation-semantics.md) + [`menu-navigation-semantics.md`](menu-navigation-semantics.md) ⟨capture⟩ * "tapping Ⓐ during the boot movie breaks the title" → **one** tap skips the movie cleanly and the title works normally. It is *hammering* (88 presses) that - breaks it. [`movie-binding.md`](movie-binding.md) + breaks it. [`movie-binding.md`](movie-binding.md) ⟨capture⟩ * "the attract movie runs ~85 s, so it is not `ADV.wmv` (137 s)" → **mine, and wrong.** Sampling began 39 s into the movie, so what was timed was its tail. The attract movie **is** `ADV.wmv`, played in full. - [`movie-binding.md`](movie-binding.md) + [`movie-binding.md`](movie-binding.md) ⟨capture⟩ * "the boot intro and the attract movie are different videos" → one asset, one - manifest slot (`ADVERTISE_MOVIE`). + manifest slot (`ADVERTISE_MOVIE`). ⟨disc⟩ * "the new-game intro is unidentified" → `MS00A` → `S00A.wmv`, decoded from the - movie manifest. + movie manifest. ⟨disc⟩ * "`GP_READY_ROOM.pak` holds the Ready Room screen" → its 317 distinct element names contain **none** of the six visible labels; it is the briefing / tactical-map content behind the `BRIEFINGS` item. - [`ready-room-probe.md`](ready-room-probe.md) + [`ready-room-probe.md`](ready-room-probe.md) ⟨disc⟩ * "the Ready Room might be 3D with a UI overlay" → it is **2D**; the pak carries - zero 3D containers. + zero 3D containers. ⟨unrecorded⟩ * "element kind `0x3002` is *the* button kind" → title-side only. All 902 `GP_READY_ROOM` bundles have **zero** `0x3002`; that pak uses `0x3000`, - `0x3004`, `0x300c`, `0x3008`. `0x3002` is one member of a `0x3000` family. + `0x3004`, `0x300c`, `0x3008`. `0x3002` is one member of a `0x3000` family. ⟨disc⟩ * "the transition between menu screens is a cut" → it is a **fade through black**; a 0.5 Hz screenshot burst simply samples too slowly to see it. - [`screen-transitions.md`](screen-transitions.md) + [`screen-transitions.md`](screen-transitions.md) ⟨capture⟩ * "the main menu's initial focus is fixed" → three boots of one script gave - `TUTORIAL`, `TUTORIAL`, `NEW GAME`. + `TUTORIAL`, `TUTORIAL`, `NEW GAME`. ⟨capture⟩ * "the title menus drop d-pad presses shorter than ~0.3 s" → **refuted by my own data.** The menu **wraps at both ends**; every press registered, and the - "missing" step was the wrap. See [`menu-navigation-semantics.md`](menu-navigation-semantics.md). + "missing" step was the wrap. See [`menu-navigation-semantics.md`](menu-navigation-semantics.md). ⟨capture⟩ * "the main menu opens with `NEW GAME` focused" → it opens on **`TUTORIAL`**, 2/2 boots (🟡 a third recorded run implies `NEW GAME`, so this is reproducible, - not invariant). + not invariant). ⟨capture⟩ * "`GP_TITLE` builds 6/8/9 are three submenus" → **8 is the JAPANESE main menu**, and 6/9 are the English and Japanese `EXTRAS` submenu. `GP_TITLE` holds eight screens shipped twice (EN/JP), and exactly one submenu. See - [`ui-title-build-map.md`](ui-title-build-map.md). + [`ui-title-build-map.md`](ui-title-build-map.md). ⟨disc⟩ * "the `PRESS Ⓐ BUTTON` plate is a state of the title build" → it is **its own - build** (2/3), composited over build 4 and faded in a beat later. + build** (2/3), composited over build 4 and faded in a beat later. ⟨disc⟩ * "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150 - `ANON_`, 0 `rtti_present`, 0 base classes. -* "the sibling vtable methods name the class" → they cannot. -* "`xrefs` can name the callers of a vtable method" → no. + `ANON_`, 0 `rtti_present`, 0 base classes. ⟨image⟩ +* "the sibling vtable methods name the class" → they cannot. ⟨unrecorded⟩ +* "`xrefs` can name the callers of a vtable method" → no. ⟨unrecorded⟩ * "the `ind_call` refutation voids existing corpus claims" → damage bounded, 4/4 caller claims verify. But **`xrefs.ind_call` is a CROSS PRODUCT** — always - filter `kind='call'`. + filter `kind='call'`. ⟨image⟩ * "`BASE_INFO` marks the 5-slot screen family" → it discriminates - screen-config from table-read, 9/9 vs 10/10. + screen-config from table-read, 9/9 vs 10/10. ⟨image⟩ * "a high key count means a rich screen" → `sub_82297550` / `sub_822A2F00`'s 27 - "keys" are coordinate pairs, i.e. a layout table. + "keys" are coordinate pairs, i.e. a layout table. ⟨image⟩ * "`EX_` = the CHALLENGE-mission debriefing" → `EX_` is **EXTRA**, mission-kind - 3. -* "the `EX_` selection has not been shown" → it has: `[[obj+4]+184] == 3`. + 3. ⟨unrecorded⟩ +* "the `EX_` selection has not been shown" → it has: `[[obj+4]+184] == 3`. ⟨unrecorded⟩ ## Stages, missions and the challenge set * "the disc's stages are numbered 1..28" → S01–S16 story, S17 **cut**, S18–S23 - tutorials, S24–S29 challenge. -* "S24–S29 are story missions" → they are the challenge missions. + tutorials, S24–S29 challenge. ⟨disc⟩ +* "S24–S29 are story missions" → they are the challenge missions. ⟨unrecorded⟩ * "the challenge missions have their own maps" → they reuse - `GP_MAIN_GAME_E.pak`'s stage records. + `GP_MAIN_GAME_E.pak`'s stage records. ⟨disc⟩ * "the challenge `REQUIREMENT` values are 16,25,26,27,29" → the chain is - 16→24→25→26→27→28. + 16→24→25→26→27→28. ⟨unrecorded⟩ * "the `Extra0n` family shares one leaderboard metric" → `RECORD_TYPE` is - per-stage: 3 Time / 3 Points. -* "stage = filled SHAB count + 1" → refuted. -* "the first TRIGGER is always the point of no return" → refuted. + per-stage: 3 Time / 3 Points. ⟨unrecorded⟩ +* "stage = filled SHAB count + 1" → refuted. ⟨unrecorded⟩ +* "the first TRIGGER is always the point of no return" → refuted. ⟨unrecorded⟩ * "`EnumUnit_S14.tbl` might be missing" / "S14's 13 are a manifest omission" / "asteroids are exempt from the manifest" → S14's 13 are **dangling - deployments**. NEEDS-HUMAN: fly S14. -* "`StageMessageSet_S02.tbl` is not in the pak" → it is. -* "`S28_p1` has an asteroid volume with no definition" → refuted. -* "`test_s8p1_asteroid.tbl` is test-only" → refuted. -* "the settings family has 28 or 29 objects" → 24. + deployments**. NEEDS-HUMAN: fly S14. ⟨disc⟩ +* "`StageMessageSet_S02.tbl` is not in the pak" → it is. ⟨unrecorded⟩ +* "`S28_p1` has an asteroid volume with no definition" → refuted. ⟨unrecorded⟩ +* "`test_s8p1_asteroid.tbl` is test-only" → refuted. ⟨unrecorded⟩ +* "the settings family has 28 or 29 objects" → 24. ⟨unrecorded⟩ ## ISL / mission scripting -* "the bytecode is in the `.embsec_` sections" → refuted. -* "only 31 built-ins take a unit" → refuted. -* "`sub_8230C398` is the message pump" → refuted. -* "`bus+8216` is the subscriber registry" → refuted. -* "the ScriptPhase vtable is ≥200 slots" → 113. +* "the bytecode is in the `.embsec_` sections" → refuted. ⟨unrecorded⟩ +* "only 31 built-ins take a unit" → refuted. ⟨unrecorded⟩ +* "`sub_8230C398` is the message pump" → refuted. ⟨unrecorded⟩ +* "`bus+8216` is the subscriber registry" → refuted. ⟨unrecorded⟩ +* "the ScriptPhase vtable is ≥200 slots" → 113. ⟨unrecorded⟩ ## IDXD, paks and naming * "IDXD record keys are `name_hash`" → record keys are **`tag_hash`** - (case-SENSITIVE); `name_hash` is case-INSENSITIVE and used for pak keys. -* "pak TOC order is stage order" / "TOC order is semantic order" → it is not. + (case-SENSITIVE); `name_hash` is case-INSENSITIVE and used for pak keys. ⟨disc⟩ +* "pak TOC order is stage order" / "TOC order is semantic order" → it is not. ⟨unrecorded⟩ * "the executable holds the asset names" → the image names **no data value at - all**; that route is powerless. -* "the image might name a data VALUE" → powerless. -* "the XPR2 manifest names hash to the DefTables tables" → refuted. + all**; that route is powerless. ⟨image⟩ +* "the image might name a data VALUE" → powerless. ⟨unrecorded⟩ +* "the XPR2 manifest names hash to the DefTables tables" → refuted. ⟨unrecorded⟩ * "the `DefTables` model names are unreachable" → reachable via the `Enumerate` - declaration tables (1 413/1 425, 99.2 %). -* "the `GP_MAIN_GAME_*` unnamed block is undiscovered data" → refuted. -* "each `GP_MAIN_GAME_*` `Enumerate` object declares something" → refuted. -* "`GP_HANGAR_ARSENAL` is missing data tables" → refuted. + declaration tables (1 413/1 425, 99.2 %). ⟨disc⟩ +* "the `GP_MAIN_GAME_*` unnamed block is undiscovered data" → refuted. ⟨unrecorded⟩ +* "each `GP_MAIN_GAME_*` `Enumerate` object declares something" → refuted. ⟨unrecorded⟩ +* "`GP_HANGAR_ARSENAL` is missing data tables" → refuted. ⟨unrecorded⟩ * "the `Enumeration` self-index can name objects" → a self-index names - **records, not files**. -* "a per-pak prefix might close the 2D blocker" → no. + **records, not files**. ⟨unrecorded⟩ +* "a per-pak prefix might close the 2D blocker" → no. ⟨unrecorded⟩ * "the `+` paths might name the 2D or `GP_READY_ROOM` keys" → the `+`-dictionary - route is exhausted, 0 of 1 817. -* "the `game:\` paths are unresolved" → refuted. + route is exhausted, 0 of 1 817. ⟨disc⟩ +* "the `game:\` paths are unresolved" → refuted. ⟨unrecorded⟩ * "a set-difference over file names can see reuse" → it cannot; **join per - USER**. Per-pak copies are ×6. + USER**. Per-pak copies are ×6. ⟨disc⟩ ## Audio @@ -281,158 +381,169 @@ neighbourhood, not just the line. * "which bank the menu plays is not on the disc" → the **cue table** cannot say (all BGM cues are numeric), but the **code** can: `sub_821C5580` plays cue **1103 = `BGM_103`**, and its two declared waves match the two streams the XMA - probe saw byte-for-byte. + probe saw byte-for-byte. ⟨image⟩ * "the observed BGM stream sizes match no bank's declared waves, so the game hands the decoder a window" → **mine, and wrong** — I checked only the `BGM_0xx` rows. - They are `BGM_103`'s two waves exactly; the game hands over the whole wave. + They are `BGM_103`'s two waves exactly; the game hands over the whole wave. ⟨disc⟩ * "an individual SE cue's audio cannot be extracted" → **mine, and wrong.** `--xma_param_probe=true` logs each stream's head bytes; searching them in - `Static.slb` locates the wave exactly. [`menu-audio-cues.md`](menu-audio-cues.md) + `Static.slb` locates the wave exactly. [`menu-audio-cues.md`](menu-audio-cues.md) ⟨capture⟩ * "`Static.slb` has no wave boundaries, so its layout is unknown" → it is a **packed run** of whole 2 048-byte XMA packets with no delimiters — the two - located cues are contiguous. There is nothing to scan for, by design. + located cues are contiguous. There is nothing to scan for, by design. ⟨disc⟩ * "`Pj_Silph.xgs` holds the cue→wave index, so parse XACT" → **no XACT container exists on this disc**: 0 × `XGSF`/`SDBK`/`WBND` in all 1.08 GB of `sound.pak`, and no `XACT`/`.xgs` string in the executable. The extensions are the authoring - tool's, not the format's. [`menu-audio-cues.md`](menu-audio-cues.md) + tool's, not the format's. [`menu-audio-cues.md`](menu-audio-cues.md) ⟨disc⟩ * "every sound cue resolves to its own `.slb` bank" → the 322 `SE_*` cues do not; - **0 of 322** are in `FILES`, and `BANK_SE` puts them all in `Static.slb`. + **0 of 322** are in `FILES`, and `BANK_SE` puts them all in `Static.slb`. ⟨disc⟩ * "`Static.slb` can be split into waves like any other bank" → it holds **0 `RIFF`, 0 `seek`, 0 `WAVE`** across all 8 353 472 readable bytes. - [`menu-audio-cues.md`](menu-audio-cues.md) + [`menu-audio-cues.md`](menu-audio-cues.md) ⟨disc⟩ * "`BGM_001.slb` is three sub-waves (10 KB + 4.47 MB + 4.67 MB)" → the 10 KB is - the **bank header**; a bank is **two** waves. + the **bank header**; a bank is **two** waves. ⟨disc⟩ * "a music bank's two waves might be intro + loop, two variations, or two halves" → they are **two stems of one performance, played together** — equal duration in 32/32 banks, and sample-synchronous. - [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md) + [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md) ⟨disc⟩ * "`BGM_106`–`BGM_109` break the two-wave rule" → they are the leading-region - straddle; realigned across entry boundaries they obey it. + straddle; realigned across entry boundaries they obey it. ⟨disc⟩ * "the cue table names which BGM belongs to which screen" → all 32 BGM cues are - numeric (`BGM_001`…`BGM_109`). + numeric (`BGM_001`…`BGM_109`). ⟨disc⟩ ## Units, weapons, effects and assets -* "`Generic` (394) is the unit datasheet" → refuted. +* "`Generic` (394) is the unit datasheet" → refuted. ⟨unrecorded⟩ * "a loadout's `Arm1` names an item" → it names a **hardpoint slot** - (`Turret_NNN`), 59/59. -* "`EnumUnit` and the unit datasheet share a vocabulary" → they do not. + (`Turret_NNN`), 59/59. ⟨disc⟩ +* "`EnumUnit` and the unit datasheet share a vocabulary" → they do not. ⟨unrecorded⟩ * "the roster is the `Generic.Model` set" → roster 40, `Generic.Model` 46, - `GameResourceID` 480 — three vocabularies. + `GameResourceID` 480 — three vocabularies. ⟨disc⟩ * "every unit ID is `UN_###__`" → the grammar is - `UN_###_[_]_`. -* "`_EXn` is the `Extra0n` index" → three different `EX` vocabularies exist. + `UN_###_[_]_`. ⟨disc⟩ +* "`_EXn` is the `Extra0n` index" → three different `EX` vocabularies exist. ⟨unrecorded⟩ * "the only two `_EX5` names on the disc are the AA gun and the DeltaSaber" → - refuted. -* "running the tutorial will instantiate the `_Ttrl` weapons" → refuted. -* "the disc has exactly three `EnumWeapon` tables" → four. + refuted. ⟨unrecorded⟩ +* "running the tutorial will instantiate the `_Ttrl` weapons" → refuted. ⟨unrecorded⟩ +* "the disc has exactly three `EnumWeapon` tables" → four. ⟨unrecorded⟩ * "the `wep_NN` package gaps are unshipped weapons" / "`wep_85` is the tip of a - family" → `wep_85` is the **only** declared-but-unshipped asset (59/0/1/26). -* "nothing is deployed without being declared" → refuted. -* "effects are one namespace" → refuted. -* "the 58 undeclared effect names are missing assets" → refuted. -* "all five orphan effects are unshipped" → refuted. -* "`eff_f0002` ships in `Base.xpr`" → refuted. -* "`Base.xpr` holds more bound effects than `ptc_pack`" → refuted. -* "the 34 unlocated are a scatter" → refuted. -* "the 9 unlocated might be under another prefix" → refuted. -* "`ptc_pack` has 532 names" → 727. + family" → `wep_85` is the **only** declared-but-unshipped asset (59/0/1/26). ⟨disc⟩ +* "nothing is deployed without being declared" → refuted. ⟨unrecorded⟩ +* "effects are one namespace" → refuted. ⟨unrecorded⟩ +* "the 58 undeclared effect names are missing assets" → refuted. ⟨unrecorded⟩ +* "all five orphan effects are unshipped" → refuted. ⟨unrecorded⟩ +* "`eff_f0002` ships in `Base.xpr`" → refuted. ⟨unrecorded⟩ +* "`Base.xpr` holds more bound effects than `ptc_pack`" → refuted. ⟨unrecorded⟩ +* "the 34 unlocated are a scatter" → refuted. ⟨unrecorded⟩ +* "the 9 unlocated might be under another prefix" → refuted. ⟨unrecorded⟩ +* "`ptc_pack` has 532 names" → 727. ⟨unrecorded⟩ * "the `_e`/`_f` law is effect-FIELD-specific" → it is the **faction law**, - 564/564. -* "a disc-wide `.xpr` byte search can show an effect is ABSENT" → it cannot. -* "`rot_n001` is on the disc" → refuted. -* "`rou_f004`'s mesh is in `Stage_S28.xpr`" → it is in `DeltaSaber_A.xpr`. + 564/564. ⟨unrecorded⟩ +* "a disc-wide `.xpr` byte search can show an effect is ABSENT" → it cannot. ⟨unrecorded⟩ +* "`rot_n001` is on the disc" → refuted. ⟨unrecorded⟩ +* "`rou_f004`'s mesh is in `Stage_S28.xpr`" → it is in `DeltaSaber_A.xpr`. ⟨unrecorded⟩ * "`parent` + `_all` + `_child` is the composite-model convention" → refuted. `_hangar` **is** real (59 of 166, 59/59 with a bare twin); `_all`/`_child` is - not. -* "`Motion_guard_start` has no damage variants" → refuted. -* "`CoverArea` bits 2 and 3 are mutually exclusive" → refuted. -* "the 27 unresolved `NamePlate` values are missing objects" → refuted. + not. ⟨disc⟩ +* "`Motion_guard_start` has no damage variants" → refuted. ⟨unrecorded⟩ +* "`CoverArea` bits 2 and 3 are mutually exclusive" → refuted. ⟨unrecorded⟩ +* "the 27 unresolved `NamePlate` values are missing objects" → refuted. ⟨unrecorded⟩ ## LOD, background and misc tables * "`EnumLODSet_*` is a per-stage family" → `EnumLODSet_test.tbl` serves 17 - stages; 17+5+1 = 23. -* "there are 8 orphan LOD tables" → 6. -* "the orphans are stale copies of `_test`" → refuted. -* "S25 is absent from the `DefTables` LOD families" → refuted. -* "`BackGroundID` has no referent anywhere" → it is an **identity**. -* "`BackGroundPackage == BG_.xpr`" → refuted. -* "`ID` + `Package` is a convention" → refuted. -* "`Placement_*` / `RouteTest_*` are unattached" → refuted. -* "the `AsteroidDefinition` join does not reproduce by hash" → it does. -* "the 8-value frame is a new finding" → it was already in the corpus. + stages; 17+5+1 = 23. ⟨disc⟩ +* "there are 8 orphan LOD tables" → 6. ⟨unrecorded⟩ +* "the orphans are stale copies of `_test`" → refuted. ⟨unrecorded⟩ +* "S25 is absent from the `DefTables` LOD families" → refuted. ⟨unrecorded⟩ +* "`BackGroundID` has no referent anywhere" → it is an **identity**. ⟨unrecorded⟩ +* "`BackGroundPackage == BG_.xpr`" → refuted. ⟨unrecorded⟩ +* "`ID` + `Package` is a convention" → refuted. ⟨unrecorded⟩ +* "`Placement_*` / `RouteTest_*` are unattached" → refuted. ⟨unrecorded⟩ +* "the `AsteroidDefinition` join does not reproduce by hash" → it does. ⟨unrecorded⟩ +* "the 8-value frame is a new finding" → it was already in the corpus. ⟨unrecorded⟩ ## Loaders, config and tuning -* "the config reader is XML" → INI. -* "`sub_822F9498` is the unit-definition loader" → it is `PlayerParams`'s. -* "`sub_822AE628` reads the main-game `Tweak`" → refuted. -* "`sub_8230D1F8` is a rank table" → it is the stage-settings loader. -* "`sub_82286BC8`'s key list is new" → refuted. -* "`sub_825F2CF0` / `sub_825F2F88` read a post-processing table" → refuted. +* "the config reader is XML" → INI. ⟨unrecorded⟩ +* "`sub_822F9498` is the unit-definition loader" → it is `PlayerParams`'s. ⟨unrecorded⟩ +* "`sub_822AE628` reads the main-game `Tweak`" → refuted. ⟨unrecorded⟩ +* "`sub_8230D1F8` is a rank table" → it is the stage-settings loader. ⟨unrecorded⟩ +* "`sub_82286BC8`'s key list is new" → refuted. ⟨unrecorded⟩ +* "`sub_825F2CF0` / `sub_825F2F88` read a post-processing table" → refuted. ⟨unrecorded⟩ * "`Booster` is a new schema" / "`Booster` is the player craft's flight - envelope" → refuted; nothing selects `Booster`. + envelope" → refuted; nothing selects `Booster`. ⟨unrecorded⟩ * "the `AnalogRevice`/`Tweak` block is unreachable" → reachable - (`sub_821A6CF0`, base `0x820A1630`). -* "a 0-xref string block has no reader" → refuted. -* "the AI table was NEEDS-HUMAN" → refuted. -* "the `PG*` HUD names are undocumented" → they are documented. -* "a base-solver row identifies a FUNCTION" → it does not. + (`sub_821A6CF0`, base `0x820A1630`). ⟨unrecorded⟩ +* "a 0-xref string block has no reader" → refuted. ⟨unrecorded⟩ +* "the AI table was NEEDS-HUMAN" → refuted. ⟨unrecorded⟩ +* "the `PG*` HUD names are undocumented" → they are documented. ⟨unrecorded⟩ +* "a base-solver row identifies a FUNCTION" → it does not. ⟨unrecorded⟩ * "a 64K-boundary base is low confidence" → **inverted**; it is high - confidence. -* "the 0x820B0000 cluster is a false positive" → refuted. + confidence. ⟨unrecorded⟩ +* "the 0x820B0000 cluster is a false positive" → refuted. ⟨unrecorded⟩ * "a pointer to a function in the image implies a registry" → refuted. - `.pdata` is not a registry. -* "the 13 player-facing chatter tables are the WINGMAN tables" → refuted. -* "the 8 undeclared chatter tables are tutorial chatter" → refuted. -* "other datasheets ship a schema too" → refuted. + `.pdata` is not a registry. ⟨unrecorded⟩ +* "the 13 player-facing chatter tables are the WINGMAN tables" → refuted. ⟨unrecorded⟩ +* "the 8 undeclared chatter tables are tutorial chatter" → refuted. ⟨unrecorded⟩ +* "other datasheets ship a schema too" → refuted. ⟨unrecorded⟩ ## Encoding and text -* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328. -* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**. +* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328. ⟨unrecorded⟩ +* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**. ⟨unrecorded⟩ * "the splash `_eff` glows hold a constant α ≈ 33, contradicting their declared 255 plateau" → **mine, and refuted within the iteration.** They ramp 34 → 255 in exact steps of 34. I had printed the series' minimum and read it as its - range. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) -* "the declared keyframe timeline reproduces the captured splash" → **refuted for - multi-keyframe elements.** `palogo_gamearts` is still at `a=255` nine frames - after its declared `a=32`, and its declared 80-frame fade-in is never drawn. - The `_eff` glows do reproduce, exactly — so this is about the group timeline, - not about the interpolation law. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) + range. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) ⟨disc⟩ +* 🟡 "the declared keyframe timeline reproduces the captured splash" — **our + reader disagrees** (was ❌ before the R1 pass). `palogo_gamearts` is still at + `a=255` nine frames after its declared `a=32`, and its declared 80-frame + fade-in is never drawn. The capture half is sound; *"declared"* is not — it is + whatever our keyframe reader said at the time, and that reader has since + changed: the [record-layout fix](ui-keyframe-record-layout.md) re-times a + group's final pose (block 4 went to `t = 80 / 74 / 269`). The `_eff` glows do + reproduce exactly, so this is about the group timeline, not the interpolation + law. **To settle:** re-derive the declared timeline under the fixed record + layout and re-compare against the same frames. + [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) ⟨our-reader⟩ * "the `_eff` elements' agreement is the whole case for the keyframe-time shift, so it stays a shape argument" → superseded. The **hold duration** is calibration-free and decides it: observed 83 frames of full alpha against a predicted **2.0** as decoded and **80.0** shifted. The shift is nonetheless **not adopted** — it moves `GP_TITLE` build 7 by 13 % of pixels, away from its - verified English twin's brightness. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) + verified English twin's brightness. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) ⟨capture⟩ * "the `GP_TITLE` build 7 render difference is evidence against the keyframe-time shift" → **mine, and withdrawn.** It is one element, `ptlogo_eff3.t32`, a transient bloom with no resting pose; `rest()`'s dwell fallback returns a different endpoint of the same movement under each reading. The brightness comparison measured our heuristic, not the decode. - [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md) + [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md) ⟨screen-render⟩ * "`rest()`'s longest-dwell fallback picks the pose the element rests at" → **refuted structurally.** A dwell gap is time spent interpolating *between* poses; an endpoint is only held when the two poses are equal, which is a plateau, which the earlier path already returned for. Every element that - reaches the fallback has a guessed rest pose. -* "`rest()` for a plateau-less element should be the **last keyframe**" → **mine, - and refuted.** The developer splash's three sibling glows are structurally - identical and differ by one byte (`a=212` vs `a=255` at `t=45`); that rule - makes `palogo_anima_eff` alone invisible while `gamearts_eff` and `seta_eff` - stay lit. Capture box-mean ratios (0.717 / 0.723 / **0.772**) go the other way - too. [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md) + reaches the fallback has a guessed rest pose. ⟨reasoning⟩ +* 🟡 "`rest()` for a plateau-less element should be the **last keyframe**" — + **our renderer disagrees** (was ❌ before the R1 pass). The developer splash's + three sibling glows are structurally identical and differ by one byte + (`a=212` vs `a=255` at `t=45`); that rule makes `palogo_anima_eff` alone + invisible while `gamearts_eff` and `seta_eff` stay lit. Capture box-mean + ratios (0.717 / 0.723 / **0.772**) go the other way too. + ⚠️ **This entry and its re-refutation below both run through our renderer and + they disagree** — see the `rest()` pair note in the reading guide. Neither + direction has a non-renderer instrument. **To settle:** a draw capture of the + developer splash naming which of the three glows is submitted at rest. + [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md) ⟨render-vs-capture⟩ * "a keyframe `scale` of 0 means *unset*, so render at 100 %" → **refuted by a disc-wide control.** 2 166 elements have a zero-scale keyframe and **not one is zero on every keyframe**, while 1 762 grow back out of zero (`ptlogo_eff3.t32` runs 0 % → 200 %). Zero means collapsed; the renderer now draws nothing. - [`structures/ui-rat-layout.md`](structures/ui-rat-layout.md) + [`structures/ui-rat-layout.md`](structures/ui-rat-layout.md) ⟨disc⟩ * "a Japanese-locale capture is impossible in this container, because canary has no `user_language` cvar" → **mine, and refuted the next iteration.** The cvar really is absent, but the language is persisted in @@ -440,181 +551,211 @@ neighbourhood, not just the line. `0x912`, located from three struct landmarks) and that file is writable. The capture is still not *taken* — a Japanese run never reached the title in 787 s — but it needs a longer run, not a rebuilt emulator. - [`tools/re-capture/set_console_language.py`](../../tools/re-capture/set_console_language.py) + [`tools/re-capture/set_console_language.py`](../../tools/re-capture/set_console_language.py) ⟨canary-source⟩ * "the Japanese-locale run never reached the title in 787 s" → **the measurement was broken, not the run.** `wait_title.sh` carried the superseded single-pixel oracle. Re-run with `is_title.py`: the game still did not present the interactive title, but that is now a measured statement (zero green-glyph - pixels, correlation ≤ 0.22 to either build-7 render) rather than an artefact. + pixels, correlation ≤ 0.22 to either build-7 render) rather than an artefact. ⟨harness⟩ * "the Japanese-locale run fails to reach the interactive title *because of the locale*" → **refuted by the English control.** 75 samples over 734 s with the same flags and oracle, every one glyph = 0. Neither locale presents the - interactive title without a pad press. + interactive title without a pad press. ⟨control⟩ * "neither locale reaches the interactive title without a pad press" / "the game sat in the attract loop for 604 s" → **withdrawn as causes.** Both rest on runs whose polling loop sampled every ~41 s, because `screenshot` costs 10.8 s while the emulator runs (0.117 s idle, 92×). A title lasting a few seconds would be missed. The observations stand; the conclusions drawn from - them do not. [`capture-harness-status.md`](capture-harness-status.md) + them do not. [`capture-harness-status.md`](capture-harness-status.md) ⟨harness⟩ * "the boot harness fails because its polling loop samples every ~41 s, slower than the title screen lasts" → **mine, and refuted by my own fix.** The sampling defect was real (3.98 s → 0.29 s per sample, 13.7×, control-verified at 753/327), but a probe running at 3.99 fps for **420 continuous seconds — 1 674 samples — still saw zero green-Ⓐ pixels.** Sampling rate was not the - cause. [`capture-harness-status.md`](capture-harness-status.md) -* ~~"neither locale reaches the interactive title without a pad press"~~ → - withdrawn last iteration for want of evidence, now **reinstated as a - measurement**: 1 674 dense samples over 420 s, English, zero glyph frames. - ⚠️ Reach: a mid-run window only; it says nothing about the boot title. + cause. [`capture-harness-status.md`](capture-harness-status.md) ⟨harness⟩ +* 🟡 "neither locale reaches the interactive title without a pad press" — **our + harness disagrees** (was reinstated as ❌ before the R1 pass). 1 674 dense + samples over 420 s, English, zero glyph frames. ⚠️ Reach: a mid-run window + only; it says nothing about the boot title. 🔴 **And the entry two below + withdrew a sibling negative — 2 391 frames, same probe — because a + long-lived `x11grab` stream degrades to 1.60 fps and then repeats one stale + frame.** That withdrawal was written *after* this reinstatement and never + reached it. A dense negative from a stream that may be frozen is not a dense + negative, and this run used the same instrument for a comparable duration. + **To settle:** re-run with the stall cross-check the withdrawal describes + (compare surface mean against an `import` grab mid-run), or sample with + per-frame `import` rather than a persistent stream. ⟨harness⟩ * "the PRESS Ⓐ plate appears only in the **boot** title window, which mid-run sampling could never catch" → **mine, and refuted.** The fast probe was attached at t=0: **2 391 frames over 600 s at 3.98 fps from launch**, max glyph 0. The plate did not appear at any point in the first ten minutes. - [`capture-harness-status.md`](capture-harness-status.md) + [`capture-harness-status.md`](capture-harness-status.md) ⟨harness⟩ * "2 391 frames over 600 s from t=0, max glyph 0, therefore the title never appears in the first ten minutes" → **withdrawn: the instrument stalls.** A single long-lived x11grab stream degrades 3.98 → 1.60 fps and then freezes, repeating one stale frame; cross-checked, it read surface mean 5.21 where `import` read 125.65 at the same moment. A dense negative from a frozen stream - is not a negative. [`capture-harness-status.md`](capture-harness-status.md) + is not a negative. [`capture-harness-status.md`](capture-harness-status.md) ⟨harness⟩ * "the `T8aD` layer key fully determines a screen's paint order" → **refuted, and the remainder is undecodable.** Elements sharing a key are tied; on the title the game paints the five tied `ptlogo_back2eff` glows `1,2,5,3,4` while the declaration table, the RATC child order and **every** field in the `T8aD` header (exhaustive 0x00–0x7f, u8/u16/u32, both directions — 0 matches against 64 for the declaration-order control) all give `1,2,3,4,5`. - [`ui-paint-order-derived-check.md`](structures/ui-paint-order-derived-check.md) + [`ui-paint-order-derived-check.md`](structures/ui-paint-order-derived-check.md) ⟨capture⟩ * "SE audio is undecodable from the disc — no XACT container exists anywhere" (as it stood on the **handoff page**) → **stale**: `menu-audio-cues.md` had already retracted it and located three cues in `Static.slb` that decode to PCM. The retraction never reached the row the port agent reads. Handoff row fixed; - `tools/re-capture/handoff_lint.py` now checks for this class. -* "which of `8AX` and `ptbase` the game draws needs a per-draw capture recording - texture base addresses" → **mine, and refuted — it is settled statically.** The + `tools/re-capture/handoff_lint.py` now checks for this class. ⟨corpus⟩ +* 🟡 "which of `8AX` and `ptbase` the game draws needs a per-draw capture + recording texture base addresses" — **our renderer is in the path** (was ❌ + before the R1 pass; it read *"refuted — it is settled statically"*). The two carry the same art at two resolutions, so neither compares usefully against a capture; their *difference* does. Correlating the capture's departure-from-upscale against the 8AX-only detail gives +0.0475 (main menu) and +0.0634 (title), both 68 % of ceiling against matched controls of ≤0.0095. The game draws the full-res `8AX`. - [`ui-8ax-fullres-background.md`](structures/ui-8ax-fullres-background.md) + 🟡 **Downgraded from ❌ by the R1 pass.** The conclusion may well be right — + the matched controls are real work — but the instrument is our decode + correlated against a capture, and *this decode changed underneath it*: `8AX` + was later found not to be a name at all (see the `ratc-child-names` entry), + which is a defect in the same reader that produced the compared texture. + **To settle:** the per-draw capture the claim says is unnecessary — texture + base addresses name the drawn surface directly, with no correlation. + [`ui-8ax-fullres-background.md`](structures/ui-8ax-fullres-background.md) ⟨render-vs-capture⟩ * "the pixel-pair ratio shows the capture is native, not an upscale" → **mine, and withdrawn as evidence.** Upscales give 0.00–0.72, native 0.98, capture 1.01 — but additive noise pushes any such ratio toward 1, and both "native + noise" and "bilinear upscale + noise" fit the observed values. The conclusion happens - to be right; this test does not establish it. + to be right; this test does not establish it. ⟨our-tool⟩ * "the render-vs-capture gamma may be canary's own BT.709 output transform, since `kernel_display_gamma_type = 2`" → **mine, and refuted from the source.** That cvar is the value a `kStub` getter (`VdGetCurrentDisplayGamma`) hands the **guest**; the game builds its own ramp from it and canary applies the guest's `DC_LUT` ramp in the swap path. No emulator-side gamma post-process exists to subtract. 🟡 Whether this game installs a ramp at all is still unestablished. - [`ui-render-tone-curve.md`](structures/ui-render-tone-curve.md) + [`ui-render-tone-curve.md`](structures/ui-render-tone-curve.md) ⟨canary-source⟩ * "the GPU trace produced nothing because either the CLI flag did not reach the cvar or `BeginTracing()` failed silently" → **both wrong.** The trace writer is **compiled out**: `trace_writer.h` gates it on `#ifdef NDEBUG`, so a release build has no writer at all. Confirmed with a control — the format string `_stream.xtr` appears **once** in the Debug binary and **zero** times in the Release binary `run-canary` actually uses. - [`capture-harness-status.md`](capture-harness-status.md) + [`capture-harness-status.md`](capture-harness-status.md) ⟨canary-source⟩ * "the `T8aD` `+0x04` bit `0x02` means the sprite's name contains `eff`" → **refuted, now on evidence.** `ptlogo_back2eff` is an `eff` name with the bit clear; the attribution is confirmed by header-order pairing (18/18 on build 4) rather than by a size match, which cannot separate it from the same-sized `ptlogo_back2eff5`. All 10 bit-set sprites *are* `eff` names, so the - implication runs one way only. + implication runs one way only. ⟨disc⟩ * "the bit `0x02` marks a transient element" → **refuted.** `pteff03` and `pteff03a` carry the bit and run to `t=250`, ramping to `a=255` and holding. - [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) + [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) ⟨disc⟩ * "bit `0x02` set ⇒ the sprite's name contains `eff`" (the one-way reading that survived the biconditional's refutation) → **mine, and refuted disc-wide the next iteration.** True 10/10 on `GP_TITLE` build 4; over 14 709 sprites it fails on **2 657 of 4 995** bit-set ones. `P(eff|set) = 0.468` against `P(eff|clear) = 0.144` — an association, not an implication. - [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) + [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) ⟨disc⟩ * "`T8aD +0x04` bit `0x02` selects premultiplied alpha" → **refuted.** Premultiplied requires `RGB ≤ A` everywhere; over 170 decoded textures the flagged group violates it on a median **52.5 %** of pixels against **30.2 %** unflagged — both far from premultiplied, and the flagged group *further*. - [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) + [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) ⟨disc⟩ * "`build-reborn test` cannot terminate" → **mine, and too strong; corrected the next iteration.** It is heavy, not hung: 19 of 166 `.xpr` containers exceed 25 s, `Stage_S02` completes in **144 s** with `rc = 0`, and one full pass is ~45–60 minutes. The 3 h 26 m observed was that work at a load average of 9–14, inflated by my own two duplicate runs. - [`test-suite-runtime.md`](test-suite-runtime.md) + [`test-suite-runtime.md`](test-suite-runtime.md) ⟨environment⟩ * "the case for the keyframe-time shift rests on a single element" → **no longer true.** Three elements across two screens discriminate and all favour it: `palogo_gamearts` and `palogo_seta` hold full alpha 83 frames, `palogo_sqex` ≥77, against 6–8 predicted by the current reading and 80–102 by the shifted one. The `_eff` glows fit both and argue against neither. - [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) -* "an element with no held pose should be drawn as NOTHING rather than at a - guessed endpoint" → **mine, and refuted.** Suppressing every plateau-less - element and re-correlating against the live captures: title +0.9500 → +0.6839, - main menu +0.9460 → +0.9037, `EXTRAS` +0.9440 → +0.9094. Worse on all three. + [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) ⟨capture⟩ +* 🟡 "an element with no held pose should be drawn as NOTHING rather than at a + guessed endpoint" — **our renderer disagrees** (was ❌ before the R1 pass). + Suppressing every plateau-less element and re-correlating against the live + captures: title +0.9500 → +0.6839, main menu +0.9460 → +0.9037, `EXTRAS` + +0.9440 → +0.9094. Worse on all three. ⚠️ *"Drawing something scores better + than drawing nothing"* is weak evidence that the **guessed pose** is the right + one: a wrong pose still puts roughly the right ink in roughly the right place, + and a correlation rewards that. **To settle:** compare the guessed pose + against the drawn one per element in a draw capture, not the whole screen. ⟨render-vs-capture⟩ * "`rest()` guesses for 24.57 % of elements (3 807 of 15 493)" → **mine, and overstated by 65 %.** The plateau test marks a **single-keyframe** element as plateau-less because it has no adjacent pair — but its one pose is unambiguously its rest. 1 502 of the 3 807 are those; the genuinely ambiguous population is **2 305 (14.88 %)**. - [`ui-resting-pose.md`](structures/ui-resting-pose.md) -* "`rest()` for a plateau-less element should be the last keyframe → refuted by - the sibling argument" → **that refutation is itself refuted, this time by - measurement.** Rendering under the rule and correlating against the live + [`ui-resting-pose.md`](structures/ui-resting-pose.md) ⟨our-tool⟩ +* 🟡 "`rest()` for a plateau-less element should be the last keyframe → refuted by + the sibling argument" — **our renderer disagrees with the refutation** (was ❌ + before the R1 pass). Rendering under the rule and correlating against the live captures: publisher splash +0.9600 → **+0.9982**, developer splash +0.9643 → **+0.9758**. Making `palogo_anima_eff` invisible *improves* the match; the - sibling symmetry was my expectation, not evidence. + sibling symmetry was my expectation, not evidence. ⚠️ But *"improves the + correlation of our render"* is the same instrument the sibling argument used, + pointed the other way. The pair above and this one cancel; the question is + **open**, not settled in either direction. **To settle:** as above — a draw + capture, not a correlation. ⟨render-vs-capture⟩ * "the port's exposure to the rest-guessing defect is 14 elements" → **two.** The fallback needs an element to be plateau-less **and** multi-keyframe; title, main menu and `EXTRAS` reach it **zero** times, the two splashes once each. - [`ui-resting-pose.md`](structures/ui-resting-pose.md) -* "the shifted time reading implies rest = the last keyframe, so the plateau rule - can be dropped" → **mine, and refuted by measurement.** Applying it to every + [`ui-resting-pose.md`](structures/ui-resting-pose.md) ⟨our-tool⟩ +* 🟡 "the shifted time reading implies rest = the last keyframe, so the plateau + rule can be dropped" — **our renderer disagrees** (was ❌ before the R1 pass; + the *blank splashes* part is a renderer-internal fact and stands regardless). + Applying it to every element collapses all five screens (title 0.9500→0.6819, main menu 0.9460→0.6416, `EXTRAS` 0.9440→0.5745) and renders both splashes **blank**. A group is entry → hold → exit and the exit is the screen's *dismissal*: a displayed screen sits at the hold, not at its final pose. - [`ui-resting-pose.md`](structures/ui-resting-pose.md) -* "`rest_plateau` renders elements the game has already finished with" (as a - general claim) → **narrowed by its control.** It holds on **transient** screens + [`ui-resting-pose.md`](structures/ui-resting-pose.md) ⟨render-vs-capture⟩ +* 🟡 "`rest_plateau` renders elements the game has already finished with" (as a + general claim) — **narrowed by its control, but the control is our renderer** + (was ❌ before the R1 pass). It holds on **transient** screens only: suppressing the finished glows takes the two splashes from 0.9604/0.9659 to **0.9982/0.9980**, while the same edit costs the title 0.002, the main menu **0.092** and `EXTRAS` **0.107**. A plateau mid-animation is evidence the element is held at that point in the timeline, not that it is on screen once the screen has settled — and where a screen does settle, the rule is right. - [`ui-resting-pose.md`](structures/ui-resting-pose.md) + **To settle:** which elements a settled screen actually submits is a draw-list + question; read it off a capture rather than off a correlation delta. + [`ui-resting-pose.md`](structures/ui-resting-pose.md) ⟨render-vs-capture⟩ * "the group header's undecoded lead-in word carries a per-element start offset" → **refuted immediately.** It is `0x00000000` for all seven elements of the developer splash — glows and logos alike — while those two families are observed to run sequentially (frames 94–115 and 116–211) despite declaring - overlapping times. [`ui-group-start-time.md`](structures/ui-group-start-time.md) + overlapping times. [`ui-group-start-time.md`](structures/ui-group-start-time.md) ⟨disc⟩ * "the splash elements share one clock origin" → **refuted.** Fitting a single origin needs f₀ ≈ 93.5 for `palogo_gamearts_eff` and f₀ ≈ 103 for `palogo_gamearts`, ~19 units apart, and aligning one throws the other off by - ~9 frames at both ends. Durations match (97.8 %, 98.5 %); starts do not. + ~9 frames at both ends. Durations match (97.8 %, 98.5 %); starts do not. ⟨capture⟩ * "the glows and logos might overlap and my size-grouping merged them" → **tested and refuted.** Across all 235 captured frames, **zero** contain both a glow and a logo; f110–115 draw two glows and f116 onward two logos, with no transition - frame. The sequencing is real. + frame. The sequencing is real. ⟨capture⟩ * "a bundle's declared elements are what the screen shows" → **refuted.** Entry 11 declares three logo/glow pairs and only two are ever drawn — `palogo_anima` gets 0 frames against `palogo_gamearts`'s 95, from byte-identical keyframe times. (Reach: within the capture's frames 1–214.) - [`ui-group-start-time.md`](structures/ui-group-start-time.md) + [`ui-group-start-time.md`](structures/ui-group-start-time.md) ⟨capture⟩ * "the glow and logo phases are one bundle with elements selectively activated" → **mine, and withdrawn as unestablished.** The alternative — two compositions shown in sequence — fits equally. The texture-base test fails its control: the publisher splash is a different bundle and shares the base `0x11C30000`, so that address is a reused upload slot, not a bundle identity. What survives is that declared elements ≠ drawn elements. - [`ui-group-start-time.md`](structures/ui-group-start-time.md) + [`ui-group-start-time.md`](structures/ui-group-start-time.md) ⟨control⟩ * "two compositions shown in sequence" (as the alternative to selective activation) → **refuted statically.** It requires a bundle declaring the glows without the logos; no such bundle exists. Only four `GP_TITLE` entries carry `palogo` elements, and both developer entries declare **all six** logos and glows — so whichever was active, a subset of its elements was drawn at a time. Selective activation is reinstated on evidence. - [`ui-group-start-time.md`](structures/ui-group-start-time.md) + [`ui-group-start-time.md`](structures/ui-group-start-time.md) ⟨disc⟩ * ~~"the splash's `measured_paint_order` records a front-to-back depth order" → mis-typed; between its glow and logo halves it records only the temporal order they were seen in."~~ → **that refutation is itself refuted (same day).** The @@ -624,7 +765,7 @@ neighbourhood, not just the line. (`0xa100` < `0xa110`, `paint_order_audit`: 0 same-key ties), so the file orders them regardless. The no-overlap measurement was correct; the inference from it was not. What survives: a capture of this screen can only cross-check the order - *within* each half. [`ui-prm-primitives.md`](structures/ui-prm-primitives.md) + *within* each half. [`ui-prm-primitives.md`](structures/ui-prm-primitives.md) ⟨our-reader⟩ * "`8AX` is the name a `T8aD` is registered under" → **it is not a name at all.** It is three bytes of the preceding record's payload (`38 41 58`) that happen to @@ -632,11 +773,11 @@ neighbourhood, not just the line. name the format actually states in its `opt ` block. The claim sat in `HANDOFF.md` and `ui-8ax-fullres-background.md` as though `8AX` were a real identifier, and cost every menu screen its full-resolution background. - [`ratc-child-names.md`](structures/ratc-child-names.md) + [`ratc-child-names.md`](structures/ratc-child-names.md) ⟨disc⟩ * "`pmbase.t32` is on the disc nowhere" (the one dangling asset behind `10 144 of 10 148 references resolve`) → **withdrawn; it is on the disc.** It is the `GP_STAGE_CLEAR` child the same scan named `8AX`. With the name decoded the - count is **10 148 of 10 148**. [`ratc-child-names.md`](structures/ratc-child-names.md) + count is **10 148 of 10 148**. [`ratc-child-names.md`](structures/ratc-child-names.md) ⟨disc⟩ ## UI timing (2026-08-29) @@ -645,7 +786,7 @@ neighbourhood, not just the line. rests at `t=251` and stops moving at **`t=42`**; the title's visible build-in ends at `t≈118`, where `pteff01`, `pteff02.prm` and `ptlogoall_eff` end their ramps together. Believing `rest.t` put a port's plate 3.97 s late — - [`title-plate-delay-measured.md`](title-plate-delay-measured.md). + [`title-plate-delay-measured.md`](title-plate-delay-measured.md). ⟨disc⟩ * "the `PRESS Ⓐ` plate is composited a measured 2.13 s after the title settles, and the port should author that" → **the measurement stands, the instruction was refuted by the port.** Build 2 has a keyframe group of its own; both builds @@ -654,12 +795,12 @@ neighbourhood, not just the line. 2.13 s was a wall-clock reading stretched by Canary presenting at ~28.1 fps. ⚠️ General lesson: **a wall-clock duration off this emulator is ~6 % long**, so a measured interval that lands near a round number of units probably *is* that - number of units. + number of units. ⟨disc⟩ * "a music bank has three sub-waves" → **refuted; it was our reader.** The third is the bank header, emitted because `to_xma_riffs` derived a leading packet stream's start as `first_riff % 2048` — valid only for a header shorter than one packet. 28/28 disc-wide — - [`structures/slb-bank-header-not-a-wave.md`](structures/slb-bank-header-not-a-wave.md). + [`structures/slb-bank-header-not-a-wave.md`](structures/slb-bank-header-not-a-wave.md). ⟨disc⟩ ## The oracle harness and the container (2026-08-29) @@ -670,28 +811,35 @@ neighbourhood, not just the line. `sylph-doctor` only checks `/work` and never `$SYLPHEED_DISC`. "sylph-doctor agrees" was two instruments sharing one blind spot. → To test for the disc, ask the variable that names it: - `sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_TITLE.pak"`. -* "the main menu returns to the title on its own after ~8–10 s idle" → **refuted.** - The menu sat untouched for **≥ 60 s** without moving (correlation never leaving - 0.9245–0.9249). The ~8–10 s idle is real but belongs to the **title**. This was - the only reason "Ⓑ leaves the main menu" was classed as authored. + `sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_TITLE.pak"`. ⟨environment⟩ +* 🟡 "the main menu returns to the title on its own after ~8–10 s idle" — **our + renderer is in the path** (was ❌ before the R1 pass). The menu sat untouched + for **≥ 60 s** without moving (correlation never leaving 0.9245–0.9249). The + ~8–10 s idle is real but belongs to the **title**. This was the only reason + "Ⓑ leaves the main menu" was classed as authored. + ⚠️ Here the renderer is a *fixed yardstick* and a defect in it biases every + sample equally, so the reading is more robust than the other + `render-vs-capture` rows — a return to the title would move the correlation + far outside a 0.0004 band. It is 🟡 rather than ❌ only because a renderer + **blind spot** is still invisible to it. **To settle:** cheap — compare + consecutive captures to each other. ⟨render-vs-capture⟩ * "whole-image statistics (green / white / mean) can tell the title from the attract movie" → **refuted.** A frame of `ADV.wmv` with a bright green laser reads green 0.0018 / white 0.086 / mean (53,67,76) — the title's numbers. A probe built on it tapped Ⓐ into the movie and waited 120 s for a menu that was never coming. → Correlate against a committed capture instead, and keep movie - frames as the negative controls. + frames as the negative controls. ⟨control⟩ * "a 360-bin angular cross-correlation can measure the focus ring's rotation angle" → **refuted by its own control**: a synthetic **30°** rotation of a live frame came back as **0°** (peak 0.596), while 90/180/270° came back exactly (peak 1.000) — it only resolves exact pixel permutations. No angle was quoted; - the spin was established from brightness conservation instead. + the spin was established from brightness conservation instead. ⟨control⟩ * "a latency read off a classified `x11grab` stream is a duration" → **refuted.** At 1503 ms per classification against an 8 fps stream the consumer ran at 0.64 fps, so frames were stale and increasingly so. Four "durations" died with it. The tell was that a screen transition, a button press and a plate fade all came out at ~20–25 s. → A backlog **preserves ordering and destroys - durations**; check consumed-fps against requested-fps before quoting a time. + durations**; check consumed-fps against requested-fps before quoting a time. ⟨harness⟩ ## UI timing and transitions (2026-08-30) @@ -706,40 +854,40 @@ reachable by the person about to repeat it. carries `t = 80` / `74` / `269`: the ramp is **decoded** at 10 / 10 / 8 units. The section asserting this stood 78 lines above its own correction, in a heading, telling the port to author a decoded value. - [`screen-transitions.md`](screen-transitions.md) + [`screen-transitions.md`](screen-transitions.md) ⟨disc⟩ * "the ~14 units the fade-out does not account for are a black HOLD" → **mine, arithmetic that fit.** Measured: the content elements' own fade-outs, starting - six frames before the quad's ramp. [`screen-transitions.md`](screen-transitions.md) + six frames before the quad's ramp. [`screen-transitions.md`](screen-transitions.md) ⟨capture⟩ * "the black interval between screens is a LOAD" → **mine, refuted three ways.** Bundle size runs backwards (the 12.3 MB screen gaps 0, the 7.0 MB one gaps 3); the gap is 3 frames in three independent runs; and press-to-change latency moved ~12 frames between those runs while the gap did not move at all. - [`screen-transitions.md`](screen-transitions.md) + [`screen-transitions.md`](screen-transitions.md) ⟨capture⟩ * "Ⓑ has no black interval" → **mine, one screen pair generalised.** `EXTRAS → menu` - via Ⓑ gives **two completely empty frames**. [`screen-transitions.md`](screen-transitions.md) + via Ⓑ gives **two completely empty frames**. [`screen-transitions.md`](screen-transitions.md) ⟨capture⟩ * "`ptloop01`/`ptloop02` do not free-run on the settled title" → **mine, measured over the wrong rectangle.** The parent's `(441,270)` 200×90 is a **pivot anchor**; the leaf sweeps a 400 px quad whose left edge travels −639…1521. The zero was - measured in a dead zone. [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md) + measured in a dead zone. [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md) ⟨disc⟩ * "the game runs the splash dwells ~8.5 % long" → **mine, and it was one element's visible span read as the screen's.** The `_eff` glow is lit from t≈0 while the logo is still transparent, so the screen's visible span is the full group. Six ratios recomputed to a mean of 1.0146 with one below unity. - [`boot-order-and-splash-dwell.md`](boot-order-and-splash-dwell.md) + [`boot-order-and-splash-dwell.md`](boot-order-and-splash-dwell.md) ⟨capture⟩ * "`EXTRAS` can supply only one transition measurement — a **structural** limit" → **mine, and refuted by one `screen info`.** Build 6 declares three buttons (`ptbtn11/12/13`, kind `0x3002`). ⚠️ `sylpheed-port` had copied this sentence out of a message into their own record as an established fact **while holding the - file that refuted it**. [`data/fade-four-transitions.txt`](data/fade-four-transitions.txt) + file that refuted it**. [`data/fade-four-transitions.txt`](data/fade-four-transitions.txt) ⟨disc⟩ * "the black gap is determined by the OUTGOING screen" → **mine, superseded twice.** The same origin gives 0 and 1 to different destinations; every repeated *pair* is identical across five replicates. The pair determines; the origin only - constrains. [`data/fade-four-transitions.txt`](data/fade-four-transitions.txt) + constrains. [`data/fade-four-transitions.txt`](data/fade-four-transitions.txt) ⟨capture⟩ ### The JP title might not draw the sweep leaves at rest — ❌ REFUTED (2026-08-30, mine) -* ~~"the JP title does not draw the sweep leaves at rest"~~ — refuted, see below. -* ~~"build 7's denser logo stack occludes the sweep leaves"~~ — refuted, see below. +* ~~"the JP title does not draw the sweep leaves at rest"~~ — refuted, see below. ⟨capture⟩ +* ~~"build 7's denser logo stack occludes the sweep leaves"~~ — refuted, see below. ⟨capture⟩ I hypothesised that build 7's denser logo stack (katakana + crystalline burst) **occludes** the two sweep leaves, to explain why two JP title captures differ by @@ -753,8 +901,8 @@ The 0.32 had a different cause: **my own gate**. See below. ### "Two sessions sample a free-running clock" — ❌ REFUTED (2026-08-30, mine) -* ~~"a free-running clock lands somewhere else on a fresh boot"~~ — refuted, the gate phase-locks it. -* ~~"the in-box capture noise of 0.32 between sessions"~~ — refuted as a noise floor; it measures the trigger. +* ~~"a free-running clock lands somewhere else on a fresh boot"~~ — refuted, the gate phase-locks it. ⟨capture⟩ +* ~~"the in-box capture noise of 0.32 between sessions"~~ — refuted as a noise floor; it measures the trigger. ⟨harness⟩ `jp-title-at-rest.txt` justified a second capture as probing the between-run axis "where a free-running clock lands somewhere else on a fresh boot", and reported @@ -788,13 +936,13 @@ not land on the logos. The ±1–2 residual is not sweep. magnitude-squared coherence. A real linear filter of wave 0 reads **0.93–0.94** in every band (positive control); the measurement reads **0.027** at 1–4 kHz. No linear filter does that in a band where both waves carry energy. - [data](data/bgm-stem-coherence.txt) + [data](data/bgm-stem-coherence.txt) ⟨audio-analysis⟩ * ~~"the rear-pair reading can be tested by coherence"~~ — ❌ **refuted, and it was my own test's premise.** The control that matters — L vs R *within* one wave, genuinely one performance in two channels — reads only **0.221–0.497**, so in this material "same performance" does not imply high coherence. A 4-channel mix's rear pair is not a linear filter of its front pair, so the discriminator - never had the power to separate the two readings. The 🟡 stands. + never had the power to separate the two readings. The 🟡 stands. ⟨control⟩ ### Refutation attempt on the corpus's "two stems of ONE PERFORMANCE" — 🟡 SURVIVED, WEAKENED (2026-08-30) diff --git a/tools/stale-instrument b/tools/stale-instrument new file mode 100755 index 00000000..dc81dfb0 --- /dev/null +++ b/tools/stale-instrument @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""What did this instrument kill? -- the re-open query for `docs/re/REFUTED.md`. + + tools/stale-instrument everything, grouped + tools/stale-instrument render-vs-capture what one instrument killed + tools/stale-instrument --ours only instruments that are ours + tools/stale-instrument --check self-test; exits non-zero if broken + +Why this exists +--------------- +Rule R1. A refutation whose instrument is one of our renderers is not a +refutation -- it is *"our renderer disagrees"*. The register carries that as a +🟡, but a colour alone does not re-open anything: the failure it is there to +prevent was a real disc field sitting dead for weeks because **nothing re-opens a +claim when the instrument that killed it improves.** + +So this is the query you run *after* you fix a renderer, a reader or the harness. +It answers "what did I just invalidate?" in one command, instead of relying on +someone remembering which of 222 entries rested on the thing they changed. + +The register is the only source. There is no second list to drift out of sync +with it -- the tags live at the end of each entry, in the file people actually +read, which is this corpus's own repeatedly-learned lesson about where a fact has +to live to exist. +""" + +from __future__ import annotations + +import os +import re +import sys +from collections import OrderedDict + +REGISTER = os.environ.get("SYLPH_REGISTER", "docs/re/REFUTED.md") + +# Ours, in the sense R1 means: an instrument whose defects we cannot rule out, +# because we built it. `our-tool` is ours too, but it is not disqualifying -- +# see the note below and in the register's reading guide. +OURS = {"render-vs-capture", "screen-render", "our-reader", "harness", "our-tool"} + +# Not a verdict. It means nobody recorded how the claim died. +UNRECORDED = "unrecorded" + +TAG = re.compile(r"⟨([a-z][a-z-]*)⟩\s*$") + + +def entries(text: str): + """Every bullet, with its instrument tag and whether it is already 🟡. + + An entry is a `* ` bullet plus its continuation lines, ending at a blank + line or a heading -- the same span rule the register's own enumeration uses. + """ + out, cur, start = [], None, 0 + for i, line in enumerate(text.split("\n"), 1): + if line.startswith("* "): + if cur is not None: + out.append((start, cur)) + cur, start = [line], i + elif line.startswith("#"): + if cur is not None: + out.append((start, cur)) + cur = None + elif cur is not None: + if not line.strip(): + out.append((start, cur)) + cur = None + else: + cur.append(line) + if cur is not None: + out.append((start, cur)) + + for ln, body in out: + joined = " ".join(x.strip() for x in body) + m = TAG.search(body[-1].rstrip()) + # 🔴 THE VERDICT IS THE LEADING GLYPH, NOT ANY 🟡 IN THE ENTRY. Matching + # anywhere counted four entries as re-opened that merely carry an inline + # 🟡 caveat mid-sentence ("🟡 a third recorded run implies NEW GAME"). + # An entry whose verdict is ❌ and whose *reach* is qualified is not an + # open claim, and reporting it as one inflates exactly the number this + # tool exists to make trustworthy. + yield { + "line": ln, + "instrument": m.group(1) if m else None, + "open": body[0].startswith("* 🟡"), + "settled": body[0].startswith("* ✅"), + "text": re.sub(r"\s+", " ", joined), + } + + +def summary(text: str) -> str: + """The claim, short enough to scan a list of them.""" + t = re.sub(r"^\*\s*(🟡|❌|✅)?\s*", "", text) + t = re.sub(r"[*`~]", "", t) + return (t[:118] + "…") if len(t) > 118 else t + + +def die(msg: str): + """A HARNESS fault, exit 2 -- distinct from 'no such instrument', exit 1. + + 🔴 `sys.exit("text")` exits **1**, not 2. Every one of these sites used it, + so three faults that this file's own docstring calls exit-2 were reporting + the same code as an ordinary miss -- a real failure wearing a wrong + diagnosis, the exact shape the register catalogues elsewhere. Its own + `--check` caught it on the first run, which is the argument for having one. + """ + print(msg, file=sys.stderr) + raise SystemExit(2) + + +def load(): + if not os.path.exists(REGISTER): + die( + f"stale-instrument: {REGISTER} is not here.\n" + " Run from the repository root, or set SYLPH_REGISTER.\n" + " Exit 2: the harness is broken, not the corpus." + ) + rows = list(entries(open(REGISTER, encoding="utf-8").read())) + # 🔴 LIVENESS. A reader that parses nothing reports "no claims rest on that + # instrument" -- which reads exactly like the good news you were hoping for. + # A renamed file, a changed bullet shape or a lost tag all produce it. This + # corpus has already produced one false zero from a reader invented minutes + # earlier, so the empty case is a harness fault with its own exit code. + if not rows: + die("stale-instrument: parsed 0 entries from " + f"{REGISTER} -- exit 2, the reader is broken.") + if not any(r["instrument"] for r in rows): + die(f"stale-instrument: {len(rows)} entries, none tagged ⟨…⟩ -- " + "exit 2, the reader is broken or the register is untagged.") + return rows + + +def main(argv: list[str]) -> int: + if "--check" in argv: + return selftest() + + rows = load() + only_ours = "--ours" in argv + wanted = [a for a in argv if not a.startswith("-")] + + by = OrderedDict() + for r in rows: + by.setdefault(r["instrument"] or UNRECORDED, []).append(r) + + if wanted: + name = wanted[0] + if name not in by: + print(f"no entry names ⟨{name}⟩. Known instruments:") + for k in sorted(by): + print(f" {k}") + return 1 + group = by[name] + mine = " -- OURS, so these are re-openable" if name in OURS else "" + print(f"⟨{name}⟩ killed {len(group)} claim(s){mine}\n") + for r in group: + mark = "🟡" if r["open"] else ("✅" if r["settled"] else "❌") + print(f" {mark} L{r['line']:<4} {summary(r['text'])}") + if name in OURS: + print(f"\n If you have improved {name}, every ❌ above is a claim that" + "\n died to an instrument that no longer exists in that form.") + return 0 + + print(f"{len(rows)} entries in {REGISTER}\n") + order = sorted(by, key=lambda k: (k not in OURS, k == UNRECORDED, -len(by[k]))) + for k in order: + g = by[k] + tag = " ← OURS" if k in OURS else (" ← nobody wrote it down" + if k == UNRECORDED else "") + opened = sum(1 for r in g if r["open"]) + note = f", {opened} already 🟡" if opened else "" + print(f" {k:20s} {len(g):3d} claim(s){note}{tag}") + + ours = sum(len(by[k]) for k in by if k in OURS) + unrec = len(by.get(UNRECORDED, [])) + print(f"\n {ours} claim(s) died to an instrument WE BUILT.") + print(f" {unrec} claim(s) record no instrument at all -- " + "neither safe nor suspect,\n just unauditable. That is the backfill queue.") + print("\n tools/stale-instrument to list one") + return 0 + + +def selftest() -> int: + """Cases that must hold, executed -- not reasoned about. + + A control that does not run is not a control; this file's neighbours in + `tools/` say so about other people's tools, so it is asserted here. + """ + import subprocess + import tempfile + + me = os.path.abspath(__file__) + ok = 0 + + def run(reg, args=()): + env = dict(os.environ, SYLPH_REGISTER=reg) + p = subprocess.run([sys.executable, me, *args], capture_output=True, + text=True, env=env) + return p.returncode, p.stdout + p.stderr + + with tempfile.TemporaryDirectory() as d: + good = os.path.join(d, "good.md") + open(good, "w", encoding="utf-8").write( + '* "a" → refuted. ⟨disc⟩\n\n* 🟡 "b" — our renderer disagrees. ⟨render-vs-capture⟩\n' + ) + cases = [ + ("a tagged register parses", (good, ()), 0, "2 entries"), + ("one instrument lists", (good, ("disc",)), 0, '"a"'), + ("an OURS instrument says so", (good, ("render-vs-capture",)), + 0, "re-openable"), + ("an unknown instrument fails", (good, ("nope",)), 1, "no entry names"), + ] + for name, (reg, args), want_rc, want_txt in cases: + rc, out = run(reg, args) + good_rc = rc == want_rc + good_tx = want_txt in out + if good_rc and good_tx: + print(f" {name:34s} exit {rc} ✅") + else: + print(f" {name:34s} exit {rc} (wanted {want_rc}), " + f"text {'found' if good_tx else 'MISSING'} 🔴") + ok = 1 + + # The two harness faults, which must be distinguishable from a clean run. + empty = os.path.join(d, "empty.md") + open(empty, "w").write("# nothing here\n") + rc, out = run(empty) + if rc == 2 and "reader is broken" in out: + print(f" {'an empty register is a fault':34s} exit 2 ✅") + else: + print(f" {'an empty register is a fault':34s} exit {rc}, wanted 2 🔴") + ok = 1 + + untagged = os.path.join(d, "untagged.md") + open(untagged, "w", encoding="utf-8").write('* "a" → refuted.\n') + rc, out = run(untagged) + if rc == 2 and "none tagged" in out: + print(f" {'an untagged register is a fault':34s} exit 2 ✅") + else: + print(f" {'an untagged register is a fault':34s} exit {rc}, wanted 2 🔴") + ok = 1 + + rc, _ = run(os.path.join(d, "does-not-exist.md")) + if rc == 2: + print(f" {'a missing register is a fault':34s} exit 2 ✅") + else: + print(f" {'a missing register is a fault':34s} exit {rc}, wanted 2 🔴") + ok = 1 + + print() + print("the query fails when it must, and says so distinctly" if not ok + else "🔴 the query's own control is broken") + return ok + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))