port: the contract I read is 3185 lines shorter than the contract

docs/port/HANDOFF.md on main is 926 lines, last touched 0fd8e69 on 2026-08-29.
The live one is 4111 lines at 27938aa, +3930/-745 across 96 commits I have never
read, several of them addressed to the port by name. The Decoder writes HANDOFF
on origin/auto/no-disc-and-menu-captures; main is a hundred-odd commits behind
it; I open main's copy every iteration as instructed.

So the rule meant to prevent this cannot detect it. tools/port/blocked-provenance
recovers each row's derivation from history rather than memory -- git log -S on
the row's key phrase -- and all 27 open rows derive from 0fd8e69, because
HANDOFF-on-main has not moved. A constant cannot separate a fresh row from a
rotten one. Withdrawn in BLOCKED.md: 'HANDOFF has not moved in four milestones'
was missing the qualifier that carried its meaning.

The tool's first version silently missed its own known positive: P6 looping vs
712cac8, whose 9.44 s answer this port already ships. 'looping' did not stem to
'loop', 'menu' was stoplisted, and a >=2-shared-words threshold dropped the rest.
The threshold was the defect -- two common words outscored one rare one -- so
ranking is now by log(N/df) with no cutoff at all, and the control passes at rank
1 of 7 without touching the stoplist. Every discard is counted: struck rows,
sub-rank pairs, stoplisted words. Same rule applied to check-claims, which now
reports the 40 occurrences it suppresses; the Decoder reached it the same day
from the opposite failure, a silent suppression path making a clean run
unfalsifiable.

The reading list found two open rows already answered: the plate's pulse period
(120, not 105) and the main menu having no idle self-return, which refutes the B
row's own reasoning.

Refutation attempted on '+0x08 is the loop length', the claim the port was about
to build on. It survives: their falsifier re-run on my own read of the disc gives
0 violations in 1781 records, and on the eight records this port animates their
table reproduces cell for cell. Adopted -- screen.rs exports loop_length_units
and ScreenView._loop_period prefers it, announcing any disagreement rather than
silently resolving it. The value does not change: authored/timing.json already
had 120 from a wall-clock measurement, so a disc field and an emulator stopwatch
agree while sharing no instrument.

Two asks filed: the field is exposed in no public API on any ref, so the port
reads four bytes it should not own; and eleven focus records declare the same
120-unit cycle while only the plate is authored to animate, which is behavioural
and not mine to infer.

Every asserting check passes; oracle RMSEs unchanged, as 120 == 120 predicts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
Sylpheed port agent
2026-08-30 20:50:33 +00:00
parent 6a45196a84
commit c909d1dc49
7 changed files with 587 additions and 13 deletions

View File

@@ -0,0 +1,106 @@
//! Run the Decoder's own falsifier for "a nested record's `+0x08` is its loop
//! length" against the bundles THIS PORT SHIPS, before shipping 120 for 105.
//!
//! HANDOFF (`27938aa`, delivered at `07e93ce`) says the plate's glow cycles over
//! **120** units while its keyframes end at 105, and instructs the port to stop
//! shipping 105. The port's `ScreenView` derives a looping record's period from
//! the element's largest keyframe time, so it does ship 105 — and the field that
//! would fix it is decoded in an *example* and a *test* on the Decoder's branch
//! and **exposed in `sylpheed_formats`' public API on no ref at all**.
//!
//! It is still reachable: `parse_build` publishes each record's `(offset, size)`,
//! so reading a big-endian `u32` at `+0x08` of a span whose magic is `RATC` is
//! consuming a delivered finding, not decoding a format. What must not be
//! consumed on trust is the READING. So this re-runs both of their controls:
//!
//! * **the falsifier** — `+0x08 < max keyframe time` must never occur; an
//! animation cannot restart before its own last pose;
//! * **non-triviality** — if every record had `+0x08 == max t` the field would
//! carry nothing and the name would be a relabelling of the keyframes.
//!
//! and adds the one they could not run: the same two, restricted to the records
//! **this port actually animates**. A disc-wide 0.00 % violation rate says
//! nothing about my six screens if all six sit in the exceptional tail.
use sylpheed_formats::{pak, ratc, ui_layout};
use std::collections::BTreeMap;
/// The records the port animates: the plate glow, the five menu focus records,
/// and the title's two sweeps. Named rather than pattern-matched, because the
/// point is to check the ones that are shipped, not the ones that match a glob.
const SHIPPED: &[&str] = &[
"ptbtn00f", "ptbtn01f", "ptbtn02f", "ptbtn03f", "ptbtn04f", "ptbtn05f",
"ptloop01", "ptloop02",
];
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
.flatten().map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
paks.sort();
let (mut total, mut exact, mut holds, mut violations) = (0usize, 0usize, 0usize, 0usize);
let mut slack_hist: BTreeMap<i64, usize> = BTreeMap::new();
let mut shipped: BTreeMap<String, (i64, i64)> = BTreeMap::new();
for p in &paks {
let Ok(ar) = pak::PakArchive::open(p) else { continue };
for e in ar.entries() {
let Ok(by) = ar.read(e) else { continue };
if !ratc::is_ratc(&by) { continue }
let Some(b) = ui_layout::parse_build(&by) else { continue };
for (rn, &(o, s)) in &b.records {
if o + 12 > by.len() || o + s > by.len() { continue }
if &by[o..o + 4] != b"RATC" { continue }
let len = u32::from_be_bytes(by[o + 8..o + 12].try_into().unwrap()) as i64;
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
let maxt = lb.elements.iter()
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
.max().unwrap_or(0) as i64;
if maxt == 0 { continue } // static: declares no cycle at all
total += 1;
let slack = len - maxt;
*slack_hist.entry(slack).or_default() += 1;
if slack == 0 { exact += 1 } else if slack > 0 { holds += 1 } else { violations += 1 }
let stem = rn.trim_end_matches(".rat");
if SHIPPED.contains(&stem) {
shipped.entry(stem.to_string()).or_insert((len, maxt));
}
}
}
}
println!("disc-wide, records with timed keyframes: {total}");
println!(" +08 == max t (exact) : {exact:5} {:5.1} %", pc(exact, total));
println!(" +08 > max t (a hold) : {holds:5} {:5.1} %", pc(holds, total));
println!(" +08 < max t <- FALSIFIER : {violations:5} {:5.2} %", pc(violations, total));
println!("\nslack distribution, most common first:");
let mut h: Vec<_> = slack_hist.iter().collect();
h.sort_by_key(|&(_, n)| std::cmp::Reverse(*n));
for (k, n) in h.iter().take(8) { println!(" slack {k:>6} : {n}"); }
println!("\nthe records THIS PORT animates:");
println!(" {:<12} {:>6} {:>7} {:>7}", "record", "+0x08", "max t", "slack");
let (mut ship_exact, mut ship_hold, mut ship_bad) = (0, 0, 0);
for (n, (len, maxt)) in &shipped {
let slack = len - maxt;
match slack { 0 => ship_exact += 1, s if s > 0 => ship_hold += 1, _ => ship_bad += 1 }
println!(" {n:<12} {len:>6} {maxt:>7} {slack:>7}{}",
if slack < 0 { " 🔴 FALSIFIED" } else { "" });
}
println!("\n shipped: {ship_exact} exact, {ship_hold} hold, {ship_bad} falsified");
if shipped.len() < SHIPPED.len() {
let missing: Vec<_> = SHIPPED.iter().filter(|s| !shipped.contains_key(**s)).collect();
println!(" ⚠️ not found on the disc: {missing:?} -- a name the port ships and");
println!(" this control never checked is worse than a violation it found.");
}
println!("\n verdict: {}", if ship_bad > 0 {
"🔴 the reading fails on a record the port animates -- do NOT adopt"
} else if ship_hold == 0 {
"⚠️ every shipped record is exact, so this port cannot tell loop length\n from max keyframe time -- adopting 120 would change nothing here"
} else {
"✅ falsifier clean and the field is non-trivial ON THE SHIPPED SET"
});
}
fn pc(n: usize, d: usize) -> f64 { if d == 0 { 0.0 } else { 100.0 * n as f64 / d as f64 } }

View File

@@ -108,10 +108,44 @@ pub struct FocusElement {
pub keyframes: Vec<Keyframe>,
}
/// A nested record's declared cycle length, or `None` if the span is not one.
///
/// Guarded rather than trusted: the magic is checked and the header must fit,
/// because an offset that has drifted returns a plausible number otherwise.
fn record_loop_units(bundle: &[u8], off: usize, size: usize) -> Option<u32> {
let span = bundle.get(off..off.checked_add(size)?)?;
if span.len() < 12 || &span[..4] != b"RATC" {
return None;
}
Some(u32::from_be_bytes(span[8..12].try_into().ok()?))
}
#[derive(Serialize)]
pub struct Focus {
/// The `.rat` leaf this came from, e.g. `ptbtn01f.rat`.
pub record: String,
/// The record header's `+0x08`: **where the cycle restarts**, in keyframe
/// units — which is not the same thing as the last keyframe's time.
///
/// `ptbtn00f`, the `PRESS Ⓐ` plate's glow, ramps 0→80→0 over **105** units
/// inside a **120**-unit cycle and rests dark for the remaining 15. Deriving
/// the period from the largest keyframe time — what the port did until now —
/// runs it 14 % fast and deletes the dark rest entirely.
///
/// Decoded by the Decoder (`07e93ce`, `docs/re/structures/ui-record-loop-length.md`,
/// delivered in HANDOFF `27938aa`) and **re-run here before adoption**, with
/// their falsifier and their non-triviality control:
/// `cargo run -p sylpheed-export --example record_loop_control`. Disc-wide
/// 1 781 timed records, 92.3 % exact, 7.7 % hold, **0 declaring less than
/// their own last pose**; on the eight records this port animates, seven
/// exact and `ptbtn00f` the one hold.
///
/// ⚠️ Read here rather than through `sylpheed_formats` because the field is
/// exposed in **no public API on any ref** — it lives in an example and a
/// test. `parse_build` publishes each record's `(offset, size)`, so this is
/// four big-endian bytes at a documented offset inside a span whose magic is
/// checked, not a second decoder. **Delete it the day the crate exposes it.**
pub loop_length_units: Option<u32>,
/// Back-to-front, in the leaf's own declaration order.
pub elements: Vec<FocusElement>,
}
@@ -453,7 +487,11 @@ pub fn export_build(
Ok(if fes.is_empty() {
None
} else {
Some(Focus { record: rec.to_string(), elements: fes })
Some(Focus {
record: rec.to_string(),
loop_length_units: record_loop_units(bundle, off, size),
elements: fes,
})
})
};
@@ -510,7 +548,11 @@ pub fn export_build(
});
}
if !fes.is_empty() {
focus = Some(Focus { record: rec, elements: fes });
focus = Some(Focus {
record: rec,
loop_length_units: record_loop_units(bundle, off, size),
elements: fes,
});
}
}
}

View File

@@ -91,9 +91,19 @@ git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconc
⚠️ Rows are **not** being back-dated: nobody knows when most were written, and inventing a sha would be worse than admitting there is none. New rows carry one. This table was last audited **2026-08-30** against a running port.
🔴 **"HANDOFF has not moved in four milestones" [refuted] — WITHDRAWN 2026-08-30,
and the missing qualifier carried the whole meaning.** HANDOFF has moved **96
times**. It has not moved *on `main`*, which is the copy this port opens. The
live document is 4 111 lines at `27938aa`; `main`'s is **926** at `9ca1eb5`, a
gap of +3 930/745, and several of those commits are addressed to the port by
name. Run `tools/port/blocked-provenance`: every row below derives from
`9ca1eb5`, so **the derivation sha this file demands cannot separate a fresh row
from a rotten one** — it is constant by construction. The rot is not that rows
are old; it is that the document they derive from is frozen while the thing it
copies moves. See `DECISIONS.md`.
**Re-checked at the voice export**, `HEAD` = `3a4c6ac` (merged with `origin/main`
at `1b1a4df`). `git log -1 --format=%h -- docs/port/HANDOFF.md` still answers
**`9ca1eb5`** — HANDOFF has not moved in four milestones. The second-half check,
at `1b1a4df`). The second-half check,
`git log --oneline 9ca1eb5..HEAD -- docs/re/`, lists four commits, of which
`3491d30` (*"the disc ships movies in TWO audio profiles, and 28 of them are
5.1"*) is the one this iteration used and `7eeae30` is still unfolded into
@@ -115,7 +125,7 @@ HANDOFF.
| P6 looping | where a menu loop restarts | Q10 | ❔ **still open, and the port shipped the ugly answer on purpose.** No loop-point field has been identified in any bank — `BGM_001` is the one characterised end to end (fades out at 167.663 s into 6.15 s of silence) and nothing suggests `BGM_103` differs in kind. `authored/audio.json` sets `loop: "restart"` — replay from sample 0 — so the listener hears the fade-out and the trailing silence at the seam. **Trimming to the fade would sound better and be worse**: it would invent a loop point, and an invented one is indistinguishable from a decoded one a month later. What settles it: a loop-point field, or a capture of the real menu looping. ⚠️ **The cost is now measured, 2026-08-30.** The bed loops at 87.8 s against the track's own 87.7 — `loop: "restart"` behaves exactly as authored — and the seam is **36 consecutive near-silent 50 ms windows, 84.4087.80 s**: about **3.4 seconds of silence** after a fade from RMS 2057 to 431. Long enough to read as the music having stopped rather than looped. That does **not** license trimming to the fade, which would still invent a loop point; it is recorded so the missing field's price is a number rather than an adjective. |
| ~~P5 focus marker~~ | ~~the focus ring's spin PERIOD, and whether it loops~~ | Q1 + *"groups hold"* | ✅ **answered 2026-08-29, and NOT ON `main` YET.** The Decoder pointed at it over the message channel and the pointer resolves: branch `auto/no-disc-and-menu-captures`, commit **`4fa3099`** (branch head `66e74d4`), file `docs/re/focus-ring-spin-measured.md`, frames under `docs/re/captures/focus-ring/`. **The ring spins continuously — period 2.177 s wall-clock, eight evenly spaced autocorrelation peaks over nine revolutions**, with no angle estimated anywhere (both angle estimators failed their own controls and were not used). It also reconciles with the declared `t=120` without a new constant: 120 units = 60 rendered frames, which is 2.00 s at a true 30 Hz and 2.082.17 s at the 27.628.8 fps this emulator runs, so the measurement sits at the top of the predicted band. 🟡 The Decoder is explicit that this is *consistency, not closure* — the guest frame rate was not measured in the same run. ⚠️ **Do not read `captures/focus-ring/ring-20s-mean-uniform.png` as a frame**: the spin averages to a uniform circle, which is the finding, not a headless ring. **The port has not implemented this yet** — it still draws 0°, which the same corpus says is a pose the game never shows. That is next iteration's work and it is no longer blocked. |
| ~~P3/P5 — the title screen~~ | ~~does the idle post-boot title show the `PRESS Ⓐ` plate?~~ | Q2 | ✅ **STALE — struck 2026-08-30, and it had been wrong for weeks.** Every factual claim in it is now false: the boot does **not** end on a plateless build 4, `press_start` is **not** unused, and the port **has** drawn two builds at once since the plate-delay work. Verified this iteration — `boot ends on title + press_start`, `overlay press_start … drew 1: ptbtn00`, plate region mean **95.70** against 33.6 for the bare title. 🔴 The row directly below it was already marked *answered and TAKEN* for the same question: two rows on one question, one struck and one live claiming the opposite, and the live one was the stale one. That is this page's own documented failure mode, caught by auditing it rather than by reading it. ⚠️ What remains open is a **different** question and has its own row: whether the plate *stays up* after its 8-unit window. |
| P5 — Ⓑ on the main menu | **is Ⓑ what returns to the title, or the idle timer?** | Q5 | 🟡 stated in HANDOFF, no capture behind it. The title self-returns after ~810 s idle, so one unrecorded observation cannot separate them. `authored/flow.json` implements it and marks it *authored — likely but UNPROVEN*. **Not blocking** — P5 shipped with it — but it is the only navigation rule on that screen with nothing under it. Settled by one run that presses Ⓑ well inside the idle window, timestamped. |
| P5 — Ⓑ on the main menu | ~~is Ⓑ what returns to the title, or the idle timer?~~ **how long does Ⓑ take?** | Q5 | 🟡 **the ORDERING is answered, the latency is not — and this row's own argument is refuted, 2026-08-30.** It reasoned that *"the title self-returns after ~810 s idle, so one unrecorded observation cannot separate them"*. HANDOFF `27938aa` measures the main menu as **not self-returning for ≥ 60 s untouched**, and places the ~810 s idle on the **title**, not the menu — so the confound the row was built on does not exist. Ⓑ is delivered (Canary logs `vk=5801`) and is the only input in ≥ 100 s before the return: **the ordering is measured**. What stays open is only the latency, from a backlogged run. `authored/flow.json` already implements the ordering and is now under-claiming rather than over-claiming. Surfaced by `blocked-provenance` at score 10.6 against `9a10258`, unread for a day. |
| ~~P6 BGM — the sub-wave count~~ | ~~is a music bank's LEADING REGION a stem, or a decoder artefact?~~ | Q10 | ✅ **CLOSED 2026-08-29 — the census was right and the port was summing a bank header into the music.** Decoded and timed, sub-wave 0 of `BGM_103`, `BGM_102` and `BGM_001` is identical: **10 300 B → 0.009 s, peak inf**, i.e. digitally silent. 10 300 B is the 10 240-byte bank header (the Decoder's disc-wide census) plus a 60-byte RIFF wrapper. So it is not a stem, and `export_bgm` had been counting it in the divisor — putting every real stem at 1/3 instead of 1/2, **3.52 dB of attenuation on all menu music shipped since P6**. Dropping a *silent* input is arithmetic, not a decoding decision, so this closed on the port's side; measured after the fix, `main_menu.ogg` goes 7.69 → **4.20 dBFS**, +3.49 dB against 3.52 predicted. Corroborates the Decoder's `c1f3608` from the other direction. The export now reports 2 sub-waves and the manifest warning is gone. |
| ~~P3 — the plate's ONSET~~ | ~~visible 2.13 s after settle, or group starts then?~~ | Q2 | ✅ **resolved 2026-08-29, and the answer is AUTHOR NOTHING.** The port's refutation held and produced a better answer than either option it offered. Correction at `5b0a6e6` on `auto/no-disc-and-menu-captures`: **both builds run on one clock, started together**, and the plate arrives at its own declared `t=238`. Checked against this export rather than taken on trust — build 4's visible build-in ends at `t=118` (`pteff01`, `pteff02`, `ptlogoall_eff` finish together), `ptbtn00` reaches alpha 255 at `t=238`, difference **120 units = 2.000 s**, against a measured 2.138 / 2.132 s at an emulator presenting 28.1 fps rather than 30. The 2.13 s constant is **deleted**. |
@@ -124,7 +134,7 @@ HANDOFF.
| ~~P4/P7 — `S00A` as a second asset~~ | ~~does a structurally different movie also put dialogue in FC?~~ | — | 🔴 **NOT OBTAINABLE IN THIS CONTAINER — closed as a route finding, 2026-08-29.** The drive works end to end (main menu +0.999, `newgame-difficulty` +0.999, `newgame-selectdata-crash` +0.997, with the focus detector validated live against a known transition) and then **the guest throws at `PC: 0x82307128` ×349**; no `S00A` stream ever decodes. ⚠️ It also refines `title-crash-stl-tree.md` rather than confirming it: the mechanism survives but the container it names, `aab216c3`, is **complete here** — the throw is on `1b556564`, which holds one file plus a stray `.tmp`. **So the new-game path builds a different cache container, and the documented remedy does not transfer** — it restores a *previously complete* cache, and no complete `1b556564` has ever existed here. **Consequence for the port: the centre-channel result rests on `ADV` alone.** `S00A` was wanted precisely because its second stream is digital silence where `ADV`'s is a 0.60× copy. That corroboration is behind a crash outside menu-port scope and neither agent is chasing it. |
| ~~P3/P5 — the title plate~~ | ~~does the idle title show `PRESS Ⓐ`~~ | Q2 | ✅ **answered and TAKEN at this iteration.** `auto/no-disc-and-menu-captures` at `fb536df`, `docs/re/title-plate-delay-measured.md`, traces in `docs/re/data/plate-timing-run{1,2}.tsv`. It is the third case: build 4 alone, then the plate composited over it. ⚠️ The delay is timed from where build 4 **stops animating**, not from where it first appears — measured the other way the two runs differ by 0.48 s against 6 ms. `ScreenView` now draws two builds at once, as a second `ScreenView` in the same `SubViewport` rather than a subordinate screen inside one. The onset question above is what is left. |
| P3 — the plate's PULSE | **does the plate's focus record loop, and with what period?** | Q2 | **open, and the port's earlier reading of it was wrong.** The port had looked for the pulse in `ptbtn00`'s own group; `5b0a6e6` identifies it as the plate's **focus record** `ptbtn00f` — a glow ramping alpha `0x00``0x50` and back, t=6…105. Measured on the running game at 2.12 / 2.19 / 2.34 / 2.31 s, mean **2.24 s**. 🟡 **The port has not taken it.** Looping that record needs a period, and its group is 105 timed units plus the **authored** 24-unit exit ramp = 129 units = 2.15 s — composing an authored constant with a loop assumption to land on a measured number is tuning, not measuring. Separately: the port draws no focus record on `press_start` at all, because the screen has no `buttons` and nothing is focused, so *whether the game always draws it* is its own question. |
| ~~P3 — the plate's PULSE~~ | ~~does the plate's focus record loop, and with what period?~~ | Q2 | **ANSWERED, and it had been answered for a day in a document this port was not reading.** HANDOFF `27938aa` (delivered at `07e93ce`): a nested record is its own RATC bundle and the header's **`+0x08` is the loop length**, so `ptbtn00f` ramps 0→80→0 over 105 units inside a **120**-unit cycle and rests dark for 15. Found by `tools/port/blocked-provenance`, which ranked that commit against this row at **14.0, the highest score in the file** — not by an experiment. ✅ **Refutation attempted and it survived**: their falsifier (`+0x08 < max t` must never occur) re-run on my own read of the disc gives **0 of 1 781**, and on the eight records this port animates, 7 exact and `ptbtn00f` the lone hold — their table cell for cell (`cargo run -p sylpheed-export --example record_loop_control`). ⚠️ The port never shipped 105: `authored/timing.json` already had 120 from a **wall-clock measurement**, so the disc and an emulator stopwatch agree while sharing no instrument. The exporter now derives it (`focus.loop_length_units`) and the authored value becomes the second witness. 🔴 **New ask below** — the field is in an example, not in the crate's API. |
| ~~P5 focus ring — implementation~~ | ~~the ring's spin period~~ | Q1 | ✅ **implemented 2026-08-29.** `ScreenView.spin_period_units` drives it: one turn per the element's own declared `t`, looping, from the screen clock. The period comes off the **disc**; what the RE agent supplied is that the turn repeats rather than stopping. Verified on the port's own render — the ring is bit-identical one period apart across the whole frame, differs by 3.6/255 inside its box at quarter-period steps, and conserves box luminance to **0.027 %** over eight phases, which is the same observable the RE agent used to separate rotation from a pulse. 🟡 **Direction is not measured** — the port turns 0°→+360°, which is the sign the disc declares, but the RE agent's angle estimators failed their controls and no signed angle was ever taken. 🟡 **Phase across a focus change is not measured** either: the port drives the ring off the screen clock, so it does not reset when focus moves. Settled by two frames straddling a focus change. |
| P7 / naming — the four unnamed builds | **which locale and variant is each of `GP_TITLE` entries 0, 1, 12, 15?** | — | 🟢 **found by the port, not blocking, and handed over.** All four are **loading screens**: every element in all four is named `pgloading_*` (`pgloading_processing.png`, `pgloading_circle1`, `pgloading_delta`, `pgloading_ring`), and `LOADING` is one of the three screen names the Decoder read out of `sub_821C6458`. They export today as `build_00`, `build_01`, `build_12`, `build_15`. Two variants: 0/1 carry 7 elements, 12/15 carry 10 (adding `pgloading_eff00`, `pgloading_loop5`, `pgloading_baseeff`). The archive's own pairing — adjacent for 2/3, `+3` for 4…9 and 10/13, 11/14 — suggests 0 is the twin of 1 and 12 the twin of 15, but **which member is which locale is an inference and the port has not named them on it**. Naming is cheap for the Decoder and a guess for the port. |
@@ -137,6 +147,13 @@ HANDOFF.
| ~~P1P7 — the keyframe record layout~~ | ~~adopt the corrected pose/time pairing~~ | — | ✅ **ADOPTED 2026-08-29 by pinning `formats-pin-2026-08-29c`.** This row was wrong twice: it said the change *"cannot be taken yet"* and that it *"reaches the port only when that branch lands on `main`"*. **It arrives when the tag is pinned**, which is what MISSION §2's tagging rule exists for. ⚠️ And the knob I tested first, `SYLPHEED_KF_TIME_SHIFT`, is a **retired partial fix** that left pose 0 untimed — the real correction is the tagged crate's default, with the old reading behind `SYLPHEED_KF_TIME_LEGACY=1`. **The blast radius was far smaller than this row predicted**: under the correction *every pose is timed* (866 keyframes, 0 untimed), so `pose_at`'s synthetic-exit branch became dead code rather than wrong code and nothing needed re-deriving. Oracle: `publisher_logo` 1.00 %→**0.75 %**, `developer_logos` 0.39 %→**0.33 %**, `extras`' differing region collapsing from 736×525 to **398×295 at the sweep position**. 🔴 Open cost: `sylpheed-cli` builds from the workspace crate, so `verify-screen` compares two decoder eras until the tag reaches `main`. Revert to the path dependency then. |
| ~~P7 / naming — the four unnamed builds~~ | ~~which locale and variant is each of entries 0, 1, 12, 15?~~ | — | ✅ **answered 2026-08-29** (`docs/re/ui-title-build-map.md`): all four are the loading screen, two variants — plain (7 elements) and dressed (10) — decoded from their own `pgloading_*` element names. ⚠️ **Not adopted as names yet, for two reasons the RE agent gave and one the port found.** Theirs: the executable names exactly two, and *which* bundle takes which name is 🟡 undecided, so `LOADING`/`LOADING2` must not go in an asset path; and locale is 🟡 — the English member of a pair is the one in the first half of `GP_TITLE.p00`, 8/8 structurally but only 3/3 where a capture can check, and the three pairs that matter are the three no capture can check. Mine: **the message gives the bundles as "0/1 and 10/11", which is the `is_build` ordinal, and `authored/screen_names.json` is keyed by PAK ENTRY** — in entry space 10 and 11 are `palogo_sqex` and `palogo_gamearts`, the splashes. See the refutation section in `DECISIONS.md`. |
## New ask, 2026-08-30 — derived from HANDOFF `27938aa`, at port `HEAD` `f33aeca`
| Milestone | Needs | HANDOFF | State |
|---|---|---|---|
| P3/P5 — the record loop length | **expose `+0x08` in `sylpheed_formats`' public API** | `27938aa` | 🔴 **the port cannot obey the instruction with anything published.** HANDOFF says *"stop shipping 105"*, which presumes the port can read the loop length. It is decoded in `examples/record_loop_length.rs`, asserted in `tests/ui_record_loop_length_disc.rs`, written up in `docs/re/structures/ui-record-loop-length.md` — and exposed in the crate's API **on no ref at all** (checked against every ref touching `crates/sylpheed-formats/src/`). `screen.rs` reads the four bytes itself, guarded on the `RATC` magic, because `parse_build` publishes each record's `(offset, size)`. That works and it is **the port holding a format detail it should not own**: one `pub` field on the record type takes it back where it belongs, and the exporter's helper is documented to be deleted the day it appears. Not blocking — the value is shipping. |
| P3/P5 — the other focus records | **do `ptbtn01f…05f` and `ptbtn11f…13f` animate while focused?** | `27938aa` | ❔ **open, and deliberately not inferred.** The export shows all eleven declaring the same 120-unit cycle, and `looping_focus_records` names only the plate. A declared cycle is not evidence that the game runs it — `authored/timing.json` already argues the pulse rule matches 82 of 212 elements and would make the copyright notice pulse. Whether a focused menu button glows is **behavioural**: outside my role, asking. |
## Answered since this file was last written — no longer blocking
Q1 (keyframe time unit — linear ramp, 2 units per rendered frame, 1 unit = 1/60 s

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
233 sections. Search this before re-deriving anything.
235 sections. Search this before re-deriving anything.
* [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28)
* [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28)
@@ -244,6 +244,8 @@ dies, which is what this file is for.
* [Full regression after a session of edits — and the phase term moving two published rows](#full-regression-after-a-session-of-edits--and-the-phase-term-moving-two-published-rows)
* [Narrowing my own hook — 33 was a measurement of the regex](#narrowing-my-own-hook--33-was-a-measurement-of-the-regex)
* [Their Q10 correction checked, and the register's cost is per-*mention*, not per-correction](#their-q10-correction-checked-and-the-registers-cost-is-per-mention-not-per-correction)
* [The contract I read every iteration is 3 185 lines shorter than the contract](#the-contract-i-read-every-iteration-is-3-185-lines-shorter-than-the-contract)
* [A refutation attempt on `+0x08 is the loop length` — it survives, and the port adopts it](#a-refutation-attempt-on-0x08-is-the-loop-length--it-survives-and-the-port-adopts-it)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -12194,3 +12196,149 @@ new occurrence needing the token, including in the sentence explaining that this
happens. **Four instances, each inside text about the mechanism.** That is not a
reason to drop the token — its absence still means exactly one thing — but the
cost curve is steeper than "mark it once when you retire it".
## The contract I read every iteration is 3 185 lines shorter than the contract
📌 **`docs/port/HANDOFF.md` on `main`: 926 lines, last touched `9ca1eb5`, 2026-08-29.
The live one: 4 111 lines, `27938aa`, today. 96 commits I have never read,
+3 930/745.** The mission tells me to read HANDOFF every iteration and I have.
I have been reading `main`'s copy. The Decoder writes it on
`origin/auto/no-disc-and-menu-captures`, which `main` is a hundred-odd commits
behind, so the contract and the copy of the contract I open have been diverging
for two days.
Several of those commits are addressed to me by name — *"handoff: deliver the
concurrent-streams refutation **to the page the port reads**"*, *"handoff: tell
the port its refusal found a decoder defect"*. They were delivered to the page I
read. The page I read is not the page they were delivered to.
### 🔴 The instruction that was supposed to prevent this cannot detect it
`BLOCKED.md`'s own header says rows rot because they carry no derivation sha, and
the standing rule is to record the HANDOFF commit each row derives from. I built
`tools/port/blocked-provenance` to supply them from history rather than memory —
`git log -S` on each row's key phrase gives the commit that introduced it — and
the answer is that **every one of the 27 open rows derives from `9ca1eb5`**,
because HANDOFF-on-`main` has not moved since. A constant cannot discriminate.
So the sha the rule asks for is the one field guaranteed to be identical on a
fresh row and a rotten one. **The rot is not that rows are old. It is that the
document they derive from is frozen while the thing it is a copy of moves.**
⚠️ And my own `BLOCKED.md` asserts *"HANDOFF has not moved in four milestones"*.
**That is withdrawn.** HANDOFF has moved 96 times. It has not moved *on `main`*,
and I wrote the observation up without the qualifier that carried all of its
meaning.
### What the tool measures instead, and the control that caught it lying
Counted against every ref rather than my own ancestry, each row has **196 unread
`docs/re/` commits** behind it — again identical for every row, because none of
that branch is my ancestor. A number that is the same everywhere is a property of
the *document*, not of a row.
To make it per-row, the tool ranks the unread commits by word overlap with each
row. **The first version silently missed its own known positive.** `P6 looping`
asks where the menu loop restarts; `712cac8` measures it at 9.44 s and this port
has shipped that value since. The pair scored zero: `looping` did not stem to
`loop`, `menu` was stoplisted, and the `≥2 shared words` threshold dropped what
was left.
The threshold was the defect, not the constant. **Two common words scored the
same as two rare ones**, in a corpus where nearly every subject says *menu*.
Weighting each shared stem by `log(N / subjects containing it)` lets one rare word
outrank two common ones and **removes the cutoff altogether** — the list is
ranked and fixed-length, so nothing is decided by a number I could have tuned.
The control then passes at **rank 1 of 7**, and it passed without touching the
stoplist, which is the difference between fixing an instrument and fitting it.
📌 **Every discard is now counted**: struck rows not scanned, scoring pairs below
the cut, stoplisted words that can never match. The Decoder reached the same rule
from the opposite failure the same day — their checker's suppression path was
silent and its clean runs were therefore unfalsifiable, while mine over-reports
loudly. **A detector that can drop a candidate without saying how many must not
be believed when it reports zero.**
### It immediately found two open rows whose answers were already written
| row | unread commit | |
|---|---|---|
| `P3 — the plate's PULSE` | `07e93ce` (score 14.0) | the period is **120, not 105** |
| `P5 — Ⓑ on the main menu` | `9a10258` (score 10.6) | the menu has **no idle self-return** — the row's own reasoning is refuted |
Both had sat unread for a day. Neither needed an experiment; they needed the
document to be looked at.
## A refutation attempt on `+0x08 is the loop length` — it survives, and the port adopts it
The claim the port was about to build on, so the one to attack (PROTOCOL:
*refutation is cheapest where the other agent is most confident*). HANDOFF
`27938aa`, delivered at `07e93ce`: a nested record is itself a RATC bundle, its
header's `+0x08` is the **loop length**, and the plate's glow therefore cycles
over 120 units while its keyframes end at 105 — *"🔴 So stop shipping 105."*
I re-ran **their own two controls** on my own reading of the disc rather than
taking the census —
`cargo run -p sylpheed-export --example record_loop_control`:
| | disc-wide | |
|---|---|---|
| timed nested records | 1 781 | |
| `+0x08 == max t` | 1 643 | 92.3 % |
| `+0x08 > max t` (a hold) | 138 | 7.7 % |
| **`+0x08 < max t`** | **0** | **0.00 % — the falsifier never fires** |
Identical to their figures. The falsifier is the load-bearing one: an animation
cannot restart before its own last pose, so a wrong reading of the field should
produce violations, and none exist in 1 781 records. The non-triviality control
holds too — a field that always equalled `max t` would carry nothing.
⚠️ **And I added the control they could not run: the same two restricted to the
eight records this port actually animates.** A disc-wide 0.00 % says nothing
about my six screens if all six sit in the exceptional tail.
```
record +0x08 max t slack
ptbtn00f 120 105 15
ptbtn01f…05f 120 120 0
ptloop01/02 600/720 600/720 0
shipped: 7 exact, 1 hold, 0 falsified
```
Their table, cell for cell. ⚠️ Note how narrowly non-trivial it is **here**:
across the disc 7.7 % of records hold, but on my shipped set exactly **one of
eight** does. The claim survives on my data; it is not richly confirmed by it.
### 🔴 The instruction cannot be complied with using anything they published
*"Stop shipping 105"* presumes the port can read the field. `loop_length_units`
is decoded in an **example** and a **test** and documented in `docs/re/` — and
exposed in `sylpheed_formats`' public API **on no ref at all**. I checked every
ref that touches the crate's `src/`.
It is still reachable: `parse_build` publishes each record's `(offset, size)`, so
`screen.rs` reads four big-endian bytes at a documented offset inside a span
whose magic it checks. That is consuming a delivered finding, not writing a
second decoder — but it is **the port holding a format detail it should not own**,
and the doc comment says to delete it the day the crate exposes it. Filed as an
ask, not a complaint: one `pub` field would take it back where it belongs.
### The value does not change. Its provenance does.
`authored/timing.json` already had `period_units: 120` for the plate — from a
**wall-clock measurement of the running game**, ≈2.37 s. The disc declares
**120**. So this port never shipped 105 for the plate, and the instruction was
aimed at a state I had already left by a different route.
📌 **That is the result worth keeping.** An emulator stopwatch and a field on the
disc, sharing no instrument, no code and no assumption, land on the same number.
`ScreenView._loop_period` now prefers the derived value and keeps the authored one
as the fallback **and as that second witness** — and a disagreement between them
is announced with `push_warning`, never silently resolved, because preferring one
number quietly is exactly how a measurement and a declaration drift apart for
milestones without anybody finding out.
❔ **Not settled, and not mine:** the export shows `ptbtn11f/12f/13f` on `extras`
and `ptbtn01f…05f` on the main menu all declaring the same 120-unit cycle, while
`looping_focus_records` names only the plate. Whether those records *animate*
while focused is behavioural — an ask, not an inference from the header.

View File

@@ -405,6 +405,32 @@ static func settle_units(element: Dictionary) -> float:
## "this loops". What the disc says is 0° → 360° over `t`; what the RE agent
## measured is that the turn repeats rather than stopping. Those are two
## different sources and the day a loop flag is decoded, this goes.
## The cycle length of a looping focus record, **derived in preference to authored**.
##
## The record header's `+0x08` says where the cycle restarts, and it is not the
## last keyframe's time: the plate's glow ramps 0→80→0 over 105 units inside a
## 120-unit cycle and rests dark for 15. The exporter now carries it as
## `focus.loop_length_units`, so the period comes off the DISC.
##
## `authored/timing.json` had 120 already, from a wall-clock measurement of the
## running game (≈2.37 s). **The two agree**, which is why this is a provenance
## change and not a pixel change — an emulator stopwatch and a field on the disc,
## sharing no instrument, landing on the same number. The authored value stays as
## the fallback and as that second witness.
##
## A DISAGREEMENT IS ANNOUNCED, never silently resolved. Preferring one number
## without saying so is how a measurement and a declaration drift apart for
## milestones without anybody learning that they had.
func _loop_period(focus: Dictionary, loop: Dictionary) -> float:
var authored := float(loop.get("period_units", 0.0))
var derived := float(focus.get("loop_length_units", 0.0))
if derived <= 0.0:
return authored
if authored > 0.0 and absf(derived - authored) > 0.5:
push_warning("focus record %s: the disc declares a %.0f-unit cycle, `authored/timing.json` says %.0f -- using the disc. One of them is wrong and this message is the only thing that will say so." % [String(focus.get("record", "?")), derived, authored])
return derived
static func spin_period_units(element: Dictionary) -> float:
var frames: Array = element.get("keyframes", [])
if frames.size() != 2:
@@ -645,7 +671,7 @@ func _draw_focus(element: Dictionary) -> void:
var was := holding
holding = false
var lt: float = time_units if loop_phase_units < 0.0 else loop_phase_units
pose = pose_at(fe, fposmod(lt, float(loop["period_units"])))
pose = pose_at(fe, fposmod(lt, _loop_period(focus, loop)))
holding = was
var pivot := _vec(fe.get("pivot", [0, 0]))
var pos := _vec(pose.get("pos", [0, 0]))

220
tools/port/blocked-provenance Executable file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""Date every open row in BLOCKED.md from history, instead of guessing.
`BLOCKED.md` is required to record the HANDOFF commit each row derives from, and
none of the rows in the two open tables do. The file itself says why: nobody
knows when most of them were written, and inventing a sha would be worse than
admitting there is none.
But git does know. A row's derivation is not a memory, it is the commit that
introduced the row -- recoverable with a pickaxe over the file's own history.
This prints, per row:
introduced the oldest commit whose diff added the row's key phrase
HANDOFF@ `git log -1 -- docs/port/HANDOFF.md` as of that commit
unread commits touching docs/re/ ON ANY REF that are not ancestors of
that commit -- decoding the row has never been read against
`--all`, not my own ancestry, and that distinction is the whole finding. Counted
against my checkout every row scores ZERO, which is true and useless: the
Decoder's live decoding sits on `origin/auto/no-disc-and-menu-captures`, `main`
is a hundred-odd commits behind it, and HANDOFF has not moved in four
milestones. So a row can be derived from the newest HANDOFF there is and still
be a day behind the decoding -- and the instruction to record the HANDOFF sha
CANNOT DETECT THAT, because the sha it asks for is constant.
That is the rot mechanism the 2026-08-30 audit found three instances of, and it
is not the one the header of BLOCKED.md describes.
Nothing here is authored. Every field is read out of git, and a row whose key
phrase has been rewritten since it was introduced reports `?` rather than a
plausible-looking sha.
"""
import re, subprocess, sys
DOC = "docs/port/BLOCKED.md"
def git(*a):
return subprocess.run(["git", *a], capture_output=True, text=True).stdout.strip()
TOP = 3
def idf_of(commits):
"""log(N / how many subjects use the word) -- rarity, from the corpus itself."""
import collections, math
df = collections.Counter()
for _, subj in commits:
df.update(tokens(subj))
n = len(commits)
return collections.defaultdict(lambda: math.log(n), {w: math.log(n / c) for w, c in df.items()})
def key_of(cell):
"""The longest markdown-free fragment -- what to pickaxe for.
Cells get struck through and re-emphasised as they are resolved, so the cell
as it stands today is not what was committed. The inner text survives that.
"""
frags = [f.strip(" ?.") for f in re.split(r"[*~`]+", cell)]
frags = [f for f in frags if len(f) >= 20]
return max(frags, key=len) if frags else None
def rows():
"""Every table row in the open sections, in file order."""
open_only, out = False, []
for line in open(DOC, encoding="utf-8"):
if line.startswith("## "):
open_only = line.startswith("## Still open")
continue
if not open_only or not line.startswith("| "):
continue
cells = [c.strip() for c in line.strip().strip("|").split(" | ")]
if len(cells) < 4 or cells[0] in ("Milestone", "---"):
continue
out.append(cells)
return out
STOP = set("""this that with from what which when does than the and are was were
have has been will would could should port game screen menu audio does not any
each only its it's whether where else same both very more most into onto over
under about after before still open blocked answered measured wrong right first
second third disc file files commit branch docs main head sha row rows table
mission handoff decoder agent claim claims""".split())
def stem(w):
"""Crudest possible stemmer, and it earns its place with a control.
Without it `looping` does not match `loop` and the P6 row whose answer is
sitting in an unread commit scores zero -- which is what happened.
"""
for suf in ("ping", "ing", "ted", "ed", "es", "s"):
if w.endswith(suf) and len(w) - len(suf) >= 4:
return w[: -len(suf)]
return w
def tokens(text):
ws = re.findall(r"[a-z0-9_]{4,}", text.lower())
return {stem(w) for w in ws if w not in STOP}
def rank(rt, commits, idf):
"""Score every unread commit against one row, rarest words first.
A COUNT of shared words is the wrong instrument: `menu` and `loop` shared
scores the same as `plate` and `pulse`, and in this corpus almost everything
says `menu`. Weighting each shared stem by log(N / commits containing it)
lets one rare word outrank two common ones -- and it removes the threshold,
which was the part that could be tuned. The list is RANKED, fixed length,
so nothing is decided by a cutoff nobody can justify.
"""
out = []
for sha, subj in commits:
shared = rt & tokens(subj)
if shared:
out.append((sum(idf[w] for w in shared), sha, subj, shared))
return sorted(out, reverse=True)
def overlap(rs):
"""Which unread commits NAME something an open row is about.
Crude on purpose, and it says so: word overlap between a row and a commit
SUBJECT, ranked by rarity, top few printed with the words that earned the
rank so the reader judges rather than trusting the match. It cannot tell
relevance from coincidence -- it narrows 196 commits to a short list worth
opening, and nothing more.
"""
log = git("log", "--all", "--not", "HEAD", "--format=%h\t%s", "--", "docs/re/")
commits = [l.split("\t", 1) for l in log.splitlines() if "\t" in l]
print(f" {len(commits)} unread docs/re/ commit(s) exist on other refs.")
print(" Crude word overlap with the open rows -- a reading list, not a verdict:\n")
idf = idf_of(commits)
hits = struck = dropped = 0
for cells in rs:
if cells[0].startswith("~~"):
struck += 1 # already struck; re-reading it settles nothing
continue
scored = rank(tokens(cells[0] + " " + cells[1]), commits, idf)
dropped += max(0, len(scored) - TOP)
for score, sha, subj, shared in scored[:TOP]:
hits += 1
print(f" {re.sub(r'[*~`]', '', cells[0])[:36]:<36} {sha} {score:5.1f} {subj[:58]}")
print(f" {'':<36} {'':<8} via {', '.join(sorted(shared))}")
if not hits:
print(" (no row shares a word with any unread commit)")
# Every discard, counted. A detector that can drop a candidate in silence
# has an unfalsifiable clean run -- which is how the P6 looping row stayed
# marked open for a day while its answer sat in `712cac8`, and how the same
# class of miss went unnoticed in the Decoder's checker on the same day.
print(f"\n suppressed: {struck} struck row(s) not scanned; {dropped} scoring")
print(f" pair(s) ranked below top-{TOP} and not shown; {len(STOP)} word(s)")
print(" stoplisted and unable to match at any rank.")
print()
def control():
"""Known positive: the row whose answer is demonstrably in an unread commit.
`P6 looping` asks where the menu loop restarts. `712cac8` measures it at
9.44 s and the port has since shipped that value, so the pair MUST match. It
did not, until stemming -- the check exists so that regression is loud.
"""
log = git("log", "--all", "--not", "HEAD", "--format=%h\t%s", "--", "docs/re/")
commits = [l.split("\t", 1) for l in log.splitlines() if "\t" in l]
scored = rank(tokens("P6 looping where a menu loop restarts"), commits, idf_of(commits))
at = next((i for i, r in enumerate(scored) if r[1].startswith("712cac8")), None)
ok = at is not None and at < TOP
print(f" control: P6-looping vs 712cac8 -> rank {at} of {len(scored)} scoring "
f"{'✅' if ok else f'🔴 OUTSIDE TOP-{TOP}, THE KNOWN POSITIVE IS MISSED'}")
return ok
def main():
if "--control" in sys.argv:
sys.exit(0 if control() else 1)
rs = rows()
if not rs:
sys.exit(f"{DOC}: no rows found under a '## Still open' heading")
head_handoff = git("log", "-1", "--format=%h", "--", "docs/port/HANDOFF.md")
print(f" {DOC}: {len(rs)} rows in the open tables")
print(f" HANDOFF is at {head_handoff} today\n")
print(f" {'row':<44} {'introduced':<12} {'date':<11} {'HANDOFF@':<9} unread")
unknown = 0
for cells in rs:
milestone, needs = cells[0], cells[1]
label = re.sub(r"[*~`]", "", milestone)[:43]
key = key_of(needs) or key_of(milestone)
sha = date = handoff = "?"
since = "-"
if key:
# oldest commit whose diff changed the number of occurrences
log = git("log", "--format=%h %ad", "--date=short", "-S", key, "--", DOC)
if log:
sha, date = log.splitlines()[-1].split()
handoff = git("log", "-1", "--format=%h", sha, "--", "docs/port/HANDOFF.md")
unread = git("log", "--all", "--not", sha, "--format=%h", "--", "docs/re/")
since = str(len(unread.splitlines())) if unread else "0"
if sha == "?":
unknown += 1
flag = ""
if since not in ("-", "0") and not milestone.startswith("~~"):
flag = f" <- never read against {since} docs/re/ commit(s)"
print(f" {label:<44} {sha:<12} {date:<11} {handoff:<9} {since:>3}{flag}")
print()
overlap(rs)
if unknown:
print(f" ⚠️ {unknown} row(s) could not be dated: the key phrase has been")
print(" rewritten since it was introduced, so history cannot place it.")
print(" Not a staleness verdict. A high `unread` is not a wrong row -- most of")
print(" that decoding is irrelevant to most rows. It is the size of the surface")
print(" nobody has looked at, and it is what the HANDOFF sha was supposed to be.")
main()

View File

@@ -23,7 +23,7 @@
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
WINDOW=400 # characters either side of a hit in which the marker must appear
fail=0
fail=0; total_marked=0
# 🔴 THE MARKER IS AN EXPLICIT SENTINEL, NOT A KEYWORD.
#
@@ -116,16 +116,16 @@ HOOK
while IFS= read -r claim; do
[ -z "$claim" ] && continue
hits=0; bad=0
hits=0; bad=0; marked=0
while IFS= read -r loc; do
[ -z "$loc" ] && continue
f=${loc%%:*}
hits=$((hits+1))
python3 - "$f" "$claim" "$MARKER" "$WINDOW" <<'PY' || bad=$((bad+1))
out=$(python3 - "$f" "$claim" "$MARKER" "$WINDOW" <<'PY'
import sys
f, claim, marker, w = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
s = open(f, encoding="utf-8", errors="ignore").read()
i = 0
i = n = 0
while True:
i = s.find(claim, i)
if i < 0:
@@ -133,17 +133,32 @@ while True:
if marker.lower() not in s[max(0, i-w):i+w+len(claim)].lower():
print(" unmarked in %s at char %d" % (f, i))
sys.exit(1)
n += 1
i += len(claim)
# Every suppression, counted. A checker that can discard an occurrence in silence
# reports the same clean run whether or not a live assertion is hiding among the
# marked ones, and its zero is unfalsifiable. Reached from the loud end here and
# from the quiet end by the Decoder on the same day: their marker language was
# vouching for 8 of 8 mentions, so their 0 was going to be 0 either way.
print(n)
sys.exit(0)
PY
) && marked=$((marked + out)) || { printf '%s\n' "$out"; bad=$((bad+1)); }
done < <(grep -rl -- "$claim" docs/ crates/ port/ tools/ authored/ 2>/dev/null || true)
if [ "$bad" -eq 0 ]; then
printf ' %-42s %d file(s), all marked\n' "$claim" "$hits"
printf ' %-42s %d file(s), %d occurrence(s) suppressed\n' "$claim" "$hits" "$marked"
total_marked=$((total_marked + marked))
else
printf ' %-42s 🔴 %d file(s) assert it unmarked\n' "$claim" "$bad"; fail=1
fi
done <<< "$REGISTER"
echo
printf ' %d occurrence(s) were SUPPRESSED by a neighbouring `%s`.\n' "$total_marked" "$MARKER"
echo " That number is the size of what this check chose not to look at. A"
echo " detector that can discard a candidate without saying how many has an"
echo " unfalsifiable clean run -- its zero reads the same whether or not a live"
echo " assertion is hiding among the marked ones."
echo
[ $fail -eq 0 ] && echo "every refuted claim appears only inside its correction" \
|| echo "🔴 a refuted claim is still being asserted"