Skip to content

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 (suspicious or clean) 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.
  • decide never 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 to decide.

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 act means 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