Skip to content

DEF-33: a cold start no longer announces "Animation paused" - #39

Merged
djDAOjones merged 3 commits into
mainfrom
post-w2/def-33-startup-announce
Sep 28, 2026
Merged

djDAOjones merged 3 commits into
mainfrom
post-w2/def-33-startup-announce

Conversation

@djDAOjones

Copy link
Copy Markdown
Owner

Post-W2 queue · DEF-33 (plan §12.1 DEF-33, P3). This changes behaviour, for screen-reader users only. Loading the app no longer announces "Animation paused". A restored session's "Previous session restored" now stays long enough to be read.

The defect

  • Every load told a screen reader "Animation paused". Startup pauses the animation itself, at the end of the route, so the whole path shows for editing. It did so after the transport listeners were registered. The pause listener then announced the pause through the polite live region, which sits outside the inert app and so is read even behind the splash.
  • A restored session lost its own announcement. "Previous session restored" was replaced by "Animation paused" about 6–9 ms later, so a screen reader heard the pause instead.

Stated new behaviour

  • A cold start announces nothing.
  • A restored session announces only its restore: "Previous session restored", or the recovery-unavailable warning.
  • Everything the author does is announced as before, by the buttons and by the keyboard: play, pause, completion and reset.
  • Nothing else moves. The pause listener is the only listener startup now skips, and the shell already shows Play and hides Pause, so the screen is unchanged.

The change

  • src/main.js: the startup pause() and seekToProgress(1.0) run before setupAnimationEngineListeners() instead of after it.

Evidence

Tests first. In tests/bootApp.test.js, each on a booted app after app.ready:

Test Fails on
a cold start does not announce a pause, but a pause the author makes does main: the live region reads "Animation paused" where it should be empty
a restored session keeps its announcement main: the announcements are "Previous session restored" then "Animation paused"
  • The cold test requires the live region to be exactly empty after startup. It then checks that the author's play and pause are announced and toggle the buttons.
  • The restored test saves a two-waypoint session through the app's own autosave and boots again from it. It records every announcement the app makes, because the live region clears itself after two seconds and a slow boot could otherwise hide one.

Mutations, each against the whole suite (parallel, JSON report, watchdog):

First round, on the first commit:

Mutation Caught by
F1 the listeners attached before the startup pause again the cold-start test
C1 control: src/main.js as on main exactly the cold-start test; 1,190 others pass

After the review:

Mutation Caught by
F1 the listeners attached before the startup pause again both tests
F2 startup announces something else once the listeners exist both tests
F3 a pause the author makes is no longer announced the cold-start test
F4 a play the author makes is no longer announced the cold-start test
F5 a restored session is no longer announced the restored-session test
C1 control: src/main.js as on main exactly both tests; 1,190 others pass
C0 control: no mutation nothing; 1,192 pass

In a real browser (headless Chromium 141, in this session's cloud container). I built main and main plus this change in throwaway clones and served each locally; nothing touched Joe's machine or the live site. A MutationObserver recorded every text the live region took during a load:

Load main this change
cold "Animation paused" nothing
restored session "Previous session restored", then "Animation paused" "Previous session restored"

Review

Codex isn't available in this cloud container. Instead, an independent Claude agent attacked five numbered claims. It had a fresh context, its own no-remote clone and the falsification brief Codex would get. It used Chromium on both builds across 16 startup routes, plus two more failure routes:

  • cold first runs and returning visitors;
  • restored sessions: plain, with timed pauses and beacons, saved while playing, behind the splash, and from each bundled example;
  • legacy recovery, recovery unavailable, a corrupt autosave and an old-format autosave;
  • spotlight, comet and camera-zoom variants.

It confirmed all five claims:

  • Claim 1: in every route, the only listener that no longer sees startup is the pause listener. The DOM (apart from the Play button's inline style, noted below), the engine and camera state, the JKL fields, the readouts and both canvases match main exactly. The app has no URL or hash entry points.
  • Claim 2: the announcement is gone from every route, and nothing else is lost or reordered. The restore and recovery-unavailable messages now survive; on main they were overwritten within about 10 ms.
  • Claim 3: the author's play, pause, completion and reset are announced as on main, by the buttons, Space, K, L, J, , and Home.
  • Claim 4: the exported player is untouched. Its bundle is byte-identical, and it announces only its designed "Ready…" summary at load.
  • Claim 5: the test fails on main and catches the order reverting.

It asked for two test changes, both adopted:

  1. The restored route needed its own test. The change's second claim rested only on the Chromium probe. The restored test above now pins it.
  2. The cold assertion was too loose. It excluded only "Animation paused", so any other startup announcement would have passed. It now requires an empty live region.

It also noted:

  • A residual, invisible in Chromium: until the first pause, the Play button keeps the stylesheet's display instead of the inline inline-block the pause listener used to set. It measured pixel-identical at four viewport and pixel-ratio settings, so I left it alone. Firefox and Safari weren't available.
  • A comment nit, fixed: the comment now says the startup pause runs before the transport listeners exist, rather than all of the engine's listeners.
  • A gate flake that predates this PR: one run hit a teardown race in another test file, which boots the app without awaiting app.ready, and the rerun passed. It's recorded for the wish-list.

Found on the way, on main as here, to propose to Joe in the close-out PR:

  • The paused editor never stops queuing frames after a cold start with no waypoints, or after restoring a preview-mode project. The camera's smoothed centre starts at (0, 0) and its target is the canvas centre, so the render loop believes the camera is still moving. The idle probe counted about 50–60 updates a second, 10 s after load. This breaks the rule that stable paused views queue no animation frame.
  • Later warnings are overwritten the same way. Legacy recovery's "Browser recovery excludes the background" is replaced by "Previous session restored" in the same moment, so it never reaches a screen reader.

If Joe wants Codex's view before merging, the same falsification brief is ready to run against this branch.

Design review gate (UI-STANDARDS.md)

No control, layout or style changes. A screen-reader user no longer hears a pause they didn't make, and now hears their session being restored. The announcement policy is unchanged: polite, and only for what the author does or needs to know.

Still manual

  • A real screen reader: the live region's text stood in for speech.
  • Firefox and Safari, including the Play button's display at load.

file-map.md: the bootApp test row describes the new coverage. The plan: a dated note on DEF-33's row.

Verify

npm run check: 85 test files · 1,192 tests · 2 todo · shell 0 · build:check 0. main: 85 · 1,190 · 2 todo.

Rollback

Revert this PR. Either way, the change reaches users only through a release.

🤖 Generated with Claude Code

https://claude.ai/code/session_013dV8F9uaxT1v6tu3JFqTkf


Generated by Claude Code

Startup paused the animation after the pause listener was attached, so every load told a screen reader "Animation paused", and a restored session's "Previous session restored" was replaced at once; startup now pauses before attaching the listeners.

Verify: 85 test files · 1,191 tests · shell 0 · build:check 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dV8F9uaxT1v6tu3JFqTkf
The review found the restored route unpinned and the cold check open to any other startup text; a booted restore now records exactly "Previous session restored", and a cold start leaves the live region empty.

Verify: 85 test files · 1,192 tests · shell 0 · build:check 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dV8F9uaxT1v6tu3JFqTkf
The areaEdit file-map row they edited sits next to the bootApp row this PR edits; both rows keep their notes.

Verify: 85 test files · 1,214 tests · shell 0 · build:check 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dV8F9uaxT1v6tu3JFqTkf
@djDAOjones
djDAOjones merged commit 69e4b97 into main Sep 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants