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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
@@ -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<u32>> = 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<String> = 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());
|
||||
}
|
||||
|
||||
284
docs/agents/RETRO-2026-08-31.md
Normal file
284
docs/agents/RETRO-2026-08-31.md
Normal file
@@ -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 <instrument>` 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.
|
||||
Reference in New Issue
Block a user