diff --git a/Cargo.lock b/Cargo.lock index e655ebf9..17a66a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4627,7 +4627,7 @@ dependencies = [ "image", "serde", "serde_json", - "sylpheed-formats 0.1.0 (git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-30)", + "sylpheed-formats 0.1.0 (git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-30b)", ] [[package]] @@ -4651,7 +4651,7 @@ dependencies = [ [[package]] name = "sylpheed-formats" version = "0.1.0" -source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-30#c8d3a6a15d5feb0416b2b589cb8e0c442f7422d3" +source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-30b#b5df02a4f74c80b93b93cd1a26296ee08040d826" dependencies = [ "anyhow", "binrw", diff --git a/crates/sylpheed-export/Cargo.toml b/crates/sylpheed-export/Cargo.toml index b941fd21..81334496 100644 --- a/crates/sylpheed-export/Cargo.toml +++ b/crates/sylpheed-export/Cargo.toml @@ -81,7 +81,7 @@ license.workspace = true # geometry -- does NOT reach this port from here: `sylpheed-cli` builds from the # WORKSPACE crate, so the reference renderer stays unrotated until the tag lands # on `main`. This bump is for the parser, not for the renderer. -sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-08-30" } +sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-08-30b" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/sylpheed-export/examples/record_loop_control.rs b/crates/sylpheed-export/examples/record_loop_control.rs index f9e5e547..eb24796d 100644 --- a/crates/sylpheed-export/examples/record_loop_control.rs +++ b/crates/sylpheed-export/examples/record_loop_control.rs @@ -8,10 +8,15 @@ //! 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: +//! ✅ **Since then the crate exposes it** — `ui_layout::loop_length_units`, taken +//! at `formats-pin-2026-08-30b` — and `screen.rs` has deleted its local copy. +//! +//! 🔴 **This file deliberately did NOT follow it.** The read below is still the +//! raw four bytes, because the moment a control calls the API it is meant to +//! check, it stops being a control and becomes the API tested against itself. It +//! is the independent reading that makes the falsifier mean anything. +//! +//! 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; diff --git a/crates/sylpheed-export/src/screen.rs b/crates/sylpheed-export/src/screen.rs index f98ad425..003f56aa 100644 --- a/crates/sylpheed-export/src/screen.rs +++ b/crates/sylpheed-export/src/screen.rs @@ -108,18 +108,6 @@ 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`. @@ -140,11 +128,12 @@ pub struct Focus { /// 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.** + /// ✅ **The port no longer owns this reading.** For one iteration `screen.rs` + /// held its own guard and byte read, because the field was decoded in an + /// example and a test and exposed in no public API on any ref. It is now + /// `ui_layout::loop_length_units`, taken at `formats-pin-2026-08-30b`, and + /// the local copy is deleted — the doc comment that promised that deletion + /// is the only reason it did not quietly become permanent. pub loop_length_units: Option, /// Back-to-front, in the leaf's own declaration order. pub elements: Vec, @@ -489,7 +478,7 @@ pub fn export_build( } else { Some(Focus { record: rec.to_string(), - loop_length_units: record_loop_units(bundle, off, size), + loop_length_units: ui_layout::loop_length_units(&bundle[off..off + size]), elements: fes, }) }) @@ -550,7 +539,7 @@ pub fn export_build( if !fes.is_empty() { focus = Some(Focus { record: rec, - loop_length_units: record_loop_units(bundle, off, size), + loop_length_units: ui_layout::loop_length_units(&bundle[off..off + size]), elements: fes, }); } diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index ba1d1fc2..2d105f57 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -239 sections. Search this before re-deriving anything. +240 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) @@ -250,6 +250,7 @@ dies, which is what this file is for. * [A refutation attempt on the fade numbers — it survives, from a third reader](#a-refutation-attempt-on-the-fade-numbers--it-survives-from-a-third-reader) * [The walk is checked too, and "only the ring moves" tested against my own renderer](#the-walk-is-checked-too-and-only-the-ring-moves-tested-against-my-own-renderer) * [The `+0x08` ask came back answered — and is not consumable yet](#the-0x08-ask-came-back-answered--and-is-not-consumable-yet) +* [The pin moves to `formats-pin-2026-08-30b`, and the port stops owning `+0x08`](#the-pin-moves-to-formats-pin-2026-08-30b-and-the-port-stops-owning-0x08) ## P0 — the exporter, 2026-08-28 @@ -12483,3 +12484,38 @@ big-endian read. So the deletion my doc comment promises is a one-line switch. branch would replace a deliberate pin with an incidental one — and `BLOCKED.md` already records this pin as load-bearing. **Asked for a tag; keeping the local read, which is guarded and controlled, until one exists.** + +## The pin moves to `formats-pin-2026-08-30b`, and the port stops owning `+0x08` + +The tag was cut within the iteration, so the deletion the doc comment promised is +done: `screen.rs` calls `ui_layout::loop_length_units` and its local `RATC` guard +and byte read are **gone**. One line, exactly as predicted — the promise in the +comment is the only reason a temporary reading did not quietly become permanent. + +**What the pin actually brings, checked before taking it.** A pin bump moves the +whole crate, not one function, and this one is recorded load-bearing, so the two +commits between the tags were read rather than assumed: + +| | | +|---|---| +| `b5df02a` | adds the public `loop_length_units` | +| `d020845` | **comment-only** — two "fixed code under an unfixed description" corrections | + +No behavioural change in either. `d020845` is worth noting for what it is: the +`rest` override's stated purpose was retired by the record-layout fix and the +comment still claimed it tested the shifted reading, and a `continue` branch was +documented with the pre-fix rule. **Both are the same failure this port hit in +`spin_period_units` — a doc comment describing the rule the body no longer +implements.** Three instances now, across two agents and two languages. + +### 🔴 The control did NOT follow the API, on purpose + +`examples/record_loop_control.rs` still reads the raw four bytes. **The moment a +control calls the API it exists to check, it stops being a control and becomes +the API tested against itself.** The falsifier — 0 of 1 781 records declaring +less than their own last pose — means something only because the reading is +independent of the crate's. Re-run at the new pin: unchanged, 7 exact and +`ptbtn00f` the one hold. + +📌 So the port now holds **one** copy of this reading instead of two, and it is +the copy whose job is to disagree.