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.pydefines every object that crosses a boundary: the inputsEventandEvidenceSnapshot; the outputDecisionRecordwith its sixChannelReportand oneHealth; the enumsVerdict,Envelope,ChannelName,Trust; andCatalogRef. All are frozen dataclasses withto_dict()andfrom_dict(). It imports nothing from the package, so anything may import it.performance.pydefinesLimits(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) andk_median_threshold()(1 + 2/e, a hardness lower bound). Neither is a guarantee of this code. It imports nothing from the package.certificates.pyholds oneCatalogRefper result in the companion math catalog that the code relies on (REF_STAR_HEIGHT,REF_RECONSTRUCTION,REF_SHARP_THRESHOLD, ... 13 in all), andtrust_for. Each check copies its references intochannels[].catalog.tests/test_docs.pyrequires everyfamilynumber here to appear indocs/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:
typesimports nothing from the package;performanceimports nothing;certificatesimports onlytypes.grammar.parsedoes not importdfa(parsing never compiles).runtime.moduleimports the four evidence report functions but notevidence.graph: turning a graph into evidence is the caller's job.integrateimportsruntimeandtypes, never the reverse.
Forbidden:
runtime→routerorcompare(decisions never call side analyses).- Anything →
examplesfrom the core orintegrate. - Anything →
service(only the command line imports it, forserve). - 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¶
- Internals overview: the compiler and solvers in algorithm detail.
- Development and automation: the commands and the release process.