Web showcase: project plan¶
Everything in this project runs in a terminal today. This plan turns it into a fast, static, embeddable website where anyone can play every game, watch every agent decide, see why the gate acted or held back, and explore the statistics behind it. It also produces an API that other sites and services can call. The plan is for contributors. It says what will be built, in what order, what "done" means for each phase, and which decisions are already made and why.
Each phase ships something usable on its own, and nothing in a later phase is needed to keep an earlier one working.
Goals¶
- A showcase site with every example: games (play, watch, compare, audit), the decision runtime, parameter learning and the risk monitor. Interactive and animated. Good on phones as well as desktops.
- Teaching and science. Every claim on the site links to the proof, test or paper behind it. Every run can be replayed exactly from a URL.
- Embeddable. Drop a game, a replay or an ambient "background screen" into any existing website with one script tag and one element, served from our own site.
- One implementation. The browser runs the same Python package as the CLI. Nothing is reimplemented in JavaScript, so the tests and proofs cover what visitors see.
- Self-managing. CI enforces correctness, protocol compatibility, performance budgets, accessibility and dependency updates. Every release is a local build of plain files that we copy to our own nginx server.
Non-goals for now: user accounts, persistent multiplayer across the internet, server-side storage of visitors' data, and anything published to or hosted on third-party services. Everything runs on our own nginx server and our own service (phase 9).
The architecture in one picture¶
┌──────────────── grounded_decision (Python, unchanged) ───────────────┐
│ rules · agents · gates · solvers · DecisionRecord · learn · monitor │
│ session layer (new, pure, no I/O, no threads, no signals) │
│ open(spec) → Session ; session.step() / submit(move) → Frame │
└───────┬──────────────────────────┬──────────────────────────┬──────┘
CLI views Pyodide Web Worker our own service
(terminal, JSON) (in the browser) (phase 9, behind nginx)
└────────── one versioned protocol (JSON Schema) ─────────┘
│
generated TypeScript types · replay format (.gdr) · design tokens
│
┌───────────────┬────────────────────────┼──────────────────────┬────────────────┐
board renderer decision inspector charts and explorables input (keys, web components
(Canvas 2D, (proposal, checks, (compare, audit, touch, mouse) <gd-game> <gd-replay>
square + hex) verdict, evidence) learning, monitor) <gd-ambient> <gd-demo>
│
SvelteKit static site → our own nginx server
Why this shape¶
- The model–view split already exists. Games describe boards by
meaning (
cells,cell_details,move_cells,stats), never as characters. The terminal styles are one projection of that. A browser renderer is another, and adding a game needs no front-end code. - Pyodide runs the package unchanged. Its only dependencies are
numpy(shipped with Pyodide) andlark(pure Python). The browser is about 1.5–3 times slower than native Python. That is still far more than 60 frames a second needs, because the engine runs in a Web Worker and never blocks the page. - Replays don't need Python. A recorded game is a compact stream of frame differences. A small JavaScript player animates it instantly, with no 10 MB download. Ambient screens, embeds and the first paint of every page use replays. Live Python loads only when the visitor interacts.
- Static first. The whole site is files we copy to our own server, with no servers to run, scale or secure. The optional server reuses the same protocol, so the front end does not know or care where Python runs.
Decisions already made¶
| Question | Decision | Why |
|---|---|---|
| Where the rules run | The Python package in Pyodide; optionally the same package on a server | One implementation, so the proofs and tests cover the site |
| Front-end framework | SvelteKit with the static adapter, TypeScript in strict mode | Small runtime, compiles to static files, good for animation-heavy pages |
| Board drawing | Canvas 2D with layered canvases and dirty rectangles; a WebGL path only if a measured budget fails | Grid boards need no scene graph; avoids about 450 KB of PixiJS. A 320 × 240 snake is 76,800 cells, which is one ImageData write. |
| Embedding | Standard web components (custom elements, Shadow DOM), plus an iframe fallback | Works in any site and framework; styles cannot leak either way |
| Protocol | JSON Schema generated from Python dataclasses; TypeScript types generated from the schema | One source of truth; drift fails CI |
| Colours and glyphs | One token file generated from views.py, used by both the terminal and the CSS |
The CLI and the site look like the same product |
| Hosting | A local, content-hashed static build that we copy to our own site and service; no third-party host, vendor CDN, account or secret | Our infrastructure, no lock-in: the build output is plain files any web server can serve |
| Cross-origin isolation | Not required | Avoids SharedArrayBuffer, so the site works on any host and inside other sites. Stopping a computation means terminating the worker. |
Repository layout¶
src/grounded_decision/session/ protocol dataclasses, Session, replay writer (Python)
web/ pnpm workspace
packages/protocol/ generated types, runtime validators, schema tests
packages/engine/ Pyodide worker, transport, wheel loader
packages/replay/ .gdr reader and player (no Python)
packages/board/ renderer: square and hex grids, animation, themes
packages/inspector/ DecisionRecord views, check timelines
packages/models/ DOM-free models shared by the site and the elements
packages/elements/ <gd-game> <gd-replay> <gd-ambient> <gd-demo>
apps/site/ the showcase site (SvelteKit)
tests/e2e/ Playwright
make stays the single entry point. New targets: web-setup, web-dev,
web-build, web-test, web-e2e, web-lighthouse, web-check, web-elements, dist, replays, protocol.
Once web/ exists, make check also runs the web checks.
Phases¶
Each phase lists what is built and its exit criteria, all automated in CI. Work inside a phase can run in parallel. The phases are sequential.
Phase 0: Foundations¶
- Workspace: a pnpm workspace with Vite and SvelteKit, TypeScript
strict, ESLint and Prettier for
web/only (Python keeps ruff), Vitest and Playwright. - CI: a
webjob beside the Python job. Exact pins, updated by hand. - Records: the decisions above written into Design decisions as D17 onwards.
- Exit: an empty site builds locally and passes lint, type check and one Playwright test in CI.
Phase 1: The session layer and the protocol (Python only)¶
Status: built. grounded_decision.session (SessionSpec,
Session, Frame, the Endpoint and protocol 1.0.0), the CLI's
play and --view json on top of it, make protocol (JSON Schema,
design tokens, golden fixtures, TypeScript types) and
web/packages/protocol (generated types, runtime validators, fixture
tests). Reference: Session protocol;
decisions D28 to D33. Not yet: Session.undo(), and compare and
audit share the session's match loop (engine.Match) but not yet
its frames.
- The session API:
grounded_decision.sessionwithSessionSpec(game, agents, world, seed, options),Session.step(),Session.submit(move),Session.legal_moves(),Session.undo()where the game allows it, andFrame. A frame holds the cells, details and move cells, the panel stats, the latestDecision(proposal, checks, verdict, notes) and the outcome. It is pure, with no I/O, threads, signals or wall clock, so it runs anywhere. A frame also carries the semantic overlays the terminal views already use (views.Overlay:CONVERTED,CONVERTING,RING,TARGET_NEARfor a one-step/clone target,TARGET_FARfor a two-step/jump target,SELECTED,LAST_FROM/LAST_TO,REJECTED/CHOSEN), the move'stransitionframes (overlays on intermediate positions, for the player to animate), themove_eventline, the board's layout (square,pointy,flat) and the fixedPanelLayoutslots, so the web player draws the same meaning in its own colours and keeps the same fixed panel. - The CLI on top of it: the terminal views,
--view jsonandcompare/auditconsume the session layer, so there is one code path. - The protocol: messages are
open,step,submit,frames,erroranddescribe.describereturns the game list, agents, worlds and checks from the module contract constants. The protocol is versioned with semver, and a schema-diff test fails a breaking change without a major bump. - Tokens: a new
protocolmake target writes the JSON Schema and a token file (colours and glyphs for everyCellKind). - Exit:
- Golden frames:
--view jsonoutput equals the session layer's frames byte for byte for every game and seed in the snapshot set. - The schema is committed and checked.
make checkis green.
Phase 2: Replays and the board renderer (no Python in the browser)¶
Status of the replay half: built. grounded-decision record and
replay-info (--verify re-records the spec and compares content
hashes), the .gdr format version 2
(grounded_decision.session.replay), make replays (a committed
gallery of every game in web/apps/site/static/replays/ with an
index.json manifest, and golden files for the decoder's tests), and
web/packages/replay (decoder with random access and a DOM-free
ReplayPlayer, 11.5 KB gzipped). Reference:
Replay format; decisions D34 to D37.
Status of the renderer half: built. web/packages/board draws any
frame on three layered canvases with dirty rectangles (square grids,
pointy hex rows, flat hex columns; every CellKind and Overlay;
one pixel per cell above 16,384 cells), animates moves from the
frame's transitions and move cells (slides along jump paths, Hexxagon
flips, Tetris drops and bursts, 2048 pops and merges), cuts under
reduced motion, and describes each frame in words; 23 KB gzipped. The
site has /gallery and a prerendered /replay/NAME page per replay
(player controls, keyboard shortcuts, ?frame= links, the fixed
decision panel, a colour-blind palette and glyph mode). Measured in
headless Chromium: 0.8 ms median and 1.0 ms 95th percentile per frame
on the 320 × 240 snake. Playwright checks every replay, screenshots of
each in light and dark, keyboard-only use, WCAG 2.2 AA with axe, and
the frame budget. Details: web/README.md; decisions D38 to D42. Not
yet: Linux screenshot baselines (CI writes and uploads them on its
first run; they need committing), and runs in WebKit and Firefox.
- Recording:
grounded-decision record GAME --agents … --world … --seed …writes a.gdrreplay: a header (game, agents, world, seed, package version, protocol version, content hash), the keyframes, one line of changes per part of the frame, then gzip. - Playing:
packages/replaydecodes it.packages/boarddraws square and hex boards for everyCellKind, with the snake gradient, jump paths and last-move highlights. It animates moves frommove_cells: pieces slide, Hexxagon pieces flip, lines clear and 2048 tiles merge. - Accessibility: colours follow the shared tokens, with a
colour-blind-safe palette and glyph fallbacks.
prefers-reduced-motionturns animation into cuts. - Exit:
- Every game's snapshot replay plays in the browser.
- Visual-regression screenshots are stable.
- The renderer holds 60 fps on a 320 × 240 snake on a mid-range laptop, with 4 ms or less per frame on the main thread, measured in CI with a headless trace budget.
- The replay player is 40 KB or less, gzipped.
This phase alone can ship: a gallery of animated, real, reproducible games.
Phase 3: The live engine in the browser¶
Status: built. web/packages/engine runs Pyodide 314.0.6 in a Web
Worker from the site's own files (make web-engine: the runtime from
the pinned npm package, lark from uv.lock, the wheel from make
build; numpy hosted for later, since the session layer no longer
imports it), behind a Transport interface and a validating Engine
client with lazy start, progress, cancellation and protocol errors.
Protocol 1.1.0 adds select (a person's piece and its targets). Each
game has a prerendered /play/GAME page: the replay first, then the
live game; agents, world, seed, size and rule options in the URL;
watching at a speed, playing by click, tap or keys, --guarded with
the gate's explanation, and Copy CLI command.
make web-pyodide-golden found and fixed a determinism bug (seeds
hashed differently on 32-bit WebAssembly) and now shows all ten games
byte-identical to CPython; make web-pyodide-test runs the session,
game and learning tests in Pyodide. Measured: the first live move in
2.5 s on a cold cache over 40 Mbit/s and 1.0 s on a warm one; about
4,100 snake moves a second in the worker with every frame on the wire
(2,700 before protocol 1.2 sent the snake's body as one path, D51),
above the plan's 3,000. Reference:
The engine in the browser; decisions
D43 to D50. Not yet: WebKit and Firefox runs. (Loading numpy on
demand came with phase 6, D65.)
- The worker:
packages/engineruns Pyodide in a Web Worker, loads the project's wheel (built bymake build) and speaks the protocol overpostMessage. One transport interface is shared with the WebSocket transport to our own service (phase 9). Pyodide and the wheel are lazy-loaded on first interaction and cached. - Determinism: the same
SessionSpecproduces identical frames in CPython and in Pyodide. A CI job runs the golden-frame set in both. - The test suite in Pyodide: the pure-library tests run under Pyodide in CI (pytest via Pyodide's test runner). Tests that need the terminal or the file system are marked and skipped.
- Exit:
- Tic-tac-toe and Snake are playable live.
- Pyodide golden frames match.
- Time to first live move is 4 s or less on a cold cache and 1 s or less on a warm cache.
- Snake engine throughput in the worker is at least 3,000 moves a second.
Phase 4: The showcase site, version 1¶
Status: built. A design system from tokens (apps/site/src/app.css:
type scale, spacing, radii, light and dark colours, shared buttons,
fields, cards, badges and callouts), a header with Play, Gallery and
Learn on every page, Display settings (scheme, colour-blind-safe
colours, glyphs, motion) kept in the browser and applied before the
first paint. The home page explains propose, check, then act, abstain
or degrade beside a replay-first example that pauses on each override.
Every game is offered three ways (watch, play against agents, play
guarded) on its play page and on every game card. The decision
inspector (web/packages/inspector) shows each move's proposal against
the move played (both marked on the board), every check with what it
tests, its evidence and result, the verdict, fallback and reason, and
the regime against the world ("Assumption not met" when it is not); a
scrubbable timeline marks overrides on replays and live games alike.
Explain panes use the catalogue's text, which protocol 1.2.0 extends
with the regimes, so they say what play --explain says. Play pages
keep the person's moves and the position on show in the URL, so a
copied link reopens the very game; every game is playable from the
keyboard alone, and moves are announced in a live log, summarised at
speed. Measured with Lighthouse CI (make web-lighthouse, mobile
emulation, median of three runs): 99 to 100 for performance and 100
for accessibility, best practices and SEO on the home page, the
gallery, a replay page and a play page; CI fails below 95. Playwright
covers watch, play (a keyboard move in every game; whole games of
tic-tac-toe and Connect Four), the guarded override, share-link round
trips, the timeline, axe on every route in both schemes, and
screenshots of the new pages. Details: web/README.md; decisions D52
to D58. Not yet: Linux screenshot baselines for the new pages (CI
uploads them), and WebKit and Firefox runs.
- Play pages: every game, with agents, world, seed, size, speed, pause, step, a timeline you can scrub, and human play by keyboard, touch and mouse. A guarded toggle lets the gate catch your blunders and explains why.
- URL state: every setting is in the URL. Sharing a link shares the exact game, and a "copy CLI command" button gives the identical terminal run.
- Decision inspector: each move shows the proposal, every check with its evidence and result, the verdict (act, abstain or degrade), the fallback, and the reason. Overrides are marked in the timeline. It also gives the regime, the world and whether the agent's assumptions hold.
- Explain panes: they reuse
AGENT_DETAILS,CHECK_DESCRIPTIONSandWORLDS, so the CLI's--explainand the site say the same thing. - Exit:
- Every game is playable, all three ways.
- Lighthouse scores are at least 95 for performance, accessibility, best practices and SEO on replay-first pages.
- Playwright covers play, watch, override and share-link round-trips for every game.
- WCAG 2.2 AA is met for controls and the inspector: keyboard-only play and screen-reader move announcements.
Phase 5: Compare, audit and teaching pages¶
Status: built. Protocol 1.3 makes compare, audit and analyse
session operations on the command line's own code (Tournament,
AuditSetup and AuditRun, refactored so the CLI and the session share
one path): a comparison's games are numbered and played one request at
a time on any engine, Python tallies the table from the set of finished
games (so it equals compare --json whatever the order), and an audit
returns its judged decisions a chunk of moves at a time, each
disagreement with its position. Golden tests compare every number with
the command line for several games. /compare/GAME runs the games on a
pool of Pyodide workers (one per core but one, at most four by
default) with a live table of Clopper–Pearson intervals, a Cancel
button and the identical command; /audit/GAME lists each disagreement
as a button that opens its position (before and after, both moves
marked) and the decision inspector. Both are prerendered for every game
and linked from the play pages. /learn/GAME is a teaching page per
game, replay-first: make lessons records real positions and the
package's own analyses (the snake's cycle and certificate,
Minesweeper's probabilities, Connect Four's column values, the Tetris
adversary and Burgiel's S and Z pieces, Go's superko and area score,
Hexxagon's ring trap and two-ply exchange, the gate on a checkers,
Chinese checkers, tic-tac-toe and 2048 position); Minesweeper and
Connect Four also analyse live as you play. Every claim names its test
(<Claim test=…>), linked to its source line, and
tests/test_site_claims.py fails a claim whose test is missing,
skipped or slow. Decisions D59 to D63. Not yet: Linux screenshot
baselines for the new pages (CI uploads them).
- Compare and audit in the browser: live tournament tables with Clopper–Pearson intervals, filling in as games finish (a worker pool, one Pyodide per core, with a configurable limit). Audits against oracles show each disagreement as a clickable position.
- Explorable explanations, one per game:
- Snake: the Hamiltonian cycle and the certificate shown live.
- Minesweeper: a probability heat map, with proved-safe cells marked.
- Connect Four: the solver's value of each column.
- Tetris: the adversary's choice and why it hurts.
- Go: superko and area scoring, stepped through.
- Sources: every theorem on a page links to its test, its source line and, where there is one, its paper.
- Exit: every claim on a teaching page has a link to a test that checks it, enforced by a link checker that maps claims to test IDs.
Phase 6: Beyond games¶
Status: built, except the embedding. Protocol 1.4.0 adds learn,
monitor and runtime (grounded_decision.session.demos): stateless
requests whose replies hold everything a page draws, computed by the
command line's own code (learning_run, the parameter_learning
example's LiveTraffic, and the reference service's pure core, now
examples/reference_core.py). The engine loads numpy on demand
(WorkerTransport's packages), only on the three new pages:
/parameters (the how-to's 3,000 decisions, the example's
authorisations, or a CSV parsed in the browser and never uploaded; risk
target, confidence, measure, procedure, seed, decision line and grid;
the certificate, explain(), the fingerprint, params.json, and every
combination's exact p-value charted), /monitor (the example's drift or
the how-to's stream, played as it arrives, with the bounds, the target
and the alarm) and /runtime (the build-a-service tutorial's requests
stepped through, routing to human_review (the live examples'
hold_for_human) and await_next_event shown, the DecisionRecord
in the inspector's styles, and more events sent live). Each page shows
the engine's recorded reply first (make protocol writes it), gives the
identical command, and follows the docs standard; the docs pages link to
them and back. Golden tests (tests/test_session_demos.py) compare every
demo with grounded-decision learn, grounded-decision examples
parameter_learning, DecisionService on SQLite and the docs' numbers;
Vitest covers the CSV reader, the chart geometry and component, and the
page models; Playwright covers each page (preview numbers, live
interaction, axe in both schemes, a phone's width) and Lighthouse runs on
/runtime. Decisions D64 to D68, and an open question on platform-exact
floating point (fingerprints computed in the browser can differ in the
last bits; the page says so). The widgets came to the docs pages with
phase 7's <gd-demo> (D76).
- Parameter learning: load a sample dataset, or your own CSV (which
stays in the browser). Set a risk target and watch Learn-then-Test
choose thresholds, with the certificate, provenance fingerprint and
explain()text. - Risk monitor: a streaming chart of the monitor on a synthetic drift scenario, showing when it raises the alarm and why.
- The decision runtime: the reference service's loop as a stepper.
Events come in, the statechart proposes, the six checks run, and a
DecisionRecordcomes out. Routing tohold_for_humanis shown. - Exit: each demo matches its documented example's numbers exactly (golden tests), and every docs page has its widget embedded.
Phase 7: Embedding and integration¶
Status: built. web/packages/elements defines <gd-replay>,
<gd-game>, <gd-demo> and <gd-ambient> (a stable API; its playlist
plays now, phase 8 adds the rest). They are standard custom elements
with an open Shadow DOM, written over the site's own packages, with the
models shared through the new web/packages/models. Attributes are the
command line's flags, mirrored as properties. They send ready,
frame, move, verdict and error events, expose ::part()s and
--gd-* custom properties, and follow the host's color-scheme, then
prefers-color-scheme, or an explicit theme. They are keyboard
driven, with live regions summarised at speed. Each element loads when
it nears the viewport. Python loads on the first interaction, from
engine-base or the bundle's own engine/, through a blob: module
when the page is on another origin (D73). make web-elements writes
one self-contained, versioned directory, elements/0.1.0/, holding the
chunks, the worker, the phase 3 engine files, the replays, the catalogue
and the recorded demo replies. It writes integrity.json with SHA-384
for every file, and a scripts-only elements/0/ alias. make dist
gathers the site, the elements and the docs into dist/web/ for our own
nginx server.
The site serves /elements/, an iframe page per replay and per game
(/embed/replay/NAME/, /embed/play/GAME/; only these may be framed)
and a static oEmbed file per replay. make docs embeds the elements
through a MkDocs hook: the local bundle, its script tag with its
integrity hash only on pages that use one, and fallback text without
JavaScript or without a build. The games pages and the learning,
provenance, monitoring and runtime pages run them in place. The guide is
Embed the games and demos.
Measured, gzipped: gd.js 9.7 KB; <gd-replay> and <gd-ambient>
53.4 KB before their first frame (the build fails above 64 KB);
<gd-game> and <gd-demo> 9.7 KB before their preview. Playwright
embeds every element in plain HTML and in a React 19 app on another
origin, under a policy with only the guide's entries, and in the MkDocs
site. It checks drawing, the keyboard, light and dark schemes, axe and
an empty console. It also checks a tampered gd.js refused by its
hash, a live game and a live demo running across origins, the iframe
and oEmbed files, and the served headers. Vitest covers attribute and
property reflection, events, the keyboard, failure and the playlist.
Decisions D70 to D76. Not yet: WebKit and Firefox runs, and import-map
integrity for the chunks (documented, not tested).
- Web components:
<gd-replay src=… autoplay loop>(no Python).<gd-game game=… agents=… world=…>(live).<gd-demo name=…>(the phase 6 widgets).<gd-ambient>(phase 8).- Delivery: versioned, immutable URLs on our own server
(
/elements/1.4.2/gd.js) with Subresource Integrity hashes, and a moving/elements/1/alias. - Theming: CSS custom properties and
::part()styling. Light and dark themes follow the host page orprefers-color-scheme. - Fallbacks: an iframe embed for hosts that forbid scripts, and an oEmbed endpoint (a static JSON file) for CMSs.
- MkDocs:
make docsembeds the elements, so every "Try it" block on the docs site runs in place. - Integration guide: a "How to embed" page for our other websites. It covers script tags, Content Security Policy entries, a self-hosting option and a size budget per element.
- Exit:
- A test page embeds every element in three host setups: plain HTML, a React app and the MkDocs site.
- SRI, CSP and dark mode are verified in Playwright.
- Initial load per embed is 64 KB or less, gzipped, for replay and ambient elements.
Phase 8: Ambient and background screens¶
Status: built. <gd-ambient> (web/packages/elements) plays
recorded games one after another with no Python: a manifest's replays
(library, else the bundle's gallery), kept by name or game (games),
in an order shuffled by seed and reshuffled by it at every pass,
paced by pace (slow, normal, fast or seconds a game: 1 to 8
moves a second, and a longer game cuts to its end) or speed, with a
cross-fade between games (a cut under reduced motion) and optional
captions: the verdict and the deciding check from the decision record,
at most one every 6 seconds, overrides first, in large type with
captions="large". kiosk fills the screen, asks for fullscreen on the
first click or key, hides the cursor after 3 seconds at rest and keeps
the screen awake (Screen Wake Lock, falling back silently). The low-power
rules are a state machine over what the browser reports: hidden pauses,
reduced motion shows still positions every 8 seconds or more, a
discharging battery (Battery API) or reduced data draws at 15 frames a
second, and otherwise at 30. The site's /ambient takes the settings in
its URL (/ambient?kiosk=1&games=snake,go&pace=fast&captions=large).
make ambient-replays records a library of the eight backdrops, three
of each from the day's seed, into build/ambient/ (never committed),
each verified as replay-info --verify does and listed with its content
hash and outcome; make dist copies it to dist/web/ambient/, and a
nightly job records one as a test only. The guide is
Embed the games and demos.
Measured on an Apple-silicon laptop in Chromium's headless mode (which
draws with the GPU), a 1920 × 1080 kiosk screen with large captions,
20 seconds each: 2.9% of one core on the page's main thread (3.5% for
the renderer process) at full power, 2.6% on battery, 0.1% under
reduced motion. The soak test, accelerated to 200 moves a second, played 20,017
positions over 74 games with no console errors: the heap after a forced
garbage collection stayed between 4.1 and 5.7 MB from the second game to
the last, the DOM between 521 and 527 nodes, and the event listeners at
67 throughout. Five minutes at the normal pace on a recorded library (make
web-soak SOAK_MINUTES=5) kept the heap between 2.5 and 5.4 MB, the DOM
at 520 to 522 nodes and the listeners at 41. gd-ambient's first
load is 60.0 KB gzipped (budget 64 KB, raised from 60 KB with protocol 1.6). Tested: Vitest for the playlist,
pacing, the power state machine, the captions and their throttle, the
capped clock and the element; Playwright for the playlist moving across
games, kiosk mode (no controls, fullscreen, the cursor hidden), the
pause when hidden and 15 fps on battery, reduced motion, axe in both
schemes and in kiosk mode, a phone's width, the CPU budget, and the
soak (2,000 positions in CI; make web-soak runs it for 30 minutes,
and make web-soak SOAK_MINUTES=1440 is the manual 24-hour check).
Decisions D81 to D83. Not yet: a 24-hour run on the television itself,
which needs the owner's screen.
/ambientand<gd-ambient>: a playlist of recorded games shuffled by seed. Snake at large sizes, six-player Chinese checkers, Go and Tetris against the adversary all make good backdrops. Optional captions from the decision records show what the agent is "thinking".- Kiosk and screensaver mode: fullscreen, no controls, cursor hidden, and a URL parameter to choose the games and pace.
- Low power:
- It pauses when hidden (Page Visibility API).
- It drops to 15 fps on battery when the browser reports it.
- With
prefers-reduced-motion, it renders still frames that change slowly. - It uses replays only, with no Pyodide, so it stays well under 5% CPU on a laptop.
- Fresh content: a nightly CI job regenerates a rotating replay library from new seeds. Each replay carries its content hash and verified outcome.
- Exit:
- A 24-hour soak test in a headless browser shows flat memory and no errors.
- The CPU budget is met.
- The playlist plays on a TV-sized screen with readable captions.
Phase 9: Our own service¶
Status: built. grounded_decision.service (grounded-decision
serve, make serve, the service extra: uvicorn and websockets,
pinned exactly) runs the package's own Endpoint, one session per
WebSocket on /ws/ and one message per POST /api/, the reference
decision service under /api/decisions/ (the same routes as
examples/reference_service.py, now DecisionRoutes, with its SQLite
tables in a file you choose) and GET /healthz. Limits on message
size, sessions per process, each connection's rate, request time, idle
time and the work in one request (moves, games, seeds) are answered
with the protocol's own error replies, so the protocol stayed 1.4.0.
Its JSON log lines name no visitor. The front end's
WebSocketTransport is the second Transport; EngineChoice picks
it by one setting (VITE_ENGINE, overridden by ?engine=), with the
worker as fallback when the service is off or full. make dist
writes dist/service: the wheel and its dependencies for the server's
platform (offline install), the hashed requirements from uv.lock, the
nginx snippets, a systemd unit per port, run.sh and release.sh
(versioned release directories, an atomic current link, health-checked
switch, automatic switch back, rollback, prune). The guide is
Run our own service.
Tested: every golden conversation through the Endpoint, the
WebSocket and HTTP, byte for byte; each limit; the logs; the decision
API against the reference service, request for request; release.sh
for real against a scratch prefix (tests/test_service.py,
tests/test_service_ops.py). Playwright runs the golden conversations
through WebSocketTransport against a local service behind an
nginx-like proxy, plays snake and /runtime live with no Python
downloaded, and falls back to the worker with the socket refused
(web/tests/e2e/service.spec.ts). Measured on an Apple-silicon laptop,
one process, snake 20 × 20 (make service-load): 64 sessions at once,
3,493 requests a second, 95th percentile 21.5 ms, against a target of
1,000 and 100 ms. Decisions D77 to D80. Not yet: running it on our
servers, which needs the owner.
- What it is: our own server process, part of the product, run on
our own machines behind our nginx (as a reverse proxy for
/api/and/ws/). No third-party platform, account or hosted service is involved. - What it does: runs the same package and speaks the same protocol over WebSocket and HTTP, for work the browser can't do well: large datasets for parameter learning, long tournaments and audits, and the reference decision service as a real HTTP API.
- Operations: stateless, rate-limited, with request size limits and
structured logs that store no visitor data. Shipped with an nginx
snippet and a service unit file;
make serveruns it locally. - The front end switches transports (worker or service) with one setting; the static site still works with the service switched off.
- Exit:
- The protocol conformance suite passes against both transports.
- A local load test reaches its target.
- Building, copying to our servers and rolling back are scripted.
Phase 10: Launch hardening and release¶
- Security: a strict Content Security Policy that allows only the
site's own origin (Pyodide is served from it too).
wasm-unsafe-evalis the only relaxation, because Pyodide needs it. Plus security headers, shipped as an nginx config snippet, and a dependency audit in CI. - Privacy: no cookies and no tracking by default. If analytics are wanted, a privacy-preserving, cookieless tool, recorded as a design decision.
- Release: semver for the package, the protocol and the elements (independently versioned). A changelog. One local command builds every artifact into one directory (the site, the versioned element bundle, the docs, the wheel, our service) that we copy to our own servers. Nothing is published to a package index or deployed to a host.
- Reproducibility: each replay embeds the package version and hash it was recorded with, so any shared game can be reproduced exactly.
- Exit:
- The security header scan scores A.
- Accessibility and performance budgets are green.
- A local release build, served by nginx with the shipped config, passes the end-to-end tests.
Cross-cutting rules (every phase)¶
| Area | Rule | Enforced by |
|---|---|---|
| Correctness | Browser and CLI produce identical frames for the same spec | Golden-frame tests in CPython and Pyodide |
| Compatibility | Protocol changes follow semver | Schema-diff test |
| Performance | Budgets per page and element (bundle size, LCP ≤ 1.5 s on replay pages, 60 fps, main thread ≤ 4 ms per frame) | Bundle-size check, Lighthouse CI, trace budgets |
| Accessibility | WCAG 2.2 AA; keyboard play; reduced motion; colour-blind palette with glyph fallback | axe in Playwright, manual audit before each phase exit |
| Honesty | Every guarantee shown links to the test or proof; measured numbers say so | Claim-link checker |
| Unification | One source for names, descriptions, colours and glyphs: the Python modules | Generated files checked to be current, as docs/reference/agents.md is today |
| Self-management | Exact pins updated by hand, generated files checked to be current, one local release build | make check, CI tests |
Risks and how we handle them¶
- Pyodide download size (about 10 MB). Pages use replays first and load live Python only on interaction. The download is cached long-term, and a progress indicator shows while the replay keeps playing.
- Modules Pyodide lacks or limits (
signal,threading, subprocesses). The session layer must not import them, and a test imports it with those modules blocked. - Safari and mobile performance. The board renderer has budgets per browser in Playwright's WebKit runs, and large boards fall back to a lower frame rate before they drop frames.
- Two languages. All game logic stays in Python. TypeScript only draws, animates and handles input, and its types are generated, so it cannot drift from the protocol.
- Scope creep. Each phase ships alone, and nothing depends on a third-party host or service: only our nginx server and our own service.