diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index a2a9febd..66a33cdc 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -171 sections. Search this before re-deriving anything. +172 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) @@ -182,6 +182,7 @@ dies, which is what this file is for. * [🔴 CORRECTION: my branch *is* the stale era, and the reference binary was never the workspace build](#correction-my-branch-is-the-stale-era-and-the-reference-binary-was-never-the-workspace-build) * [`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) ## P0 — the exporter, 2026-08-28 @@ -9805,3 +9806,48 @@ opening the definition, which took thirty seconds and is the whole difference. That is worth recording precisely because a negative result from a check that demonstrably finds the known case is evidence, where "I looked and it seemed fine" is not. + +## Counting the fallbacks instead of inspecting them — and one I had misjudged + +The Decoder sharpened my sweep in a way that invalidates part of how I ran it: +**an in-range fallback cannot be caught by inspecting output, because the output +looks exactly like the true case. The only way to know is to count how often it +fires.** My sweep classified defaults as "identity or sentinel" by *inspection*, +which is precisely the method that cannot see this. + +Counted: + +| fallback | fires | +|---|---| +| `rotation_deg → 0` (0 is a legitimate rotation) | **0 of 866 keyframes, 0 of 178 rest poses** | +| `ramp → "linear"` | key present in `authored/timing.json` | + +✅ So rotation is **read, not invented** — the same conclusion they reached for +design size, and reachable only by counting. + +### 🔴 The count exposed one I had waved through + +`black_hold_units` defaults to `0.0` **and its authored value is 0**. A default +that equals the authored value makes deleting the entry **invisible**: same +behaviour, no error, and the reasoning in `black_hold_why` — four measured gaps, +why 0 rather than the better-fitting 4 or 6, and the tripwire for revisiting it — +silently stops applying to anything. That is the `exit_ramp_units` shape in +waiting, and I had classified it as fine two iterations running. + +✅ Fixed the same way: the fallback is now `-1.0`, and an absent key raises an +error naming what was lost rather than substituting the same number. + +**The control is the demonstration:** + +| | errors | render | +|---|---|---| +| key present | 0 | — | +| key **deleted** | **1** | **byte-identical** | + +📌 The render being identical either way *is* the finding. No output inspection +could ever have detected that deletion — which is exactly the property that makes +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. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index fe79382f..93af659e 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -206,7 +206,23 @@ func _ready() -> void: # Negative means "not supplied": ScreenView then declines to invent a duration # and says so, rather than making one up. See ScreenView.exit_ramp_units. view.exit_ramp_units = float(timing.get("exit_ramp_units", -1.0)) - _black_hold = float(timing.get("black_hold_units", 0.0)) + # ⚠️ THE FALLBACK IS -1.0, NOT 0.0, EVEN THOUGH THE AUTHORED VALUE IS 0. + # + # This is the `exit_ramp_units` shape in waiting: a default that EQUALS the + # authored value makes deleting the authored entry invisible -- same + # behaviour, no error, and the reasoning in `black_hold_why` (four measured + # gaps, why 0 rather than the best-fitting 4 or 6, and the tripwire for + # revisiting it) silently stops applying to anything. + # + # The Decoder's sharpening is what surfaced it: an IN-RANGE fallback cannot + # be caught by inspecting output, because the output looks exactly like the + # true case. 0 is a legitimate hold. So the absence is made loud instead. + _black_hold = float(timing.get("black_hold_units", -1.0)) + if _black_hold < 0.0: + push_error("authored/timing.json has no black_hold_units. Using 0, which is " + + "what it said -- but the REASONING for 0 lived there and is now gone. " + + "See black_hold_why in git history before trusting this transition.") + _black_hold = 0.0 # Which focus records draw unconditionally and loop. Kept out of ScreenView's # own logic on purpose -- see `looping_focus` there for the census that says # this cannot be a rule.