Auto-Verification Rule Configuration

Auto-verification is the decision to release a result to the patient chart without a technologist touching it, and that decision is only as trustworthy as the rule set behind it. This page is about the rule set itself — how the laboratory authors, reviews, versions, and promotes the declarative rules that the engine later executes — rather than the engine that runs them. The distinction matters because configuration is where clinical judgment enters the pipeline: a range bound, a delta threshold, a reflex trigger, and an interference exclusion are all human decisions, encoded as data, that must be reproducible on inspection years after a result was signed out. Treat the rule set as a versioned, dual-approved artifact with an immutable release history, and auto-verification stops being a black box and becomes an auditable, testable contract.

Context and Pipeline Position

Within the Clinical Result Validation & Rule Engine Architecture, configuration is the control plane that feeds the data plane. The engine evaluates each result against a directed acyclic graph of checks — reference range, delta, critical value, reflex, and interference — but it does not decide what those checks are; it loads a compiled, versioned rule set that this configuration path produces. Upstream, results arrive already canonicalized by the Instrument Data Ingestion & HL7/CSV Pipelines tier, with units harmonized to UCUM and analyte identity resolved to LOINC through the laboratory’s Test Code Taxonomy & Standards. Those canonical keys are exactly what a rule row binds to, which is why configuration and ingestion must agree on identity before any rule can be written.

The individual checks that configuration composes each have their own dedicated implementation. The bounds a range rule references are owned by Reference Range Check Implementation; the trend logic a delta rule invokes lives in Delta Validation & Trend Analysis; a critical breach escalates through Critical Value Alert Routing; and the numeric limits themselves are validated and retuned by Threshold Tuning & Calibration. Configuration is the layer that decides which of those checks apply to which analyte, in what order they compose, and how their verdicts combine into one release decision. It also governs the conditional cascades handled by Reflex Testing Routing Logic, since a reflex order is itself a configured rule action rather than engine-internal behavior.

Auto-verification rule configuration promotion flow A left-to-right control-plane flow. An authored rule source in YAML or decision-table form passes through a dual-approval review gate, then a validation stage that checks schema and precedence. Valid rule sets compile into an immutable, effective-dated versioned ruleset, which the rule engine hot-reloads for DAG evaluation. A rejection branch returns invalid or ambiguous configurations from validation back to the rule source for revision. Rule Source YAML · decision table Review Gate dual approval Validation schema · precedence Versioned Ruleset immutable · dated Rule Engine hot-reload · DAG invalid · ambiguous Rejected — revise configuration no promotion; version not cut
The configuration control plane: authored rules pass a dual-approval gate and validation before compiling into an immutable, effective-dated version the engine hot-reloads; invalid or ambiguous rule sets are rejected and never promoted.

The controlling idea is that no rule ever reaches the engine except as a member of a named, immutable version that survived review and validation. A rule cannot be edited in place; a change produces a new version with a new identifier, and the old version remains resolvable so any historical result can be replayed against the rule set that was live when it was signed out.

Stage Boundaries

Configuration exposes three sharply separated states, and the transition between each is the point where governance attaches. Blurring them — letting a draft edit take effect before approval, or letting the engine read an unvalidated file — is how a laboratory loses the ability to explain why a result auto-verified.

Draft — the mutable working set. An author edits rules as ordinary source: a YAML file per analyte family, or a decision table. Drafts are freely mutable and carry no authority; the engine never reads them. A draft references analytes by their canonical (loinc, specimen, method_version) key and expresses each check declaratively — a condition and an action — without embedding evaluation logic.

Validated version — the promotion boundary. A draft becomes a candidate version only after it passes validation: every rule parses against the schema, every analyte key resolves, every numeric bound is well-formed, and the composed rule set has no ambiguous precedence. Validation is deterministic and produces either a compiled, content-addressed version or a list of defects; it never partially promotes. This boundary is also where dual approval is recorded — two distinct authorized identities must sign the candidate before a version identifier is minted.

Released ruleset — the hot-reload boundary. A minted version is written to an immutable, effective-dated store. The engine watches that store and hot-reloads on a new effective version, swapping the active rule set atomically so no result is ever evaluated against a half-loaded configuration. A released version is never mutated; a correction is a new version whose effective_from supersedes the prior one, and the superseded version stays queryable for replay.

Failure semantics. A validation failure blocks promotion entirely — the candidate is rejected and the previously released version stays active, so a bad edit degrades toward the last known-good rule set rather than toward an empty or partial one. An infrastructure failure loading a released version is retryable and must never cause the engine to fall back to auto-releasing without rules; if the active rule set cannot be loaded, evaluation holds results for review rather than releasing them unchecked.

Rule Configuration Schema

Every rule is one row in a declarative model. The row binds a canonical analyte key to a condition and an action, carries the metadata that makes it reviewable and replayable, and never contains executable code. The table below is the field contract for a single auto-verification rule.

Field Type Semantics
rule_id string Stable identifier for the rule across versions; how a change is traced.
analyte string Canonical (loinc, specimen, method_version) binding the rule applies to.
check enum range, delta, critical, reflex, interference — which subsystem evaluates it.
condition string Declarative predicate (e.g. value within reference or delta_pct <= 20); data, not code.
action enum verify, hold, reject, or reflex_order — the rule’s contribution to the decision.
priority int Tie-break weight when two rules of equal restrictiveness both fire.
version string The released rule-set version this row belongs to; immutable once cut.
effective_from / effective_to date Window used at replay time to select the rule live at collection.
author / approver string The two distinct identities required for dual approval; recorded, not editable.

Modeling the rule as a table rather than a script is deliberate. A row is diffable in review, serializable to the audit sink, and impossible to make Turing-complete by accident — an author cannot smuggle a side effect into a condition string because the schema only admits a fixed grammar of comparisons. The concrete authoring formats for these rows are covered in depth in Writing Auto-Verification Rules as Versioned YAML, and the tabular condition-to-action mapping is built out in Building an Auto-Verification Decision Table in Python.

Deterministic Composition and Precedence

The engine evaluates every applicable rule for a result, producing a set of per-check verdicts. Configuration decides how that set collapses into one release decision, and it must do so deterministically — the same result against the same version must always produce the same disposition. The composition rule is most-restrictive-wins: order the possible actions by restrictiveness (reject > hold > reflex_order > verify), and the composed decision is the most restrictive action any firing rule contributed. A single hold from a delta check overrides five verify verdicts from range checks; a reject from an interference exclusion overrides everything.

Most-restrictive-wins is what makes the composition safe by construction: adding a rule can only ever make the pipeline more cautious, never less, so a partial or in-progress edit cannot accidentally widen what auto-releases. Priority is the tie-break for the rare case where two rules produce the same restrictiveness but must be distinguished for auditing — it never lets a lower-restrictiveness action win over a higher one. If two rules fire with equal restrictiveness and equal priority and disagree on anything material, that is an ambiguous-precedence defect: validation must reject the version rather than let query order decide.

Most-restrictive-wins composition of rule verdicts Five configured checks for one result — range, delta, critical, reflex, and interference — each emit an action. The actions feed a composition step that ranks them by restrictiveness, with reject most restrictive, then hold, then reflex order, then verify. The composed decision is the most restrictive action any check emitted; here a hold from the delta check wins over three verify verdicts, so the result is held for review. Range verify Delta hold Critical verify Reflex verify Interference verify Most-restrictive-wins composition rank: reject > hold > reflex_order > verify priority breaks ties within a rank only Composed decision: HOLD delta hold overrides verify verdicts equal rank + equal priority + disagreement → reject version

Because composition is monotonic in restrictiveness, the engine can evaluate the checks in any order — even concurrently — and still reach the same decision, which is what lets a burst of results fan out across the async batch processing workers without any risk that scheduling order changes a verdict.

Implementation Patterns

Model the rule row and the composed decision with Pydantic v2 so that a malformed rule fails at load rather than at release, and so the decision is serializable to the audit sink. The condition grammar is constrained to a small enum-plus-operands form; the action is an ordered enum whose integer value is its restrictiveness.

python
from __future__ import annotations

from datetime import date
from enum import Enum, IntEnum

from pydantic import BaseModel, ConfigDict, Field, model_validator


class Check(str, Enum):
    RANGE = "range"
    DELTA = "delta"
    CRITICAL = "critical"
    REFLEX = "reflex"
    INTERFERENCE = "interference"


class Action(IntEnum):
    """Integer value is the restrictiveness rank; higher wins composition."""

    VERIFY = 0
    REFLEX_ORDER = 1
    HOLD = 2
    REJECT = 3


class Rule(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    rule_id: str
    analyte: str  # canonical (loinc, specimen, method_version) key
    check: Check
    condition: str  # declarative predicate, constrained grammar
    action: Action
    priority: int = 0
    version: str
    effective_from: date
    effective_to: date | None = None
    author: str
    approver: str

    @model_validator(mode="after")
    def _distinct_signers(self) -> "Rule":
        if self.author == self.approver:
            raise ValueError(f"{self.rule_id}: author and approver must differ")
        if self.effective_to is not None and self.effective_to <= self.effective_from:
            raise ValueError(f"{self.rule_id}: effective_to must follow effective_from")
        return self

The composed decision is a pure reduction over the firing rules. Because Action is an IntEnum ordered by restrictiveness, composition is a max — the single most-restrictive action, with priority only distinguishing ties:

python
class Decision(BaseModel):
    model_config = ConfigDict(frozen=True)

    action: Action
    deciding_rule_id: str
    version: str
    reason: str


def compose(rules: list[Rule], fired: list[str]) -> Decision:
    """Reduce firing rules to one decision, most-restrictive-wins."""
    active = [r for r in rules if r.rule_id in fired]
    if not active:
        # No rule fired: absence of a positive verify never auto-releases.
        return Decision(action=Action.HOLD, deciding_rule_id="",
                        version="", reason="no_rule_fired")
    winner = max(active, key=lambda r: (r.action, r.priority))
    ties = [r for r in active
            if r.action == winner.action and r.priority == winner.priority]
    if len(ties) > 1 and len({r.check for r in ties}) > 1:
        # Same rank and priority but different checks disagree on ownership.
        return Decision(action=Action.HOLD, deciding_rule_id=winner.rule_id,
                        version=winner.version, reason="ambiguous_precedence")
    return Decision(action=winner.action, deciding_rule_id=winner.rule_id,
                    version=winner.version, reason=winner.check.value)

The safety default is encoded twice: an empty firing set composes to HOLD, and an unresolved precedence tie also composes to HOLD. Neither degrades to VERIFY. This mirrors the invariant enforced at the reference range check — the absence of a positive, resolvable verdict never resolves to auto-release.

Error Classification and Handling

Configuration errors are tiered so each has one correct disposition, and — critically — so a defect in the control plane never silently degrades the data plane into releasing unchecked.

Tier Example Disposition Blocks promotion
Schema Unknown check, malformed condition, unparseable date Reject candidate at validation, list defects Yes
Semantic Analyte key does not resolve, bound inverted, self-approval Reject candidate, route to author Yes
Precedence Two equal-rank, equal-priority rules disagree Reject candidate as ambiguous Yes
Runtime Released version fails to load into engine Hold results, retry with backoff N/A (never releases)

The first three tiers are caught before a version is minted, which is the point: a rule set either validates completely and becomes promotable, or it is rejected in full and the previously released version stays active. There is no partial promotion, so an author cannot ship half a change. The runtime tier is the one that touches live results, and its rule is strict — if the engine cannot load the active version, it holds rather than releases. An invalid config and an unloadable config both fail closed, toward human review, never open toward auto-release.

Semantic failures carry stable reason codes (analyte_unresolved, bound_inverted, self_approval, ambiguous_precedence) so a review dashboard can distinguish an authoring mistake from an identity-resolution gap upstream and route each to the right owner.

Regulatory Touchpoints

Auto-verification is a named, regulated activity, and the configuration path is where the regulatory obligations actually land.

  • CLIA §493.1291(a) and the auto-verification provisions of §493.1256 require that the criteria used to verify results without human review be defined, documented, and validated before use — which is exactly what the released, dual-approved version represents. The version identifier persisted with every released result is what lets an inspector see which documented criteria applied.
  • CAP GEN.41350 and the automated-verification checklist items require that auto-verification rules be documented, version-controlled, validated before implementation, and re-verified periodically, and that any result failing a rule route to human review. The immutable version history, the shadow-mode validation described below, and the HOLD-by-default composition satisfy these together, building on the CLIA/CAP Data Boundaries established at the platform layer.
  • 21 CFR Part 11 §11.10(d),(e),(k) demands controlled, attributable, tamper-evident change management for the electronic records that govern a decision. Dual approval with two distinct recorded identities, an append-only version history, and effective-dating together provide the attributable audit trail; the HIPAA-compliant audit trail patterns apply to the configuration store as much as to results.
  • CLIA §493.1105 retention applies to the rule set itself: because a result may be reviewed years later, the version that was effective at collection must remain resolvable for the full retention window, which is why released versions are immutable and superseded rather than deleted.

Because these clauses attach to the configuration change, the approval event, the validation outcome, and the minted version identifier must all be written to the audit sink before the version can go effective — not reconstructed after the fact.

Testing and Validation

A rule set is only trustworthy if its behavior is proven before it can release a patient result. Three layers do that work.

Config contract tests load a candidate version and assert structural invariants without touching live results: every analyte key resolves against the current taxonomy, every referenced bound exists, no two rules share equal rank and priority with conflicting actions, and every rule carries two distinct signers. Run these in CI on every draft so a defect is caught at authoring time, before review even begins.

python
import pytest

from ruleset import Rule, Action, compose


def test_hold_dominates_verify() -> None:
    common = dict(analyte="2951-2|serum|v4", version="rs-2026-06",
                  effective_from=__import__("datetime").date(2026, 6, 1),
                  author="a.tech", approver="b.director")
    rules = [
        Rule(rule_id="na-range", check="range", condition="within",
             action=Action.VERIFY, **common),
        Rule(rule_id="na-delta", check="delta", condition="delta_pct<=20",
             action=Action.HOLD, priority=5, **common),
    ]
    decision = compose(rules, fired=["na-range", "na-delta"])
    assert decision.action is Action.HOLD
    assert decision.deciding_rule_id == "na-delta"


def test_empty_firing_set_holds() -> None:
    decision = compose([], fired=[])
    assert decision.action is Action.HOLD
    assert decision.reason == "no_rule_fired"

Shadow-mode validation runs a candidate version against a stream of real recent results in parallel with the active version, recording what the candidate would have decided without acting on it. Comparing the shadow decisions against the released ones surfaces exactly how many results the change would newly hold, release, or reflex — the signal a laboratory director needs to sign the dual approval with evidence rather than intuition. A candidate that would flip a meaningful fraction of results from HOLD to VERIFY is precisely the change that must not ship without scrutiny.

Round-trip tests dump every Rule and Decision through model_dump_json() / model_validate_json() to guarantee the version written to the immutable store and the decision written to the audit sink deserialize back identically — the property an inspector relies on when replaying a years-old result.

Part of: Clinical Result Validation & Rule Engine Architecture.

Frequently Asked Questions

Why separate rule configuration from the engine that runs the rules?

Configuration is where clinical judgment enters the pipeline and where regulators attach change-control obligations. Keeping it a separate, versioned, dual-approved artifact means the engine only ever executes a named immutable version, and any historical result can be replayed against the exact rule set that was live when it was signed out.

What does most-restrictive-wins mean for combining rule verdicts?

When several checks fire for one result, the composed decision is the most restrictive action any of them contributed, ordered reject > hold > reflex_order > verify. Because adding a rule can only make the pipeline more cautious, a partial or in-progress edit can never accidentally widen what auto-releases.

What happens when no rule fires for a result?

The decision composes to HOLD, not VERIFY. The absence of a positive, resolvable verify verdict never resolves to auto-release — an uncovered analyte is a configuration gap to close, not a value to wave through, so it degrades toward human review.

How is dual approval enforced in the rule model?

Each rule carries an author and an approver, and a model validator rejects any rule where the two identities match. A version can only be minted after two distinct authorized identities sign the candidate, and both are written to the append-only audit sink before the version goes effective.