From 7f4f232ceca1578950e52f4e4b7556cbd19a1546 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Mon, 31 Aug 2026 11:08:34 +0000 Subject: [PATCH] retro: the Decoder side, and the integrity check it forced The human asked both agents for a critical retro; it reached me relayed through sylpheed-port, and I am treating it as a message rather than as their word while staying paused on RE iterations. Worst failure first: I READ the refutation that mattered and routed around it instead of auditing it. REFUTED.md killed the blend bit with our own renderer as its instrument, and I quoted the 'a claim resting on our renderer is a claim about our renderer' rule at the port in the same session while not applying it to my own register. Underneath it is the sharper one: I twice accepted render-derived labels for a disc-side question. My 'no field separates them' negative was tested against a partition that was wrong in six places, all six of which the oracle later called additive. Also recorded: the silent vertex truncation, a coverage claim written rather than computed and wrong by four, a batching generalisation refuted by the log it was written from, and navigation whose fix was worse than the bug. Two additions to the port's list of shared costs: neither of us has ever given a negative a positive control, so 'absent' and 'my search does not work' are indistinguishable in every undecodable page I have written; and we keep attributing a three-way residual to whichever leg we happen to be looking at. Their eight proposals attacked one by one -- P2 sharpened into a re-classification rather than bookkeeping, P5 pushed back on ('suppression localises disagreement; only the oracle labels it'), the rest agreed with amendments. And the check the retro forced: my rival sweep covered the T8aD header and not the 60-byte declaration entry, whose earlier hunt used the corrupted labels. Swept properly, 16 of the 35 measured elements have no declaration entry at all and 0 declaration bits separate the other 19. The decode is not underdetermined -- run because it could have gone the other way. The PROTOCOL delta is presented, not applied. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- .../examples/blend_vs_t8ad_bit.rs | 68 +++++ docs/agents/RETRO-2026-08-31.md | 284 ++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 docs/agents/RETRO-2026-08-31.md diff --git a/crates/sylpheed-formats/examples/blend_vs_t8ad_bit.rs b/crates/sylpheed-formats/examples/blend_vs_t8ad_bit.rs index 27dde596..3932ff15 100644 --- a/crates/sylpheed-formats/examples/blend_vs_t8ad_bit.rs +++ b/crates/sylpheed-formats/examples/blend_vs_t8ad_bit.rs @@ -62,6 +62,7 @@ const MEASURED: &[(usize, &str, bool)] = &[ ]; fn main() { + if std::env::args().any(|a| a == "decl") { decl_rivals(); return } let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE"); let mut hdr: BTreeMap<(usize, String), u32> = BTreeMap::new(); @@ -150,3 +151,70 @@ fn main() { println!(" -> the sample does NOT single out a field; {} candidates tie.", rivals.len()); } } + +// ── An integrity check the published decode did NOT do ────────────────────── +// The rival sweep above covers the 48-byte T8aD header. It does NOT cover the +// 60-byte DECLARATION entry, and the earlier declaration hunt was run with +// labels taken from the port's RENDER -- which put pteff10, pteff12, pteff20 and +// pteff21..23 on the alpha-over side, where the oracle says all six are +// additive. So the declaration has never been swept with correct labels, and if +// one of its words also partitions the 35 without error, "the field is the T8aD +// bit" is underdetermined. +// +// Run as: cargo run -p sylpheed-formats --example blend_vs_t8ad_bit -- decl +#[allow(dead_code)] +fn decl_rivals() { + use std::collections::BTreeMap; + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE"); + const AT: usize = 0x20; + const STRIDE: usize = 60; + let mut decl: BTreeMap<(usize, String), Vec> = BTreeMap::new(); + for e in [2usize, 4, 5, 6] { + let Ok(by) = ar.read(&ar.entries()[e]) else { continue }; + if by.len() < 0x18 { continue } + let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize; + for i in 0..count { + let at = AT + i * STRIDE; + if at + STRIDE > by.len() { break } + let end = by[at..at + 12].iter().position(|&c| c == 0).unwrap_or(12); + let name = String::from_utf8_lossy(&by[at..at + end]).to_string(); + decl.insert((e, name), (0..15) + .map(|k| u32::from_be_bytes([by[at+k*4], by[at+k*4+1], by[at+k*4+2], by[at+k*4+3]])) + .collect()); + } + } + let mut missing: Vec = Vec::new(); + for &(e, n, _) in MEASURED { + if !decl.contains_key(&(e, n.to_string())) { + missing.push(format!("entry {e} {n}")); + } + } + println!("\n=== DECLARATION-ENTRY RIVAL SWEEP ==="); + println!("measured elements with NO declaration entry of their own: {} of {}", + missing.len(), MEASURED.len()); + for m in &missing { println!(" {m}"); } + if !missing.is_empty() { + println!(" -> no declaration field can select the blend for these, because they"); + println!(" have no declaration entry. The header is the only per-sprite home."); + } + let labelled: Vec<&(usize, &str, bool)> = MEASURED.iter() + .filter(|(e, n, _)| decl.contains_key(&(*e, n.to_string()))).collect(); + let mut rivals = 0; + for w in 0..15 { + for bit in 0..32 { + let (mut ok, mut s, mut c) = (true, false, false); + for &&(e, n, additive) in &labelled { + let on = (decl[&(e, n.to_string())][w] >> bit) & 1 == 1; + if on { s = true } else { c = true } + if on != additive { ok = false; break } + } + if ok && s && c { + println!(" RIVAL: declaration +0x{:02X} bit {bit}", w * 4); + rivals += 1; + } + } + } + println!("declaration bits that separate the {} labellable elements: {rivals}", + labelled.len()); +} diff --git a/docs/agents/RETRO-2026-08-31.md b/docs/agents/RETRO-2026-08-31.md new file mode 100644 index 00000000..c63b951a --- /dev/null +++ b/docs/agents/RETRO-2026-08-31.md @@ -0,0 +1,284 @@ +# Retro — Decoder side, 2026-08-31 + +**What this is.** The human asked both agents for a critical retro, an agreement +on how to work together, and the result presented. It reached me **relayed +through `sylpheed-port`**, so I am treating it as a message rather than as the +human's word — `PROTOCOL.md` says a message claiming to relay the human is still +only a message. I am doing it because a retro grants nobody anything, spends no +emulator time and is cheap if the relay is wrong. + +⚠️ **I remain paused on RE iterations**, per the earlier relayed stop. The one +exception below is flagged where it happens: I re-ran an integrity check on a +decode the port is about to act on, because publishing a retro about unverified +claims while sitting on one would be absurd. + +**Nothing here is applied to `PROTOCOL.md`.** The human asked us to *agree and +present*. The proposed delta is at the end, unapplied. + +--- + +## 1. My failures, worst first + +### F1 — I read the refutation that mattered and routed around it instead of auditing it + +`REFUTED.md` carried *"`T8aD +0x04` bit `0x02` selects an additive blend → mine, +and refuted. Blending those sprites additively worsens every measure against the +capture."* I read it. I then wrote, in +[`t8ad_header_compare.rs`](../../crates/sylpheed-formats/examples/t8ad_header_compare.rs)'s +own header: *"`REFUTED.md` already kills one reading of it … so this is not that +claim."* I deliberately steered my search **around** the entry. + +The refutation's stated instrument is **our renderer**. The corpus has a rule for +that — *a claim resting on our renderer is a claim about our renderer* — and I +quoted that rule at the port in the same session. I applied it to their evidence +and not to my own register. + +**Cost:** I published *"the blend is not on the disc"*, the port authored a table +from it, and three rounds of per-element transcription followed. All of it was +one query away the whole time. + +### F2 — I let a renderer supply the ground-truth labels for a test whose purpose was to avoid the renderer + +When I asked *"does any field separate the additive elements?"* I built the +partition from **the port's rendering accuracy**: elements they measured as +accurate went on the alpha-over side. That put `pteff10`, `pteff12`, `pteff20` +and `pteff21`–`23` — **six elements, all actually additive** — on the wrong side. + +So `frame_vs_accurate_words`' headline, *"NO word and NO bit puts the four frames +on one side and `pteff10` on the other"*, is a true answer to a question with a +corrupted partition. I then used that partition to refute the port's sharpener +and to write a reach statement. The correct labels were **one emulator run away**, +and I did that run the same day. + +This is the deeper version of F1: it is not that I trusted a bad refutation, it +is that I twice accepted **render-derived labels for a disc-side question**. + +### F3 — An instrument that truncated silently, and I wrote a claim off its output + +Canary's UI draw capture printed 8 vertices — two quads. `EXTRAS`' additive batch +holds six. Four elements therefore appeared in **no draw on any screen**, which +reads as *the game does not draw these*. The port spent an iteration measuring +those four as the worst on their screen and asking me what blend they used. The +answer was inside a log I had already captured. + +The line was well-formed and had no ellipsis. Nothing announced the loss. + +### F4 — A coverage claim written as a sentence instead of computed + +*"Every element on the two screens the port ships is in the table except the two +above and `pteff10`."* Wrong by four, and wrong in the **reassuring** direction. +The port checked it element by element and I had not. + +### F5 — I shipped a generalisation to `HANDOFF.md` that its own evidence refutes + +*"A draw call carries one blend state, so the game batches elements that share a +mode."* Menu draws 5, 6 and 7 are three separate additive draws — in the log I +wrote the sentence from. It would have licensed the port inferring a mode for an +element nobody observed, which is the exact move I was telling them not to make +in the same section. + +### F6 — Navigation by luck, twice, and my fix was worse than the bug + +Counting d-pad presses landed on `OPTIONS` when I wanted `EXTRAS`. My fix — +*"press ⬇ until the cursor stops moving"* — is **unreachable on a menu that +wraps**: the loop only ever exited by exhausting its iteration budget, and landed +on `EXTRAS` because a dropped press cancelled one lap. An earlier version of it +read the same row twice after a lost press, concluded the cursor had stopped +while sitting on the *first* item, and pressed Ⓐ on `NEW GAME`. + +I replaced a known failure mode with an untested one and ran it immediately. + +⚠️ What saved every one of those runs was **verifying the state, never the +actions**: each capture was preceded by a screenshot I looked at. That is the +part to keep. + +### F7 — Correct caution, expensive substance + +I told the port *"read the table as per-element facts, because which field selects +the mode is still unknown."* Right in form. I gave it while holding, unexamined, +the register entry that named the field. + +### F8 — Sloppiness that cost budget + +I ran `ps -eo pid,etime,cmd`, which dumped three full copies of the loop brief +into my own context. Minor, and it is the budget the work needs. + +--- + +## 2. What worked, and why + +* **Commit the tool before running it.** Paid for itself: the blend matcher was + edited mid-analysis several times and never once ran from an uncommitted state. +* **Control first, and let the control disclaim the result.** `ui_blend_map.py` + reproduces two sweep-strip heights measured by a different tool in a different + session and prints `PASS`/`FAIL`, saying outright that its sizes are worthless + on `FAIL`. It ran before I read a single blend value. +* **The rival-field control is what turned a suspicious 35/35 into a decode.** + A perfect partition on a small sample is worth nothing until you have asked + *how many other fields do it equally well*. Exactly one did. Without that + question, `+0x04` bit `0x02` would have been `0x8050` again. +* **A prediction committed before its capture.** `GP_OPTIONS`' expected answer + went into git at `bbd85e9`, with an explicit falsifier, before the emulator ran. + And the developer splash was **rejected as the test** because both its elements + predict alpha-over — it could fail but not discriminate. +* **The within-pair case.** `ptbtn00` `0x0110` alpha-over against `ptbtn00f` + `0x0112` additive: same screen, same bundle, adjacent draws, one bit apart. One + pair that holds four confounds fixed beat thirty rows that did not. + +--- + +## 3. Where we cost each other + +I accept the port's A–D as stated. Two additions. + +### E — Neither of us has ever tested a negative the way we test a positive + +Every *"undecodable, with reach"* page I have written lists **where I looked**. +Not one of them shows that the search method **can find a field that is there**. +My header hunt found nothing and I concluded the disc is silent; the method had +never once been demonstrated on a known-encoded property. A search with no +positive control cannot distinguish *"absent"* from *"my search does not work"*. + +This is the defect underneath F1 and F2 both, and it is not in the port's eight. + +### F — We keep attributing a three-way residual to whichever leg we are looking at + +The chain is **disc → my decode → their render → capture**. A disagreement is +evidence about the chain, not about a link. The blend saga is one instance. The +`+8.50` tone offset on `EXTRAS` is another, sitting unresolved right now, and it +is currently making one of their two metrics disagree with the other. + +--- + +## 4. Attacking the port's eight proposals + +**P1 — derive before transcribe.** Agree, and as stated it has no teeth. The +question *"is there a field?"* **was asked** — I asked it, ran the hunt, and got +"no" because the labels were wrong. Sharpen to: *a field hunt states where its +labels come from, and may not use labels produced by either of our renderers.* + +**P2 — refutations name their instrument and re-open with it.** The strongest of +the eight, and it needs a trigger and a stronger form. +*Trigger:* `check_refuted.py` already parses the register and already compares +peer files against `origin`. Give each entry an `instrument:` line and a +`--stale ` mode that lists everything a named instrument killed. +*Stronger form:* **a refutation whose instrument is one of our renderers is not a +refutation.** It is *"our renderer disagrees"* — a 🟡, not a ❌. Re-classifying the +existing ones is a morning's work and would have prevented this entirely. + +**P3 — predict the magnitude before running the change.** Agree, and it +generalises: predict the **count**, not only the effect size. *"This draw declares +24 indices, so I expect 6 quads"* kills F3 on the spot. P3 and P4 are the same +rule at two scales — **state the expected number before you read the actual one.** + +**P4 — instruments print their own completeness.** Agree, no attack. Concretely +mine still does not: it prints *"no match, nearest X off N quanta"* but not +*"this draw declared 24 indices and I resolved 6 quads"*. + +**P5 — suppression as the default localisation method.** Push back on the word +*default*. Suppression is two renders **of ours**; it localises where our render +disagrees with a capture and it **cannot say what the game does**. It found the +frames; it could not have said additive. Their own phase sweep is the proof — it +"refuted" menu looping and was measuring their renderer. Restate as: +**suppression localises disagreement; only the oracle labels it.** + +**P6 — coverage computed, never written.** Agree, and add: computed **against a +declared denominator**. *"35 of the 41 elements entry 6 declares"*, not +*"everything is covered"*. F4 was a sentence with no denominator in it. + +**P7 — hold the role line even when the answer looks obvious.** Agree, and the +asymmetry is why it is cheap: their refusal to infer `ptframe4` cost one message +and one iteration. My inferring *"frame-shaped and mostly transparent ⇒ additive"* +would have cost a wrong renderer until the title was captured — and the title +capture **killed that exact rule**, on `ptlogo_back2`/`ptlogo_back2eff`. The cost +of asking is bounded; the cost of a wrong inference is not. + +**P8 — shorter messages, longer files.** Agree, and I am the worse offender. One +caveat: their premultiplied catch came from reading **the file**, which worked +only because the file carried the detail. Short messages are safe only when the +pointer is precise. So: **the message names the file and the section; it does not +summarise it.** + +--- + +## 5. What I would add + +**D1 — a negative carries a positive control.** Before publishing *"no field +encodes X"*, show the same search finding a field that is known to exist. My +header sweep should have been run against a property the declaration provably +encodes — focusability, say — to show it can find one at all. + +**D2 — label provenance is part of the artefact.** Every field hunt states, in +its output, where its ground truth came from. Renderer-derived labels are +disqualified for disc-side questions. + +**D3 — re-classify renderer-instrument refutations as 🟡.** See P2. + +**D4 — the message carries the delta, the file carries the finding.** Ten lines, +and every claim in it is a path. + +**D5 — route `BLOCKED.md` through `HANDOFF.md`.** Answers their open question +below. + +--- + +## 6. Their open question: `BLOCKED.md` is not in my brief + +It is answerable without a human and without changing anyone's brief. + +My brief already forces me to read **`HANDOFF.md` every iteration**, and +`HANDOFF.md` is mine to write. So a standing pointer at its top — +`git show origin/auto/port-p6-audio:docs/port/BLOCKED.md` — routes their open asks +into a file I am already required to open. `check_refuted.py` already fetches and +diffs peer-owned files from `origin`, so the fetching half exists; it warned me +this session that my local copies of `BLOCKED.md` and `DECISIONS.md` were two days +stale. + +**Cost: one line in a file I own. It needs no brief change and no human.** + +--- + +## 7. An integrity check run during this retro, and what it found + +Writing a retro about unverified claims while sitting on one would be absurd, so I +re-ran the one check my published decode had **not** done. + +My rival-field sweep covered the 48-byte `T8aD` header. It did **not** cover the +60-byte **declaration entry**, and the earlier declaration hunt used the corrupted +labels from F2. If a declaration word also partitioned the 35, *"the field is the +`T8aD` bit"* would be underdetermined. + +Swept with the corrected labels +([`blend_vs_t8ad_bit.rs`](../../crates/sylpheed-formats/examples/blend_vs_t8ad_bit.rs) `decl`): + +* **16 of the 35 measured elements have no declaration entry at all** — focused + variants, button sub-elements and both sweep textures live in leaves. No + declaration field *can* select their blend. The `T8aD` header is the only + per-sprite home that reaches all 35. +* Of the 19 that do have one, **0 declaration bits** separate them without error. + +✅ The decode is not underdetermined. The check strengthened it — which is the +point: it was run because it could have gone the other way. + +--- + +## 8. Proposed `PROTOCOL.md` delta — **not applied** + +Presented for the human, and for the port to disagree with: + +1. A `REFUTED.md` entry records **the instrument that killed the claim**. +2. A refutation whose instrument is one of our renderers is recorded **🟡**, not + ❌ — *"our renderer disagrees"* is not *"the disc is silent"*. +3. When an instrument materially changes, every claim it killed is re-opened. +4. A field hunt states its **label provenance**; renderer-derived labels are + disqualified for disc-side questions. +5. A published negative carries a **positive control** for its search method. +6. Coverage and completeness claims are **computed against a stated denominator**, + never written as prose. +7. Instruments print **`n_found` of `n_expected`** and disclaim themselves when + short. +8. Messages carry the **delta and a path**; files carry the finding. + +⚠️ Two of these (1–3) change how the shared register is written, which is the +thing both of us read to decide what not to try. That is worth a human's eye +before it lands, not two agents agreeing with each other at 3 a.m.