Skip to content

Package structure

Before you change something you need to know which file owns it, what that file is allowed to import, and which tests will tell you if you broke it. This page is that map for src/grounded_decision/ and tests/. It is for contributors; it assumes you have read Anatomy of a decision and know what a module, an event, the evidence and the six checks are (the glossary defines them).

How the pieces fit together at run time is on Architecture; who may change what is on Encapsulation rules; what is public is on Stability and versioning.

The shape in one picture

The core that makes a decision is four packages: grammar (policy text to state machine), statechart (the state machine and the progress check), evidence (the four data checks) and runtime (Module, which ties them together). Three small files sit underneath everything. integrate sits on top of the core for services; router and compare sit beside it and are never called by decide; the examples and the command line sit on top of everything.

flowchart BT
  subgraph base["Base (imported by everyone)"]
    types[types.py]
    perf[performance.py]
    certs[certificates.py]
  end
  subgraph core["Decision core"]
    grammar[grammar/]
    statechart[statechart/]
    evidence[evidence/]
    runtime[runtime/]
  end
  integrate[integrate/]
  router[router/]
  compare[compare/]
  examples[examples/ and cli.py]
  base --> core
  grammar --> statechart
  grammar --> runtime
  statechart --> runtime
  evidence --> runtime
  runtime --> integrate
  base --> router
  base --> compare
  integrate --> examples
  runtime --> examples
  router --> examples
  compare --> examples

Arrows point from the imported package to the importer. Nothing in src/grounded_decision/ imports the companion math catalog (kept outside this repository); references to it are plain strings in certificates.py, so the package needs nothing from the catalog to run.

Directory tree

Line counts are from wc -l at the time of writing, to give a sense of size.

grounded_decision/
├── pyproject.toml            # distribution grounded-decision, Python >=3.12, uv_build
├── uv.lock                   # pinned tool and dependency versions (commit it)
├── Makefile                  # every command; `make help`
├── mkdocs.yml                # this site
├── README.md, CHANGELOG.md, CONTRIBUTING.md, PLAN.md, LICENSE
├── src/grounded_decision/
│   ├── __init__.py           # __all__ (36 names), __version__, main()
│   ├── py.typed              # ships type annotations (PEP 561)
│   ├── types.py              # every object that crosses a boundary (284 lines)
│   ├── certificates.py       # CatalogRef constants: which math result each check relies on
│   ├── performance.py        # Limits, DEFAULT_LIMITS, gw_alpha, k_median_threshold
│   ├── cli.py                # grounded-decision: demo | examples | interfaces
│   ├── grammar/
│   │   ├── policy.lark       # the policy language (Lark LALR)
│   │   ├── parse.py          # text → Expr
│   │   ├── ast.py            # Expr tree and builders (evt, act, seq, union, star, complement)
│   │   ├── height.py         # star height, StarHeightError
│   │   └── dfa.py            # Thompson NFA → subset DFA, CompileError
│   ├── statechart/
│   │   ├── machine.py        # Statechart builder, Transition, dfa_to_statechart
│   │   ├── game.py           # chart → GameGraph (final-state self-loops, owners), GameTooLarge
│   │   ├── parity.py         # Zielonka parity solver, ParityUnsolved
│   │   └── mean_payoff.py    # Karp mean-cycle solver
│   ├── evidence/
│   │   ├── reconstruction.py # neighbour-agreement check
│   │   ├── thresholds.py     # knife-edge check
│   │   ├── conformal.py      # unusual-case check
│   │   ├── support.py        # support check
│   │   └── graph.py          # snapshot_from_graph: labels + edges → EvidenceSnapshot
│   ├── runtime/
│   │   ├── module.py         # Module (build + decide), compose_parent
│   │   └── pipeline.py       # Pipeline
│   ├── integrate/
│   │   ├── codec.py          # handle_request, JSON helpers
│   │   ├── session.py        # DecisionSession, trajectory
│   │   └── priors.py         # EvidencePrior, Volatility, draw_snapshot
│   ├── router/
│   │   ├── dispatch.py       # Subgoal, dispatch, RouterResult, RouterRefuse
│   │   ├── ugc.py            # Max-Cut, Vertex Cover, k-median note
│   │   └── wl.py             # Weisfeiler–Leman refinement, WLRejected
│   ├── compare/
│   │   ├── courtade_kumar.py # best_coordinate
│   │   ├── ot_stability.py   # w2_1d, compare_outcome_laws
│   │   ├── tables.py         # sample_two_row, sample_switch
│   │   └── prophet.py        # single_item_max_of_samples, single_item_one_sample
│   ├── service/              # our own service: `grounded-decision serve` (needs the `service` extra)
│   │   ├── config.py         # ServiceConfig, ServiceLimits: every setting from a flag, GD_SERVICE_*, a default
│   │   ├── limits.py         # Refusal, WorkLimits, RateLimiter, SessionSlots: limits as protocol errors
│   │   ├── connection.py     # ProtocolConnection (one session), PythonThreads (deadlines)
│   │   ├── app.py            # ServiceApp: the ASGI routes /ws/, /api/, /api/decisions/, /healthz
│   │   ├── logs.py           # JSON log lines with no visitor data
│   │   ├── server.py         # ServiceServer (uvicorn), BackgroundService (tests)
│   │   └── load.py           # LoadTest: sessions stepping snake; `make service-load`
│   └── examples/
│       ├── catalog.py        # registry: list_examples, run_example, run_all, interface_map
│       ├── common.py         # shared evidence fixtures, two_path_module, naive comparison
│       ├── explain.py        # the one human-readable printer (1,737 lines)
│       ├── reference_service.py  # runnable HTTP service; `make service` (2,261 lines)
│       ├── reference_core.py # the service's pure core: versions, engine, routing, evidence walk
│       ├── livefeed.py       # LiveDesk harness for the live track
│       ├── live.py           # 9 live service desks
│       ├── stepwise.py       # 11 step-by-step examples
│       ├── adaptive.py       # 8 adaptive-loop examples
│       ├── combo.py          # 6 combination examples
│       ├── markets/          # sim.py, graphs.py, desks.py: 11 market desks
│       └── <17 product cases>.py   # clinical_triage.py, fraud_review.py, ...
├── tests/                    # see "Tests" below
│   └── snapshots/examples/   # golden outputs: <id>.txt and <id>.decisions.json
└── docs/                     # this site

Every subpackage also has an __init__.py that re-exports its public names.

What each part owns

Base: types.py, performance.py, certificates.py

  • types.py defines every object that crosses a boundary: the inputs Event and EvidenceSnapshot; the output DecisionRecord with its six ChannelReport and one Health; the enums Verdict, Envelope, ChannelName, Trust; and CatalogRef. All are frozen dataclasses with to_dict() and from_dict(). It imports nothing from the package, so anything may import it.
  • performance.py defines Limits (every size cap and check threshold; table on Defaults and limits), DEFAULT_LIMITS, and two published constants: gw_alpha() (0.8786, the best Max-Cut ratio a polynomial algorithm can promise if the Unique Games Conjecture holds) and k_median_threshold() (1 + 2/e, a hardness lower bound). Neither is a guarantee of this code. It imports nothing from the package.
  • certificates.py holds one CatalogRef per result in the companion math catalog that the code relies on (REF_STAR_HEIGHT, REF_RECONSTRUCTION, REF_SHARP_THRESHOLD, ... 13 in all), and trust_for. Each check copies its references into channels[].catalog. tests/test_docs.py requires every family number here to appear in docs/theory/.

grammar/: policy text to state machine

parse.py reads policy.lark once (cached with lru_cache) and turns text such as test(suspicious) ; act(block) into a frozen Expr tree from ast.py. Errors are ParseError with line, column and the expected tokens listed in sorted order. height.py measures star height (nesting of *) and raises StarHeightError above Limits.max_star_height. dfa.py builds a Thompson NFA, then a deterministic automaton by subset construction, raising CompileError above the size caps or when one event can lead to two different actions (an ambiguous policy). Actions sit on states, so the automaton is a Moore machine (outputs on states), not a Mealy machine (outputs on transitions). not(...) compiles its inner expression under the caller's Limits. The DSL keyword is test, but the Python builder is evt, because pytest collects any function named test.

Full walk-through: Grammar compile.

statechart/: the state machine and the progress check

machine.py is the Statechart builder (states with optional verdict, action, payoff, priority and environment owner; transitions with an event) and dfa_to_statechart, which names compiled states s0, s1, ..., leaves out the automaton's dead state, and gives states with an action or no way out priority 2 and the rest priority 1. step returns the matching transition with the highest transition priority, or None for an event the state does not handle. The parent= argument of state() is stored but not used: there is no hierarchy.

game.py turns a chart into a GameGraph for the progress check. It adds a _final self-loop only to states with no way out, so every play is infinite, and marks states declared environment=True as owned by the environment (owner 1). No other state can "stay put", so the service has to keep the workflow moving along the policy's arrows. See Can the workflow keep going?.

parity.py is the standard Zielonka algorithm: it supports environment-owned states, and tests/test_games.py checks it against brute force on random one- and two-player games. mean_payoff.py is Karp's algorithm for the best average payoff per step on a cycle reachable from the start. Both run once, when a Module is built. Details: Games and solvers.

evidence/: the four data checks

Each file returns one ChannelReport from plain numbers:

File Check Passes when
reconstruction.py neighbour agreement degree * lambda_hat**2 > 1 + reconstruction_margin
thresholds.py knife-edge p_hat is outside the operational window around p_threshold
conformal.py unusual case conformal p-value > conformal_alpha (empty scores passes unless require_conformal_calibration)
support.py support support >= min_support (None passes unless require_support)

graph.py is not a check: snapshot_from_graph(labels, edges, ...) computes n, density, average links and neighbour agreement from your own graph, and the support share when you pass supporting_label. Only the support check looks at which outcome the evidence favours; see The six checks.

runtime/: the decision object

module.py holds Module. Building one validates the chart, checks star height and solves the progress check (cached as booleans). decide(event, evidence) steps the chart, runs all six checks, fills Health, and returns a DecisionRecord: abstain if any check failed, act if the state proposes an action, degrade if the state is marked degrade, otherwise abstain (event '…' is not handled in state '…'; nothing proposed or no action marked on current state). Module holds the current state, so it is not thread-safe. compose_parent combines child records (any abstain → abstain; else any degrade → degrade; all act → act; none → abstain). pipeline.py runs several modules on the same input. Step by step: One decision, step by step.

integrate/: services and simulation

codec.py has handle_request(module, body): restore state (or reset), decode event and evidence, decide, return the record dictionary. session.py replays a list of ticks through one module. priors.py draws noisy EvidenceSnapshots for simulations. Recipes: Integrate into a service.

router/ and compare/: beside the core

Module.decide never imports these. The router (dispatch.py) takes a Subgoal tagged with an Envelope; it refuses undec and exptime_trap before any handler runs and passes the caller's Limits to the handler. Handlers: max_cut (top Laplacian eigenvector plus local search, always at least half the edges cut), vertex_cover (both ends of a maximal matching, factor 2 on general graphs; bipartite graphs can be solved exactly, which this does not do), k_median (returns the 1 + 2/e lower bound as a note, does not solve), wl (refinement up to max_wl_k), rational_zeros (always refuses).

compare/ has four side analyses: best_coordinate (Courtade–Kumar), compare_outcome_laws with w2_1d (exact 1-D Wasserstein-2 for any two sample sizes), sample_two_row (exact hypergeometric sampling of two-row tables, mixer_status="exact_hypergeometric"; this is the Fisher-exact-test distribution, not uniform over tables) and sample_switch (a switch Markov chain, labelled switch_steps=<n>_not_paper_fpras), and two one-slot stopping rules: single_item_max_of_samples (bar = the largest of one past sample per arrival; factor_claimed=0.5 is a proven guarantee in expectation) and single_item_one_sample (a scalar bar; factor_claimed=None).

Product framing and limits: Router and compare.

examples/ and cli.py

catalog.py registers 62 examples in six tracks and builds interface_map(). explain.py is the only code that prints a human-readable report (the INPUT / DECISION / OUTPUT / NEXT blocks). common.py holds fixtures the examples and tests share. They are not public API. reference_service.py is a complete HTTP service (persistence, locking, idempotency keys, policy fingerprints) that integrators are told to copy; make service runs it. cli.py imports the examples lazily, so grounded-decision --version compiles nothing.

Import rules

Allowed, and checked in review:

  • types imports nothing from the package; performance imports nothing; certificates imports only types.
  • grammar.parse does not import dfa (parsing never compiles).
  • runtime.module imports the four evidence report functions but not evidence.graph: turning a graph into evidence is the caller's job.
  • integrate imports runtime and types, never the reverse.

Forbidden:

  • runtime → router or compare (decisions never call side analyses).
  • Anything → examples from the core or integrate.
  • Anything → service (only the command line imports it, for serve).
  • Anything → the companion math catalog (it is kept outside this repository and is never imported).

Tests

File What it pins
test_runtime.py version 0.1.0 and default limits; strong evidence acts; weak evidence abstains and keeps the proposal; compose_parent; restore
test_grammar.py parsing, error messages, star height (complement adds nothing), compile under caller limits
test_games.py parity and mean payoff on a reward loop; Zielonka against brute force on random games
test_evidence.py reconstruction line, lambda_from_agreements, vacuous theorem width, knife-edge window, conformal (including empty calibration)
test_router_compare.py WL cap, refusals, caller limits in dispatch, Max-Cut size, w2_1d with unequal sizes, the compare helpers
test_integrate.py codecs, handle_request restore and reset, sessions, priors, require_conformal_calibration
test_examples.py every example runs; interface_map shape
test_example_snapshots.py every example's report and decisions match tests/snapshots/examples/
test_explain.py the printer shows a full decide cycle
test_live.py, test_markets.py the live and market tracks build evidence from graphs and hold bad data
test_reference_service.py the reference service end to end over real HTTP
test_service.py our own service: every golden conversation through the Endpoint, WebSocket and HTTP, byte for byte; each limit; the logs; the decision API against the reference service; a load test
test_service_ops.py ops/service: release.sh run against a scratch prefix, the nginx snippet, the unit and the settings file against the service's defaults
test_session_demos.py the demos beyond games equal learn, examples parameter_learning, the reference service and the docs
test_docs.py, test_docs_math.py, test_docs_commands.py the documentation: nav, openings, coverage, formulas, quoted commands

Run one area with uv run pytest -q tests/test_games.py; run everything CI runs with make check. After an intended change to an example's output, make snapshots and review the diff (Development and automation).

Where a change should live

You want to… Touch Then run
Change the wording of abstain reasons the check's summary in evidence/*.py or runtime/module.py make snapshots, review the diff
Change a default threshold Limits in performance.py; Defaults and limits; changelog (stricter is breaking) make test, make snapshots
Add a policy-language feature policy.lark, parse.py, ast.py, height.py, dfa.py uv run pytest -q tests/test_grammar.py
Add a router handler router/ with @_register("name"); never call it from decide uv run pytest -q tests/test_router_compare.py
Add a compare helper new file in compare/, re-export in compare/__init__.py uv run pytest -q tests/test_router_compare.py
Rely on a new math catalog result certificates.py and a page in docs/theory/ uv run pytest -q tests/test_docs.py
Add an example a module in examples/, register in catalog.py, docs page (steps) make snapshots, make check
Add a public name __init__.py __all__, Stability and versioning, changelog make check
Add a seventh check A breaking change: every stored record and consumer assumes six. Before 1.0, only in a minor release with migration notes in the changelog; after 1.0, a major version (Stability and versioning) make snapshots, make check
Add two-player tropical mean-payoff (Truffet) Don't: the catalog records a counterexample (REF_TRUFFET_BAN)

Next