Skip to content

Development and automation

This page is for anyone changing the code or the documentation of this repository: how to set up, what to run before you push, how to add an example or a page, and how a release is cut. Everything is a make target, so a person, CI and an automated agent all run the same commands, and uv.lock pins every tool version, so a result on one machine is the result on every machine. make help prints the list.

Related pages: Writing the documentation (the standard every page follows), Package structure (where each piece of code lives) and Internals (how the compiler and solvers work).

The development loop

make setup        # once: uv sync --all-groups (runtime, dev, docs)
# edit code or docs
make format       # rewrite into the project style, apply safe lint fixes
make check        # exactly what CI runs; green here means green there

The Python version is pinned too: the repository commits a .python-version file (3.12), so uv creates every checkout's .venv with Python 3.12 even when your default interpreter is newer. The repository's .gitignore contains !.python-version, which overrides a global git ignore rule that would otherwise leave the file out. requires-python = ">=3.12" in pyproject.toml remains the floor for people who install the package.

make check runs, in order:

Step Target Tool Fails when
Formatting format-check ruff format a file is not in the project style
Lint lint ruff a lint rule fires
Types typecheck pyright, strict any annotation is missing, unknown, or wrong
Tests test pytest a unit, docs-integrity, golden-output or performance-budget test fails
Docs docs mkdocs --strict a nav entry, link, or autodoc reference is broken
Package build uv build the sdist or wheel cannot be built
Web web-check tsc, svelte-check, ESLint, Prettier, Vitest, Vite only once web/ is installed (below); any web check fails

Strict type checking is deliberate: the package ships py.typed, which tells every downstream type checker to trust these annotations. Configuration lives in pyproject.toml under [tool.pyright].

Slow tests and the memory budget

make test skips the few long measurements marked @pytest.mark.slow (a regime table over 20 openings, a 20 x 20 snake measurement, the largest Chinese-checkers board); each has a quicker test of the same claim in the default run. make test-slow runs only the slow ones, and the Nightly workflow runs them every night. Mark a new test slow only when it takes several seconds and a faster test already covers what it proves, or when it measures speed (throughput, latency): make test runs on every core, so a timing would measure the other tests too, and make test-slow runs one test at a time.

How a run behaves (addopts in pyproject.toml): tests run in parallel on every core with pytest-xdist, each file's tests on one worker, so a file's fixtures and the files its tests write are never shared; each test fails after five minutes (pytest-timeout), so a hang says where it is instead of running for hours; and every run ends with its fifteen slowest tests. A rule that must hold for every case is tested with Hypothesis, on generated cases, besides the chosen examples (tests/test_session_arrows.py).

Every catalog example runs once per test session (the example_runs fixture in tests/conftest.py), and the golden-output tests and the catalog smoke test share that run. tests/conftest.py also guards memory: a full run peaks at about 350 MB, and the test during which the process first passes 1.5 GB fails, naming the culprit, while a watchdog stops the run at 4 GB. tests/test_memory_budget.py pins the caps of the caches that grow with use (search tables, per-board memos): a cache must be bounded and belong to the object that uses it.

The web showcase

web/ holds the website described in the web plan: a pnpm workspace of TypeScript packages and a SvelteKit app built to static files. It needs Node 26 and pnpm 10, pinned in web/package.json; Python-only work never needs them.

make web-setup    # once: install the pinned packages from web/pnpm-lock.yaml
make web-dev      # live-reloading site at http://localhost:5173
make web-check    # type check, lint, format check, unit tests, build
make web-e2e      # Playwright against the built site (downloads Chromium once)
make web-lighthouse  # Lighthouse CI: every category at least 95 on the replay-first pages
make web-pyodide-golden  # every game's frames in Pyodide equal CPython's, byte for byte
make web-pyodide-test    # the session, game and learning tests inside Pyodide

make check adds web-check only when web/node_modules exists, so a Python-only checkout stays green without Node; make check WEB=1 forces it and WEB=0 skips it. CI never skips it: the web job in .github/workflows/ci.yml runs make web-setup web-check web-e2e and make web-lighthouse on every push and pull request (and its pyodide job the two Pyodide checks). make clean also removes the web build output. ESLint and Prettier cover web/ only; Python stays on ruff, and Prettier never formats Markdown, because it breaks MathJax formulas. web/README.md lists the packages, the commands and the architecture standard the TypeScript code follows. Dependencies are pinned exactly and change only when someone updates them by hand and the checks pass; the choices behind the tooling are D17 to D27 in Design decisions. The site serves the live engine (Pyodide and this package's wheel), so make web-check and make web-build first run make web-engine, which needs uv too: The engine in the browser.

Building and reading the docs

The site is MkDocs with the Material theme. Sources are the Markdown files in docs/; API pages are generated from the docstrings in src/ by mkdocstrings. site/ is build output and is never committed.

Command Result
make docs-serve live-reloading site at http://127.0.0.1:8000; watches docs/, src/
make docs static HTML in site/; fails on any warning or broken link

The docs dependencies are a separate uv group (uv sync --group docs, included in make setup), so installing the runtime never pulls MkDocs.

Publishing the site

The documentation is served from our own server; nothing is deployed anywhere else. Run make docs and copy site/ to the server.

Stack

Piece Role
Lark 1.x LALR front-end for the policy DSL (grammar/policy.lark)
MkDocs 1.6 static site generator (stay on 1.x; Material documents MkDocs 2 as a break)
Material for MkDocs 9 theme, search, tabs, admonitions, mermaid fences
mkdocstrings + mkdocstrings-python API pages from live docstrings (Google style)
Ruff formats autodoc signatures; lints and formats the code
MathJax 3 \( ... \) and \[ ... \] via pymdownx.arithmatex

Configuration: mkdocs.yml at the repository root.

Golden-output tests for examples

Every example in the catalog is a fixed story with a fixed outcome. tests/test_example_snapshots.py pins two files per example under tests/snapshots/examples/:

  • <id>.txt — the human-readable report (grounded-decision examples <id>);
  • <id>.decisions.json — verdict, action, proposal, state and failed checks for every record, plus the run summary.

Any refactor that changes a decision or a line of explanation fails with a diff. When the change is intended:

make snapshots                 # rewrite the snapshot files
git diff tests/snapshots       # review exactly what changed

Commit the snapshot diff together with the code change, so a reviewer sees the behaviour change next to its cause. Output is deterministic: every example seeds its randomness, and parser error messages sort their expected tokens, so results do not depend on PYTHONHASHSEED.

Adding things

Extending the system

Three guides cover the larger extensions step by step, with a worked example and a definition-of-done checklist each:

  • Add a game: a new game module, from make new-game to a game that is played, compared, audited, recorded and taught.
  • Add a protocol operation: a new kind of request, from the session layer through the protocol, the engine and our own service to the page that uses it.
  • Add a web page: a new page of the showcase site, its models, tests and accessibility checks.

A new example

  1. Copy the shape of src/grounded_decision/examples/fraud_review.py: module docstring with the business problem, the evidence graph in domain terms, and a "how the pieces map to a real system" section; ID, TITLE, build(), run(*, as_json=False).
  2. Build evidence from realistic raw records with snapshot_from_graph where you can; name every constant and say how to pick it.
  3. Register it in examples/catalog.py.
  4. For a product example, add a case page docs/examples/cases/<id>.md that names the id; for any other track, name the id in that track's page (docs/examples/steps.md, adaptive.md, combo.md, live.md or markets.md). Add the id to docs/examples/index.md and any new page to nav in mkdocs.yml.
  5. make snapshots, read the new report, then make check.

A new documentation page

Read Writing the documentation first. Write the page under docs/, add it to nav in mkdocs.yml, link it from at least one existing page, and run make docs (strict mode catches broken links). tests/test_docs.py fails if a nav entry points at a missing file, if a page is not in the nav, or if a page does not open with a title and a plain paragraph.

A new math catalog result

A check or helper that relies on a result from the companion math catalog records which one.

  1. Add a CatalogRef constant in src/grounded_decision/certificates.py (family is the catalog result number; also title, lean, note).
  2. Implement the module, with tests for the honesty caveats (vacuous width, banned step, unused constant).
  3. Add docs/theory/<slug>.md: statement used, what the code does, what it does not do, product reading, pin name.
  4. Link it from docs/theory/index.md and mkdocs.yml, and add a row to Where each result stands.
  5. If there is a product scenario, add an example (above).

tests/test_docs.py checks that every REF_* family string appears in docs/theory/.

A public API change

  1. Public names go in grounded_decision.__all__ and in the table on Stability and versioning; check that page's breaking-change rules and add a changelog entry.
  2. Docstrings are the autodoc source (Google style).
  3. Narrative pages that mention the old name need a sentence, not just autodoc.
  4. uv run grounded-decision interfaces should still describe DecisionRecord.to_dict().

Integrity tests

tests/test_docs.py runs without MkDocs installed:

  • every markdown file listed in mkdocs.yml nav exists, and every page under docs/ is in the nav;
  • no page is a placeholder, and every page opens with a title and a plain paragraph;
  • every CatalogRef.family in certificates.py appears under docs/theory/;
  • every example id appears in docs/examples/index.md, each product example has a case page, and every other example appears on its track page;
  • docs/how-to/integrate.md names the integration entry points, and this page names the documentation stack.

tests/test_docs_math.py fails if a Markdown formatter has escaped underscores or asterisks inside formulas.

tests/test_docs_commands.py keeps quoted commands honest:

  • every make <target> mentioned in the docs, README, or CONTRIBUTING exists in the Makefile;
  • every grounded-decision examples <id> command names a real example.

make docs is the second half: it builds the site in strict mode.

Releasing

The version lives in one place, version in pyproject.toml; grounded_decision.__version__ reads it from the installed metadata.

  1. Bump version in pyproject.toml, run uv lock, update CHANGELOG.md. Before 1.0, a minor release may change behaviour; every such change goes under "Changed" or "Removed" with a migration note (Stability and versioning).
  2. make check.
  3. Commit and tag the version (git tag v0.2.0).
  4. Build the artifacts locally (make build for the wheel, make docs for the documentation, make web-build for the site) and copy them to our own server.

Nothing is published to a package index or a hosting service. Users install from the wheel or a checkout (Install).

tests/test_performance_budget.py builds the fraud_review module and checks that the median of 200 decide calls stays under Limits.decide_ms_budget (50 ms) and the median of 20 builds under compile_ms_budget (200 ms). The budgets are generous on purpose: the test catches an accidental slowdown by orders of magnitude, not noise.

README vs this site

README.md This site
Compact contract for the repository Full depth
Start-here table, commands, 30-second usage Tutorials, concepts, how-to guides, examples, theory, API
Must stay accurate Must stay accurate and complete

If they disagree, certificates.py and tests/ win; then fix both.

PLAN.md

PLAN.md is the design the code was written against (status: done). It is not the public guide; it is kept as a historical architecture note. Where it and the code disagree, the code and this site are current.