Architecture¶
This page shows the library's parts and how they fit around your service: what you build once at start-up, what runs on every request, and what you store afterwards. Read it to decide where the library sits in your system and which part to look at when something surprises you.
It is for the evaluator who wants the big picture and the engineer planning the integration. It assumes you have read Anatomy of a decision. Contributors who want the file-by-file layout should continue to Package structure and the internals pages.
The parts in one picture¶
flowchart TB
subgraph SVC["Your service"]
EV["Event source<br/>(model, rule, upstream system)"]
EB["Evidence builder<br/>(your data → EvidenceSnapshot)"]
ST["State store<br/>(one row per entity)"]
RT["Routing<br/>(execute / review / wait)"]
end
subgraph RUN["runtime"]
MOD["Module.decide"]
PIPE["Pipeline, compose_parent"]
end
subgraph KER["Inside a module"]
GR["grammar<br/>policy text → automaton"]
SC["statechart<br/>states, arrows, progress game"]
EVC["evidence<br/>four per-decision checks"]
end
subgraph BASE["Foundations"]
TY["types<br/>Event, EvidenceSnapshot, DecisionRecord"]
LIM["Limits<br/>size caps and settings"]
CAT["certificates<br/>pointers to the math catalog"]
end
BES["router and compare<br/>(called by your code, never by decide)"]
EV --> MOD
EB --> MOD
ST --> MOD
MOD --> RT
MOD --> ST
PIPE --> MOD
MOD --> GR
MOD --> SC
MOD --> EVC
KER --> BASE
SVC -.-> BES
| Part | Package | Owns | You touch it when |
|---|---|---|---|
| Your service | your code | Choosing the event, building evidence, storing state and records, routing verdicts | Always: the library does none of this for you |
| Runtime | runtime/ |
Module (one compiled policy plus one current state), Pipeline, compose_parent |
Every integration |
| Policy compiler | grammar/ |
Parsing policy text, checking its nesting depth, compiling it to states | Writing policies as text (Write a policy) |
| Statechart and progress game | statechart/ |
States, arrows, actions, verdict markings; the build-time progress check | Hand-building charts; reading the progress check |
| Evidence checks | evidence/ |
Neighbour agreement, knife-edge, unusual cases; helpers that build a snapshot from a graph | Build evidence from your data |
| Foundations | types.py, performance.py, certificates.py |
The data types, Limits, and the catalog pointers copied onto records |
Reading records, tuning limits |
| Beside the kernel | router/, compare/ |
Side computations with honest cost labels (graph cuts, fingerprints, table sampling, distribution shift) | Only if your evidence needs them (Questions the library refuses) |
| Integration helpers | integrate/ |
handle_request (JSON in, JSON out), DecisionSession, synthetic evidence for testing |
Wiring into a web handler (Integrate into a service) |
Two boundaries matter most:
- Your service decides the direction; the library decides whether to
trust it. The event you send (
suspiciousorclean) chooses which action the policy proposes. The checks ask whether the related cases are strong, stable and familiar enough to act without a person. They do not ask which action the evidence favours; see Act, abstain, degrade. decidenever calls the router or the compare helpers. They exist for evidence that needs a heavier computation first. You call them, then put the result into the evidence you pass todecide.
Build once, decide per request¶
flowchart LR
subgraph BOOT["At start-up, once per policy version"]
P["Policy text or chart"] --> B["Build Module<br/>parse, check nesting,<br/>compile, solve progress game"]
B --> F["Fingerprint the policy"]
end
subgraph REQ["Per request"]
L["Load entity state"] --> R["restore / reset"]
E["Build evidence"] --> D["decide(event, evidence)"]
R --> D
D --> RC["DecisionRecord"]
end
subgraph STORE["After the request"]
RC --> S1["Save record + evidence<br/>+ policy fingerprint"]
RC --> S2["Save extras.state"]
RC --> RO["Route the verdict"]
end
B --> D
| When | What happens | Cost (fraud example) |
|---|---|---|
| Start-up, once per policy version | Parse the policy text; check it does not nest repetition more than 3 deep; compile it to states and arrows; build and solve the progress game; compute a fingerprint of the result | about 9 µs for a 4-state chart; about 10 milliseconds for a 100-alternative text policy |
| Per request | Restore the entity's state; build the evidence from your data; take one step; run the four evidence checks; read the two cached policy checks; build the record | about 7 µs for decide itself; your evidence query usually dominates |
| After the request | Store the record, the evidence, the policy fingerprint and the new state; route the verdict | your database |
Costs come from Limits and performance. Because a Module holds
one entity's state while it decides, guard it with a lock or give each worker
its own; State and policy versions explains the
options and what to do when the policy changes.
What fails where¶
The library separates "this policy is not acceptable" (an error when you build, so you find out at deploy time) from "this case is not certain enough" (an abstain, so a request never crashes on weak evidence).
| Problem | Where | What you see |
|---|---|---|
| Policy text does not parse | build (parse) |
ParseError listing the expected tokens |
Policy nests repetition more than max_star_height deep |
build | StarHeightError |
Compiled policy exceeds max_nfa_states or max_dfa_states |
build | CompileError |
| A hand-built chart has a missing start state or an arrow to an unknown state | build | ValueError from Statechart.validate() |
Progress game too large, or its solver exceeds max_parity_depth |
build, but does not raise | The module is built; every decision abstains with the reason in reason |
| Progress check fails on a valid chart | build, does not raise | Every decision abstains |
| Saved state name not in this policy | per request (restore) |
KeyError |
| Evidence too weak, too close to the cut-off, or unfamiliar | per request | abstain, proposal kept |
| Event the current state has no arrow for | per request | abstain, nothing proposed, state unchanged |
Design choices, and why¶
Records, not merged policies. Modules combine through their
DecisionRecords (Composing modules). Merging policies
would multiply their states, exceed the size caps and blur which policy held
a decision.
Unknown events leave the state unchanged. An event a state has no arrow for is not an error; the entity stays put and nothing is proposed. The build-time progress check models this by giving every state a "stay here" loop. A side effect is that the progress check passes for almost any chart whose start state is acceptable; Can the workflow keep going? says exactly what it does and does not catch.
Errors at build time, abstains at decision time. An illegal policy has nothing to inspect, so it is refused before it can decide anything. A policy that is legal but whose progress game could not be analysed is still built, so you can step it and read its records, but it never acts.
No clocks. Nothing in build or decide measures time. A timer inside
decide would make results depend on machine load. Enforce latency budgets
in your service.
Settings are frozen and per module. Limits cannot be changed in place,
so two modules in one process cannot disagree about a shared global. A
tenant with stricter caps gets its own Limits instance.
The math catalog is referenced, not imported. Each check report carries
pointers (catalog) to the mathematical results it is based on, and a
trust tag saying how much of the result the check relies on. Nothing is
proved or checked at decide time. See Trust levels and honest
limits.
What the library is not¶
- Not a model. It does not score cases or choose between actions. Your model or rule does that through the event you send.
- Not a direction finder. The event you send chooses the action. The
support check can hold an action the related cases contradict, but only
when your evidence carries a support share; without it, an
actmeans the evidence was strong, stable and familiar, not that it supports this action over another. - Not a workflow engine. It has no timers, schedules or retries; it reacts to the events you send and tells you what to do next.
- Not thread-safe per module. One module decides for one entity at a time.
Where to go next¶
- One decision, step by step: inside
decide. - State and policy versions: state, threads and policy changes.
- Encapsulation rules: what code outside a module may touch.
- Package structure: which file owns what, for contributors.