Scaffold project and author 14-phase roadmap
Browser co-op kitchen game: FastAPI + native WebSockets, server-authoritative 20 Hz simulation, in-memory anonymous lobbies, data-driven YAML levels, Canvas 2D vanilla ES-module client with locally authored SVG art and no external CDNs. .agents/PLAN.md records the LOCKED decisions; .agents/user_stories/ holds the 12 flow contracts; .agents/phases/todo/ breaks them into 14 sequential, test-gated phases (71 task files), each leaving the app launchable and the suite green. LLM-Generated: true
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Phase 14 — Accessibility, input parity, and release hardening
|
||||
|
||||
**Story:** `US-11_play_on_phone`, `US-12_play_accessibly`
|
||||
**Context:** `.agents/PLAN.md` §4 (UI/UX rules), §9 (security/anonymity), §10 (budgets)
|
||||
|
||||
## Objective
|
||||
The last mile: every user story demonstrably satisfied, WCAG 2.1 AA verified mechanically,
|
||||
every input path equivalent, the security guardrails re-checked against reality, and the docs
|
||||
telling the truth about what ships.
|
||||
|
||||
## Dependencies
|
||||
- `13_endless_mode_and_scoreboard` (complete) — the full feature set is in place
|
||||
- all prior phases complete
|
||||
|
||||
## Tasks
|
||||
1. `01_accessibility_gate.md` — axe-powered e2e gate on every screen, contrast/colorblind/motion audits, keyboard-only and focus-order passes
|
||||
2. `02_canvas_alternative_and_redundancy.md` — text/table mode for the kitchen state, live regions, screen-reader narrative of the game state
|
||||
3. `03_input_parity_and_localization_hooks.md` — keyboard/touch/gamepad-free parity, pointer fallback, all strings in one catalog with no hard-coded English in logic
|
||||
4. `04_security_and_abuse_review.md` — re-run the audit against the finished app: headers, WS auth, rate limits, DoS surfaces, anonymity claims vs reality
|
||||
5. `05_release_hardening_and_docs.md` — production profile, CSP tightening, perf re-measure, README/docs truth pass, demo-vs-production statement
|
||||
|
||||
## Testing & Quality
|
||||
- E2E accessibility gate (axe-core vendored locally — **no CDN**, loaded from
|
||||
`app/static/vendor/axe/` with its version pinned and recorded) failing on any
|
||||
critical/serious violation on every screen, including lobby, countdown, play, result, scoreboard,
|
||||
and each error screen.
|
||||
- Redundancy: every game state readable without the canvas (text mode), every hazard without
|
||||
color/motion (already asserted in phase 10 — re-asserted here across the full campaign).
|
||||
- Security review re-runs phase 06's abuse suite plus new surfaces (scoreboard, endless start,
|
||||
metrics endpoint, reconnect storms) with an OWASP-shaped checklist written into the report.
|
||||
- Budgets re-measured and compared to `BUDGETS.md`; any regression > 10 % must be fixed, not
|
||||
documented away.
|
||||
- Coverage: **>90%** project-wide (the full-suite gate is re-run with `--cov-fail-under` set to
|
||||
the project's real number in `pyproject.toml`).
|
||||
- The phase is "Complete" only when the **entire** project test suite passes 100%.
|
||||
|
||||
## Documentation & Consistency
|
||||
- The final pass reviews `README.md` and `AGENTS.md` in full against the project as it
|
||||
stands after this phase — fixing every inconsistency found, **including pre-existing
|
||||
ones not caused by this phase**. This is the release-truth pass: every claim in README is
|
||||
verified against the running app before it stays in the file.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_accessibility_gate.py -q` → green, zero critical/serious axe violations on all screens
|
||||
- [ ] every screen is completable with keyboard alone and with touch alone (asserted parity matrix)
|
||||
- [ ] the canvas has a text alternative that conveys orders, score, hazards, and positions (screen-reader test)
|
||||
- [ ] phase-06 abuse suite plus the new-surface tests pass with no new findings open
|
||||
- [ ] `BUDGETS.md` re-measured with dated values, no regression > 10 %
|
||||
- [ ] every README claim verified against a running instance (the report lists how each was checked)
|
||||
- [ ] full test suite **100% passing**, coverage >90% project-wide
|
||||
- [ ] README.md and AGENTS.md are accurate and consistent with the whole project (verified in the final pass)
|
||||
- [ ] no behavior change in completed phases
|
||||
@@ -0,0 +1,47 @@
|
||||
# Task 01 — Accessibility gate on every screen
|
||||
|
||||
**Phase:** `14_accessibility_polish_and_hardening` · **Story:** `US-12_play_accessibly`
|
||||
|
||||
## Objective
|
||||
Make WCAG 2.1 AA a build gate rather than an intention, on every screen the app has.
|
||||
|
||||
## Work
|
||||
1. Vendor axe-core for **tests only**: download the pinned release once into
|
||||
`tests/e2e/vendor/axe.min.js` (version + SHA-256 recorded in `tests/e2e/vendor/VERSIONS.json`),
|
||||
injected with `page.add_script_tag(path=...)`. It is never served from `app/static/` and never
|
||||
shipped — the app's bundle stays dependency-free and the no-CDN rule stays intact.
|
||||
2. `tests/e2e/test_accessibility_gate.py`:
|
||||
- a `check_a11y(page)` helper run on every screen state: landing, lobby (1/2/4 seats, host and
|
||||
guest views), countdown, play, round-end (win/lose), scoreboard (populated/empty), and each
|
||||
error screen (`room_full`, `room_gone`, `level_locked`, `server_full`, `rate_limited`,
|
||||
`protocol_mismatch`, `evicted`);
|
||||
- fails on any `critical` or `serious` violation; the report prints the rule id, node selector,
|
||||
and help URL — with an explicit, reviewed `allowed` list (empty by default; every entry must
|
||||
carry a comment explaining why it is a false positive);
|
||||
- contrast audits run at 1.0 and 1.25 zoom and in the colorblind palette;
|
||||
- `prefers-reduced-motion` and `forced-colors: active` emulations re-run the scan.
|
||||
3. Fix every violation found (this is the bulk of the task): focus order on the lobby and result
|
||||
screens, labelled controls, `aria-live` regions, target sizes, disabled-vs-`aria-disabled`
|
||||
(locked levels must be reachable and explained, not removed), heading order on the landing page,
|
||||
`alt`/`aria-label` on the atlas-derived icons, and skip-link to the game controls.
|
||||
4. `tests/e2e/test_keyboard_only_matrix.py` — every flow completed with `keyboard` only
|
||||
(no mouse): create lobby, join, ready, start, cook+serve one dish, reach the scoreboard, leave;
|
||||
plus the same flows on the touch context with pointer input only (parity matrix: each cell
|
||||
asserts the flow completed).
|
||||
5. `docs/ACCESSIBILITY.md` — what is guaranteed, how it is tested, known limitations stated plainly
|
||||
(e.g. the canvas is a visual medium; the text mode from task 02 is the alternative), and add it
|
||||
to PLAN §13's documentation index.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E gate across ~12 screen states × 3 rendering contexts; the keyboard/touch parity matrix.
|
||||
- Coverage: **>90%** on any server change.
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_accessibility_gate.py -q` → green with an empty `allowed` list (or a justified one)
|
||||
- [ ] `uv run pytest tests/e2e/test_keyboard_only_matrix.py -q` → green (every cell completed)
|
||||
- [ ] axe version + hash recorded and the file is not present under `app/static/` (asserted)
|
||||
- [ ] `docs/ACCESSIBILITY.md` exists, is listed in PLAN §13, and states the limitations honestly
|
||||
- [ ] full test suite **100% passing**
|
||||
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes (the phase's final pass re-checks both project-wide)
|
||||
- [ ] no behavior change in completed work
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Task 02 — Text mode: the kitchen without pixels
|
||||
|
||||
**Phase:** `14_accessibility_polish_and_hardening` · **Story:** `US-12_play_accessibly`
|
||||
|
||||
## Objective
|
||||
Give the game a non-visual channel: the same state, readable and listenable, at the same
|
||||
latency budget as the canvas.
|
||||
|
||||
## Work
|
||||
1. `app/static/js/ui/text_mode.js` (toggled by `?text=1`, the in-game `T` key, and
|
||||
`prefers-reduced-motion` + a screen-reader hint):
|
||||
- a live text/table rendering of the state: `orders` (each with its recipe chain, remaining
|
||||
time as seconds **and** a bar of block characters, points), `you` (position, facing, carried
|
||||
item, station in reach and its state), `others` (same, condensed), `hazards` (kind + cell +
|
||||
stage), `score`/`target`/`timer`/`streak`/`strikes`, `dirty` count, connection status;
|
||||
- DOM `role="table"` with proper headers, `aria-live="polite"` on the order list and score,
|
||||
`aria-live="assertive"` only for run-ending events; announcements are de-duplicated and
|
||||
rate-limited (≤ 1 per 900 ms per region) so a busy kitchen never floods a screen reader;
|
||||
- the text mode is a *consumer of the same snapshot buffer* — it must not open a second socket
|
||||
or recompute rules; a test asserts both modes derive from one buffer instance;
|
||||
- canvas hidden (not removed) in text mode so switching back is instant; both modes can be on
|
||||
at once for side-by-side debugging.
|
||||
2. Announcement vocabulary in the string catalog (task 03) with deterministic phrasing, e.g.
|
||||
`Order 2: soup — need tomato (chopped), onion (chopped) — 14 seconds — 30 points`.
|
||||
3. `tests/e2e/test_us12_text_mode.py`:
|
||||
- text mode reflects the live state within 200 ms (a scripted cook makes the strings change);
|
||||
- order/score/hazard strings match the authoritative snapshot exactly (compare against the
|
||||
server's test-seam state, not another client-side guess);
|
||||
- announcement rate limit holds under an event storm (count DOM live-region updates);
|
||||
- the same flows (create/join/ready/start/serve/result/scoreboard) are fully usable in text
|
||||
mode with keyboard only;
|
||||
- single-buffer assertion (two modes, one socket).
|
||||
4. `docs/ACCESSIBILITY.md` — text-mode contract, phrasing examples, and the rate-limit rule.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E comparing rendered text against the server's authoritative state (the strongest available
|
||||
check).
|
||||
- Coverage: **>90%** on any server change (none expected beyond test seams).
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_us12_text_mode.py -q` → green
|
||||
- [ ] text-mode strings match the server snapshot for orders, score, hazards, and carried items (asserted)
|
||||
- [ ] live-region updates stay ≤ the rate limit during an event storm (asserted)
|
||||
- [ ] one socket serves both the canvas and text mode (asserted)
|
||||
- [ ] full test suite **100% passing**
|
||||
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes (the phase's final pass re-checks both project-wide)
|
||||
- [ ] no behavior change in completed work
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Task 03 — Input parity, pointer fallback, and a string catalog
|
||||
|
||||
**Phase:** `14_accessibility_polish_and_hardening` · **Story:** `US-12_play_accessibly`, `US-11_play_on_phone`
|
||||
|
||||
## Objective
|
||||
No input path is a second-class citizen, and no user-facing string is buried in logic.
|
||||
|
||||
## Work
|
||||
1. `app/static/js/input/index.js` — one input abstraction: every producer (keyboard, touch stick,
|
||||
pointer drag, `?text=1` command bar) emits the same `{intent, action}` into the phase-08 input
|
||||
channel; `getCapabilities()` reports what is available; parity matrix enforced by tests:
|
||||
keyboard ↔ touch ↔ pointer produce identical server-observed movement for the same scripted
|
||||
commands.
|
||||
2. Pointer fallback for unusual devices: mouse-drag on the play surface steers the cook (same dead
|
||||
zone/rim as touch), click = action, with a one-time hint; works on desktop without a keyboard
|
||||
and on a touch device with a stylus; `pointercancel`/`blur` release exactly like touch.
|
||||
3. Accessibility input options (persisted in `sessionStorage`, exposed in a settings panel):
|
||||
remappable keys (with conflict detection and a reset), sticky/hold toggle for the action,
|
||||
input repeat rate, dead-zone size, and a "slow motion" practice flag that is **client-side
|
||||
interpolation only** (it never changes the server tick rate — asserted, because it must not be a
|
||||
cheat in a shared room).
|
||||
4. `app/static/js/i18n.js` + `app/static/strings/en.json` — every user-facing string (HUD, screens,
|
||||
error copy, text-mode phrasing, announcements) in one catalog with named placeholders; en is the
|
||||
only locale shipped, but: no English literals in logic modules (guard test scans
|
||||
`app/static/js/**` for user-visible sentence-shaped string literals outside the catalog —
|
||||
`console.*` and internal identifiers excluded), and `/api/meta` exposes
|
||||
`{languages: ["en"], default: "en"}` so the client never guesses.
|
||||
5. `tests/e2e/test_us12_input_parity.py` — the parity matrix (3 input methods × move/action),
|
||||
remapping applies immediately and persists for the session, conflict detection refuses a
|
||||
duplicate binding, slow-motion is client-only (server tick rate and final state unchanged for
|
||||
the same input script), pointer fallback works with the keyboard absent.
|
||||
6. `tests/unit/test_string_catalog.py` — catalog parses, every key referenced by JS exists
|
||||
(static scan), no unused keys, en.json has no unreplaced placeholders, and `docs/CLIENT.md`
|
||||
documents the "add a string here, not in code" rule.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E parity matrix + catalog completeness (static scan + runtime reference check).
|
||||
- Coverage: **>90%** on any server change.
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_us12_input_parity.py tests/unit/test_string_catalog.py -q` → green
|
||||
- [ ] keyboard, touch, and pointer produce the same server-observed movement (asserted parity)
|
||||
- [ ] slow motion does not change the server's tick count or final state for a fixed input script (asserted)
|
||||
- [ ] the catalog scan fails when a user-visible string is hard-coded in a logic module (demonstrated, then reverted)
|
||||
- [ ] full test suite **100% passing**
|
||||
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes (the phase's final pass re-checks both project-wide)
|
||||
- [ ] no behavior change in completed work
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Task 04 — Security and abuse review of the finished app
|
||||
|
||||
**Phase:** `14_accessibility_polish_and_hardening` · **Story:** `US-12_play_accessibly` (trust), all stories
|
||||
|
||||
## Objective
|
||||
Re-examine the whole shipped surface against the OWASP-shaped questions that matter for a
|
||||
WebSocket game, and close what is open.
|
||||
|
||||
## Work
|
||||
1. `docs/SECURITY.md` — the review itself, with a finding table (`id`, `area`, `severity`,
|
||||
`status`, `evidence test`), and the explicit threat model: anonymous public lobbies, hostile
|
||||
clients, hostile players (griefing, spam, DoS), no accounts, no persisted personal data.
|
||||
2. Verify each item with a test that fails if the property regresses (extend
|
||||
`tests/integration/test_ws_abuse.py`, add `tests/integration/test_http_abuse.py`):
|
||||
- **Transport/headers**: CSP `default-src 'self'` + `ws:`/`wss:` same-origin, `X-Content-Type-Options`,
|
||||
`Referrer-Policy: no-referrer`, `Permissions-Policy`, HSTS when behind TLS, no `Server` version
|
||||
leak, no directory traversal in the static mount (encoded `..` attempts), no symlinked asset
|
||||
served, `Cache-Control` correctness (`no-store` on `/api/*` and `/ws`, fingerprinted static
|
||||
cacheable);
|
||||
- **Auth/authz**: room code is the only capability and is 5-char Crockford with ≥ 60 bits of
|
||||
entropy claim verified by counting the code space; token guessing rate-limited; host-only
|
||||
actions refused for guests (each whitelisted action tested as a guest); spectator escalation
|
||||
refused; `set_level`/`start`/`restart`/`endless` host-gated; scoreboard writes require a real
|
||||
finished run (phase 13's authority rule re-asserted);
|
||||
- **Injection/XSS**: hostile display names (`<script>`, `"`, RTL override, zero-width, emoji,
|
||||
200 chars, control chars, ANSI) sanitized and never rendered via `innerHTML` (guard test scans
|
||||
the client for `innerHTML`/`outerHTML`/`document.write` with non-literal arguments); names in
|
||||
logs and metrics are sanitized; no HTML-in-JSON reflected unsanitized into DOM text nodes;
|
||||
- **DoS**: message size/rate caps (re-verify), code-space enumeration throttled with a growing
|
||||
penalty, room creation per IP capped, `/api/levels` and `/api/scores` cheap and cacheable, no
|
||||
unbounded loops over player input inside `step()`, generated endless levels bounded regardless
|
||||
of requested `wave_index` (request wave 10⁹ and assert a clamped, cheap result);
|
||||
- **Anonymity claims**: no cookies set unless strictly necessary and then `SameSite=Strict`,
|
||||
`Secure`, no tracking pixels, no fonts/images from anywhere but origin (re-run the phase-01
|
||||
guard across the final asset tree including the vendor dir rule), no IP persisted in any
|
||||
in-memory structure beyond the sliding rate-limit window (documented, TTL-verified).
|
||||
3. Fix everything rated `medium` or higher; `low` items either fixed or recorded in
|
||||
`docs/SECURITY.md` with the reason and a non-goal statement. No finding is left unlabeled.
|
||||
4. `tests/unit/test_security_docs_sync.py` — every finding row in `docs/SECURITY.md` names a test
|
||||
file that exists; every `open` finding is `low` or the build fails.
|
||||
|
||||
## Testing & Quality
|
||||
- Integration + unit as above; the review is only as real as its evidence tests.
|
||||
- Coverage: **>90%** on new/modified code.
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/integration/test_ws_abuse.py tests/integration/test_http_abuse.py tests/unit/test_security_docs_sync.py -q` → green
|
||||
- [ ] `docs/SECURITY.md` has a finding per reviewed area with severity, status, and evidence test
|
||||
- [ ] no `medium`+ finding remains open
|
||||
- [ ] hostile-name fixtures round-trip sanitized through DOM, logs, metrics, and the scoreboard (asserted)
|
||||
- [ ] requesting endless wave 10⁹ is clamped and cheap (asserted)
|
||||
- [ ] full test suite **100% passing**, coverage >90%
|
||||
- [ ] README.md and AGENTS.md are accurate and consistent with the whole project (verified in the final pass)
|
||||
- [ ] no behavior change in completed phases
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Task 05 — Release hardening, re-measurement, and the docs truth pass
|
||||
|
||||
**Phase:** `14_accessibility_polish_and_hardening` · **Story:** all
|
||||
|
||||
## Objective
|
||||
Ship it: production profile, budgets re-measured against the real app, and every document
|
||||
verified claim by claim against a running instance.
|
||||
|
||||
## Work
|
||||
1. Production profile:
|
||||
- `config.py` gains `profile: dev | prod` (default from `UNDERCOOKED_PROFILE`); `prod` ⇒
|
||||
debugpy off, `metrics_enabled` on, `test_mode` refused at startup (boot fails loudly if
|
||||
`UNDERCOOKED_TEST_MODE=1` with `profile=prod`), `log_level=info`, static caching on, verbose
|
||||
tracebacks off, `docs/*` routes not required (they stay as static markdown in the repo only);
|
||||
- `Containerfile` builds the prod profile, runs as a non-root user, healthcheck hits
|
||||
`/api/health`, `compose.yaml` sets limits (`mem_limit`, `cpus`) consistent with the measured
|
||||
capacity from phase 12, restart policy, and no host-path mounts for app code;
|
||||
- `.env.example` regenerated so every documented variable exists in `config.py` and vice versa
|
||||
(guard test already exists — make it pass with the new keys).
|
||||
2. Re-measure every budget in `BUDGETS.md` (tick cost, snapshot size, downlink rate, echo, drift,
|
||||
rejoin, capacity, endless growth) on the full feature set, append a dated table, and fix any
|
||||
regression > 10 % instead of editing the target.
|
||||
3. `tools/release_check.py` — one command that runs, in order, and prints a single verdict table:
|
||||
`uv sync --extra dev`, `ruff`, `pyright`, `undercooked-levels validate`, `pytest` (full, with the
|
||||
coverage gate), the `slow` perf suites, and a fresh-container smoke (`podman build` +
|
||||
`podman run` + `curl /api/health` + one Playwright round). `bash .agents/validate.sh` calls it.
|
||||
4. Docs truth pass — for each document, verify the claim, then fix or delete it:
|
||||
- `README.md`: quickstart commands run verbatim on a clean clone; the controls list matches
|
||||
`getKeyMap()`; the campaign list matches the manifest; the "what this is / is not" paragraph
|
||||
states in-memory persistence, single process, no accounts, and the trademark/homage note;
|
||||
- `AGENTS.md`: every command, path, and test-location claim matches the tree; guardrails each
|
||||
name the test that enforces them;
|
||||
- `docs/`: `PROTOCOL.md` message tables match `protocol.py` field-for-field (existing sync test
|
||||
extended to cover every message added since phase 06), `SIMULATION.md` rules each cite their
|
||||
test, `LEVELS.md` covers all 10 levels + generator, `CLIENT.md` module map matches
|
||||
`app/static/js/**` (new: a test walks the tree and asserts each documented module appears),
|
||||
`DEPLOYMENT.md` commands work, `ACCESSIBILITY.md`/`SECURITY.md` limitations are current;
|
||||
- `.agents/PLAN.md`: the phase list matches `.agents/phases/`, the documentation index matches
|
||||
`docs/`, and any LOCKED decision that was amended has its amendment recorded with the phase
|
||||
that changed it.
|
||||
5. `tests/unit/test_docs_module_map.py` — the client module-map and docs-tree guards that make the
|
||||
truth pass permanent rather than a one-time read-through.
|
||||
|
||||
## Testing & Quality
|
||||
- The whole suite plus `slow` perf suites plus a container smoke; docs guards are real tests.
|
||||
- Coverage: **>90%** project-wide; `pyproject.toml` coverage floor raised to the measured value.
|
||||
- Full suite green, from a clean checkout (`uv sync --extra dev` then `bash .agents/validate.sh`).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `bash .agents/validate.sh` exits 0 on a fresh clone/container build, including the smoke round
|
||||
- [ ] `uv run python tools/release_check.py` prints the verdict table with every stage green
|
||||
- [ ] `BUDGETS.md` has the re-measured dated table, no target silently relaxed
|
||||
- [ ] `profile=prod` + `UNDERCOOKED_TEST_MODE=1` refuses to boot (asserted)
|
||||
- [ ] every doc claim verified; the report lists how each was checked and what was fixed
|
||||
- [ ] full test suite **100% passing**, coverage >90%
|
||||
- [ ] README.md and AGENTS.md are accurate and consistent with the whole project (verified in the final pass)
|
||||
- [ ] no behavior change in completed phases (all prior story suites green in the same run)
|
||||
Reference in New Issue
Block a user