Merge remote-tracking branch 'origin/human/r1-retro-tick' into auto/port-p6-audio

This commit is contained in:
Sylpheed port agent
2026-09-01 16:01:22 +00:00
7 changed files with 462 additions and 2 deletions

View File

@@ -97,7 +97,33 @@ reading as progress because each entry is individually rigorous.
## 7. What needs the human
1. **The register re-classification (R1).** It changes the file both agents read to
decide what *not* to try. Two agents agreeing is not the right authority.
1. **The register re-classification (R1) — DONE 2026-09-01, by the human**, on
`docs/re/REFUTED.md` at the Decoder's tip. All **222** entries now carry an
`⟨instrument⟩`; the file opens with a reading guide naming which instruments
are ours; R1 is now standing text in `PROTOCOL.md`; and
`tools/stale-instrument` is the `--stale` query — run it whenever you improve
a renderer, a reader or the harness, and it lists what that instrument killed.
**Ten entries moved ❌ → 🟡**, each naming what would settle it: eight
`render-vs-capture`, one `our-reader`, one `harness`.
Three things the pass turned up that neither self-review had:
* **The `rest()` question is open, and had been reading as settled in both
directions.** *"rest = last keyframe"* was refuted by the sibling argument;
that refutation was then refuted by correlating our render against
captures. Both legs run through our renderer, so under R1 neither survives
— and which one you believed depended on which entry you found first.
🔴 **This one is load-bearing for the port**: `rest()` decides the pose
every plateau-less element is drawn at.
* **A withdrawal never reached its sibling.** *"2 391 frames, max glyph 0"*
was withdrawn because a long-lived `x11grab` stream degrades and then
repeats a stale frame. The 1 674-sample negative three lines above it —
same probe, same instrument, comparable duration — was left standing as a
*reinstated measurement*. §1's lesson, inside the register itself.
* **83 of 222 entries — 37 % — record no instrument at all.** Not disputed,
not safe: **unauditable**. `stale-instrument unrecorded` is the backfill
queue, and it is larger than every other group combined.
2. **P5's gate** — a person clicking through the port. Unchanged, and it is the
only thing standing between the milestone and done.

View File

@@ -84,6 +84,52 @@ command rather than by trusting the date:
git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconcile
```
## New asks, 2026-09-01 — from a HUMAN PLAY-TEST on real hardware, port `HEAD` `6b713e8` + input fix
**The first play-test on a physical controller found four things.** Two were port
defects and are fixed; **two are oracle questions and are recorded here unguessed.**
⚠️ Read the fixed pair first, because the *reason* they survived so long is a
method finding that applies to the Decoder's harness as much as this one:
> **`--script` sends `InputEventAction`, which bypasses the input map.** So every
> check this port had asserted the code *below* the map and nothing about the map
> — which turned out to have **no joypad binding for `ui_accept` or `ui_cancel`
> at all** in Godot 4.7.2, while binding the d-pad *and* the left stick to
> `ui_up`/`ui_down`. Ⓐ and Ⓑ were dead on a real pad for the whole of P5 while
> the unattended walk passed every iteration. The same blind spot hid the second
> defect: an `InputEventAction` is not an analog axis, so nothing could observe
> that a held stick fires once per *jitter*. Now asserted by
> `tools/port/verify-input`, with a control.
| # | ask | why the port cannot answer it |
|---|---|---|
| **H1** | **Does a held direction REPEAT in the menus, and at what rate?** Initial delay and repeat interval, for the d-pad and for the left stick separately if they differ. | Behavioural. The port now does **one step per deflection**`authored`, and deliberately the conservative half: it cannot run away and it invents no rate. If the game repeats, a human will feel it as *"I have to flick it again"*. |
| **H2** | **The splash blur/fade-in does not match the game.** A human watching both says the game's is **more pronounced** than the port's. | Needs a capture of the publisher/developer splash across its fade, at a known frame cadence. The port's ramp comes from the declared keyframes; whether the game applies a **blur** on top — and whether what we read as a fade is one — is not answerable from the export. |
| **H3** | **The `PRESS Ⓐ` plate arrives LATE.** The port raises it at `t=236`, derived `238 118 = 120 units = 2.000 s`. | 🟡 **Half of this may be the port's, and that half must be settled here first.** Three candidate causes: the **unit→seconds constant** (Decoder's), the **clock origin** — do builds 2 and 4 really start together? — and **`rest.t`** (both ours). `REFUTED.md` records that believing `rest.t` already put a plate **3.97 s late** once. Establish which half it is *before* asking, and say how. |
**On H2, three things the port can say that narrow it**, none of which settle it:
* The port draws the splash from the declared keyframe alphas only. It applies
**no blur at all**, so "more pronounced in the game" is consistent with a
post-effect the export does not describe, with a different ramp shape, or with
both.
* 🔴 **The `rest()` question is open in both directions** — see
[`REFUTED.md`](../re/REFUTED.md)'s `rest()` pair after the 2026-09-01 R1
reclassification. The two splashes are the *only* screens that reach the
plateau-less fallback (title, main menu and `EXTRAS` reach it zero times), so
**H2 lands exactly where our resting-pose heuristic is least trustworthy.**
That is not a coincidence worth ignoring.
* The register also has *"the declared keyframe timeline reproduces the captured
splash"* now sitting at 🟡 `⟨our-reader⟩` rather than ❌, because the
record-layout fix re-times a group's final pose and the entry was never
re-derived under it. **H2 may already be half-answered by re-running that.**
**What would settle H2:** a capture of the developer splash across its build-in
at a known cadence, compared frame-by-frame against the port's ramp — and, if
they differ in shape rather than in extent, a draw capture naming what is
submitted per frame.
## Still open — these block work
🔴 **This table's rows carry no derivation sha, and that is why they rot.** The standing instruction is to record the HANDOFF commit each row was derived from; every *prose* section added since does, and **every row in this table and the next does not**. On 2026-08-30 an audit found three stale rows here — one of them contradicted by a struck row four lines below it, claiming the boot ends on a plateless title when it had drawn the plate for weeks. The undated rows are exactly the ones that went stale, which is as close to a controlled experiment as this page is going to get.

View File

@@ -103,6 +103,16 @@ func _ready() -> void:
if args.has(flag) and not _has_display(flag):
get_tree().quit(4)
return
# 🔴 BEFORE ANYTHING ELSE, and announced. Godot 4.7.2 binds no joypad button
# to `ui_accept` or `ui_cancel`, so on a real pad Ⓐ and Ⓑ did nothing while
# navigation worked — which reads as a broken controller. `Gamepad` explains
# it. Both lines are printed because "the pad is seen" and "Ⓐ is bound" are
# different facts and the human debugging this needs to separate them.
var _pad_bound := Gamepad.bind_missing()
if _pad_bound != "":
print(_pad_bound)
print(Gamepad.report_devices())
var export_tree := ExportTree.locate()
_tree = export_tree
if export_tree.root == "":
@@ -400,6 +410,8 @@ func _ready() -> void:
var _frozen := false
## Holds the left stick's latch state. See `gamepad.gd`.
var _pad := Gamepad.new()
var _flow: Variant = null
var _menu: MenuFlow = null
var _play := false
@@ -774,6 +786,13 @@ func _report_unused_mods() -> void:
func _unhandled_input(event: InputEvent) -> void:
# The left stick is bound to ui_up/ui_down by Godot's own defaults, and an
# analog axis is not an edge: held at deflection it emits an event per
# jitter, each reporting the action as pressed. `accepts()` latches it to one
# step per deflection. Everything already edge-shaped passes through
# untouched. See `gamepad.gd` -- including what is authored about it.
if not _pad.accepts(event):
return
# HANDOFF Q9, measured: one (A) press skips a movie -- the title was reached
# at 57 s against a 193 s baseline.
if _player != null:

147
port/scripts/gamepad.gd Normal file
View File

@@ -0,0 +1,147 @@
class_name Gamepad
extends RefCounted
## The physical controller: the two buttons Godot does not bind, and the one
## input that is not an edge.
##
## 🔴 BOTH DEFECTS WERE REPORTED BY A HUMAN PLAYING THE PORT (2026-09-01), and
## neither could have been caught by the `--script` harness, because that harness
## sends `InputEventAction` — which bypasses the input map and is not an analog
## axis. The unattended P5 walk passed on every iteration while Ⓐ did nothing at
## all on a real pad. **A synthetic-input test asserts the code after the input
## map, never the input map itself.**
##
## ## 1. Godot 4.7.2 binds no joypad button to `ui_accept` or `ui_cancel`
##
## Measured on this exact build rather than remembered, because the answer has
## changed between Godot versions and the remembered one was wrong:
##
## ```
## ui_accept key:Enter, key:Kp Enter, key:Space <- no joypad at all
## ui_cancel key:Escape <- no joypad at all
## ui_up key:Up, JOYBTN:11, JOYAXIS:1- <- d-pad AND left stick
## ui_down key:Down, JOYBTN:12, JOYAXIS:1+
## ui_left key:Left, JOYBTN:13, JOYAXIS:0-
## ui_right key:Right, JOYBTN:14, JOYAXIS:0+
## ```
##
## That asymmetry is the whole bug report: navigation worked on the pad and Ⓐ/Ⓑ
## did nothing, which reads like a broken controller and is a complete input map
## for four actions out of six.
##
## The events are **added to** the built-in actions, never redefined. Declaring
## `ui_accept` in `project.godot` replaces the built-in wholesale, so the
## keyboard bindings would have to be restated there and would silently rot the
## next time Godot changes them.
##
## ## 2. A stick is not a button
##
## `ui_up`/`ui_down` are bound to **axis 1**, so the left stick navigates — which
## is correct, the real game accepts it too. But an axis emits a fresh
## `InputEventJoypadMotion` every time the value *changes*, and a real stick held
## at deflection jitters continuously. Every one of those events reports the
## action as pressed, so a held stick was one cursor step per jitter: the human's
## words were "moves the cursor too fast", and on a five-item menu it crosses
## faster than the eye follows.
##
## So the stick is **latched**: it fires once when it leaves the neutral zone and
## not again until it comes back. That makes it behave exactly like the d-pad,
## which needs no latch because a button already is an edge.
##
## ⚠️ **AUTHORED, NOT MEASURED — and deliberately the conservative half.**
## Whether the real game *repeats* while a direction is held, and how fast, is
## unknown; it is an oracle question. One deflection = one step cannot run away
## and cannot invent a rate. If the game does repeat, this is a difference a
## human will notice as "I have to flick it again", and the fix is a measured
## repeat interval — not a guessed one. Logged for the Decoder as `pad-repeat`.
## Godot's action deadzone for the `ui_*` actions is 0.50, so the latch must not
## arm below it — the action itself would not read as pressed and the step would
## be swallowed anyway, leaving the latch armed against a press that never
## happened.
const ENTER := 0.5
## Release lower than it arms. Without the gap a stick resting near 0.5 chatters
## across the boundary and re-arms on noise, which is the original bug wearing a
## smaller number.
const RELEASE := 0.4
## Only the left stick. The triggers are axes too, and latching them here would
## silently swallow input the port does not read yet but might.
const STICK := [JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y]
var _latched: Dictionary = {}
## Add the joypad buttons the built-in map omits. Returns a human-readable line,
## or "" if nothing needed adding — so a future Godot that ships these bindings
## makes this quietly stop reporting rather than double-binding.
static func bind_missing() -> String:
var added := PackedStringArray()
for pair in [["ui_accept", JOY_BUTTON_A, ""], ["ui_cancel", JOY_BUTTON_B, ""]]:
var action: String = pair[0]
var button: int = pair[1]
if not InputMap.has_action(action):
# Not a warning we can act on, but silence here would present as the
# original bug and send the next person back to the controller.
push_warning("gamepad: no such action %s -- pad button unbound" % action)
continue
if _has_button(action, button):
continue
var ev := InputEventJoypadButton.new()
ev.button_index = button
InputMap.action_add_event(action, ev)
added.append("%s -> %s" % [pair[2], action])
if added.is_empty():
return ""
return "pad: bound %s (Godot 4.7.2 binds no joypad button to either)" % \
", ".join(added)
static func _has_button(action: String, button: int) -> bool:
for e in InputMap.action_get_events(action):
if e is InputEventJoypadButton and e.button_index == button:
return true
return false
## True if this event should be acted on. Everything that is already an edge —
## keys, d-pad, mouse — passes straight through; only the analog stick is
## latched, and only on the two axes the navigation actions are bound to.
func accepts(event: InputEvent) -> bool:
if not (event is InputEventJoypadMotion):
return true
var axis: int = event.axis
if not STICK.has(axis):
return true
var value: float = event.axis_value
var direction := 0
if value >= ENTER:
direction = 1
elif value <= -ENTER:
direction = -1
if direction == 0:
# Neutral enough to re-arm? The gap between RELEASE and ENTER is the
# hysteresis band: inside it the stick is neither a new press nor
# released, so the latch is left exactly as it was.
if absf(value) <= RELEASE:
_latched[axis] = 0
return false
if int(_latched.get(axis, 0)) == direction:
return false # still held in the same direction: not a new press
_latched[axis] = direction
return true
## The pads Godot can see, for the startup line. A run where the human believes
## a controller is connected and Godot disagrees should say so on its own,
## rather than presenting as unresponsive buttons.
static func report_devices() -> String:
var pads := Input.get_connected_joypads()
if pads.is_empty():
return "pad: none connected -- keyboard only (Enter/Space = Ⓐ, Escape = Ⓑ)"
var names := PackedStringArray()
for j in pads:
names.append("[%d] %s" % [j, Input.get_joy_name(j)])
return "pad: " + ", ".join(names)

View File

@@ -0,0 +1 @@
uid://b34gxuqrvncbp

View File

@@ -66,6 +66,15 @@ step transcode-bands must-pass tools/port/verify-transcode-fidelity
step bands-harness must-pass tools/port/verify-transcode-fidelity --selftest
step capture-controls must-pass tools/port/check-capture-controls
step menu-audio must-pass env OUT="$OUT/audio" tools/port/verify-menu-audio
# 🔴 ADDED 2026-09-01 after a human found Ⓐ dead on a real controller while the
# unattended P5 walk passed. `--script` sends `InputEventAction`, which BYPASSES
# the input map, so every check here asserted the code BELOW the map and nothing
# about the map -- which was missing a joypad binding for `ui_accept` and
# `ui_cancel` entirely. The same blind spot hid a second defect: an
# `InputEventAction` is not an analog axis, so nothing could see that a held
# stick fired once per jitter.
step input-map must-pass tools/port/verify-input
step input-control must-pass tools/port/verify-input --control
# A stale index is worse than none: it answers "is this already decided?" with a
# confident no. That is not hypothetical -- see the entry it was built after.
step decisions-index must-pass tools/port/index-decisions --check

212
tools/port/verify-input Executable file
View File

@@ -0,0 +1,212 @@
#!/usr/bin/env bash
# The input map, and the stick latch -- asserted against Godot, not reasoned about.
#
# tools/port/verify-input
# tools/port/verify-input --control # each check fails when its subject is removed
#
# 🔴 WHY THIS EXISTS. A human played the port on a real controller and Ⓐ did
# nothing. Skipping the intro did nothing; opening a submenu did nothing. The
# unattended P5 walk had passed on every iteration while this was true, and the
# reason is exact:
#
# `--script` sends `InputEventAction`, which BYPASSES the input map.
#
# So the harness asserted every line of code *after* the input map and nothing
# about the map itself -- and the map was missing half the actions. Godot 4.7.2
# binds NO joypad button to `ui_accept` or `ui_cancel`, while it binds the d-pad
# AND the left stick to `ui_up`/`ui_down`. Four actions worked on the pad, two
# did not, which reads as a broken controller.
#
# The second defect had the same blind spot: `InputEventAction` is not an analog
# axis, so the harness could not have seen that a held stick fires once per
# jitter. The human's words were "moves the cursor too fast".
#
# ⚠️ THE GENERAL LESSON, worth more than either fix: **a synthetic-input test
# cannot assert the input map.** Anything injected below the map is evidence
# about the code above it only.
#
# ## The control, and what it can and cannot cover
#
# 🔴 The first version of `--control` inverted ALL NINE assertions and demanded
# every one fail with the fixup skipped. Seven of them do not depend on the
# fixup, so it reported them as broken -- a control that fails a correct check
# is the same defect as one that passes a dead check, and this file would have
# shipped claiming its checks were untrustworthy. Each check now names its
# SUBJECT, and the control removes exactly that subject:
#
# bind -- skip `Gamepad.bind_missing()`; the check must fail
# latch -- run the same events through no latch at all; the count must differ
# godot -- NOT CONTROLLABLE HERE, and said so rather than faked. These assert
# what Godot itself binds. There is nothing of ours to remove; they
# exist to make a future Godot dropping the d-pad a failing check
# instead of a bug report.
set -euo pipefail
cd "${PROJECT_DIR:-$(git rev-parse --show-toplevel)}"
GODOT="${GODOT:-godot}"
mode="assert"
[ "${1:-}" = "--control" ] && mode="control"
probe="port/.verify-input-probe.gd"
trap 'rm -f "$probe" "${probe}.uid"' EXIT INT TERM
cat > "$probe" <<'GD'
extends SceneTree
var mode := OS.get_environment("VERIFY_INPUT_MODE")
var fail := 0
var ran := 0
## `subject` is what the check depends on, and decides whether the control
## removes it. A check whose subject cannot be removed is skipped there and
## counted, not silently dropped -- a control that quietly tests four of nine
## things reports the same green line as one that tests all nine.
func ok(name: String, subject: String, cond: bool, detail: String = "") -> void:
if mode == "control" and subject == "godot":
print(" %-44s -- not controllable (Godot's own binding)" % name)
return
if mode == "control" and subject == "negative":
# 🔴 R4: a NEGATIVE carries a positive control, it does not carry an
# inversion. "The latch must not touch buttons" cannot be controlled by
# removing the latch -- with no latch, buttons pass, which is the same
# answer. What shows the method has power is that the SAME counter, on
# the same code path, reduces 6 stick events to 1. That row is the
# positive control for this one, and naming it is the honest move;
# inverting it would have been a green line that meant nothing.
print(" %-44s -- negative; positive control is the stick row (6 -> 1)"
% name)
return
ran += 1
var want: bool = cond if mode != "control" else not cond
print(" %-44s %s%s" % [name, "ok" if want else "🔴 FAILED",
(" " + detail) if detail != "" else ""])
if not want:
fail = 1
func has_button(action: String, button: int) -> bool:
for e in InputMap.action_get_events(action):
if e is InputEventJoypadButton and e.button_index == button:
return true
return false
## Feed a run of axis values through a latch (or through none) and count the
## presses it would produce.
func steps(values: Array, latched: bool) -> int:
var pad := Gamepad.new()
var n := 0
for v: float in values:
var e := InputEventJoypadMotion.new()
e.axis = JOY_AXIS_LEFT_Y
e.axis_value = v
# No latch = what the port did before: every event above the action
# deadzone is a press. That is the bug, reproduced, as the control.
if pad.accepts(e) if latched else absf(v) >= Gamepad.ENTER:
n += 1
return n
## The latch as the port actually uses it -- and REMOVED under `--control`, so
## the rows that depend on it invert.
func nav(values: Array) -> int:
return steps(values, mode != "control")
func _init() -> void:
# The control removes the repair. Everything else runs with it applied.
if mode != "control":
Gamepad.bind_missing()
# ── 1. subject `bind` -- the two actions Godot leaves unbound ─────────────
ok("Ⓐ reaches ui_accept", "bind", has_button("ui_accept", JOY_BUTTON_A),
"JOY_BUTTON_A")
ok("Ⓑ reaches ui_cancel", "bind", has_button("ui_cancel", JOY_BUTTON_B),
"JOY_BUTTON_B")
# ── 2. subject `godot` -- what the engine binds, and must keep binding ────
#
# The keyboard events must SURVIVE the fixup: declaring `ui_accept` in
# project.godot would have replaced the built-in wholesale and dropped them
# silently. Adding to the action must not.
var keys := 0
for e in InputMap.action_get_events("ui_accept"):
if e is InputEventKey:
keys += 1
ok("ui_accept keeps its keyboard events", "godot", keys >= 2,
"%d key event(s)" % keys)
ok("d-pad reaches ui_down", "godot", has_button("ui_down", JOY_BUTTON_DPAD_DOWN))
var axis := false
for e in InputMap.action_get_events("ui_down"):
if e is InputEventJoypadMotion and e.axis == JOY_AXIS_LEFT_Y:
axis = true
ok("left stick reaches ui_down", "godot", axis, "axis %d" % JOY_AXIS_LEFT_Y)
# ── 3. subject `latch` -- one step per deflection, not one per jitter ─────
#
# A push to full deflection followed by jitter that never returns to
# neutral: what a real stick emits, and what produced "moves the cursor too
# fast". The control runs the identical values with no latch and must count
# every one of them, which is what makes this a discriminator rather than a
# number that happens to be 1.
var held := [0.92, 0.95, 0.91, 0.99, 0.93, 0.97]
# `nav()` is the latch under control: in `--control` the latch is REMOVED,
# which is what makes these rows invert. Reading `steps(..., true)` in both
# modes was the earlier defect -- the control ran the repaired code and then
# demanded it fail.
ok("a held stick is ONE step, not six", "latch", nav(held) == 1,
"latched %d, unlatched %d" % [steps(held, true), steps(held, false)])
# Release, then push again: that IS a second press, or the stick becomes
# single-use.
ok("release then push is a second step", "latch",
nav([0.92, 0.95, 0.10, 0.88]) == 2,
"%d step(s)" % nav([0.92, 0.95, 0.10, 0.88]))
# Hysteresis: drifting back only as far as the release threshold must not
# re-arm, or a stick resting near the boundary chatters -- the original bug
# with a smaller number.
ok("boundary drift does not re-arm", "latch",
nav([0.9, 0.45, 0.9, 0.45, 0.9]) == 1,
"%d step(s)" % nav([0.9, 0.45, 0.9, 0.45, 0.9]))
# A button already IS an edge; latching it would swallow the second of two
# quick taps.
var pad := Gamepad.new()
var passed := 0
for i in 3:
var b := InputEventJoypadButton.new()
b.button_index = JOY_BUTTON_DPAD_DOWN
b.pressed = true
if pad.accepts(b):
passed += 1
ok("d-pad presses are not latched", "negative", passed == 3, "%d of 3" % passed)
if ran == 0:
print("🔴 no check ran -- the harness asserted nothing")
quit(2)
quit(fail)
GD
out=$(VERIFY_INPUT_MODE="$mode" "$GODOT" --headless --path port \
--script "res://$(basename "$probe")" 2>&1 \
| grep -v "^Godot Engine\|^$" || true)
rc=0
printf '%s' "$out" | grep -q "🔴" && rc=1
if [ "$mode" = "control" ]; then
echo "control -- each check must fail when ITS OWN subject is removed:"
printf '%s\n' "$out"
echo
if [ $rc -eq 0 ]; then
echo "every controllable check fails without its subject -- the control holds"
exit 0
fi
echo "🔴 a check did not invert -- it is not testing what it claims to test"
exit 1
fi
echo "input map and stick latch:"
printf '%s\n' "$out"
echo
if [ $rc -eq 0 ]; then
echo "Ⓐ and Ⓑ reach the game, and a held stick is one step"
exit 0
fi
echo "🔴 the input map is not what the port needs"
exit 1