Skip to content
Michael Kinney edited this page Oct 15, 2026 · 1 revision

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

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 escalation ladder

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.

Caches and cache keys

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 rides EngineIdentity.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/run engines are assembled fresh per request with a throwaway private cache; the shared serving cache is never consulted (D19).

The relational solver

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).

LadderPolicy and ladder profiles

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 record policy_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 via into_ladder(); the CLI surfaces it as --ladder <PATH> on decide, serve, and mcp serve through one shared config path, so the three interfaces cannot drift. Shipped profiles live in ladders/: fusion-v1 and proofs-only-v1.
  • The fitted boolean boundary: opencodifier ladder fit-boolean sweeps 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.

Cross-rung escalation

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 (Verify or Abstain — 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_fired and a rung_chain fact naming each rung's model, answer, and gate outcome — deterministic explainability applied to routing.
  • An engine without fallbacks emits byte-identical traces (fixture-asserted).

Focused extraction

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: declarative DAGs

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, plus cache (side-effecting), threshold, output.
  • Contract: unique ids, acyclic edges, single output, ≤ 128 nodes (limits.max_graph_nodes), validated by the same DecisionGraph constructor 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_backend before 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 (retrieve nodes) 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_version is 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.

The optimizer

opencodifier_engine::optimize (D22) ships exactly the two passes whose correctness is provable — and refuses the passes whose "optimizations" would be guesses:

  1. Dead-node elimination — reachability from output, minus cache (cache is observable across requests, so it is never eliminated).
  2. CSE over identical pure specs in the pure node set (cache, threshold, output excluded).

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).

Decision registry

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.

Clone this wiki locally