Skip to content

Decisions

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

Decisions — the canonical IR

Everything OpenCodifier decides is expressed in one canonical intermediate representation (IR), owned by opencodifier-core. Wire formats (OpenAI, Anthropic, Jev/System One) are adapters that normalize into the IR — never the internal representation, and never named outside opencodifier-schema.

Related pages: Confidence (how an answer becomes a routed outcome) · Engine (what produces it) · Interfaces (the transports).

The three decision kinds

Every question is one of three internally-tagged ("type") kinds, validated at construction (candidate/level uniqueness and non-emptiness are constructor errors, not runtime surprises):

Kind Answer shape Notes
Choice one candidate from a runtime-defined candidate list candidates may be static in the request or dynamically supplied by the caller
Boolean true/false with probability the Jev adapter names this noul = probability of yes
Score ordered levels retains the full distribution over levels plus the expected value — never a bare number

A single request may carry several questions (the repo's own fixture does: choice + score + boolean together). An empty questions list is a constructor error (ir.empty_questions).

State, questions, answers

IR type Shape Contract notes
State text + BTreeMap<String, FactValue> facts canonical (sorted) order is part of the cache-key contract — see Engine
DecisionQuestion Choice / Boolean / Score question text plus candidates or levels
DecisionAnswer full Distribution keys = candidate ids / level ids, normalized within SUM_TOLERANCE = 1e-6
DecisionRequest state + questions + metadata + policy the canonical unit every transport decodes into
DecisionResponse answers + confidence + outcome + trace + metrics machine-readable; no generated prose anywhere

Facts are tagged enums, not free strings — structural facts ride every view unchanged even when focused extraction shrinks the text (D18).

Policy

DecisionPolicy rides on the request, not on the server. There is no server-wide policy override by design — serve --policy validates the file and then reports cli.policy_inapplicable, because pretending a server-wide override existed would be a lie.

Defaults (frozen v1 contract):

Field Default
min_confidence 0.80
verify_below 0.65
abstain_below 0.50
risk RiskLevel (Low / Medium / High / Critical)

Ordering is validated: abstain_below ≤ verify_below ≤ min_confidence.

Policy extensions (all default-absent and byte-identical when unset):

  • boolean_threshold: Option<f64> — answer true when p_true ≥ threshold; strictly inside (0, 1); the distribution is never rewritten (D25 amendment, 2026-10-08).
  • abstain_candidate: Option<AbstainCandidate> — a "none of these" candidate appended to choice questions; electing it is a terminal abstention regardless of confidence (D25 amendment).
  • Uncertainty gates (§19): entropy ceiling, minimum margin, OOD ceiling — disabled by default.
  • ood_mode — answer_entropy (default) or lexical_band (D28); see Engine.

Distributions

  • Every answer carries the full distribution — a winner alone is a typed refusal, never a synthesized probability.
  • Normalization tolerance is 1e-6; anything else is ir.distribution_not_normalized.
  • All decision math (softmax, calibration, margins, entropy, weighted score means) happens in Rust f64 — never inside a model graph — so CPU/GPU/quantization cannot change a decision (D7).
  • The lexical classifier's BM25 scores pass through a spread bound before softmax: lexical overlap may never certify better than 99:1 odds (top probability capped below 1.0, D35) — saturated lexical answers escalate instead of self-accepting.

Stable error codes

One error enum per crate, each with a code() returning a stable string. Wire surfaces map codes to payloads verbatim; codes are additive-only after v1.

Core (ir.*) codes include:

ir.empty_field              ir.empty_candidates        ir.empty_questions
ir.duplicate_candidate      ir.duplicate_level         ir.too_few_levels
ir.invalid_id               ir.invalid_policy          ir.invalid_probability
ir.distribution_not_normalized

Other prefixes follow the owning crate: schema.*, engine.*, graph.*, cli.*, mcp.*, ffi.*. Malformed or hostile input produces a typed error with a stable code — never a panic.

Traces

DecisionTrace { trace_version: u32 = 1, entries } — the explainability story is a deterministic execution trace, never chain-of-thought:

  • trace_version is frozen at 1 (D8) and every public enum is #[non_exhaustive], so no consumer can be silently broken.
  • Entries carry node + sorted detail facts; serialization is byte-stable (BTreeMap everywhere).
  • Gates are never hidden: ladder overrides record policy_source, rung calibrations record calibration_source, escalation records rungs_fired + rung_chain, boolean boundaries record boolean_threshold / boolean_flipped, elicited abstention records abstain_candidate / abstain_elicited — each absent when the feature is unconfigured, so unconfigured runs are byte-identical.

Print a trace with decide --trace; the same JSON projection serves MCP codify_explain.

Resource limits

The IR carries a per-request ceiling set (Limits) that the schema validates against: max graph nodes (128), max execution time (120 s — raised from 10 s in D32 after the model rung's measured ~22 s CPU tail killed every long-document escalation), max batch (16), and a 1 MiB HTTP body cap at the socket. Parsers enforce the limits and never panic on malformed input.

Clone this wiki locally