Skip to content

Changelog

Every notable change, newest first. This page is generated from CHANGELOG.md at the repository root.

All notable changes to this project are recorded here. The format follows Keep a Changelog, and the project uses semantic versioning.

[Unreleased]

Added

  • Rule options as data (protocol 1.11): each game's OPTIONS (games/options.py) says what it takes for each option (a range, the values, the option values it needs, whether a browser can offer it), and each flag has a label. play GAME --list prints every option's range and default; the catalogue's rules carry them to the web, whose settings drop their own copy of the flag table. Numbers are bounded fields with the range by the label and the default in the field, a value the game would refuse is marked and not applied, an option whose needed value is unset is disabled ("only when Mode is realtime") and dropped when it stops applying, and options that change the sizes (the board) come before the size. A test checks every game takes exactly the values it declares.
  • Board sizes as data (protocol 1.9): each game declares SIZES, a SizeRule (games/sizes.py) of every size its board may take (one number for a side, or a width and a height, each a range with a step; snake's "one side even"; ranges that differ by rule option, such as a Hexxagon donut's), its default and its usual sizes. The game checks every size with it, play --list prints it, and the catalogue's size_offers carry it to front ends. The play page's free-text size box is now a size picker: the usual sizes in a menu, number fields bounded by the rule's ranges and steps for any other size, and the rule in the engine's words below; a size the game would refuse is marked in place and never reaches the link. A game with a fixed size (tic-tac-toe) offers none. The protocol package's generated/size-probes.json holds the engine's verdicts on sizes at every bound, which the web's checks are tested against.
  • Move arrows as data (protocol 1.7): each frame's arrows say which piece went where (and, on an override, the proposal the gate turned down), checked when the frame is built against the board before the move (session/arrows.py). replay-info FILE --moves prints every move with its arrows and fails on one that does not start at the mover's piece. The web board draws only these: over the position before the move while it animates, and the next move's at rest.
  • One default tempo for every replay, live game and embed: a game of turns plays one move a second, a game for one sixteen; each move animates for 70% of its time and then holds still.
  • /bench/drawing and drawing.spec.ts: every gallery replay drawn move by move must equal each frame drawn from scratch, pixel for pixel.
  • tools/stage_hunks.py stages chosen hunks of a file, so one file's changes go into separate commits (CONTRIBUTING.md, "Commits"); make web-format formats the web code and make format runs it.
  • The test suite runs on every core (pytest-xdist: about 6 minutes, from 15), fails any test after five minutes (pytest-timeout), lists its fifteen slowest tests, and checks the arrow rule with Hypothesis on generated boards and games. make web-e2e E2E_ARGS=... runs chosen end-to-end specs.
  • Breakout (breakout): Arkanoid in real time (20 ticks a second) with exact integer physics (64 units to a cell, swept collisions in rational time), Arkanoid's paddle angles, silver, gold and capsule bricks, seven capsules, eight built-in levels, --board random, --layout FILE, and spin (--epsilon). Worlds pick the capsules and the spin (random, kind, adversarial). Agents: random, follow, intercept, aim (probabilistic), worst_case, gated (an exact intercept certificate and spin-proof bounces, proved) and a clairvoyant benchmark; an audit oracle and a regime table. Docs: docs/examples/breakout.md.
  • A real-time terminal clock for people: play GAME --agents you in a game with controls (Breakout, Tetris --mode realtime) reads keys without Enter while the ticks run on, p pauses, q quits; one line per tick without a terminal. Protocol 1.5: SessionInfo.controls and CellNote.extent (sub-cell positions), and eight Breakout cell kinds.
  • Extending the system: docs/contributing/add-a-game.md, add-a-protocol-operation.md and add-a-web-page.md; grounded-decision new-game NAME (make new-game NAME=…) writes a game skeleton that passes the game-module contract, which tests/game_contract.py now checks for every game. Game rule flags are declared once (games/options.py).

  • Ambient and background screens (web plan, phase 8): <gd-ambient> now plays a seeded, reshuffled playlist of recorded games (no Python) from a manifest (library, falling back to the gallery), chosen by games, paced by pace (slow, normal, fast or seconds) or speed, with a cross-fade between games (a cut under reduced motion), and optional captions from the decision records: the verdict and the deciding check, at most one every 6 seconds, in large type on request. kiosk fills the screen, goes fullscreen on the first gesture, hides the cursor at rest and keeps the screen awake where the browser allows. Low power: paused while hidden, 30 frames a second at most, 15 on a discharging battery or with reduced data, slow still positions under reduced motion; measured at 2.9% of one core on a 1920 × 1080 kiosk screen. New events game and power. The site's /ambient page takes the same settings in its URL (?kiosk=1&games=…&pace=…). make ambient-replays records and verifies a fresh library from the day's seed into build/ambient/ (make dist puts it in dist/web/ambient/; a nightly job records one as a test); make web-soak checks a long run for flat memory and no errors.

  • Our own service (web plan, phase 9): grounded-decision serve (and make serve; the new service extra, uvicorn and websockets pinned exactly) speaks the session protocol with the package's own Endpoint, one session per WebSocket on /ws/ and one message per POST /api/, serves the reference decision service's HTTP API under /api/decisions/ with its SQLite tables, and answers GET /healthz. Limits on message size, sessions, each connection's rate, request and idle time and the work in one request are answered with the protocol's own errors; settings come from flags or GD_SERVICE_* variables; logs are JSON lines that name no visitor. The front end's WebSocketTransport and EngineChoice switch the site's engines to the service by one setting (VITE_ENGINE=service, or ?engine=), falling back to the browser's worker when the service is off or full. make dist also writes dist/service: the wheels for an offline install on the server's platform, hashed requirements from uv.lock, nginx snippets, a systemd unit and a release script that switches, health-checks and rolls back versioned releases. make service-load measures it; the guide is docs/how-to/run-the-service.md. Conformance tests compare every golden conversation through the Endpoint, the WebSocket and HTTP byte for byte, in Python and in Playwright.
  • Tetris in real time: --mode realtime (with --level, --lock-delay and --gravity) plays tick by tick, one action per tick (left, right, rotate_cw, rotate_ccw, soft_drop, hard_drop, wait), with the guideline's gravity curve, a lock delay, SRS rotation with the published wall-kick tables, T-spins (mini and full, three-corner rule), guideline scoring with combos and back-to-back, and the same worlds and preview. Every agent now chooses among all reachable resting places (tucks and spins included) and an exact planner (BFS over the tick's finite state) keys them in; the gated agent gains an executable_before_lock check and falls back to the safest lock still reachable in time; a person keys actions in the terminal (letters or arrow keys) or through the session protocol. Placement mode stays the default and unchanged. See docs/examples/tetris.md.
  • Embedding and integration (web plan, phase 7). The embeddable elements <gd-replay>, <gd-game>, <gd-demo> and <gd-ambient> (web/packages/elements, version 0.1.0) are standard custom elements with Shadow DOM, attribute and property reflection, events, parts, custom properties and light and dark themes that follow the host. Replays need no Python. Live games and demos load the engine on first interaction, also from another origin. make web-elements builds one self-contained versioned directory with integrity.json and a moving major alias. The build fails above 60 KB gzipped for the replay elements' first load; it is 53.4 KB.
  • The site serves the elements, iframe embed pages (/embed/replay/NAME/, /embed/play/GAME/) and a static oEmbed file per replay.
  • make docs runs the elements in place on the games, learning, monitoring and runtime pages, through a MkDocs hook (tools/mkdocs_elements.py).
  • make dist gathers the site, the elements and the docs into dist/web/ for our own nginx server.
  • A new guide, "Embed the games and demos in another website" (docs/how-to/embed.md).
  • The DOM-free models the site and the elements share moved to web/packages/models.
  • WorkerTransport takes a workerClass.
  • Playwright tests embed every element in plain HTML, a React app and the MkDocs site. They check integrity, the content security policy, dark mode, the keyboard and axe.
  • The demos beyond games (web plan, phase 6). Protocol 1.4 adds learn, monitor and runtime, stateless session operations on the command line's own code, with golden tests against grounded-decision learn, grounded-decision examples parameter_learning, the reference service and the numbers the docs quote. /parameters learns the check settings in the browser from the how-to's history, the example's authorisations or a CSV that never leaves the browser, for a chosen risk target, procedure and grid, with the certificate, explain(), the fingerprint, params.json and every combination's exact test charted; /monitor plays a drift scenario through a RiskMonitor with its bounds and alarm; /runtime steps through the reference service's decision loop and routing. Each shows the engine's recorded reply at once and gives the identical command; the docs pages link to them. The browser's engine loads numpy only for these pages (WorkerTransport's packages). Small in-repo SVG charts with data tables, no chart library.
  • learning_run (and CandidateTest, LearningRun) in grounded_decision.learn: what learn_parameters computes, with every combination's counts, exact p-value and place in the testing order.
  • examples/reference_core.py: the reference service's pure core (the versions, DecisionEngine, routing and the device-neighbourhood walk, with an in-memory DeviceGraphTables), re-exported by reference_service; parameter_learning.LiveTraffic and how_to_history.
  • Compare, audit and teaching pages (web plan, phase 5). Protocol 1.3 adds compare, audit and analyse: comparisons and audits run as session operations on the command line's own code, a game or a chunk of moves per request, with the same numbers as grounded-decision compare --json and audit --view json (golden tests for several games); the catalogue lists each game's audit oracles. /compare/GAME plays a comparison on a pool of browser engines (one Pyodide worker per core but one, at most four by default), with a ranked table of exact Clopper–Pearson win-rate intervals that fills in as games finish and is the same whatever order they finish in, a Cancel button and the identical command. /audit/GAME judges every decision of one agent against the game's oracle and lists each disagreement as a position you can open, with both moves marked and the decision inspector. Both are linked from every play page.
  • Teaching pages, one per game (/learn/GAME, with an index at /learn, now the header's Learn link): the snake's Hamiltonian cycle and certificate in two worlds side by side, Minesweeper's mine probabilities with proved-safe and proved-mine cells, the Connect Four solver's value of each column, the Tetris adversary's choice and Burgiel's S and Z pieces, Go's superko and area scoring, Hexxagon's ring trap and two-ply exchange, and the gate's guarantee on a real checkers, Chinese checkers, tic-tac-toe and 2048 position. Positions and analyses are recorded by the package (make lessons); Minesweeper and Connect Four also analyse live as you play. Every claim names the test that checks it, linked to its source line (the site's own /source/ pages), and its paper where there is one; tests/test_site_claims.py (make claims) fails a claim whose test is missing, skipped or left out of the default run.

  • The showcase site, version 1 (web plan, phase 4). A design system from tokens (type scale, spacing, radii, colours for light and dark, shared buttons, fields, cards, badges and callouts), a header with Play, Gallery and Learn (the documentation) on every page, a skip link and a footer. The home page explains the idea in plain words (propose, check, then act, abstain or degrade) beside a live example: a recorded Hexxagon game that pauses on each override and shows the proposal, the checks and the verdict (no Python; pausable; still under reduced motion), and lists every game with its three ways.

  • Every game, three ways: Watch, Play against agents and Play guarded, as links at the top of each play page and on every game card.
  • The decision inspector (web/packages/inspector, DecisionInspection, DecisionTimeline, GameGlossary, MoveAnnouncer), on replay and play pages: who moved and with which agent, the proposal against the move played (both marked on the board: a dashed outline for the rejected proposal, a heavy one for the fallback), the verdict with its fallback and reason, every check with what it tests, its evidence and its result, and the agent's information regime against the world, saying "Assumption not met" when a probabilistic agent plays outside the random world. A scrubbable timeline marks every override, with Previous and Next override (keys P and N); a live game keeps its positions so it can be scrubbed back, too. Explain panes list every agent, check, world and regime in the engine's own words, the text of play --explain.
  • Links reopen a game exactly: a play page's URL holds the person's moves (moves=) and the position on show (frame=) as well as every play flag; Copy link to this game beside Copy CLI command.
  • Display settings in a dialog from the header: colour scheme (system, light, dark), colour-blind-safe colours, shapes and letters on pieces, and motion; kept in the browser only, applied before the first paint.
  • Accessibility: buttons over the board are one Tab stop with arrow keys between them, every game playable from the keyboard alone (the list of legal moves), focus moves into a game when it starts, and moves are announced in a live log, summarised at most every four seconds while a game plays fast.
  • Protocol 1.2.0: describe's catalogue lists the information regimes (name, label, description), so the site and play --explain describe them in the same words.
  • make web-lighthouse: Lighthouse CI (@lhci/cli 0.15.1, pinned) on the home page, the gallery, a replay page and a play page; CI fails below 95 in performance, accessibility, best practices or SEO.

  • The live engine in the browser (web plan, phase 3): web/packages/engine runs Pyodide 314.0.6 in a Web Worker, loaded from the site's own files (no other host at run time; Content Security Policy 'self' plus 'wasm-unsafe-eval'), installs lark and the project's wheel, and answers the session protocol as JSON text. A Transport interface (WorkerTransport; room for the phase 9 WebSocket) and an Engine client: requests validated before they are sent, replies validated on the way in, lazy start with progress events, cancellation by terminating the worker, every failure an EngineError carrying a protocol error reply.

  • make web-engine writes the engine's files into the site (the runtime from the pinned npm package, lark from the file uv.lock pins, numpy from the Pyodide release, each checked against its SHA-256, and the wheel from make build) with a manifest.json; downloads are cached, the files are build output.
  • /play/GAME, prerendered for all ten games: the recorded replay first, then the live game once Python has loaded (progress bar); an agent per seat with descriptions and regimes, world, a fresh seed shown and kept in the URL, size and rule options from the engine's catalogue; watching with play, pause, step and a speed up to 1,000 moves a second; playing by clicking or tapping a highlighted cell, selecting a piece to see its targets (Hexxagon: clones green, jumps yellow), Tetris placement by keyboard, arrow keys for Snake and 2048, and a list of every legal move; a guarded toggle that explains the gate and shows why it overrode a move; Copy CLI command with the identical grounded-decision play command. /play lists the games.
  • Protocol 1.1.0: a select request and its selection reply (a person's piece, its highlights and one legal move per target), from Session.select; Endpoint.handle_text answers JSON text with JSON text. Games may add a cell_name(state, position) hook (Hexxagon does).
  • make web-pyodide-golden: every game on the phase-1 golden seeds through CPython and through Pyodide in Node, compared byte for byte (91 replies, 944 frames, identical); make web-pyodide-test: the session, game and learning tests inside Pyodide, with tests that need a terminal or subprocesses marked needs_native. CI runs both in a new pyodide job.
  • Measurements and budgets in Playwright: the first live move (2.5 s on a cold cache over an emulated 40 Mbit/s connection, 1.0 s warm), the engine's throughput (about 2,700 snake moves a second with every frame encoded), latency and memory on /bench/engine, and bundle and download sizes. End-to-end tests play a whole tic-tac-toe game by clicking, watch Snake live, check the guarded override's explanation, the copied command, Hexxagon selection, Tetris by keyboard, console errors and WCAG 2.2 AA.
  • Docs: docs/reference/browser-engine.md (The engine in the browser); design decisions D43 to D50.

  • Replays (web plan, phase 2): grounded-decision record GAME … -o FILE.gdr plays a match of agents (play's options and validation) and saves every frame; grounded-decision replay-info FILE.gdr prints the header and checks the frames against its SHA-256 content hash, and --verify records the spec again and compares (exit 3 on a mismatch). The .gdr format version 1 (gzip of JSON lines: a header, then keyframes every 32 frames and [path, value] deltas between them, transitions included) is in grounded_decision.session.replay; reference: docs/reference/replay-format.md. 100 to 400 bytes per move on the gallery.

  • make replays records the site's gallery (every game, in web/apps/site/static/replays/ with an index.json manifest) and the replay package's golden files; both are committed and a test checks they are current.
  • web/packages/replay: a .gdr decoder (gzip via DecompressionStream, a validated header, every frame validated against the protocol schema, random access through keyframes) and a DOM-free ReplayPlayer (play, pause, step, seek, speed, frame and status events). Vitest compares its output with the frames Python exported for all ten games; pnpm run build checks the 40 KB gzipped budget (11.5 KB today).
  • web/packages/board (web plan, phase 2): a Canvas 2D board renderer for any protocol frame. Three layered canvases redrawn only where cells changed, sharp at any devicePixelRatio, pieces stamped from sprites (on OffscreenCanvas where available), and one pixel per cell for boards over 16,384 cells. Square grids, pointy hex rows and flat hex columns; shaded discs, crowned kings, Go stones, a joined snake with a gradient and eyes, bevelled Tetris blocks, square 2048 tiles, Minesweeper counts, flags and mines; every highlight with its own outline. Moves animate from the frame (slides along jump paths, Hexxagon flips, Tetris drops and row bursts, 2048 pops and merges, fades), are cut under prefers-reduced-motion, and can be interrupted. A colour-blind-safe palette (Okabe–Ito, checked by simulation), a glyph mode, and a text description of each frame for screen readers. 23 KB gzipped; 0.8 ms median per frame on a 320 × 240 snake in headless Chromium.
  • The site's /gallery and a prerendered /replay/NAME page per replay: play, pause, step, seek and speed, keyboard shortcuts, ?frame= links, the decision panel in the session's fixed rows (verdict, every check, notes, metrics, stats, standings, events, sparkline), light and dark, a phone layout, no tracking.
  • Web tests: Vitest for the renderer's geometry (cell to pixel and back on all three layouts), look and token completeness, animation timelines and the site's panel and controls; Playwright for every replay, screenshots of each in light and dark, keyboard-only use, WCAG 2.2 AA with @axe-core/playwright (4.13.0), console errors and the frame budget (/bench and a Chromium trace).
  • The session layer, grounded_decision.session (web plan, phase 1): SessionSpec (validated by the same code as play), Session (open, step, submit, run, legal_moves with each move's notation, describe) and semantic Frames (cells by CellKind, details, overlays, transitions, move cells, the fixed panel's contents, the decision with every check's evidence, the outcome, the world's assumption note). Pure: no I/O, threads, signals or clock, so it runs in Pyodide; a test enforces it.
  • The session protocol 1.0.0: messages describe, open, step, submit, frames and error, an Endpoint that answers them, a JSON Schema generated from the dataclasses, and a compatibility test against a recorded baseline that fails a breaking change without a major version bump. Reference: docs/reference/protocol.md.
  • make protocol writes the schema, design tokens (colours and glyphs for every cell kind and overlay, dark and light, from the terminal palette), golden fixtures and the TypeScript types; make protocol-baseline records a released schema. web/packages/protocol has the generated types, a runtime validator and Vitest tests that validate the Python fixtures.
  • games/registry.py (GameModule): loading, configuring and seating a game, shared by play and the session layer; engine.Match and run_match: the one match loop under play_match and sessions.

  • compare: random openings for games whose start never varies (Connect Four, checkers, Chinese checkers, Go, Hexxagon, tic-tac-toe). The first --opening-plies N moves of every game are drawn uniformly from the legal moves by the seed alone, so every seating of a seed starts from the same position; the default is a module's COMPARE_OPENING_PLIES, else 2 for a game of two or more players whose initial_state is the same for every seed, else 0 (tic-tac-toe: 1). The openings are printed under the table and in the JSON (opening_plies, openings), and compare reports games that repeat an earlier seed's game move for move (repeated_games). play_match(start=...) plays from a given position.

  • Rule options on the command line: --size beginner|intermediate|expert and --first-click safe|opening (Minesweeper; named sizes come from a module's SIZE_PRESETS), --preview N and --pieces N (Tetris), --target TILE and --stop-at-target (2048), on play and compare. Another game exits 2 with --X is not an option of GAME.
  • make test-slow and the slow pytest marker: make test skips the few long measurements that a faster test already covers; a nightly workflow runs them.
  • A memory budget for the test suite (tests/conftest.py): the test during which peak memory first passes 1.5 GB fails, and a watchdog stops the run at 4 GB, naming the running test.
  • Game views: a fixed panel. The decision panel beside every game's board now has the same rows on every frame of a match, first to last (PanelLayout): rows for every check the game's gate can run (from CHECK_DESCRIPTIONS), several notes, metrics, a fixed stats schema, standings, recent events, sparkline and a status row that shows GAME OVER. Empty slots are blank or a dim —, numbers are padded so labels keep their columns, overflow ends in +k more. Tic-tac-toe's stats now use fixed keys (to move, perfect play). Games may declare stats_schema for keys that appear later.
  • Game views: pieces that change hands flash in their new owner's colour with a marked glyph (◉; marked shapes without colour, r/p in plain ASCII), about-to-change pieces flash amber, and move targets are green (one step) or yellow (two steps), all as semantic overlays (Overlay.CONVERTED, CONVERTING, RING, TARGET_NEAR, TARGET_FAR, SELECTED).
  • Game views: --zoom 1|2|3|auto (default auto on a terminal): cells drawn as boxes with disc pieces and outline highlights, for square and hexagonal boards in every style.
  • Game views: optional game hooks transition(before, move, after) (move animations as semantic frames, played by the live view within the per-move delay and skipped by fast runs), move_event (an event line kept for three moves, big ones bold), move_overlays and select (highlight a picked piece's targets for a person).
  • Tetris: pieces drop instead of appearing. In the live view each placement is animated after it is decided (DropAnimation): the piece spawns at the top centre, turns, slides along the top, falls (long slides and falls thinned to at most 4 and 8 frames), lands highlighted, and full rows flash, then burst, before they collapse; a ghost marks the landing cells. The preview panel keeps one fixed three-row slot per piece. Event lines name each clear (T r1 c4 · clears 2 (double, +300), bold for a tetris). The rules, moves and records are unchanged; the live view's default delay for Tetris is 0.25 s per move (a module's LIVE_DELAY). The live view now thins a move's transition frames to what --fps allows within 60% of the delay (views.thin_out).
  • A board geometry layer (games/geometry.py): hexagonal cells with holes, distances, labels and pointy or flat screen layouts.
  • Hexxagon: drawn flat-topped like the original (--layout flat, the default, or pointy); board variants --board standard|open|donut|rectangle and --holes e4,c2; --start ring sets up the classic six-piece infection on move 1; the infection is animated (land, flash, flip); typing one of your pieces shows its clone (green) and jump (yellow) targets. Checkers and Go report captures in the event line.
  • play --list shows each game's boards, starts and layouts (module BOARDS, STARTS, LAYOUTS).

  • Web showcase, phase 0 (docs/contributing/web-plan.md): a pnpm workspace in web/ (SvelteKit static site with a landing page, six empty TypeScript packages, strict TypeScript, ESLint, Prettier, Vitest, Playwright); make web-setup, web-dev, web-build, web-test, web-browsers, web-e2e, web-check, web-clean; make check runs web-check once web/ is installed (WEB=1/WEB=0 override); a web CI job, a pull-request workflow that uploads the built site, and Dependabot for npm, uv and GitHub Actions. Design decisions D17 to D27.

  • Connect Four (play connect4, example games_connect4): bitboard rules for any width and height up to 16 and any line length (--size WxH, new flag --connect K) in games/connect4_rules.py; an exact negamax solver (Pons-style scores, null-window driver, transposition table, pruning of moves that lose at once) and a budgeted heuristic alpha-beta in games/connect4_search.py. Agents random, greedy (win, block, centre), search (20,000 positions per move), gated (greedy checked by four exact checks: takes_win, blocks_threat, not_under_threat, no_loss_in_two, each applied when avoidable), solver (exact when provable within its budget, else says "not proved"), and regime agents alphabeta, expectimax, clairvoyant, noisy_greedy. The solver is tested against an independent brute-force minimax; the report solves 4x4, 5x4, 4x5, 4x6, 6x4 (second player wins) and 5x5 from the empty board, matching John Tromp's published table. 7 x 6 (first player wins; Allen, Allis 1988) is cited, not re-proved. Audit oracles solver (exact where proved, the count of proved positions in its name) and alphabeta; a regime table. make play-connect4, watch-connect4, audit-connect4.

  • 2048 (play 2048, example games_2048; module games/game_2048.py because a Python module cannot start with a digit, rules on bitboards with memoised row tables in games/game_2048_rules.py). Spawns come from a hash of the seed and the spawn number, so the random world's future is fixed in advance. Agents random, greedy, corner, expectimax (probabilistic), minimax (worst-case), gated (expectimax proposals; exact checks survives_any_spawn and best_survival_horizon, a worst-case survival search up to 3 turns that only explores nearly full boards, by a proved lemma) and clairvoyant (sees the next spawns; random world only). Worlds random, kind, evil (fixed adversary), adversarial (simulates the agent's reply) and mixtures. Audit oracles expectimax, minimax, survival; a regime table on 3 x 3. Views: CellKind.TILE and TILE_EMPTY (numbered tiles in 2048's colours, labels over several cells). make play-2048, watch-2048, history-2048, audit-2048; make help and the docs-command test now accept digits in target names.
  • Tetris (play tetris, example games_tetris): turn-based hard-drop placements (T r1 c4), guideline line-clear scoring, a preview of the next pieces (games/tetris_rules.py, games/tetris.py). Agents random, greedy (Dellacherie's six-feature evaluation, weights as in Thiery and Scherrer 2009), lookahead (through the preview), expectimax (probabilistic), gated and clairvoyant. The gate's checks are exact searches over the previewed pieces plus a clear-top lemma; proved and tested: the gated player never tops out while its moves keep the next unseen piece placeable, and every loss is preceded by a WARNING at least preview + 1 pieces earlier. Worlds random, bag (7-bag), sz (alternating S and Z), adversarial (Bastet-style) and kind. An exact search decides whether S Z S Z ... can be survived forever on small wells (forced loss on 5x5, 6x4 to 6x8, 10x4, ...; survivable on 4xH and 8x5, 8x6); the published results (Burgiel 1997 for widths 2n, n odd; Brzustowski 1992 for adaptive adversaries) are cited with their scope. Audit oracles lookahead and safety, a regime table, make play-tetris, watch-tetris, audit-tetris. Views: CellKind.TETROMINO_I ... TETROMINO_L in the guideline colours (letters without colour).
  • Minesweeper (play minesweeper, example games_minesweeper): hidden information with act, degrade and abstain (games/minesweeper_solver.py, games/minesweeper_rules.py, games/minesweeper.py). An exact solver counts every mine layout consistent with the board and the global mine count (independent groups, memoised branching, exact fractions) and samples consistent layouts uniformly; single-cell rules name the number behind each simple proof. Agents random, rules, solver (probabilistic), gated (worst-case: acts only on proved-safe cells, degrades to a proved cell or to the least risky guess with its exact risk on the record; with a person proposing, --you --guarded, it abstains when nothing is provable) and clairvoyant. Worlds random, no_guess (generated, with the deduction trace as proof), adversarial and kind (mines decided lazily). Proved and tested: solver and gated win the same games; in the adversarial world gated wins exactly when no guess is needed. Presets by size (9x9/10, 16x16/40, 30x16/99), --mines N (a new rule option on play and compare), audit oracles risk and truth, a regime table, win rates compared with Tu et al. (AAAI-17 workshop). Views: CellKind.COVERED, FLAG, MINE, NUMBER (digits coloured by value) and an ABSTAIN line in the decision panel. make play-minesweeper, watch-minesweeper, audit-minesweeper. A rule option a game does not take now says "--X is not an option of GAME".

  • Hexxagon (play hexxagon, example games_hexxagon): Ataxx on the 58-cell hexagonal board of the 1992 original, on bitboards (games/hexxagon_rules.py, games/hexxagon.py). Agents random, noisy_greedy, greedy, gated, expectimax, search (alpha-beta under a node budget) and clairvoyant. The gate's four checks enumerate every reply exactly: take a forced win, never allow a winning reply, never let the best reply take back more than the move gained when avoidable. An exact solver for the side-2 board is cross-checked by brute force; audit oracles search and exact; a regime table; human play by notation (e5 clones, a1-c2 jumps). Views: Overlay.CONVERTED flashes converted pieces. make play-hexxagon, watch-hexxagon, history-hexxagon, audit-hexxagon. An audit oracle factory that rejects the board now exits 2 with its message.

  • Worlds: a second axis next to the information regime. --world MODE on play, compare and audit chooses who controls a game's randomness; compare --world A,B,C prints one table per world. Snake's worlds (games/snake_placement.py) place the apple at a random free cell (the default, identical draws to before), the nearest or farthest free cell by path from the head (unreachable cells count as farthest), or adversarially: a bounded search (at most 64 candidate cells and 20,000 simulated moves per apple, exact on boards with at most 64 free cells) that simulates a copy of the actual player and picks the cell it needs longest for, or dies on. MODE:p mixes a mode with random. The shared rules live in games/worlds.py (parsing, clairvoyance only in worlds independent of the agent's moves, an "assumption not met" note for probabilistic statements outside the random world); engine.bind_agents lets a world see the players. play GAME --list, the JSON start event, the --view none summary and the panel (when not random) name the world. New page: "Who controls the randomness: worlds". Tests show the cycle proof's N − 1 bound holds in every world and is attained exactly by the adversary against cycle.

  • compare.single_item_max_of_samples(values, samples): the prophet rule whose bar is the largest of one past sample per arrival. It is guaranteed, in expectation, at least half of the hindsight-best value (Rubinstein–Wang–Weinberg; proof in the module docstring), so it reports factor_claimed=0.5.
  • learn_parameters(..., procedure="fixed_sequence" | "bonferroni"). The new default, fixed-sequence testing, tests the candidate settings in an order fixed on the calibration part, each at the full significance, and stops at the first failure; Bonferroni divides the significance by the grid size. Both are proven valid in the docstring. The procedure is recorded in the provenance (provenance.procedure) and in explain().
  • The learner learns the support check too: ParameterGrid.minimum_supports (default (0.0, 0.5, 0.6, 0.7, 0.8, 0.9), 0.0 = off), LearnedParameters.minimum_support (sets Limits.min_support), predicted_acts(..., minimum_support=), CaseArrays.support (NaN when not measured; passes, as in Module.decide without require_support).
  • HistoricalCase.was_acted_on (True / False / None = unknown). The learner warns when every case with a known value was acted on (outcomes of held decisions missing: the certificate describes a filtered population) and notes when none records it. The reference service's GET /history.jsonl fills it from the audit log (enforced and routed to execute / execute_degraded).
  • A sixth check, support: do the related cases support the proposed action? EvidenceSnapshot.support (share of related cases whose label is the one the proposed action implies; None = not measured), Limits.min_support (default 0.5) and Limits.require_support (default off), ChannelName.SUPPORT, Health.support, and grounded_decision.evidence.support_report. snapshot_from_graph(..., supporting_label=...) measures it. Records and snapshots written before this load with support=None.
  • Statechart.state(..., environment=True) marks a state where the environment, not the service, picks the next event; the progress (game) check then assumes the worst choice there.
  • tests/test_performance_budget.py checks decide_ms_budget and compile_ms_budget on the fraud-review module (median of repeated calls).
  • grounded_decision.learn: learn the check settings (reconstruction margin, knife-edge window, decision line, conformal alpha, calibration scores per action) from labelled history with Learn-then-Test, so the chosen settings carry a certified error rate and a provenance record. RiskMonitor watches the live error rate afterwards. Available on the command line as grounded-decision learn.
  • Go under Tromp–Taylor rules (games_go, play go, audit go with a random-playout oracle), N-player play for the board games, and play GAME --list describing every agent with example commands.
  • A games track (games_tictactoe, games_snake) and grounded-decision play: an exact two-player game solver built on the library's parity solver, and the act-or-degrade gate applied to a rule-of-thumb tic-tac-toe player (never loses) and a greedy snake (provably never dies, fills the board faster than the safe cycle).
  • Module.peek_proposed_action returns the action decide would propose without changing state.
  • snapshot_from_graph(..., agreement_baseline="label_mix"): neighbour agreement measured against the graph's own label mix (lambda_against_label_mix), which stops imbalanced labels from inflating evidence strength.

  • make targets for every project command, and CI, docs-deploy, and release workflows that run them.

  • Strict pyright type checking over src/ and tests/.
  • Golden-output snapshot tests for every example (make snapshots).
  • A runnable reference HTTP decision service (grounded_decision.examples.reference_service, make service) and a step-by-step tutorial, "Build a service".
  • grounded-decision examples --all to print every report.

Changed

  • Snake is played in real time, in the terminal and on the web: eight moves a second, the snake keeps its heading between keys, the arrows, W A S D or the number pad's 8 4 6 2 turn it, a turn back into the neck is ignored, and the game starts on the first key. Protocol 1.8: RealtimeControls.repeat_last and BoardPath.heading.
  • The snake planner searches far less to play the same way: a planning attempt one move after the last reuses its re-entry searches where that is proven to give the same answer, the search builds its tables once per apple, keeps each node as one tuple, checks its own path as a bitmask, and knows when a descending run can no longer finish. The same searches run 2.4 times faster; a complete 24 x 16 game plays in 17 s instead of 27, with routes of the same lengths (ties between equally short routes may now break differently).
  • Replay format 2: a .gdr file holds the keyframes, then one line of changes per part of the frame (board.cells, decision.metrics …), with add and splice operations beside set and no stored frame index. The same frames take a third to four fifths of the bytes (the gallery 270 KB instead of 730 KB; a complete 24 x 16 snake game 380 KB instead of 2.1 MB). A keyframe every 256 frames by default. replay-info --bytes shows where a replay's bytes go. Format 1 files are refused: record them again.
  • make ambient-replays records the library in about a minute (it took over eight): each replay is played once and its bytes checked against its hash (the test suite checks that recording again gives the same frames), every replay is printed as it finishes, and the largest snake boards are 24 x 16 (random apples) and 16 x 12 (partly adversarial, whose apples each play the agent forward). A library recorded in another replay format, or for other specs, is recorded again; the site's build stands the gallery in for one it cannot read, and says so.
  • The gallery's snake, adversarial snake and Breakout games, and the ambient screen's snakes, play to their end instead of stopping at a move budget (both 10 x 10 snakes fill the board; Breakout clears its level).
  • The play page has one layout for the recorded game, a live one and one opening: the controls in one row (two on a phone), a session bar with the message beside Start live game (then New game and Restart), and the decision, game and settings cards always in place. Small boards fill their box on both play and replay pages.
  • make build writes the wheel and sdist into dist/python/ (it used to replace all of dist/), so building never removes dist/web or dist/service.
  • examples/reference_service.py: the HTTP routes moved from the standard-library handler into DecisionRoutes (and HTTPReply), so any server answers them identically; the handler only moves bytes.
  • The TypeScript Engine gives an error reply with a null id to the oldest request waiting, instead of failing it as a transport error.
  • Learning's numbers are the same on every platform (design decision D69). Binomial tails are computed exactly in integers; p_value_risk_below rounds up, binomial_cdf to nearest, and the learner certifies with the new learn.statistics.certifies_risk_below (an exact rational comparison). upper_confidence_bound and lower_confidence_bound return the tightest 12-significant-digit decimal on the conservative side of the exact Clopper-Pearson bound (BOUND_SIGNIFICANT_DIGITS), so 0.038920846860168785 is now 0.0389208468602. The synthetic histories draw through the new examples.portable_random (uniforms in Python from the raw word, exp and log in decimal, polar normals, Marsaglia-Tsang gamma and beta), and the examples' shifted normals apply the mean and scale unfused. lambda_against_label_mix is one correctly rounded ratio of integers and d_lambda_sq multiplies instead of calling pow. Changed fingerprints: the how-to's history data@2b3afb2c32b5 → data@6575a29dabe3 (same counts), its 5% result params@29497e9f6264 → params@e55f023199be, the parameter_learning example's history data@a6f121a9dcfd → data@83aa73d12869 (another sample of the same world: its counts, learned settings and monitor trace changed: the drift alarm now comes at batch 6), its result params@74fd26d71c44 → params@8560e292ea87; the how-to's 2% and 0.5% results params@51fe958e15e6 → params@04d671beb0cd and params@3b595be040fd → params@ecb597c52b55; the build-a-service tutorial's history data@d9599858837e → data@f3b0ff0e9b59, its results params@57512e497d04 → params@ff131423f2e2 and params@7271ac0bdca7 → params@c1b67d6a0eda, and its settings version settings@1ba37d6027cb → settings@1c275e611777. tests/test_portability.py records them and runs in CI and in Pyodide; the /parameters page no longer says a browser's fingerprint may differ.

  • compare_agents plays its games through Tournament (games numbered by seed and seating, each playable alone, the table tallied in index order from any set of finished games) and audit_match through AuditRun (a move at a time); run_audit resolves its options in AuditSetup. Match.step is now Match.decide then Match.play. Every comparison and audit prints exactly what it printed before.

  • The header's Learn link opens the teaching pages (/learn); the documentation stays in the footer and on /learn.

  • Protocol 1.2.0: a board's optional paths (ordered cell lists with a kind, today body) carry the snake's body head first, and front ends derive each body cell's order, connections and heading from it (the views' CellPath.details, the web board's pathNotes) instead of reading a note per cell. Games may add a cell_paths(state) hook; the snake's cell_details became cell_paths, and the terminal output is unchanged. The snake's engine throughput in the browser's worker rose from about 2,700 to 4,100 moves a second with every frame encoded (Pyodide under Node 2,900 to 4,400, CPython 4,700 to 7,500), and the wire encoder writes nested records inline. A full 10 × 10 snake replay fell from 1.37 MB to 360 KB, and the gallery's snake replay grew from 400 to 1,200 moves within its 100 KB budget.

  • grounded_decision and grounded_decision.learn import their public names on first use (PEP 562), so importing the session layer no longer imports numpy or the rest of the package (51 ms to 29 ms in CPython). The names and __all__ are unchanged.
  • The protocol's wire encoder compiles one encoding function per class: about three times faster on a snake frame.
  • tests/conftest.py turns its memory guard off where there is no getrusage (Pyodide).

  • play and --view json run on the session layer. Output, flags and behaviour are unchanged; --view json's lines are now defined as a projection of the session's frames, checked byte for byte by a golden test. JsonLinesView moved from games.views to grounded_decision.session.jsonlines (its unused boards and timings options were dropped).

  • Hexxagon is fast on large boards, with identical decisions. The gated agent's checks (and expectimax) no longer play out every reply to every move: hexxagon.TwoPlyTable counts the replies once and updates only the cells a move can change (within 3 of its target, 2 of a jump's origin), and decides which replies can end the game from far-apart "witness" pieces (proofs in its docstring; Exchange.by_enumeration is the reference it is tested against on hundreds of positions). Exchange now carries reply counts and sums instead of every reply value, and the best reply comes from TwoPlyTable.best_reply. The search agent produces its moves lazily, one gain level at a time, in the same order; has_move scans the smaller of the pieces and the empty cells. A full side-20 gated against greedy game: 1,248 s → about 4 s; the same game for search: 74 s → 20 s; standard board gated against greedy: 1,250 → about 6,000 moves/s. Every move, check and snapshot is unchanged.

  • audit.measure_regime: a finished one-player game without a win (the snake died, Tetris topped out, a mine went off) counts as a loss, not a draw, as in compare; only a cut-off is shared. Minesweeper's regime table no longer relabels draws itself.
  • --guarded prompts: when more than eight moves pass the gate, the prompt gives their count and four examples instead of every name (a fresh Minesweeper board listed all 81 cells).
  • Tests: every catalog example runs once per session; the golden-output tests, the catalog smoke test and the game report tests share the run (as_json only changes what is printed).
  • 2048's per-line-length row tables and Connect Four's solver transposition table (now at most 1,000,000 entries per bound) are capped, so memory stays bounded on large boards and long solves.
  • The information regime adversarial is now worst_case (Regime.WORST_CASE, shown as worst-case), so "adversarial" names only a world that plays against the agent. Two-player searches are described as minimax searches.
  • Behaviour change: learn_parameters now uses fixed-sequence testing by default. On the bundled synthetic history (3,000 cases, 5% target at 95%) the learned settings act on 18.2% of fresh cases instead of 15.7% (true error rate 0.93% vs 0.91%); in the parameter_learning example the learned settings act on 32.0% of fresh authorisations instead of 15.9%, 3.1% wrong (target 5%). Migration: pass procedure="bonferroni" for the previous behaviour.
  • The default ParameterGrid has 1,200 combinations (it adds six support minimums). With procedure="bonferroni" this needs more history than before; fixed-sequence testing does not divide by the grid size. LearnedParameters JSON gains parameters.minimum_support, provenance.procedure and provenance.grid.minimum_supports; files without them load as support check off and procedure "bonferroni". Fingerprints of learned parameters change.
  • single_item_one_sample reports factor_claimed=None (was 0.5) and a new guarantee field: a single scalar bar has no worst-case guarantee. ProphetChoice.factor_claimed is float | None and gains guarantee. The cache_admission example and the mkt_fut_roll desk now use single_item_max_of_samples; the combo_* runners keep the scalar bar and say it has no guaranteed factor.
  • The reference service exits cleanly on Ctrl-C (one line on stderr, status 130, no traceback), also during start-up and seeding; SIGTERM takes the same path and exits 0. reference_service.main returns the exit status.
  • Module.decide runs six checks (certificate, game, reconstruction, threshold, conformal, support). The fraud_review and clinical_triage examples measure support; in fraud_review a clean authorisation on a card inside the dense fraud ring is now held for review (support 0.19 against the 0.50 minimum) instead of passed. Migration: a check-count assertion or a fixed-length unpacking of record.channels needs the extra entry; evidence without support behaves as before.
  • Grammar-derived charts no longer contain the DFA's reject sink. An event the policy names but does not allow in the current state is now "not handled", exactly like an unknown event: the state stays put and nothing is proposed (before, the entity moved to a dead state and stayed there for good). Migration: a persisted state name that pointed at the old sink no longer exists; Module.restore raises KeyError for it, so reset such entities.
  • The progress (game) check is now meaningful. compile_game adds a self-loop only to final states (no outgoing transitions) instead of an idle loop on every state, and grammar-derived charts give states that propose an action, and final states, priority 2 and every other state priority 1. A policy that can loop forever without proposing anything now fails the check.
  • compose_parent and Pipeline: abstain now dominates degrade (any abstain → abstain; else any degrade → degrade; all act → act).
  • The certificate check's metrics are star_height, dfa_states and max_dfa_states (it also fails when the chart has more states than max_dfa_states); parity_win and nonnegative_mean are reported only by the game check. Its summary reads, for example, "star-height 1 ≤ 3; 3 states ≤ 256".
  • Size caps that were declared but not enforced now are: max_karp_vertices (mean-payoff raises GameTooLarge, and the game check fails closed), max_table_cells_exact (sample_two_row raises ValueError) and max_switch_steps (sample_switch raises ValueError). Both samplers take a limits= argument.
  • The default neighbour-agreement baseline stays "uniform"; the docstrings now say why and when to use "label_mix".
  • Documentation reorganised by reader task (Get started, Concepts, How-to guides, Examples, Theory, Reference, Contributing) and rewritten to a written standard (docs/contributing/writing-docs.md); formulas damaged by a Markdown formatter repaired, with a test that catches it.

  • Every example rewritten with full names, domain explanations, and production wiring notes; most now build evidence from raw records with snapshot_from_graph.

  • Policy parse errors list expected tokens in a stable, readable order.
  • The package version is read from pyproject.toml metadata.

Removed

  • Everything remote. The project is shared only as local build artifacts copied to our own nginx server:
  • Publishing: the PyPI release workflow (release.yml) is gone, and the install docs use a checkout or the wheel make build writes.
  • Deploys: the GitHub Pages docs deploy (docs.yml) and the pull-request preview upload (web-preview.yml) are gone.
  • Update bot: Dependabot (dependabot.yml) is gone; pins are updated by hand (D27).
  • Web plan: phase 9 is now our own service (our servers, behind our nginx), phase 10 releases are one local build, and D14 and D23 are rewritten.
  • Kept: test-only CI (ci.yml, nightly.yml).

Fixed

  • Connect Four's greedy player, the gated player's proposer, played "the most central column" whenever it had nothing to win or block, so the default game stacked the middle columns and lost to the search; and with every agent deterministic, every seed replayed one game. Greedy now plays the column whose disc extends the most of its own open lines and closes the most of the opponent's (one ply, as before), the seed choosing among near-ties, so each seed is its own game. The play page opens on greedy against the same player gated, where the gate's overrides win the games (17 of 20 seeds) rather than the gated greedy losing to the 20,000-position search; the gallery replay (seed 15) shows ungated O losing in 7 moves and gated O, turned down three times, winning in 26. The report's endgames, the docs' transcripts and tables are re-recorded from the new runs.
  • Every page and embed that reads the engine carried the protocol schema with all its descriptions, which validation never reads: the replay and ambient embeds are now about 12 KB smaller gzipped (gd-replay 59 to 47 KB, gd-ambient 64 to 52 KB), which brought gd-ambient back under its 64 KB budget. make protocol writes protocol.validation.json beside the schema for the validator.
  • Rule help gave wrong numbers (Breakout's paddle speed "8-64, default 32" is 8 to 160, default 24; the speed-up default is 10, not 12; the spin named worlds that no longer exist); the numbers now come from the game. Breakout offered its --layout file path in the browser as an empty menu; it no longer does. A refused setting's message no longer pushes the settings form down: it shares the note's two-line slot.
  • Real-time games told a person to "pick from the list" as if it were a turn; the hint now says the game moves on its own clock and a picked move plays on the next tick. While the recorded game is on show, the command under it is headed "The recorded game in a terminal".
  • A gate whose guarantee had lapsed still read "Assumption holds … hard guarantees": in the Breakout gallery game a ball was lost while the panel said so, two flights after the gate had found no spin-proof bounce, and the move that kept the proposal was called an override. A decision now carries its lapse (protocol 1.10): why the gate cannot certify the move played, when no move meets what its checks protect. Breakout, Tetris, Minesweeper (a guess), Connect Four (a loss forced within two) and snake say so; the web panel shows "Guarantee lapsed" with the reason, the terminal a "guarantee lapses" row, and keeping the proposal is "NO SAFE MOVE", not an override. The worst-case regime no longer promises "hard guarantees" for every agent in it: what an agent proves holds whatever happens.
  • The gallery's Connect Four replay showed the gated player losing to the deeper search under a blurb about its gate, without saying the gate looks only three plies ahead. It is now greedy against the same player gated: the gate stops one move that loses within two, and a game greedy loses in 15 moves is drawn.
  • Breakout accepted fields that its levels do not fit: the built-in levels need 13 x 17 (not 8 x 12), and a procedural layout of seven rows needs 16 rows, so about a third of --board random games on a field under 16 rows failed to start. The rule now says so, by board.
  • Checkers and Chinese checkers drew each move twice on the web: the arrow while the piece moved, then a line along the move's squares with no arrowhead that stayed after the arrow was gone. The line is the terminal's (which has no arrows), now its own game hook, move_path_details, which the frames leave out (move_path=False to board_image), so the web shows the move once, as its arrow.
  • In the browser a person played real-time games (Breakout, Tetris in real time, now Snake) one waiting key press per tick, each animating for 600 ms, with keys dropped while a move was on its way: held keys moved unevenly and fast taps were lost. The play page now runs the game's clock (ticks spaced evenly at its pace, the speed menu sets it, the keyboard's auto-repeat ignored, a tap shorter than a tick still acts once, the pause key and a hidden page stop it), and choosing a speed hands the arrows back to the game.
  • A one-cell snake's eyes faced right whatever way it moved.
  • A game's keys were listed with the number keys first, and a person's moves rewrote a link of every move on every move: links now store runs (up*12) and the address bar follows at most every 300 ms.
  • Pages bounced as a game played: panels grew from move to move, a long name widened the side column (and the page, on a phone), and the timeline's marks spilled over the panels with few moves. Every panel is now built of fixed slots, and layout.spec.ts checks every region in Chromium and Firefox at desktop and phone sizes.
  • A long snake's whole body flashed on every move; its cells now change shade in place.
  • tools/stage_hunks.py --context 0 staged a hunk too far down when an earlier hunk that adds lines was left unstaged.
  • The web board drew the last move's arrow from the cell the piece had left (an empty cell), left fragments of earlier arrows behind, and at playing speed cut every animation before its arrow appeared.
  • Effects that drew past their rectangles (2048 tiles' shadows) left smears; a finished move now settles with one whole redraw.
  • The featured Snake replay plays random apples; fully adversarial apples against a short snake always land in the corner opposite its head, so the game was a lap of the walls (the adversarial replay now mixes 30%).
  • Text that contradicted the frame: "every check passed" when most never ran (now only 1 of the 8 checks ran), board descriptions that miscounted pieces, bricks and board sizes, Connect Four's winning discs drawn without their owner (now a winning_line overlay, protocol 1.7), and panel stats shown as identifiers (black_men; the game contract now rejects them).
  • The play page: game keys did nothing until a click; Breakout's launch, stay and laser keys (declared in its controls) were never read; the end-of-game card hid its buttons, stole and dropped focus, and "Watch it again" did not replay.
  • make check failed to start the web checks without pnpm on PATH; it now runs the pinned pnpm through npx.
  • The reference service refused bursts of more than five simultaneous connections (socketserver's default backlog); it now queues 128.
  • Checkers, Connect Four and Go agents that randomise per position seeded their generator with hash() of an integer tuple, which differs between 64-bit and 32-bit platforms, so the browser's engine played other games than the command line. They now use portable_hash, which gives 64-bit CPython's value everywhere: no existing game changes.

  • min_mean_payoff started Karp's table from the wrong infinity and could return 0.0 for a chart whose only cycle has mean 3.0; it now negates the weights and reuses the maximising recurrence, and both functions are tested against brute-force cycle enumeration.

  • A policy where one event leads to two different actions, such as test(e) ; (act(a) | act(b)), now raises CompileError naming the state, the event and the actions. Before, one action was picked silently.
  • dispatch refuses a subgoal sent with Envelope.FPRAS (ok=False, "no FPRAS handler is implemented; send the subgoal with its own envelope") instead of answering it with a handler of another envelope.
  • The parity-game solver (statechart.parity) mis-implemented Zielonka's algorithm and disagreed with exhaustive search on about one game in six. It is now the standard algorithm, supports environment-owned states, and is tested against brute force on one- and two-player games.
  • Spectral Max-Cut used the Fiedler vector, which finds small cuts. It now uses the top Laplacian eigenvector plus local search and always cuts at least half the edges.
  • w2_1d silently dropped samples when the two inputs had different sizes; it now computes the exact 1-D Wasserstein distance for any sizes.
  • The two-row table sampler was labelled exact_uniform; it samples the hypergeometric (Fisher exact test) distribution. The label is now exact_hypergeometric.
  • Weisfeiler–Leman refinement with k > 2 (possible only by raising max_wl_k) ran 2-WL but reported "WL-k"; it now refuses instead. The refusal message explains the cost (about n^(k+1) steps) rather than calling fixed-k refinement EXPTIME.
  • Catalog notes and the prophet helper no longer present the classical 1/2 bound as a guarantee of the single-threshold rule the code runs, or describe the two-row sampler as uniform.
  • dispatch ignored a caller's Limits, and policies inside not(...) compiled under default limits; both now honour the caller's limits.
  • Several examples whose scenarios did not produce the verdicts their descriptions claimed (community detection, A/B tables, sensor fusion, combo parent/session, adaptive threshold walk).
  • The example printer labelled DEGRADE verdicts and policy waits as human holds. An abstain with nothing proposed now routes to await_next_event in both the printer and the reference service.

[0.1.0]

Initial version.