diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 66a33cdc..16209782 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -172 sections. Search this before re-deriving anything. +173 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) @@ -183,6 +183,7 @@ dies, which is what this file is for. * [`exit_ramp_units`: the refuted constant was living in a default](#exit_ramp_units-the-refuted-constant-was-living-in-a-default) * [Auditing the whole tree for "a deleted value that something still supplies"](#auditing-the-whole-tree-for-a-deleted-value-that-something-still-supplies) * [Counting the fallbacks instead of inspecting them — and one I had misjudged](#counting-the-fallbacks-instead-of-inspecting-them--and-one-i-had-misjudged) +* [The oracle harness was nondeterministic, and I quoted its numbers for a dozen iterations](#the-oracle-harness-was-nondeterministic-and-i-quoted-its-numbers-for-a-dozen-iterations) ## P0 — the exporter, 2026-08-28 @@ -9851,3 +9852,57 @@ an in-range fallback dangerous, shown rather than argued. ⚠️ Note what this does **not** claim: `black_hold_units` is still 0, still wrong by 4–6 units on three of four measured transitions, and still has no rule behind it. What changed is only that its *absence* is now audible. + +## The oracle harness was nondeterministic, and I quoted its numbers for a dozen iterations + +Reviewing my own logs: `verify-capture`'s `main_menu` row reads **13.30 / 13.27 / +13.25 / 13.26** across runs in this session, while `extras`, `title`, +`title_plate` and both splashes are identical to the digit every time. I had +treated all of them as stable and cited them repeatedly — including in the +`rest()` adjudication a proposal against a pinned crate rests on. + +### Cause: the one thing on a settled screen that is *supposed* to keep moving + +The focus ring spins on `time_units` **raw**, not the pose clamped by `holding` — +deliberately, and correctly: *"a spinning ring is the one thing on the settled main +menu that keeps moving, and the whole point of the finding is that it does not +stop."* So its angle at the moment of capture is set by the wall clock. `extras` +is stable because nothing there spins. + +⚠️ `--loop-phase` already existed and did **not** cover this. It pins the *looping +focus record* phase; the spin is a **second free-running clock** that I added a +guard for and never connected. Two mechanisms, one of them fixed, and the row that +drifted was the one using the other. + +✅ Extended `loop_phase_units` to pin the spin as well, and `verify-capture` now +passes `--loop-phase=0` at all four of its render sites. Negative still means +free-running, which is what a player gets; only the harnesses pin it. + +### The control, because three passing runs would not have been evidence + +The drift was **intermittent** — three unpinned runs gave 13.25, 13.26, 13.26. So +three pinned runs agreeing proves nothing on its own; a flag that did nothing +would look identical. The test that separates them is whether the pin **changes** +the answer: + +| phase | RMSE | +|---|---| +| 0 | 13.2583 | +| 30 | **13.1991** | +| 60 | 13.2637 | +| 90 | 13.2588 | + +✅ Live. The spread is **0.065**, which is the size of the drift I observed — so +the spin is the whole of it. Three pinned runs then return 13.26 exactly. + +📌 **A non-finding worth stating so nobody mines it later.** Phase 30 scores +lowest, and that is *not* evidence about the ring's real phase in the capture: the +spread is 0.065 against a gamma floor of ~13.2, roughly 200× smaller. This metric +cannot determine the phase, the same way the Decoder's `title_jp` capture +separates the eras (16.7) but cannot separate the pose policies (1.5) against its +own 1.2 flatness. **A margin only means something against the noise it sits on.** + +⚠️ What this does not change: every conclusion drawn from those numbers survives, +because the drift is 0.065 RMSE and the smallest margin any of them turned on was +0.14 % differing area. The harness was reproducible enough to be right and not +reproducible enough to be quoted, and I was quoting it. diff --git a/port/scripts/screen_view.gd b/port/scripts/screen_view.gd index acd60090..44201c2d 100644 --- a/port/scripts/screen_view.gd +++ b/port/scripts/screen_view.gd @@ -641,7 +641,19 @@ func _draw_focus(element: Dictionary) -> void: # `time_units` raw, NOT the pose clamped by `holding`: a spinning # ring is the one thing on the settled main menu that keeps moving, # and the whole point of the finding is that it does not stop. - rot = 360.0 * fposmod(time_units, period) / period + # + # 🔴 WHICH MADE THE ORACLE HARNESS NONDETERMINISTIC, and I quoted its + # numbers for many iterations without noticing. `verify-capture`'s + # `main_menu` row read RMSE 13.30 / 13.27 / 13.25 / 13.26 across + # runs -- the ring's angle at the moment of capture -- while + # `extras`, `title` and both splashes are identical to the digit. + # + # `loop_phase_units` already pins the LOOPING FOCUS RECORD phase for + # the same reason; the spin is a second free-running clock and needs + # the same pin. Negative still means free-running, which is what a + # player gets. Only the harnesses pass it. + var st: float = time_units if loop_phase_units < 0.0 else loop_phase_units + rot = 360.0 * fposmod(st, period) / period _draw_quad(tex, placement(pose, pivot, tex.get_size()), modulate_of(pose), pivot, pos, rot) drawn.append(fe.get("id", "")) diff --git a/tools/port/verify-capture b/tools/port/verify-capture index 59af5e13..8e6f236b 100755 --- a/tools/port/verify-capture +++ b/tools/port/verify-capture @@ -144,20 +144,20 @@ for row in "${MAP[@]}"; do if [ ${#want[@]} -gt 0 ] && ! printf '%s\n' "${want[@]}" | grep -qx "$name"; then continue; fi [ -f "$cap" ] || { printf '%-17s %s\n' "$name" "no capture"; continue; } if [ "$pose" = band ]; then - godot --path port --resolution 1280x720 -- "--screen=title" --overlay=press_start \ + godot --path port --resolution 1280x720 -- --loop-phase=0 "--screen=title" --overlay=press_start \ --time=3.95 "--capture=$OUT/$name.full.png" >"$OUT/$name.log" 2>&1 || true [ -f "$OUT/$name.full.png" ] && convert "$OUT/$name.full.png" \ -crop 1279x120+0+520 +repage "$OUT/$name.render.png" elif [ "$pose" = focus ]; then - godot --path port --resolution 1280x720 -- "--menu=main_menu" "--focus=$forced" \ + godot --path port --resolution 1280x720 -- --loop-phase=0 "--menu=main_menu" "--focus=$forced" \ --script=wait "--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true [ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png" elif [ "$pose" = menu ]; then - godot --path port --resolution 1280x720 -- "--menu=$name" --script=wait \ + godot --path port --resolution 1280x720 -- --loop-phase=0 "--menu=$name" --script=wait \ "--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true [ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png" elif [ "$pose" = plate ]; then - godot --path port --resolution 1280x720 -- "--screen=title" --overlay=press_start \ + godot --path port --resolution 1280x720 -- --loop-phase=0 "--screen=title" --overlay=press_start \ --time=3.95 "--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true elif [ "$pose" = t357 ]; then # 🔴 NO `--time` HERE EITHER, and the row's note used to claim otherwise. @@ -177,7 +177,7 @@ for row in "${MAP[@]}"; do # # So: pose at the settle, which is what was actually being measured, and let # the leaf loop carry the sweeps' phase. - godot --path port --resolution 1280x720 -- "--screen=$name" \ + godot --path port --resolution 1280x720 -- --loop-phase=0 "--screen=$name" \ "--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true else # NO `--time`. It used to pass `--time=99` as an idiom for "settled", and @@ -189,7 +189,7 @@ for row in "${MAP[@]}"; do # that accident. Now that `--time` is honoured, asking for it explicitly # would pose past the end of every group, so the request is simply dropped # and the settled pose asked for by omission. - godot --path port --resolution 1280x720 -- "--screen=$name" \ + godot --path port --resolution 1280x720 -- --loop-phase=0 "--screen=$name" \ "--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true fi [ -f "$OUT/$name.render.png" ] || { printf '%-17s %s\n' "$name" "render failed"; continue; }