port: check the contract's numbers instead of reading 4111 lines of it

HANDOFF on main is 926 lines frozen at 0fd8e69; the live one is 4111 at 27938aa,
99 commits and 70 sections this port has never opened. 'Read the other one' is
not good enough -- more arrive daily and nothing says which of them contradict
what the port ships.

tools/port/contract-check pulls each expected value OUT OF THE LIVE HANDOFF TEXT
by pattern, never restating it, and reconciles it against export/ and authored/:
the three fade quads' poses, the fade-out ramps, the plate glow cycle, the menu
BGM loop window, the black hold, the menu bank, and the boot splash dwells. All
seven agree. It reads the newest HANDOFF on any ref and prints how far the
working tree's copy is behind.

Three outcomes, and ANCHOR LOST is the point: a check whose pattern no longer
matches the contract has stopped checking, and passes forever while measuring
nothing. Reported as loudly as a mismatch.

--control perturbs the contract one token per check -- 120 becomes 121, -ss 9.44
becomes 9.45 -- and requires every check to fail. All seven do. Without it these
would be seven passes from an instrument nobody had seen react to anything, the
same unfalsifiable clean run the suppression count fixed in check-claims. Both
steps now assert in check-all.

Refutation attempted on 'your menu fade-in is 5x too slow', which corrected a
0.97 s fade-in to 12 units after their helper was found reading each pose's time
from the next record's word. The port never held that number -- ScreenView
animates pteff00 from its own exported keyframes, so the error could not reach
it, the second consecutive iteration where an instruction was aimed at a state I
was not in. But it makes the export an independent check on the correction, and
it holds exactly: their rebuilt tools and my pinned crate are different readers
of the same bytes, so agreement means both got the record-layout fix -- which is
what their helper had not had. Same for the 190/145 splash retraction, now
re-derived a third time.

Filed for the human: only a human merges a topic branch, so the port cannot land
HANDOFF on main and will not merge another agent's branch into its own.

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 21:00:20 +00:00
parent c909d1dc49
commit ad80938efc
4 changed files with 305 additions and 1 deletions

View File

@@ -147,6 +147,12 @@ 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`. |
## Ask for the HUMAN, 2026-08-30 — derived from HANDOFF `27938aa`
| Milestone | Needs | HANDOFF | State |
|---|---|---|---|
| P0P7 — the contract itself | **land HANDOFF on `main`, or tell the port to read the branch** | `27938aa` | 🔴 **the document the mission calls the contract is not the document the port opens.** `main`'s copy is **926 lines** frozen at `9ca1eb5`; the live one is **4 111** at `27938aa`, **99 commits** unread, **70 sections** this port has never opened — several titled *"deliver … to the page the port reads"*. Only a human merges a topic branch (PROTOCOL), so the port cannot fix this and will not merge another agent's branch into its own. **Mitigated, not solved:** `tools/port/contract-check` now reads the newest HANDOFF on any ref and reconciles seven of its numbers against `export/` and `authored/` — all seven agree — and `check-all` asserts it plus its known-negative control. That is seven values out of 4 111 lines. ⚠️ The rest is still read by eye, and two consecutive iterations have found instructions addressed to the port sitting unread for a day. |
## New ask, 2026-08-30 — derived from HANDOFF `27938aa`, at port `HEAD` `f33aeca`
| Milestone | Needs | HANDOFF | State |

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 -->
235 sections. Search this before re-deriving anything.
237 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)
@@ -246,6 +246,8 @@ dies, which is what this file is for.
* [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)
* [The contract is checked now, not read — `tools/port/contract-check`](#the-contract-is-checked-now-not-read--toolsportcontract-check)
* [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)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -12342,3 +12344,74 @@ milestones without anybody finding out.
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.
## The contract is checked now, not read — `tools/port/contract-check`
Last iteration established that `docs/port/HANDOFF.md` on `main` is **926 lines
frozen at `9ca1eb5`** while the live document on the Decoder's branch is **4 111
lines at `27938aa`** — now **99** unread commits. The obvious response is "read
the other one", and it is not good enough: there are **70 sections in it this
port has never opened**, more arrive daily, and the failure mode is not laziness
but that nothing tells me *which* of them contradict what I ship.
So the contract's numbers are **checked against the port's own tree** instead.
Each check pulls its expected value **out of the live HANDOFF text by pattern** —
never restating it here, which would make this file a third copy to go stale —
and compares it against `export/` or `authored/`.
| | contract | port | |
|---|---|---|---|
| fade quad, title / menu / extras | `[0,16,261,269]` `[0,12,70,80]` `[0,12,64,74]`, α 255/0/0/255 | identical | ✅ |
| fade-out ramps | 10, 10, **8** on the title | 10, 10, 8 | ✅ |
| plate glow cycle | 120 | 120 derived, 120 authored | ✅ |
| menu BGM loop window | `-ss 9.44 -t 61.87` | 9.44 / 61.87 | ✅ |
| black hold between screens | 0 | 0 | ✅ |
| menu BGM bank | `BGM_103` | `BGM_103.slb` | ✅ |
| boot splash dwells | 190 and 145 | 190 and 145 | ✅ |
### Three outcomes, and the third is the point
`ok`, `MISMATCH`, and **`ANCHOR LOST`** — the pattern no longer matches the
contract. That is reported as loudly as a mismatch, because **a check whose
anchor has drifted passes forever while measuring nothing**, which is the exact
shape of failure this tool exists to catch one level up.
### The known negative, because a clean first run is not evidence
`--control` perturbs the contract by one token per check — `120, not 105` becomes
`121, not 105`, `-ss 9.44` becomes `-ss 9.45` — and **requires every check to
fail**. All seven do. Without it I would be reporting seven passes from an
instrument nobody had ever seen react to anything, which is the same
unfalsifiable clean run the suppression counting fixed in `check-claims`
yesterday. Both are in `check-all` now, the control as its own asserting step.
### 🔴 What a pass does not mean
Seven values out of a 4 111-line contract. **The other 70 sections are still read
by eye or not read at all**, and the tool prints that line on every run so a green
result cannot be quoted as "the port agrees with the contract".
## A refutation attempt on the fade numbers — it survives, from a third reader
The claim to attack, per PROTOCOL's *prefer what the port is about to build on*:
HANDOFF's *"🔴 the transition is OVERLAP, not ramp-then-hold. **And your menu
fade-in is 5× too slow**"*, which corrected `screen-transitions.md` from a 0.97 s
menu fade-in to **12 units, 0.20 s**. Their cause: `fade_quads.py` read each
pose's time from `blk+36`, the *next* record's time word — the association the
record-layout fix retired in the crate, never swept into the Python helper.
**The port never held that number.** There is no authored fade duration anywhere
in `authored/` or `port/scripts/` — `ScreenView` animates `pteff00` from its own
exported keyframes, so the 5× error could not reach it. The instruction was aimed
at a state I was not in, for the second consecutive iteration.
✅ **But it makes my export an independent check on their correction**, and it
holds exactly: `[0,16,261,269]`, `[0,12,70,80]`, `[0,12,64,74]` with α
255/0/0/255, and fade-outs 10/10/8. **Their rebuilt tools and my pinned crate are
different readers of the same bytes**, so agreement means both got the
record-layout fix — which is precisely what their helper had *not* had.
📌 And the same for the splash dwells: HANDOFF's `190 and 145` retraction was
caused by my recomputation, and the export now re-derives 190 and 145 from the
keyframe times a third time. A retraction confirmed by the party that provoked it
is worth less than one confirmed by a third reading; this is the third reading.