Skip to main content
Version: v0.0.12

Tolerance tiers & the warning-code model

@cosyte/ccda follows the cosyte parser archetype's tiered tolerance model. Real-world C-CDA is vendor-quirky; the parser is liberal on input (Postel's Law) so a deviation becomes a warning you triage, not an exception that halts your pipeline, while a genuinely unrecoverable or hostile document is a hard failure.

The tiers​

TierBehaviorExample
0 / 1Accepted silently: conformant or trivially recoverable.A section recognized by its templateId.
2Warning with a stable code + bounded structural position; recovery continues. Escalates to a throw under { strict: true }.An unrecognized section LOINC code, a missing doseQuantity, a code/narrative mismatch.
3Fatal: a thrown CcdaParseError, always (even in lenient mode).Malformed XML, a non-ClinicalDocument root, a security tripwire.

The warning-code model​

Every Tier-2 warning carries a stable string code (WARNING_CODES.*), a message, and a structural position. Consumers branch on w.code, so renaming a code is a breaking change.

The message comes whole from a frozen registry and interpolates nothing: no warning factory takes a value parameter, so no attribute value, coded token or element name from the document can reach one. A handful of codes carry a variant per closed-set key (which CodeSlot, which catalog section), and those variants are generated from the parser's own tables. The position is bounded rather than copied: path is echoed only for an element name this parser navigates, sectionCode only for a LOINC-shaped code, and templateId only for a token shaped like an HL7 v3 UID. So you can log the whole .warnings array without leaking PHI.

sectionCode and templateId are carried by a short list of codes and no others, which is what you need to know before narrowing a profile tolerance with match. sectionCode comes with UNKNOWN_SECTION_CODE and SECTION_MATCHED_BY_LOINC_FALLBACK; templateId comes with those two (the section's first <templateId> root) and with TEMPLATE_EXTENSION_ABSENT (the matched document-type root). Everything else carries neither, so a match keyed on one of them elsewhere applies to nothing rather than to everything. That includes MISSING_TEMPLATE_ID, which has no template to name, and UNKNOWN_DOCUMENT_TEMPLATE, whose subject is the whole templateId set rather than any one root.

Most of the specifics a message used to name are still on the model: a section's <code> is on section.code, a unit on PQ.unit, an unmodelled datatype's raw text on the observation value. Two are not, and it is worth naming them rather than implying the model covers everything. MULTIPLE_EFFECTIVE_TIMES_UNRESOLVED no longer says how many siblings it could not classify and no model field counts them, so that number is only in the source. And a PROFILE_QUIRK_APPLIED carries the tolerated warning's code and position but not its text, so where the original had a per-slot wording (which CodeSlot an UNEXPECTED_CODE_SYSTEM was about) the slot is not recoverable from the re-badged warning. In every case doc.toString() re-emits the parsed DOM byte-for-byte.

Warnings are collected on doc.warnings and also delivered live to the onWarning callback, in discovery order:

import { parseCcda, WARNING_CODES } from "@cosyte/ccda";

// A medication with no doseQuantity and no routeCode: both safety-critical, both flagged.
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<ClinicalDocument xmlns="urn:hl7-org:v3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<realmCode code="US"/>
<templateId root="2.16.840.1.113883.10.20.22.1.1" extension="2015-08-01"/>
<templateId root="2.16.840.1.113883.10.20.22.1.9" extension="2015-08-01"/>
<id root="2.16.840.1.113883.19.5.99999.1" extension="DOC-0005"/>
<code code="34133-9" codeSystem="2.16.840.1.113883.6.1"/>
<title>Synthetic Progress Note</title>
<effectiveTime value="20240101"/>
<recordTarget><patientRole>
<id root="2.16.840.1.113883.19.5" extension="MRN-00042" assigningAuthorityName="Sample Hospital"/>
<patient>
<name><given>Jane</given><family>Doe</family></name>
<administrativeGenderCode code="F" codeSystem="2.16.840.1.113883.5.1"/>
</patient>
</patientRole></recordTarget>
<component><structuredBody>
<component><section>
<templateId root="2.16.840.1.113883.10.20.22.2.1.1" extension="2015-08-01"/>
<code code="10160-0" codeSystem="2.16.840.1.113883.6.1"/>
<title>Medications</title>
<text><content ID="m1">Aspirin</content></text>
<entry><substanceAdministration classCode="SBADM" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.16" extension="2014-06-09"/>
<statusCode code="active"/>
<consumable><manufacturedProduct classCode="MANU">
<templateId root="2.16.840.1.113883.10.20.22.4.23" extension="2014-06-09"/>
<manufacturedMaterial><code code="1191" codeSystem="2.16.840.1.113883.6.88" displayName="Aspirin"/></manufacturedMaterial>
</manufacturedProduct></consumable>
<text><reference value="#m1"/></text>
</substanceAdministration></entry>
</section></component>
</structuredBody></component>
</ClinicalDocument>`;

const collected: string[] = [];
const doc = parseCcda(xml, { onWarning: (w) => collected.push(w.code) });

// The drug is read; the missing safety-critical fields are preserved-as-absent, never defaulted.
doc.getMedications()[0]?.drug?.code; // => "1191"
doc.getMedications()[0]?.dose; // => undefined
doc.warnings.some((w) => w.code === WARNING_CODES.MISSING_DOSE_QUANTITY); // => true
doc.warnings.some((w) => w.code === WARNING_CODES.MISSING_ROUTE_CODE); // => true
collected.length; // => 2

Strict mode​

{ strict: true } escalates the first tolerated Tier-2 deviation to a thrown CcdaParseError carrying the same code: a spec-conformance gate for a trusted sender. Fail-safe by design: a clean, conformant document parses identically in both modes.

Fatal codes (always throw)​

Seven Tier-3 codes are unrecoverable and throw a CcdaParseError regardless of strict. The first five are security fatals raised by the hardened XML substrate before/while building the DOM, the load-bearing defense against hostile XML:

Fatal codeMeaning
XXE_OR_DTD_PRESENTThe document declared a DTD or an external entity.
ENTITY_EXPANSION_LIMITToo many &…; entity references (billion-laughs).
INPUT_SIZE_LIMIT_EXCEEDEDDecoded input exceeds the byte cap.
ELEMENT_DEPTH_LIMIT_EXCEEDEDElement nesting too deep.
NODE_COUNT_LIMIT_EXCEEDEDToo many element nodes.
NOT_WELL_FORMED_XMLThe bytes did not parse as XML.
NOT_A_CLINICAL_DOCUMENTWell-formed, but the root element is not ClinicalDocument.

Narrow on err.code:

import { parseCcda, CcdaParseError, FATAL_CODES } from "@cosyte/ccda";

let code: string | undefined;
try {
// Well-formed XML, but the root is not a ClinicalDocument.
parseCcda("<Foo>hello</Foo>");
} catch (err) {
if (err instanceof CcdaParseError) code = err.code;
}
code; // => "NOT_A_CLINICAL_DOCUMENT"
code === FATAL_CODES.NOT_A_CLINICAL_DOCUMENT; // => true

The safety caps (maxInputBytes, maxDepth, maxNodeCount, maxEntityExpansions) have library defaults; tighten them (or, at your own risk, loosen them) via ParseCcdaOptions.limits.