From c909d1dc4993d7a1e4bb155e6084d9dbc81db7d0 Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Sun, 30 Aug 2026 20:50:33 +0000 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF --- .../examples/record_loop_control.rs | 106 +++++++++ crates/sylpheed-export/src/screen.rs | 46 +++- docs/port/BLOCKED.md | 25 +- docs/port/DECISIONS.md | 150 +++++++++++- port/scripts/screen_view.gd | 28 ++- tools/port/blocked-provenance | 220 ++++++++++++++++++ tools/port/check-claims | 25 +- 7 files changed, 587 insertions(+), 13 deletions(-) create mode 100644 crates/sylpheed-export/examples/record_loop_control.rs create mode 100755 tools/port/blocked-provenance diff --git a/crates/sylpheed-export/examples/record_loop_control.rs b/crates/sylpheed-export/examples/record_loop_control.rs new file mode 100644 index 00000000..f9e5e547 --- /dev/null +++ b/crates/sylpheed-export/examples/record_loop_control.rs @@ -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 = BTreeMap::new(); + let mut shipped: BTreeMap = 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 } } diff --git a/crates/sylpheed-export/src/screen.rs b/crates/sylpheed-export/src/screen.rs index d74a65a1..f98ad425 100644 --- a/crates/sylpheed-export/src/screen.rs +++ b/crates/sylpheed-export/src/screen.rs @@ -108,10 +108,44 @@ pub struct FocusElement { pub keyframes: Vec, } +/// 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 { + 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, /// Back-to-front, in the leaf's own declaration order. pub elements: Vec, } @@ -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, + }); } } } diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 098e8ebc..c784504b 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -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.40–87.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.08–2.17 s at the 27.6–28.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 ~8–10 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 ~8–10 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 ~8–10 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. | ~~P1–P7 β€” 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 diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 4e8cfeae..4f2f0930 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -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) ## 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. diff --git a/port/scripts/screen_view.gd b/port/scripts/screen_view.gd index e0a0b94d..1cf5324f 100644 --- a/port/scripts/screen_view.gd +++ b/port/scripts/screen_view.gd @@ -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])) diff --git a/tools/port/blocked-provenance b/tools/port/blocked-provenance new file mode 100755 index 00000000..a78c91ec --- /dev/null +++ b/tools/port/blocked-provenance @@ -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() diff --git a/tools/port/check-claims b/tools/port/check-claims index fdca3312..a9a07770 100755 --- a/tools/port/check-claims +++ b/tools/port/check-claims @@ -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"