Test Code Taxonomy Standards: Resolving Vendor, Local, and Universal Codes into One Canonical Assay Identity

An instrument does not report a “sodium result.” It reports the string NA+, or 2951-2, or SODIUM_SER, depending on which analyzer, which firmware, and which interface configuration produced it. A clinical LIMS is only trustworthy if all of those strings resolve — deterministically, and with a defensible version history — to exactly one canonical assay identity that the rest of the pipeline can route, validate, and release against. This page specifies the ingress and egress contracts for that taxonomy subsystem, the code-system precedence it enforces, the tiered errors it raises when a code cannot be resolved, and the tests that prove no ambiguous mapping ever reaches a patient record.

Context and Pipeline Position

The taxonomy subsystem sits inside the analytical phase of the LIMS Architecture & Regulatory Compliance Foundations reference, immediately downstream of transport parsing and immediately upstream of result release. It receives typed observation objects produced by the HL7 v2 Segment Mapping stage — where the raw OBX-3 observation identifier and OBX-6 units have been lifted out of the message but not yet interpreted — and it hands back the same objects enriched with a resolved, canonical assay definition. Only after that resolution is a result eligible to cross the analytical → post-analytical boundary defined in CLIA/CAP Data Boundaries; an unresolved code is a hard stop, not a warning.

The subsystem is not a lookup table wrapped in a function. It is the authority that owns the meaning of every observation the laboratory reports: which universal concept a local code denotes, which units are canonical for that concept, which mapping version was in force when a given result was resolved, and which codes are deprecated and must never be emitted onward. The instrument-facing acquisition work that delivers those raw codes in the first place lives in the Instrument Data Ingestion & HL7/CSV Pipelines reference; the numeric plausibility of the value once its identity is known belongs to the clinical result validation rule engine. Taxonomy resolution is the narrow, load-bearing step between the two.

Resolving one raw observation into a single canonical assay identity A raw observation carrying a vendor mnemonic, a namespaced code, and free-text units enters a normalization gate. The gate fans out to four namespace lanes in fixed precedence: LOINC (authoritative, trusted first), LOCAL (versioned dictionary), SNOMED CT (specimen and method), and UCUM (unit coercion). All lanes converge on a single canonical AssayDefinition carrying assay_key, LOINC, specimen, UCUM unit, active status, and a pinned dict_version. A dashed tap records every resolution to an append-only mapping audit, and a separate off-ramp diverts unmapped, ambiguous, or deprecated codes to quarantine. Raw observation NA+ · LOCAL:NA_SER OBX-3 identifier OBX-6 free-text units Normalization gate namespace + precedence P1 P2 P3 P4 LOINC: LOCAL: SN: UCUM: universal identity · trusted first vendor mnemonic · versioned dict SNOMED CT · specimen / method canonical unit coercion Canonical AssayDefinition assay_key (routing) loinc · specimen · ucum_unit status: active dict_version pinned raw code preserved verbatim Quarantine off-ramp unmapped · ambiguous · deprecated-only held for attributed review — never guessed Append-only mapping audit canonical key · source code system · dict_version time-stamped · attributed · 21 CFR Part 11

Stage Boundaries

Two contracts define this subsystem. Each has an explicit ingress contract (what may enter), an egress contract (what is guaranteed to leave), and defined failure semantics (what happens to anything that violates the contract).

Boundary N — raw observation → normalized identity. Ingress requires an observation carrying at least one code in a declared namespace and a units token (which may be free text). Egress guarantees that the code has been resolved to exactly one canonical assay key, that its units have been coerced to a UCUM canonical form, and that the original raw code and units are preserved verbatim alongside the canonical values. Failure semantics: a code that matches zero known mappings, or matches more than one with equal precedence, is quarantined as a SEMANTIC fault — never resolved by guesswork or nearest-string heuristics.

Boundary V — normalized identity → release-eligible result. Ingress requires a canonical assay key whose mapping is active (not deprecated or retired) under the version pinned for this run. Egress guarantees that every result crossing onward carries a complete provenance triple — canonical assay key, source code system, and dictionary version — so that a surveyor can reconstruct exactly which mapping produced any historical result. Failure semantics: a result whose only resolution path runs through a deprecated code is held in review rather than silently released against a stale meaning.

The invariant that makes both boundaries auditable is that the raw identifier is immutable and the canonical identifier is derived, never the reverse. The pipeline never overwrites what the instrument said; it annotates it. That single decision is what lets an inspector answer “what did the analyzer actually transmit, and which dictionary version turned it into this LOINC code?” from the record itself rather than from tribal memory.

Schema and Code-System Specification

A production taxonomy spans four code systems, each isolated in its own namespace so that a collision between, say, a local mnemonic and a LOINC part-code is structurally impossible. Namespaces are declared with an explicit prefix on the wire (LOCAL:, LOINC:, SN:, UCUM:) and are resolved in a fixed precedence order.

Code system Namespace Governs Example Cardinality to canonical assay
Local / vendor LOCAL: Instrument- and LIS-specific mnemonics LOCAL:NA_SER Many-to-one
LOINC LOINC: Universal observation identity (OBX-3) LOINC:2951-2 One-to-one
SNOMED CT SN: Specimen, method, and qualifier semantics SN:87612001 Many-to-one
UCUM UCUM: Canonical unit of measure (OBX-6) UCUM:mmol/L One-to-one per assay

Resolution precedence is not arbitrary. An explicit LOINC: code on the message is authoritative and is trusted first; a LOCAL: code is resolved through the versioned mapping dictionary; a bare vendor mnemonic with no declared namespace is the lowest-trust path and is only accepted if it maps unambiguously. The canonical target every path converges on is a single assay definition whose fields are fixed and version-pinned.

Field Type Constraint Purpose
assay_key string ^[A-Z][A-Z0-9_]{2,31}$ Stable internal primary routing key
loinc string LOINC syntax (\d{1,5}-\d) Universal identity for OBX-3 egress
specimen string SNOMED CT concept id Specimen-type compatibility check
ucum_unit string Valid UCUM unit Canonical units for OBX-6 egress
status enum active | deprecated | retired Gates Boundary V eligibility
dict_version string Semver of the dictionary generation Provenance + reproducibility
Lifecycle of a single mapping entry, from draft to retirement A mapping entry advances through four states. DRAFT is editable and not resolvable. Publishing it, with approver sign-off and a dict_version bump, makes it ACTIVE — resolvable and release-eligible. Superseding it moves it to DEPRECATED, where it still resolves but only for historical replay, never for new releases. Retiring it moves it to RETIRED, which refuses resolution entirely. Every transition is computer-generated, time-stamped, and attributed, and supersedes rather than overwrites, satisfying 21 CFR Part 11.10(e). DRAFT ACTIVE DEPRECATED RETIRED publish supersede retire approver sign-off · v+ replay-only · v+ refuse · v+ editable not resolvable resolves release-eligible resolves for replay only, not release resolution refused Every transition: computer-generated · time-stamped · attributed approver supersedes, never overwrites · append-only dict_version history · 21 CFR Part 11.10(e)

Implementation Patterns

The taxonomy is loaded as an immutable, version-stamped dictionary and applied by stateless resolver functions, so the same input plus the same dict_version always yields the same output. A Pydantic v2 model enforces the shape and constraints of every canonical assay before it is admitted to the dictionary.

python
from __future__ import annotations

import enum
import re
from typing import Annotated

from pydantic import BaseModel, Field, StringConstraints, field_validator

AssayKey = Annotated[str, StringConstraints(pattern=r"^[A-Z][A-Z0-9_]{2,31}$")]
LoincCode = Annotated[str, StringConstraints(pattern=r"^\d{1,5}-\d$")]


class MappingStatus(str, enum.Enum):
    ACTIVE = "active"
    DEPRECATED = "deprecated"
    RETIRED = "retired"


class AssayDefinition(BaseModel):
    """One canonical assay identity, version-pinned and immutable once loaded."""

    model_config = {"frozen": True, "extra": "forbid"}

    assay_key: AssayKey
    loinc: LoincCode
    specimen: str = Field(min_length=1)          # SNOMED CT concept id
    ucum_unit: str = Field(min_length=1)         # canonical UCUM unit
    status: MappingStatus = MappingStatus.ACTIVE
    dict_version: str = Field(pattern=r"^\d+\.\d+\.\d+$")

    @field_validator("specimen")
    @classmethod
    def _numeric_sctid(cls, value: str) -> str:
        if not re.fullmatch(r"\d{6,18}", value):
            raise ValueError("specimen must be a numeric SNOMED CT concept id")
        return value

The resolver refuses to guess. It looks up the incoming namespaced code, and it treats “no match” and “more than one equal-precedence match” as equally fatal — both raise rather than pick a winner. Preserving the raw code on the resolved object is what keeps the derivation auditable.

python
class TaxonomyError(Exception):
    tier: str = "SEMANTIC"


class UnmappedCode(TaxonomyError):
    """No active mapping exists for the incoming code."""


class AmbiguousCode(TaxonomyError):
    """The incoming code resolves to more than one canonical assay."""


class ResolvedObservation(BaseModel):
    model_config = {"frozen": True}

    raw_code: str                  # exactly what the instrument sent, untouched
    raw_units: str
    assay: AssayDefinition         # the derived canonical identity


class TaxonomyResolver:
    """Stateless resolution against one immutable dictionary generation."""

    def __init__(self, index: dict[str, list[AssayDefinition]], dict_version: str) -> None:
        self._index = index                 # namespaced code -> candidate assays
        self.dict_version = dict_version

    def resolve(self, code: str, units: str) -> ResolvedObservation:
        candidates = [
            a for a in self._index.get(code, [])
            if a.status is MappingStatus.ACTIVE
        ]
        if not candidates:
            raise UnmappedCode(f"{code!r} has no active mapping in {self.dict_version}")
        if len(candidates) > 1:
            raise AmbiguousCode(f"{code!r} resolves to {len(candidates)} assays")
        return ResolvedObservation(raw_code=code, raw_units=units, assay=candidates[0])

Under the high-throughput ingestion the pipeline demands, resolution runs concurrently across observations, but every worker must resolve against the same dictionary generation for the duration of a batch — a mid-flight dictionary swap that changed a mapping between two results in one run would be a silent provenance defect. A generation counter loaded atomically into each worker, rather than a mutable shared table, provides that guarantee.

python
import asyncio


async def resolve_batch(
    resolver: TaxonomyResolver,
    observations: list[tuple[str, str]],
) -> list[ResolvedObservation]:
    """Resolve a batch against a single pinned dictionary generation."""

    async def _one(code: str, units: str) -> ResolvedObservation:
        # CPU-light, pure lookup; to_thread keeps the event loop responsive
        return await asyncio.to_thread(resolver.resolve, code, units)

    return await asyncio.gather(*(_one(c, u) for c, u in observations))

The concrete, worked mapping task — taking a panel of vendor codes and binding each to its LOINC identity — is documented step by step in how to map LOINC codes to LIMS test panels.

Error Classification and Handling

Every fault this subsystem raises is classified into one of three tiers, and the tier — not the exception name — decides whether the observation is retried, quarantined for human disposition, or rejected outright. The tiering keeps an operational blip from being confused with a clinical hazard.

Tier Example Recoverable Disposition
TRANSPORT Dictionary cache miss, transient Redis timeout on load Yes Bounded exponential-backoff reload; escalate after N attempts
SCHEMA Malformed code token, units not parseable as UCUM, missing namespace prefix No (as received) Dead-letter quarantine with the raw payload preserved verbatim
SEMANTIC Unmapped code, ambiguous mapping, resolution only via a deprecated entry No (needs review) Hold in review; require attributed reviewer disposition

The distinction is load-bearing. A TRANSPORT fault — the dictionary was momentarily unreachable — is safe to retry because the observation content is unchanged. A SCHEMA fault will fail identically on every retry, so retrying it only hides the defect; it goes straight to a dead-letter queue with the original bytes intact for a survey. A SEMANTIC fault is the dangerous one: the token is well-formed and would route cleanly, but committing it would attach a wrong or stale meaning to a patient’s result, so it is held rather than dropped. Deprecated-code resolution deserves special mention: the mapping still exists and would resolve, which is exactly why it must be caught here rather than downstream — a deprecated entry is kept resolvable only so that historical results can be replayed, never so that new results can be minted against it.

python
async def resolve_guarded(
    resolver: TaxonomyResolver,
    code: str,
    units: str,
    *,
    max_reloads: int = 5,
) -> ResolvedObservation:
    for attempt in range(1, max_reloads + 1):
        try:
            return resolver.resolve(code, units)
        except TaxonomyError as err:
            await quarantine(code, units, tier=err.tier, error=err)  # no blind retry
            raise
        except CacheUnavailable:                                     # TRANSPORT only
            await asyncio.sleep(min(2 ** attempt, 30))
    await escalate(code, reason="dictionary reloads exhausted")
    raise CacheUnavailable(code)

Regulatory Touchpoints

Each taxonomy control answers to a specific clause. Naming the clause is what turns “we map codes” into an evidence artifact an inspector accepts.

  • CLIA §493.1291© (test report content). The requirement that a report identify the test performed is met by binding every result to a canonical assay key and its LOINC identity, so the reported analyte is unambiguous regardless of which instrument produced it.
  • CLIA §493.1251 (procedure manual). The version-pinned dictionary is the machine-readable expression of the laboratory’s approved test menu; a dict_version on every resolved result ties the result to the mapping generation that was in force.
  • 21 CFR Part 11.10(e). Each dictionary change — an added mapping, a deprecation — is a computer-generated, time-stamped, attributed event that supersedes rather than overwrites, giving a secure audit trail of how a code’s meaning evolved.
  • 21 CFR Part 11.10(a) (validation). The property-based and golden-file tests below are the documented evidence that the resolver performs its intended function and flags ambiguity rather than silently choosing.
  • CAP Laboratory General checklist (test-code and LOINC mapping). Preserving the raw instrument code alongside the canonical LOINC/UCUM values satisfies CAP’s expectation that terminology mapping be traceable and reversible for review.
  • HIPAA Security Rule §164.312© (integrity). The immutable raw-code field and hash-chainable dictionary-version log provide tamper-evidence for the identity attached to PHI-bearing results. The credentialed approval of mapping changes is enforced by the RBAC controls in Security & Access Controls.

Testing and Validation

Because a mismapped code is a patient-safety defect that no numeric range check will catch, this subsystem’s tests are adversarial by design. Property-based testing with hypothesis explores the resolution space far more aggressively than hand-written cases, and the core invariant is simple: resolution is total and unambiguous — every input either yields exactly one active assay or raises, and no input ever silently mutates the raw code it carried in.

python
from hypothesis import given, strategies as st

codes = st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=90), min_size=1, max_size=16)
units = st.text(min_size=0, max_size=8)


@given(code=codes, unit=units)
def test_unmapped_code_always_raises(code: str, unit: str) -> None:
    resolver = TaxonomyResolver(index={}, dict_version="1.0.0")   # empty dictionary
    try:
        resolver.resolve(code, unit)
        assert False, "empty dictionary must never resolve a code"
    except UnmappedCode:
        pass


@given(unit=units)
def test_ambiguous_mapping_is_never_silently_chosen(unit: str) -> None:
    dupe = [
        AssayDefinition(assay_key="NA_SERUM", loinc="2951-2", specimen="87612001",
                        ucum_unit="mmol/L", dict_version="1.0.0"),
        AssayDefinition(assay_key="NA_PLASMA", loinc="2947-0", specimen="119361006",
                        ucum_unit="mmol/L", dict_version="1.0.0"),
    ]
    resolver = TaxonomyResolver(index={"LOCAL:NA": dupe}, dict_version="1.0.0")
    try:
        resolver.resolve("LOCAL:NA", unit)
        assert False, "ambiguous code must raise, not pick a winner"
    except AmbiguousCode:
        pass


@given(unit=units)
def test_raw_code_is_preserved_verbatim(unit: str) -> None:
    assay = AssayDefinition(assay_key="NA_SERUM", loinc="2951-2", specimen="87612001",
                            ucum_unit="mmol/L", dict_version="1.0.0")
    resolver = TaxonomyResolver(index={"LOCAL:NA_SER": [assay]}, dict_version="1.0.0")
    resolved = resolver.resolve("LOCAL:NA_SER", unit)
    assert resolved.raw_code == "LOCAL:NA_SER"       # never rewritten to the canonical
    assert resolved.assay.assay_key == "NA_SERUM"

Beyond property tests, hold a set of golden-file fixtures — real (de-identified) instrument feeds paired with the exact resolved assay and dict_version they should produce — and run them as contract tests in CI, so a dictionary edit that would change a historical mapping decision fails the build before it reaches production. Pinning dict_version in those fixtures is what makes the suite a true regression guard rather than a moving target.

Why keep both the raw instrument code and the canonical LOINC code?

The raw code is the primary evidence of what the analyzer actually transmitted; the LOINC code is a derived interpretation. Retaining both means an inspector can prove which dictionary version turned a given vendor mnemonic into a universal identity, and a mapping error can be corrected and replayed without having lost the original signal. Overwriting the raw code would make that reconstruction impossible.

How should the resolver handle a code that maps to two assays?

It must raise rather than choose. An ambiguous mapping almost always signals a real defect — a specimen-type distinction that was collapsed, or a duplicate dictionary entry — and picking either candidate would attach a plausible but unverifiable meaning to a patient result. The observation is held as a SEMANTIC fault for attributed review, and the ambiguity is resolved by fixing the dictionary, not the individual message.

Can a deprecated code still resolve?

Only for historical replay, never for new results. A deprecated entry is retained so that a result released under an older dictionary version can be reconstructed exactly, but Boundary V refuses to let a freshly ingested result cross into release when its only resolution path runs through a deprecated mapping. A retired entry refuses resolution entirely.

Why pin a dictionary version to every resolved result?

Without a pinned dict_version, two identical instrument codes resolved months apart could carry different meanings and you could not prove which was in force for a given result. Pinning the version makes resolution reproducible, ties the result to the laboratory’s approved test menu at the moment of resolution, and turns the golden-file suite into a genuine regression guard.

Part of: LIMS Architecture & Regulatory Compliance Foundations — the foundational architecture reference for this site.