maniator / maniator/verticopolis
[P2] `27c-elevator-schedule-unsaved` renders nondeterministically, failing the screenshot determinism guard at random
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 5
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
Found on PR #840, which it failed and which does not cause it. Filed separately because it is main's render/capture surface, not that PR's area.
capture / shoot (features) failed the determinism guard on 3c5a6a1:
screenshot check 'features' is nondeterministic: pixel bytes differ for
docs/screenshots/27c-elevator-schedule-unsaved.png. A wall-clock/RNG read leaked into the build or a capture (likely a new time-driven decoration in the engine render path, or a build-time timestamp)
The guard renders one commit twice and compares the two legs, so this is a property of the code, not of the diff under test. It is intermittent: a re-run of the same job on the same commit passed, and four later runs on three commits have all been green. drift-gate fails with it as a downstream consequence (CAPTURE: failure, VERIFY_DRIFT: skipped), so one root cause shows as two red checks.
Why it is not PR #840's. That diff touches no shipped render code: five src/analytics*.ts files plus their tests, three scripts/posthog-* files, two _bmad-output docs, and one src/ui/ entry that is a test file. No src/engine/, no src/render/, no shipped src/ui/ or src/game/. Analytics is also inert during a capture, since the telemetry host gate is closed on the preview server.
What I ruled out (so the next person does not repeat it):
holdRepeatinsrc/ui/templates/elevatorSchedule.tssets a 400ms timeout and a 150ms interval, but it is bound to@pointerdownand the scene drives the preset withpreset.click(), so it never arms.- The
.es-adviceline derives fromstate.dayviascheduleAdvice, not from the clock.
Where I would look next. The 27c scene (scripts/scenes/schedule.ts) is a cropped shot of #modal .modal-box whose scroll position is computed from live layout:
box.scrollTop = Math.floor((box.scrollHeight - box.clientHeight) / 2);
taken after a fixed wait: 300. Anything that changes the dialog's measured height between the two legs (a font that resolves late, an async populate that races the 300ms, a reflow after the preset click) moves the crop and changes every pixel below it, which matches "pixel bytes differ" for one file while its neighbours are stable. The two sibling shots (27, 27b) use the same dialog and did not differ, and 27b uses the same mid-scroll trick, so whatever it is, it is specific to the armed/unsaved state 27c stages (preset click, then an Esc that arms the guard and renders .modal-warn).
Worth checking whether the scene should assert a settled layout rather than sleeping 300ms, which would make the failure deterministic either way instead of a coin flip.
Impact. It costs a CI cycle at random and it burns the one re-run the drive-to-green rules allow, so the PR that hits it next has no re-run left for a genuine flake. It also trains readers to treat a determinism failure as noise, which is the opposite of what that guard is for.
docs/screenshots/27c-elevator-schedule-unsaved.png, scripts/scenes/schedule.ts, src/ui/templates/elevatorSchedule.ts. Evidence artifact: determinism-diff-features on run 34910028648, which holds both legs' copies of the differing file.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with scripts/scenes/schedule.ts and run capture / shoot (features) to reproduce the two-leg determinism check for 27c-elevator-schedule-unsaved. Inspect the referenced elevatorSchedule.ts state and layout timing, using the determinism-diff-features artifact and the 27, 27b sibling shots for comparison; done means both legs produce identical 27c screenshot bytes without intermittent drift-gate failures.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100