Repository navigation
Engine
opencodifier-engine is the deterministic decision core: a DAG executor, a rule engine, candidate narrowing, hand-written BM25, exact-decision caching, the relational solver, the calibration and ladder seams, and the cross-rung escalation walk. It is synchronous (D5) — no Tokio, no async runtime; Clock, Deadline, and CancellationToken are traits the caller implements.
Related pages: Decisions (the IR it consumes) · Confidence (the gate semantics) · Architecture (layering) · Interfaces (what drives it).
- The pipeline
- The escalation ladder
- Caches and cache keys
- The relational solver
- LadderPolicy and ladder profiles
- Cross-rung escalation
- Focused extraction
- Graphs: declarative DAGs
- The optimizer
- Decision registry
Every request walks a fixed stage order; each stage must declare its cost class before running (per-stage budgets are criterion-measured, D9):
| # | Stage | What happens | Skippable? |
|---|---|---|---|
| 1 | Normalize | wire request → canonical IR (DecisionRequest) |
no |
| 2 | Exact rule match | deterministic rules produce a distribution outright; hits never touch a model | yes |
| 3 | Exact decision cache | keyed by CacheKey (below); hits are traced and returned as-is |
yes |
| 4 | Candidate narrowing | metadata filters, then BM25 lexical scoring, then (if configured) embeddings — until the candidate set fits budget | yes |
| 5 | Semantic scoring | embedding similarity ranks surviving candidates | yes |
| 6 | Decision model | candidate-conditioned scorer produces the answer distribution (ONNX backend, llama.cpp rung, or the deterministic fallback) | yes |
| 7 | Calibration + confidence gate | raw scores → calibrated confidence; outcome_for(policy) routes accept / verify / abstain |
no |
| 8 | Verification | disagreement or low margin triggers a second, independent pass; agreement feeds the report | confidence-gated — never two classifiers on every request |
| 9 | Respond |
DecisionResponse + DecisionTrace + DecisionMetrics
|
no |
Stages 2–6 are skippable by configuration; 7–9 always run.
The central optimization principle (PLANNING.md §3): always the cheapest reliable mechanism first, never a more expensive layer when a cheaper one can decide with sufficient confidence.
1. Exact deterministic rule
2. Cached decision
3. Metadata/filtering
4. Lexical matching (hand-written BM25 — no search-engine dependency, D4)
5. Lightweight semantic similarity (embeddings)
6. Small classifier
7. Candidate-conditioned decision model
8. Secondary verifier
9. Larger local model
10. External generative model
The zero-ML floor is real: the default stack (relational solver over the lexical classifier) is a measured, tiered arm in its own right — see Benchmarks and the board in docs/BENCHMARKS.md.
One normative site (D6): CacheKeyBuilder in opencodifier-engine is the only code that computes a decision-cache key. Other crates reference CacheKey, never re-derive it.
- SHA-256 over length-prefixed fields: canonical request (sorted candidates), graph version, model id, calibration version, engine semver. Adding a field = append + version bump; never re-order.
-
The model id is the live classifier's id, derived from
Classifier::model_id()at construction; wrapper classifiers compose their ids (relational-v1|builtin-lexical-v2). A hand-set identity is never trusted over the decider that actually runs. -
Every behavioral knob decorates the key. A ladder adds
|ladder-v1@<id>; a rung list adds|rungs-v1@<model>@<cal-ver>+…in list order; focused extraction adds|focused-v1@<budget_tokens>; an embedding backend ridesEngineIdentity.embedding_model. A changed composition re-keys mechanically — it can never replay an old gate. - All cache keys include model/calibration/policy/graph versions so any artifact update invalidates cached decisions (PLANNING.md §73).
- The cache stores completed (gated) responses — which is exactly why identity decoration, not a cache flush, is the correctness mechanism.
-
/v1/graph/runengines are assembled fresh per request with a throwaway private cache; the shared serving cache is never consulted (D19).
The engine's default zero-ML stack (EngineHandle::lexical assembles it): state text is mined for relational facts with an exact grammar ("X depends on Y", "X is healthy", "X: healthy, degraded, down", "X comes back online only after Y"); three general operators — root cause by transitive dependency closure, healthiest group by strict count, first restored by ordering — prove an answer over the whole extracted structure.
It answers only when the proof is unique, inside the candidate set, and agreed by every operator; everything else delegates to the lexical classifier. No likelihood, no model — which is why it broke the 0.50 relational ceiling that bound every measured model arm (locked-suite relational 0.350 → 0.950 on the solver's introduction; blended 0.483 → 0.683 at 1.3 ms p50, Board B in docs/BENCHMARKS.md).
One request-level policy cannot gate rungs whose confidences are not commensurable (a relational proof is p = 1.0 by construction; the embedding rung is ordering-only; the model rung carries a fitted temperature). So gates are engine-internal, per-node and per-kind (D25) — never an IR or wire change:
- Resolution order at the confidence gate:
per_node[id]→per_kind[kind]→ request policy. Traces recordpolicy_source(node:<id>/kind:<name>) — no hidden thresholds. -
Per-rung calibration rides the same keys; a rung calibration replaces the engine-level calibration for that rung's questions (trace fact
calibration_source). - Empty ladder is byte-identical to no ladder — no decoration, no trace facts, locked wire fixtures. The ladder is opt-in.
- A non-empty ladder without an id is refused at assembly (
EngineError::InvalidConfig) — an anonymous ladder is a correctness bug, not a style choice. -
Profiles are data:
LadderProfile(deny_unknown_fields) loads a JSON document into a validated, fitted ladder viainto_ladder(); the CLI surfaces it as--ladder <PATH>ondecide,serve, andmcp servethrough one shared config path, so the three interfaces cannot drift. Shipped profiles live inladders/:fusion-v1andproofs-only-v1. -
The fitted boolean boundary:
opencodifier ladder fit-booleansweeps thresholds 0.50–0.99, scores F1 on the true class, and emits a profile document — the hard argmax is the wrong operating point for skewed tasks, and the boundary is policy, not code.
D27 completes the walk: engines can carry an ordered Rung { classifier, calibration, policy } list.
-
The gate is the only trigger. A rung's gate is evaluated exactly as today; only a non-Accept outcome (
VerifyorAbstain— both mean "this rung could not decide") fires the next rung. An accepted question runs exactly one classifier. - The walk stops at the first accepting rung or the end of the list; the existing verifier cascade applies to the final distribution; each firing passes the deadline/cancellation guard.
- Traces gain
rungs_firedand arung_chainfact naming each rung's model, answer, and gate outcome — deterministic explainability applied to routing. - An engine without fallbacks emits byte-identical traces (fixture-asserted).
Long state, per-token model cost: with a FocusPolicy configured (EngineConfig::with_focus, CLI --focus-budget), questions on state longer than the token budget are decided on a deterministic BM25 view of the evidence-bearing sentences (D18):
- Extraction is recall-oriented and verbatim — zero-evidence sentences are never selected; a view that saves nothing is the full state; decisive sentences are never amputated.
- Reverse escalation, once, pre-gate: a weak focused decision re-decides on the full state before any gate or verifier sees an answer; a blind-decline rule never gambles on an empty view.
- Facts pass through untouched; only the text view shrinks.
- Measured A/B on the long suite: answer-identical to full-state decisions on 120/120 items at
--focus-budget 512, engaged views p50 93 tokens. The standing threat model (named in D18) is an adversarial distractor that out-scores the decisive sentence — caught, not prevented, by escalation.
Graphs are declarative, serializable DAGs — no embedded scripting language.
- Node kinds include
normalize,rule,filter,lexical,embedding(annotate-only),retrieve(narrows;top_n+floor; a floor that cannot starve — if the floor would eliminate everyone, top-1 survives and the gate decides),rerank(reorders, never removes),choice/boolean/score,branch, pluscache(side-effecting),threshold,output. - Contract: unique ids, acyclic edges, single output, ≤ 128 nodes (
limits.max_graph_nodes), validated by the sameDecisionGraphconstructor everywhere (/v1/graph/validate,/v1/graph/run, recipes). - Narrowing is disclosed, never silent: every dropped candidate is named in the trace with its score; candidates are never silently discarded on weak semantic evidence (§73 Rule 11).
-
Missing backend ⇒ refuse at assembly: a graph naming embedding/retrieve/rerank nodes on a handle without an embedding backend fails with
engine.missing_backendbefore a socket opens. The zero-ML binary stays useful by refusing graphs that need a model, not by degrading them into lies. -
Safe mode (the default) refuses semantic pruning (
retrievenodes) at assembly; the opt-out is explicit (with_safe_mode(false)), per-engine, never per-request — hostile input cannot switch it. -
Content-addressed ad-hoc graphs (D19): a client-supplied graph's
graph_versionis the first 8 bytes of SHA-256 over its canonical serialization — two runs of the same graph share one identity, any node edit changes the hash, and a client-asserted version number gets a hash instead.
opencodifier_engine::optimize (D22) ships exactly the two passes whose correctness is provable — and refuses the passes whose "optimizations" would be guesses:
-
Dead-node elimination — reachability from
output, minuscache(cache is observable across requests, so it is never eliminated). -
CSE over identical pure specs in the pure node set (
cache,threshold,outputexcluded).
Equivalence is over decisions, not traces (identical answers, outcome, and confidence — traces legitimately shrink). graph_version is preserved so optimization never silently changes cache identity. Refused: constant folding (rule conditions read hostile input), early exit / parallelization hints (change traces and timing contracts), candidate pruning (that is narrowing, and D21 owns its disclosure rules).
Reusable decision definitions (D20) are content-hashed artifacts, never a second decision path: a DecisionDefinition document (id, version, question shape, candidates, optional policy) whose identity is the SHA-256 of its canonical serialization. Labels (id/version) are for humans; identity is content. Definition identity never enters cache keys — two definitions instantiating byte-identical requests legitimately share a cached decision. The registry is an in-memory index that rejects duplicate ids; loading documents from disk is the caller's job.
OpenCodifier · local-first, deterministic-first decision runtime · Apache-2.0
Releases (current: v0.6.1) · Weights on HuggingFace (aliasfox org) · Live site + playground · Measured benchmark board · Benchmarks
OpenCodifier
Learn
Build
Participate
External