# Cosyte Developer Documentation: full text > The complete current documentation of every Cosyte package on this site, as Markdown, one package after another. Superseded versions and packages that have not published documentation are not included. Each package's section is also published on its own, as llms-full.txt under that package's path, and each page as plain Markdown at its own URL with .md appended. The index of packages is https://docs.cosyte.com/llms.txt. --- # @cosyte/hl7 > HL7 v2 parser, serializer, and builder for JavaScript and TypeScript. These are the 31 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/hl7/intro Markdown: https://docs.cosyte.com/hl7/intro.md # @cosyte/hl7 Parse real-world, vendor-quirky HL7 v2 and pull fields out in one line, without reading the spec. `@cosyte/hl7` is a zero-dependency TypeScript toolkit: a lenient parser, an immutable model with dot-path access, named helpers for the common segments, a spec-clean serializer, a message builder, and a profile system for vendor quirks. ## Install ```bash npm install @cosyte/hl7 ``` ## Parse a message ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); msg.get("PID.5.1"); // "Smith" msg.patient?.familyName; // "Smith" msg.observations(); // typed OBX rows msg.warnings; // stable, positional tolerance warnings ``` The parser is **lenient by default** (vendor quirks become warnings, not failures) while `msg.toString()` always emits spec-clean HL7 (Postel's Law). Pass `{ strict: true }` to escalate every tolerated deviation to a thrown `Hl7ParseError`. ## Vendor profiles Apply a built-in profile, or declare your own with `defineProfile()`: ```ts import { parseHL7, profiles } from "@cosyte/hl7"; const msg = parseHL7(raw, profiles.epic); ``` Built-ins ship for Epic, Cerner, Meditech, Athena, a generic lab, and the Visage 7 and Philips Vue PACS imaging systems, each authored through the same public `defineProfile()` API you'd use yourself. ## Next - Read the **API reference** for every export, generated from source. - Start from the **profile starter kit** to publish your own vendor profile package. --- URL: https://docs.cosyte.com/hl7/installation Markdown: https://docs.cosyte.com/hl7/installation.md # Installation `@cosyte/hl7` is a **zero-dependency** TypeScript toolkit for Node.js. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration. > **Status:** `0.1`, published on npm and public. The `npm install` command below is live. The > exported functions, the message and helper surfaces, and the 20 stable warning codes are the > surface we keep stable; below 1.0 a breaking change moves the minor version, and the changelog > says how to migrate. For the current published version, ask the registry > (`npm view @cosyte/hl7 version`) rather than this page. ## Prerequisites - **Node.js >= 22.** The whole `@cosyte/*` suite targets ES2023 / Node 22+. - A package manager: `pnpm`, `npm`, or `yarn`. - **No runtime dependencies.** Nothing else is pulled in; the parser is Node stdlib only. ## Install ```bash npm install @cosyte/hl7 ``` ## Smoke test Confirm the package resolves and its version symbol is present: ```ts runnable import { VERSION } from "@cosyte/hl7"; typeof VERSION; // => "string" ``` If that resolves, the install is good. Head to the [Quickstart](https://docs.cosyte.com/hl7/quickstart.md). ## Module systems `@cosyte/hl7` is `"type": "module"` and exposes both conditions, so both of these resolve to the right build without extra configuration: ```ts // ESM / TypeScript import { parseHL7 } from "@cosyte/hl7"; ``` ```js // CommonJS const { parseHL7 } = require("@cosyte/hl7"); ``` The types are published per-condition (`.d.ts` for `import`, `.d.cts` for `require`) and gated by `attw` on every release, so editor IntelliSense matches the build you actually load. --- URL: https://docs.cosyte.com/hl7/quickstart Markdown: https://docs.cosyte.com/hl7/quickstart.md # Quickstart Parse a real HL7 v2 message and pull fields out in a few lines. `@cosyte/hl7` is **lenient by default** (Postel's Law): vendor-quirky input parses into an immutable message plus a list of tolerance **warnings**, rather than throwing. The serializer, by contrast, is conservative. `msg.toString()` always emits spec-clean HL7. ## Parse a message The named helpers (`msg.patient`, `msg.meta`, …) give you the common fields with **zero HL7-path knowledge required**: ```ts runnable import { parseHL7 } from "@cosyte/hl7"; // Synthetic ADT^A01: each segment ends with a carriage return, per the spec. const raw = "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01^ADT_A01|MSG00001|P|2.5\r" + "EVN|A01|20260419101500\r" + "PID|1||MRN12345^^^HOSP^MR||Doe^John^Q||19800115|M|||123 Main St^^Boston^MA^02101||^PRN^PH^^^617^5551212\r" + "PV1|1|I|ICU^101^A^HOSP|||||ATTEND^Smith^Jane^^^^MD|||||||||||VISIT001\r"; const msg = parseHL7(raw); // `msg.patient` is `undefined` when the message has no PID segment. msg.patient?.mrn; // => "MRN12345" msg.patient?.fullName; // => "John Q Doe" msg.patient?.sex; // => "M" msg.meta.type; // => "ADT^A01^ADT_A01" // A clean, spec-conformant message parses with no tolerance warnings. msg.warnings; // => [] ``` ## Reach any field by path When there's no named helper for what you need, dot-paths reach any field, component, or subcomponent: `SEGMENT.field.component.subcomponent`: ```ts runnable import { parseHL7 } from "@cosyte/hl7"; const raw = "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01^ADT_A01|EX00001|P|2.5\r" + "PID|1||MRN12345^^^HOSP^MR||Doe^John^Q||19800115|M"; const msg = parseHL7(raw); msg.get("PID.5.1"); // => "Doe" ``` ## Warnings carry stable codes Each tolerance warning carries a **stable code** you can branch on without it churning between releases (the 20 warning codes are a public, versioned contract): ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); for (const w of msg.warnings) { if (w.code === "SEGMENT_CASE") { // a segment name arrived in the wrong case (`obx`, `Pid`); it still // resolves as the segment it names, so `observations()` and `patient` // see it. Serialization re-emits the spelling that arrived. } } ``` > **About runnable examples.** The blocks tagged ` ```ts runnable ` above are extracted by the docs > build, executed against the package, and their `// =>` results asserted, so a documented example > can never silently drift from the code. Illustrative fragments (like the one directly above, which > references an undeclared `raw`) stay plain ` ```ts ` blocks. ## Next - [Core Concepts](https://docs.cosyte.com/hl7/spec-notes-primer.md): the tolerance model and the spec grounding behind it. - [Guides](https://docs.cosyte.com/hl7/guides-overview.md): task-oriented recipes: lab results, vendor profiles, ACKs, building. - [Troubleshooting](https://docs.cosyte.com/hl7/troubleshooting.md): warnings vs. errors, strict mode, and known limitations. --- URL: https://docs.cosyte.com/hl7/spec-notes-primer Markdown: https://docs.cosyte.com/hl7/spec-notes-primer.md # Spec notes: the HL7 v2.x standard, for someone building a parser The other pages in this section are **implementation notes**: each says what `@cosyte/hl7` does about one spec area, and why. This page is the **standard reference** they elaborate: what the HL7 v2.x specification itself says about encoding, grammar, message typing, acknowledgement, and the composite data types a parser must decode. It is the "why the parser is shaped this way" companion, not a description of the parser. Scope note: this covers the pipe-and-hat v2.x standard only, **not** FHIR and **not** C-CDA. Lower-layer framing (MLLP) is a separate standard and is touched here only at its boundary. > **Provenance.** Every statement below is drawn from the primary HL7 Chapter 2 / > Chapter 2.A control-and-datatypes text (cross-checked across v2.2, v2.5, v2.5.1, > v2.7, v2.8.2 and v2.9), corroborated by the NIST v2+ data-type reference, and > was adversarially fact-checked (each claim independently voted; the few that > failed are called out inline as *refuted*). The spec text was read from the > widely-cited `hl7.eu` mirror. For a production conformance system, treat the > **official HL7-published specification as the authoritative text of record**. > A mirror is a convenience, not the source. ## 1. The encoding model: how bytes become a tree A v2.x message is a delimited, hierarchical stream with a fixed depth: ``` message → segment → field → component → subcomponent ``` Subcomponents are terminal: the standard requires that a subcomponent be a **primitive** type only. Components and subcomponents **do not repeat**: only **fields** repeat, via the repetition delimiter. So the repetition delimiter is meaningful at exactly one level of the tree, which bounds the parse. ### The MSH segment bootstraps every delimiter The parser cannot read anything until it has read `MSH`, because `MSH` declares the delimiters the rest of the message uses: | Where | What it is | Canonical value | |---|---|---| | **MSH-1**: the 4th character of the message (immediately after `MSH`) | Field separator | `|` | | **MSH-2**, in fixed order | Component, repetition, escape, subcomponent | `^` `~` `\` `&` | Whatever values appear in MSH-1/MSH-2 apply **message-wide**. A parser reads them once and uses them throughout. The four classic delimiters and their order have never changed across v2.x. v2.5+ adds an optional 5th character in MSH-2, a **truncation** character (`#`); the four classic delimiters are unaffected. ### The segment terminator is not configurable Unlike the field/encoding delimiters, the **segment terminator is always ASCII carriage return (hex `0D`) and cannot be redefined by implementers.** Real feeds sometimes terminate segments with `LF` (`0A`) or `CRLF`. That is documented **non-conformance to tolerate on input**, not a redefinition the standard permits. A conservative serializer emits `CR` only. ## 2. Field population: populated, not-populated, and null are three states This is the single most common correctness trap, and a parser must not collapse it: | On the wire | Meaning | Effect on a receiver's stored value | |---|---|---| | a value | **populated** | set it | | nothing between delimiters | **not-populated** (omitted) | leave the existing value unchanged | | exactly two double-quotes: `""` | **null** | **delete** the existing value | The null token is literally `|""|`. Using consecutive double-quotes as field content for any other purpose is prohibited by the standard. The consequence for a parser: **null must be represented distinctly from empty**: `""` and "absent" are different instructions (delete vs. leave-as-is), and a data model that maps both to `""`/`undefined` has silently lost a semantic the sender relied on. Some vendors mishandle `|""|`; a tolerant parser still keeps the distinction it is given. Escape sequences and character-set handling are their own topics: see [Escapes & round-trip](https://docs.cosyte.com/hl7/spec-notes-escapes.md) and [Character sets (MSH-18)](https://docs.cosyte.com/hl7/spec-notes-charset.md). ## 3. The abstract message grammar, and reconstructing groups from the wire ### The notation Message definitions in the standard are written in an abstract grammar: | Notation | Meaning | |---|---| | `[ X ]` | optional: 0 or 1 | | `{ X }` | repeating, required: 1 or more | | `[{ X }]` | optional **and** repeating: 0 or more | `[{ … }]` and `{[ … ]}` are **explicitly equivalent**. A **segment group** is "two or more segments organized as a logical unit," itself optional/required and repeating/non-repeating; the notation nests to compose groups. The canonical example from the v2.7 text is `[{ NK1 }]`: Next of Kin, usage `RE`, cardinality `[0..3]`. ### Two optionality vocabularies: keep them apart The base segment-definition table and the conformance/profile layer use different code sets for the *same concept*, and conflating them is a real bug source: - **Base OPT column:** `R` required · `O` optional · `C` conditional · `X` not-used-with-this-trigger · `B` backward-compatibility-only · `W` withdrawn. - **v2.7+ conformance Usage codes:** `R` · `RE` (required-but-may-be-empty) · `O` · `C` (conditional: if the predicate holds, follow `R`, else follow `X`) · `CE` · `X`. Cardinality is a **separate** `[m..n]` pair (`[n..*]` = unbounded max), introduced with the v2.7 conformance model. **Usage and cardinality are bound, not independent**. A profile engine must enforce this: `X` ⇒ `[0..0]`; `R` ⇒ minimum ≥ 1; `RE`/`C`/`O` ⇒ minimum 0, with a documented exception permitting `RE` to carry a non-zero minimum. ### The rule that makes a flat stream reconstructable The wire is a flat `CR`-delimited list of segments with **no explicit group markers**. The abstract tree is reconstructed from segment order alone, and one normative rule is what makes that deterministic: > If a named segment appears in two individual-or-group locations and **either** > appearance is optional or repeating, the occurrences **must be separated by at > least one required segment of a different name.** That required, differently-named segment is what breaks the ambiguity between adjacent optional/repeating occurrences of the same segment. A parser relies on exactly this property to place a wire segment into the correct group. **Two things to *not* build on** (both were adversarially *refuted*): 1. **There is no universal, hardcoded anchor list.** The intuition that `MSH`/`PID`/`ORC`/`OBR`/`OBX` are fixed hierarchy anchors (with `PID` a child of `MSH`, etc.) does **not** generalize: reconstruction rests on the separator rule plus a grammar, not a fixed segment list. A parser that special-cases those names *for general grouping* is relying on a coincidence. 2. When an expected parent segment is simply **absent**, the standard itself concedes the hierarchy "cannot be derived from message structure alone." A grammar-driven implementation can synthesize an empty placeholder ("ghost") segment to preserve structure, an engineering technique, not a normative rule. This is precisely why this repo's [Message-type & structure awareness](https://docs.cosyte.com/hl7/spec-notes-structure.md) safety net is scoped the way it is. It is **not** an abstract-syntax validator and does **not** claim a universal anchor model: it checks, per specific `(message code, trigger event)` pair, only whether the segments the v2.5.1 abstract syntax marks **Required** are present, a deliberately narrow heuristic that can never false-positive on a conformant-but-sparse message. "Anchor" there means "a Required segment for *this* trigger," which is a different and legitimate use of the word from the refuted universal-anchor claim above. ## 4. Message typing: MSH-9 has three components `MSH-9` (data type `MSG`) carries **three** components: ``` MSH-9 = message code ^ trigger event ^ message structure (abstract message ID) ``` The two-component characterization (message code + trigger only) is a common belief and was adversarially **refuted**. Do not model MSH-9 as two components; `MSH-9.3` exists and maps to the abstract message definition. - **Message type ↔ trigger event is one-to-many:** a trigger maps to exactly one message type, but a message type has many triggers. (This is why the structure safety net keys on the `(code, trigger)` *pair*, not the family.) - **`Z` is reserved:** any message code or trigger beginning with `Z` is locally defined and is never assigned by the standard. The same reservation applies to locally-defined **Z-segments**, which a tolerant parser accepts and surfaces rather than rejects. ## 5. Acknowledgement: two modes, two code vocabularies, one admitted ambiguity Acknowledgement mode is selected by two MSH fields: - **MSH-15**: accept acknowledgement type. - **MSH-16**: application acknowledgement type. **Original mode** applies when both are null/absent; **enhanced mode** when either is valued. Original mode is exactly equivalent to enhanced mode with `MSH-15 = NE`, `MSH-16 = AL`. Enhanced mode separates two code vocabularies, and a parser/engine must know which applies: | Level | Field | Codes | |---|---|---| | **Accept** (transport/commit) | `MSA-1` | `CA` commit accept · `CR` commit reject · `CE` commit error | | **Application** (business) | `MSA-1` | `AA` accept · `AE` error · `AR` reject | A positive **accept** acknowledgement means the receiver has committed the message to **safe storage**, releasing the sender from resending, a stronger guarantee than "received." > **The load-bearing real-world caveat, admitted by HL7 itself:** the standards > body's own Conformance work-group states that Chapter 2 "is unclear in that > regard and does not provide clear guidance. It allows for interpretations > leading to different flows and results." Independent multi-system testing has > found wide variation in ACK behavior. The ambiguity is **in the specification**, > not only in bad implementations, which is the concrete reason a tolerant engine > is required, and why ACK behavior is configured, not assumed. ## 6. Composite data types: the layouts a parser must not get wrong Component counts and types are **version-specific**: the biggest single hazard in decoding composites. Branch on the message's stated version (`MSH-12`). ### CX: Extended Composite ID (identifiers) **10 components in v2.5 / v2.5.1; 12 in v2.9.** | # | Component | Type | Notes | |---|---|---|---| | CX.1 | ID Number | ST | the identifier, the **only Required** component | | CX.2 | Check Digit | ST | | | CX.3 | Check Digit Scheme | ID | | | **CX.4** | **Assigning Authority** | **HD** | embedded HD, the namespace scoping the identifier | | CX.5 | Identifier Type Code | ID | | | **CX.6** | **Assigning Facility** | **HD** | embedded HD | | CX.7 / CX.8 | Effective / Expiration Date | DT | | | CX.9 / CX.10 | Assigning Jurisdiction / Agency | CWE | **present already in v2.5** (a common myth dates these to v2.7) | | CX.11 / CX.12 | Security Check / Scheme | ST / ID | **added in v2.9** | Load-bearing: `CX.1` (the id), `CX.4` (the assigning-authority HD, an id is meaningless without the namespace that scopes it), `CX.5` (type code). ### XPN: Extended Person Name **14 components in v2.5 / v2.5.1; 15 in v2.9** (adds `XPN.15` Called By). `XPN.1` Family Name is itself the `FN` composite (surname plus own/partner prefixes). `XPN.2`/`XPN.3` are given / further-given. **`XPN.7` Name Type Code** (`L` legal, `A` alias, …) is load-bearing: it decides *which* name is authoritative. Version drift to watch: `XPN.6` Degree is `IS` in v2.5, deprecated there, withdrawn as of v2.7, and listed as `ST` in v2.9; the date components (`XPN.12`/`XPN.13`) are `TS` in v2.5 and `DTM` in v2.7+. ### XAD: Extended Address **14 components.** The trap: **`XAD.1` Street Address is the `SAD` composite (not plain `ST`) from v2.5 onward**. So a v2.5+ parser must treat street address as sub-componentized (street-or-mailing / street-name / dwelling-number). Load-bearing fields: `XAD.1` street, `XAD.3` city, `XAD.4` state/province, `XAD.5` zip/postal, `XAD.7` Address Type. ### HD: Hierarchic Designator (the reusable namespace type) **Exactly 3 components:** `HD.1` Namespace ID (`IS`, a local mnemonic), `HD.2` Universal ID (`ST`), `HD.3` Universal ID Type (`ID`, e.g. ISO OID, UUID, DNS). HD is both a standalone field type **and** an embedded component: inside `CX.4` and `CX.6`, and in the MSH sending/receiving facility fields. When HD is nested as a component, **its own component separator `^` is demoted to a subcomponent separator `&`**, the classic delimiter demotion a parser must apply when reading a composite-inside-a-composite. (A variant claim placing the embedded HDs at `CX.3`/`CX.5` was **refuted**: the correct positions are `CX.4`/`CX.6`.) ### Date / time: precision is signalled by length The date/time family carries **variable precision determined by how many characters are populated**, not by a precision flag: ``` DT = YYYY[MM[DD]] TM = HH[MM[SS[.S…]]] TS (v2.5) = YYYY[MM[DD[HH[MM[SS[.S…]]]]]][+/-ZZZZ] DTM (v2.7+) = YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ] ``` A parser must **not** zero-pad or assume missing components: `20240115` is day-precision and is not the same value as `202401150000`. The timezone is an optional `+/-HHMM` offset that, **when absent, defaults to the sender's local zone**: never UTC, never the parser's zone. `TS` (which carried a deprecated "degree of precision" component) was superseded by the pure `DTM` type in v2.7+. This repo's handling (parse to parts without materializing a `Date`, keep the precision, refuse to guess a missing offset) is detailed in [Datetime precision & timezone](https://docs.cosyte.com/hl7/spec-notes-datetime-precision.md). Coded fields (`CWE`/`CNE`/`CE` and coding-system provenance) are their own topic: see [Coding-system provenance](https://docs.cosyte.com/hl7/spec-notes-coding-system.md). Which of these composite layouts changed at which version is tracked in the [Version-sensitivity matrix](https://docs.cosyte.com/hl7/spec-notes-version-matrix.md). ## PHI None. This page contains only structural facts about the standard (segment, field, and component names and their spec semantics) and no field values or realistic message content. ## References - **HL7 v2.x** Chapter 2 (Control) and Chapter 2.A (Data Types), cross-checked across v2.2, v2.5, v2.5.1, v2.7, v2.8.2 and v2.9. - **NIST v2+** data-type reference (`usnistgov.github.io/v2plusDemo`). - **HL7 Conformance work-group**, "HL7 V2 ACK Guidance": the standards body's own statement that Chapter 2's acknowledgement guidance is ambiguous. - The official HL7-published specification is the authoritative text of record; the sources above are convenience mirrors and companions. --- URL: https://docs.cosyte.com/hl7/warning-and-fatal-codes Markdown: https://docs.cosyte.com/hl7/warning-and-fatal-codes.md # Warning and fatal codes Every deviation the parser notices lands in one of two places, and the split is the whole tolerance model: - A **warning** is data on `msg.warnings`. The parse still succeeded and the value is still there. Twenty codes, listed below, exported as `WARNING_CODES`. - A **fatal** is a thrown `Hl7ParseError`. The input is not recoverable as HL7 at all. Four codes, listed below, exported as `FATAL_CODES`. Both registries are a **public, versioned contract**. A code is never renamed under you without a breaking release, so `w.code === WARNING_CODES.UNKNOWN_SEGMENT` is safe to branch on and safe to persist. Prefer logging `w.code` and `w.position` over `w.message`: the codes are stable, and the messages are bounded but not absolutely content-free (see [Troubleshooting](https://docs.cosyte.com/hl7/troubleshooting.md#known-limitations)). ```ts runnable import { parseHL7, WARNING_CODES } from "@cosyte/hl7"; // A ZZZ segment is not a standard HL7 segment: tolerated, and surfaced as a warning. const raw = "MSH|^~\\&|LAB|MAIN|EHR|REF|20260419143000||ADT^A01|EX00003|P|2.5\rZZZ|1"; const msg = parseHL7(raw); msg.warnings.some((w) => w.code === WARNING_CODES.UNKNOWN_SEGMENT); // => true ``` ## Warnings Emitted by the parser unless the row says otherwise. Under `{ strict: true }` every one of them is promoted to a thrown `Hl7ParseError` instead. | Code | What it means | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `MLLP_FRAMING_STRIPPED` | The input arrived wrapped in MLLP transport framing bytes. They were removed before parsing and the message itself is untouched. | | `FIELD_WHITESPACE_TRIMMED` | Leading or trailing whitespace was trimmed from a field value. The warning carries only the character counts, never the value. | | `UNKNOWN_ESCAPE_SEQUENCE` | An escape was not recognisable HL7 escape grammar, or ran to end of input unterminated. It is preserved verbatim in the parsed value. | | `TIMESTAMP_FALLBACK_FORMAT` | A timestamp did not match the strict HL7 shape, but a declared or built-in fallback format resolved it. Not produced by a parse: see below. | | `SEGMENT_CASE` | A segment identifier carried a lowercase letter. It resolves as the segment it names, and the spelling that arrived is re-emitted unchanged. | | `EXTRA_FIELDS` | A segment carries more fields than the active profile declares. The extras are preserved on the raw segment rather than dropped. | | `UNKNOWN_SEGMENT` | No standard segment carries that name, compared ignoring case, and no active profile claims it as a custom segment either. | | `DUPLICATE_REQUIRED_SEGMENT` | A segment the active profile marks as a singleton appears more than once. Both copies are kept for you to reconcile. | | `ENCODING_MISMATCH` | The encoding characters declared in MSH-2 disagree with the separators the later segments actually used. | | `MISSING_REQUIRED_FIELD` | A field the active profile marks as required is absent or empty. Distinct from the fatal raised when MSH itself is missing. | | `MISSING_EXPECTED_GROUP` | The published structure for this message type gives a minimum of one of some segment or group, and the message carries none of it. | | `OUT_OF_ORDER_SEGMENT` | A segment appears outside the order the active profile declares for it. Nothing is reordered; the deviation is reported. | | `VERSION_MISMATCH` | MSH-12 declares an HL7 version that differs from the one a profile or the parse options explicitly expected. | | `UNKNOWN_CHARSET` | MSH-18 or a charset override names a value that HL7 Table 0211 does not contain. Bytes are read as latin1 rather than guessed at. | | `UNSUPPORTED_CHARSET` | A recognised Table 0211 set was not decoded, either because it is out of scope here or because a strict decode of it failed. | | `ACK_NO_CORRELATION_ID` | The inbound message carried no MSH-10 control id, so the built acknowledgement leaves MSA-2 empty instead of inventing one. | | `MERGE_MISSING_PRIOR_OR_SURVIVOR` | An identity merge or move event is missing one side of the prior and surviving pair, or that side carries no usable identifier. | | `BATCH_COUNT_MISMATCH` | A batch or file trailer declares a count that differs from what the splitter found. Nothing is dropped to make the numbers agree. | | `BATCH_MISSING_TRAILER` | An envelope header opened a scope that no matching trailer segment ever closed. The split still returns every message it found. | | `UNTERMINATED_STREAM_MESSAGE` | The last message in a stream ended with no segment terminator. It is still yielded in full, but the feed may have been cut off. | Three of these are emitted by a surface other than `parseHL7`, and arrive on that surface's own result rather than on `msg.warnings`: `ACK_NO_CORRELATION_ID` from the acknowledgement builder, `MERGE_MISSING_PRIOR_OR_SURVIVOR` from `identityEvents()`, and the two batch codes plus `UNTERMINATED_STREAM_MESSAGE` from `splitBatch()` and `parseStream()`. `TIMESTAMP_FALLBACK_FORMAT` is a fourth exception, of a different kind: it is a published, stable code with no emit site a parse reaches, so it does not appear on `msg.warnings` at all. Read `matchedFormat` on the timestamp instead. It names the format that resolved a non-canonical value, and is absent when the value was canonical HL7 and parsed strictly. The code is documented here because it is exported and would otherwise be an undocumented member of `WARNING_CODES`; do not write a check that waits for it to arrive. ## Fatals Thrown as `Hl7ParseError` regardless of mode. They mean the bytes are not recoverable as HL7, not that the sender is quirky. | Code | What it means | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `NO_MSH_SEGMENT` | The input carries no MSH header, so there are no delimiters and no message type to read, and nothing that can be parsed. | | `MSH_TOO_SHORT` | The MSH header is truncated before the fields delimiter discovery needs, so the rest of the message cannot be tokenized. | | `INVALID_ENCODING_CHARACTERS` | MSH-1 and MSH-2 do not yield a usable delimiter set, so every field boundary in the message would have to be a guess. | | `EMPTY_INPUT` | The input was empty, or held nothing but whitespace, so there is no message present to parse in the first place. | Catch the error and read its `code` to tell them apart: ```ts runnable import { parseHL7, FATAL_CODES, Hl7ParseError } from "@cosyte/hl7"; let matched = false; try { parseHL7(""); } catch (err) { matched = err instanceof Hl7ParseError && err.code === FATAL_CODES.EMPTY_INPUT; } matched; // => true ``` `Hl7ParseError.snippet` carries up to 40 characters of the raw input verbatim and the library never redacts it. It is the field to scrub at your logging edge. --- URL: https://docs.cosyte.com/hl7/spec-notes-coding-system Markdown: https://docs.cosyte.com/hl7/spec-notes-coding-system.md # Spec notes: coding-system provenance `@cosyte/hl7` surfaces the coding-system a code **claims** (CWE.3 / CE.3, "Name of Coding System"), drawn from **HL7 Table 0396 (Coding System)**. This is **provenance only**: read-only, no validation, no lookup, no network, no bundled codeset. `codingSystem(id)` / `codingSystemOf(coded)` answer "which system?"; they never assert a code is valid or current. ## What ships (the frozen `KNOWN_CODING_SYSTEMS` subset) A deliberately small, safety-relevant subset of Table 0396, not the full registry. Each row: the registered acronym carried in CWE.3, the canonical name, and tolerated aliases (matched case-insensitively). | Acronym (`id`) | Name | Aliases | Source | |---|---|---|---| | `LN` | LOINC | `LOINC` | HL7 Table 0396; Regenstrief LOINC | | `SCT` | SNOMED CT | `SNM`, `SNOMED`, `SNOMEDCT` | HL7 Table 0396; SNOMED International | | `I10` | ICD-10 | `ICD-10`, `ICD10` | HL7 Table 0396; WHO ICD-10 | | `I10P` | ICD-10-PCS | `ICD-10-PCS`, `ICD10PCS` | HL7 Table 0396; CMS ICD-10-PCS | | `RXN` | RxNorm | `RXNORM` | HL7 Table 0396; NLM RxNorm | | `NDC` | National Drug Codes | none | HL7 Table 0396; FDA NDC | | `CVX` | CDC Vaccine Codes | none | HL7 Table 0396; CDC/NCIRD (codes = Table 0292) | | `MVX` | CDC Vaccine Manufacturer Codes | none | HL7 Table 0396; CDC (codes = Table 0227) | | `UCUM` | Unified Code for Units of Measure | none | HL7 Table 0396; Regenstrief/UCUM.org | ## The `I10` nuance (a deliberate fail-safe choice) Table 0396 registers `I10` as **ICD-10**, the WHO base classification. US v2 feeds very frequently send `I10` in DG1-3 when they actually mean ICD-10-**CM** (the US clinical modification). That CM specificity is a sender convention, **not** what the acronym registers. Because provenance is the *sender's claim* and our hard rule is "never guess," this library reports the **registered** claim (`"ICD-10"`) for `I10` and does **not** silently upgrade it to ICD-10-CM. We also do not ship an unverified `I10C` entry. A consumer that knows its feed uses `I10` for ICD-10-CM applies that knowledge itself; the parser will not assert specificity the wire did not carry. ## Fail-safe behavior - **Unknown / local / mistyped id** → surfaced **verbatim** in `claimed` with `known: false`. Never dropped, never guessed (e.g. `"L"`, `"99zL"`). - **No claim** (absent / empty / whitespace-only CWE.3) → `undefined`. - **Alias normalization** maps a tolerated spelling to the registered acronym (`"LOINC"` → `LN`, `"SNOMED"` → `SCT`, `"RxNorm"` → `RXN`) while preserving the original spelling in `claimed`. ## Known limitations - Provenance is the sender's claim, **not verified**. A registered system with a wrong/deleted code still reports `known: true`. - The map is a *provenance* map, not a *versioned codeset*: code-system versions move (LOINC twice-yearly, RxNorm monthly, ICD-10-CM annually) and the map intentionally pins no version. - Subset, not the full Table 0396. An unlisted-but-registered system reports `known: false`; widening the subset is additive future work. ## References - **HL7 Table 0396 (Coding System)**: [terminology.hl7.org/CodeSystem-v2-0396.html](https://terminology.hl7.org/CodeSystem-v2-0396.html); HL7 Vocab registry: [www.hl7.org/special/committees/vocab/table_0396/index.cfm](https://www.hl7.org/special/committees/vocab/table_0396/index.cfm) --- URL: https://docs.cosyte.com/hl7/spec-notes-structure Markdown: https://docs.cosyte.com/hl7/spec-notes-structure.md # Spec notes: message-type & structure awareness `@cosyte/hl7` ships a **misroute / truncation safety net**: for the message types it covers it knows which segments HL7's own published message-structure definitions give a minimum of one, and tells you when a message does not carry one of them. `msg.structure` is the read-side view; the parser also emits a single additive Tier-2 `MISSING_EXPECTED_GROUP` warning per absent segment, naming that segment. This is **not** a conformance validator. It checks segment presence, not ordering, cardinality or content, and it covers twelve message codes rather than the whole standard. It never throws and never rewrites the message; `strict` mode may promote the warning per the usual Postel's-Law model. If you want the ordering and cardinality question answered, ask for it: `validateMessageStructure` is an opt-in check over the same publication, described in its own section further down this page. It changes nothing about a parse that does not call it. ## Where the expectations come from Every expectation is **derived**, not transcribed. The package vendors a byte-for-byte snapshot of HL7's machine-readable message structures and derives the registry from it offline; there is no leniency list, no suppression list and no network call at build time or at run time. The provenance ships with the code, so a warning can be audited without leaving the package: ```ts runnable import { STRUCTURE_REGISTRY_PROVENANCE, findMessageStructureDefinition } from "@cosyte/hl7"; STRUCTURE_REGISTRY_PROVENANCE.publication.repository; // => "HL7/v2ig" // Which published structure is behind an ADT^A01 expectation, and which // variants of it were read? const def = findMessageStructureDefinition("ADT", "A01"); def?.structureId; // => "ADT_A01" def?.structureIds.length; // => 4 def?.requiredSegments.join(","); // => "EVN,MSH,PID,PV1" ``` `STRUCTURE_REGISTRY_PROVENANCE` carries the publication and the exact commit it was read at, the byte count, sha256 and upstream URL of all 83 vendored files, the variant family behind each referenced structure, and the structure id behind each recognized (message code, trigger event) pair. ## The three rules the derivation follows 1. **Effective minimum from the root.** A segment is expected only when its minimum is at least one **along the whole path from the structure root**. A segment the publication marks required inside a group it marks optional is therefore not expected, which is why a conformant `ORU^R01` with no `OBX` and no top-level `PID` stays warning-free. 2. **A variant family must agree.** The publication splits some structures into single-uppercase-letter variants (`ADT_A01-A` through `ADT_A01-D`) while its message list references the unsuffixed name (`ADT_A01`). The family of a referenced id is that id plus any id formed by appending a hyphen and exactly one uppercase letter, and a segment is expected only when **every** member of the family gives it a minimum of one. Where variants disagree the publication has not settled the question, and a warning heuristic takes the silent side. 3. **Unrecognized is silent.** A (message code, trigger event) pair with no registry entry yields `recognized: false` and emits nothing. ## What ships `msg.structure.requiredSegments` lists the expectations for the matched type and `msg.structure.missingSegments` lists the ones the message does not carry. `missingGroups` is the same list under its older name. | Message | Trigger events | Expected segments | Published structure | | --- | --- | --- | --- | | ACK | (message code alone) | `MSA`, `MSH` | `ACK` | | ADT | A01, A04, A08, A13 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A01` | | ADT | A02 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A02` | | ADT | A03 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A03` | | ADT | A05, A14, A28, A31 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A05` | | ADT | A06, A07 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A06` | | ADT | A09, A10, A11 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A09` | | ADT | A12 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A12` | | ADT | A15 | `EVN`, `MSH`, `PID`, `PRT`, `PV1` | `ADT_A15` | | ADT | A16 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A16` | | ADT | A17 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A17` | | ADT | A20 | `EVN`, `MSH`, `NPU` | `ADT_A20` | | ADT | A21, A22, A23, A25, A26, A27, A29, A32, A33 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A21` | | ADT | A24 | `EVN`, `MSH`, `PID` | `ADT_A24` | | ADT | A37 | `EVN`, `MSH`, `PID` | `ADT_A37` | | ADT | A38 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A38` | | ADT | A40, A41, A42 | `EVN`, `MRG`, `MSH`, `PID` | `ADT_A39` | | ADT | A43, A49 | `EVN`, `MRG`, `MSH`, `PID` | `ADT_A43` | | ADT | A44, A47 | `EVN`, `MRG`, `MSH`, `PID` | `ADT_A44` | | ADT | A45 | `EVN`, `MRG`, `MSH`, `PID`, `PV1` | `ADT_A45` | | ADT | A50, A51 | `EVN`, `MRG`, `MSH`, `PID`, `PV1` | `ADT_A50` | | ADT | A52, A53 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A52` | | ADT | A54, A55 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A54` | | ADT | A60 | `EVN`, `MSH`, `PID` | `ADT_A60` | | ADT | A61, A62 | `EVN`, `MSH`, `PID`, `PV1` | `ADT_A61` | | DFT | P03 | `EVN`, `FT1`, `MSH`, `PID` | `DFT_P03` | | DFT | P11 | `EVN`, `FT1`, `MSH`, `PID` | `DFT_P11` | | MDM | T01, T03, T05, T07, T09, T11 | `EVN`, `MSH`, `PID`, `PV1`, `TXA` | `MDM_T01` | | MDM | T02, T04, T06, T08, T10 | `EVN`, `MSH`, `OBX`, `PID`, `PV1`, `TXA` | `MDM_T02` | | OMG | O19 | `MSH`, `OBR`, `ORC` | `OMG_O19` | | OMI | O23 | `IPC`, `MSH`, `OBR`, `ORC` | `OMI_O23` | | OML | O21 | `MSH`, `ORC` | `OML_O21` | | OML | O33 | `MSH`, `ORC`, `SPM` | `OML_O33` | | OML | O35 | `MSH`, `ORC`, `SAC`, `SPM` | `OML_O35` | | OML | O39 | `MSH`, `ORC` | `OML_O39` | | OML | O59_A | `MSH`, `ORC` | `OML_O59` | | OMP | O09 | `MSH`, `ORC`, `RXO`, `RXR` | `OMP_O09` | | ORM | O01 | `ORC` | (retained transcription) | | ORU | R01, R40, R42, R43 | `MSH`, `OBR` | `ORU_R01` | | ORU | R30, R31, R32 | `MSH`, `OBR`, `OBX`, `ORC`, `PID` | `ORU_R30` | | SIU | S12 to S24, S26, S27 | `MSH`, `RGS`, `SCH` | `SIU_S12` | | VXU | V04 | `MSH`, `PID` | `VXU_V04` | `MSH` appears throughout because the publication requires it. It can never produce this warning in practice: a message with no `MSH` fails earlier, with the Tier-3 `NO_MSH_SEGMENT` error. ## The opt-in published-structure validator `validateMessageStructure(msg)` answers the question the presence net does not: **does this message follow the shape the publication gives its trigger event, in the published order, with each segment appearing as many times as the publication allows?** You ask for it explicitly; nothing calls it for you. It reports three things, each under its own stable code: | Code | What it means | | --- | --- | | `STRUCTURE_SEGMENT_OUT_OF_ORDER` | The message's segment sequence stops being a beginning the published order allows, reading that order with every published bound honoured. | | `STRUCTURE_SEGMENT_CARDINALITY` | The segment occurs fewer times than the published minimum along the path from the structure root, or more times than the published maximum. | | `STRUCTURE_SEGMENT_UNEXPECTED` | The published structure does not name this segment anywhere, a `Z` segment included. It carries `severity: "warning"` rather than `"error"`, because HL7 reserves `Z` segments for exactly this. | A finding carries the code, a severity, a **PHI-free locus** (segment name, 0-indexed occurrence, published structure id) and a human-readable message. No field, component or subcomponent value is ever read into one. **How the order check reads the publication.** It reads it as written: every node may occur as few and as many times as the publication says **at that locus**. A group it lets occur more than once may repeat, which is what lets a conformant `ORU^R01` carry `OBR OBX OBR OBX`; a group bounded at one occurrence may not be re-entered, so a `VXU^V04` carrying `PV2` before `PV1` inside a `PATIENT_VISIT` group the publication bounds at `[0..1]` is reported; and no occurrence of a group may begin without the child the publication requires first, so an `ADT^A01` carrying `IN2` before its `IN1` is reported too, though `INSURANCE` repeats and both counts are inside their bounds. **One bound is dropped, and only where the count check has already reported it.** The three questions are kept apart so that one defect is reported as one thing. Reading the order with every bound honoured would make an absent required segment stop the sequence dead, and everything after it would read as misplaced on top of the missing-segment finding. So for a segment name the cardinality check reports, that bound alone is dropped from the order reading: too few occurrences drops its minimum, too many drops its maximum. Every other bound still stands, including every bound on every other segment, so a transposition further down the same message is still reported. **Which occurrence the finding names: the one that arrived late.** Two segments in an order the publication does not allow give two candidates, the one that came early and the one that came late, and the finding names the late one: the segment the publication puts first and the message delivered second. So a `PV2` before its `PV1` is reported against `PV1`, and a `GSP` before its `PD1` against `PD1`. Where the reading stops is deliberately not what gets named. The order reading stops at the first segment it cannot place, and where the publication is free to skip past the segment the message delayed, that is somewhere else entirely: an `ADT^A01` carrying `GSP` before `PD1` reads cleanly as far as the `PD1`, because skipping the optional `PD1` is how the `GSP` was read, so it stops on the `PD1` and every segment after the stop is in its published place. Naming the stop would name a healthy segment about as often as the defect. So the question is asked directly instead: is there an adjacent pair the message delivered in one order and the publication takes in the other. Where an exchange leaves a sequence the publication derives, that pair is the defect and its second member is the finding's locus. Where more than one pair would do, the earliest is taken, because a message is read from the front. **Every adjacent pair is asked**, rather than a shortlist near the point the reading stopped, and in a message that repeats a group the two are often nowhere near each other. An `OML^O21` carrying three orders, with the first order's note delivered after the second order's `ORC`, reads on for six more segments before the publication runs out of places to put what arrives: the second order absorbs the note, and it is the third order's controller that finally has nowhere to go. Asking every pair is affordable because the answer for all of them comes out of one pass backwards over the message and one forwards, rather than one pass per pair, so the cost is a fixed handful of readings however long the message is. Where no exchange derives, the defect is something other than two segments in the wrong order and there is no second member to name. The finding then names the segment that belonged where the reading stopped, if the message carries one later, and failing that the segment that could not be placed. **The one silence left.** The order check asks whether the message so far is a **beginning** the publication allows. It does not ask whether the message stops in the middle of one. A message that ends inside a group occurrence, with a required child of that occurrence that never arrives, is left to the cardinality check, and that check reads minimums from the structure root, where a segment inside an optional group is not required. So a message that ends part way through an optional group's occurrence draws no finding. That is a missing segment rather than a misplaced one, and it is the end of the message alone: the same gap with any segment after it is reported, because that next segment cannot be placed. A line carrying no segment name at all is not validated. A message that ends with a segment terminator parses as a trailing segment whose type is the empty string; the publication names segments rather than blank lines, so that line draws no structural finding. The parse still reports it, under `UNKNOWN_SEGMENT`. ```ts runnable import { parseHL7, validateMessageStructure } from "@cosyte/hl7"; // A synthetic ADT^A01 whose PV1 arrives before its PID. const raw = [ "MSH|^~\\&|SEND|FAC|RECV|FAC|20260419101500||ADT^A01|EX00001|P|2.5.1", "EVN|A01|20260419101500", "PV1|1|I", "PID|1||", ].join("\r"); const result = validateMessageStructure(parseHL7(raw)); result.validated; // => true result.structureId; // => "ADT_A01-A" result.findings.length; // => 1 result.findings[0]?.code; // => "STRUCTURE_SEGMENT_OUT_OF_ORDER" result.findings[0]?.locus.segment; // => "PID" result.findings[0]?.locus.occurrence; // => 0 ``` ### Zero findings is not an attestation An empty `findings` list means **this message did not break the published structure in the three ways above**. It is not a statement that the message is conformant, and it is not a certificate anybody issued: - Field content, components, datatypes, lengths, value sets and HL7 tables are all unchecked here, and exhaustive coded-value validation is permanently out of scope for this package. - Coverage is twelve message codes, not the standard. - The publication behind it is vendored at a fixed commit, and its publisher states that the guide is in development and not yet production ready. - Where the publication splits a structure into variants, conforming to **one** variant is conforming to the family, so findings come back only when every variant is violated; `structureId` names the one variant the returned findings belong to and `structureIds` lists the family that was considered. ### Read `validated` before you read `findings` `validated: false` means the publication cannot answer for this message, which is a different answer from "no findings". `reason` says which case it is, and `reasonMessage` says it in prose: | `reason` | When | | --- | --- | | `NO_MESSAGE_TYPE` | MSH-9.1 is absent or blank. | | `UNRECOGNIZED_MESSAGE_TYPE` | The registry models no published structure for this message type. | | `RETAINED_TRANSCRIPTION` | The type is recognized only through the one retained transcription (`ORM^O01`), which has no published order or maximum behind it. | | `EMPTY_EXPECTATION` | The published structure resolves to no ordered expectation at all. A structure that says nothing is not a structure that forbids everything. | ```ts runnable import { parseHL7, validateMessageStructure } from "@cosyte/hl7"; const raw = "MSH|^~\\&|SEND|FAC|RECV|FAC|20260419101500||QRY^A19|EX00002|P|2.5.1\rQRD|20260419101500"; const result = validateMessageStructure(parseHL7(raw)); result.validated; // => false result.reason; // => "UNRECOGNIZED_MESSAGE_TYPE" result.findings.length; // => 0 result.structureId; // => "" ``` ### How it differs from the other two structural surfaces | | What it checks | Who wrote the rules | When it runs | | --- | --- | --- | --- | | `msg.structure` | Whether a segment the publication gives a minimum of one is **present at all** | HL7's publication | Every parse, and it is what emits `MISSING_EXPECTED_GROUP` | | `validateMessageStructure` | Segment **order**, **occurrence counts** and **unnamed segments** against the published structure | HL7's publication | Only when you call it | | `validateAgainstProfile` | Usage, cardinality, length, value sets and coding systems, down to the component | **You**, in a profile you author | Only when you call it | The two opt-in checks are complementary rather than alternatives: the published structure knows nothing about your interface spec, and your interface spec usually does not restate the publication's own message shape. ### Nothing changes for a caller who does not ask No new warning code exists, `msg.warnings` is unchanged, `msg.structure` is unchanged, and validation is read-only: the message's segments, warnings and serialization are byte-identical afterwards. That separation is deliberate. The volume section below records what happened the last time structural expectations widened underneath a live channel, and folding these findings into the default parse would do it again to every channel that treats a warning as a rejection. ## What changed, difference by difference The registry used to be a hand transcription of the v2.5.1 chapters covering twelve message codes, eight ADT trigger events, and a hand-picked "anchor" segment per group. Every difference between that table and the derived registry is listed below, with the segment, the published minimum along the path from the structure root, and an adjudication. **Nothing here is a bug fix to your messages.** These are expectations that widened, so a feed that already emitted non-conformant traffic will see warnings it did not see before. Read the "before you upgrade" section at the end. ### Expectations added to a pair that was already recognized | Pair | Segment | Published minimum | Adjudication | | --- | --- | --- | --- | | ADT^A01, A02, A03, A04, A05, A08, A11, A13 | `EVN` | 1 | Intended correction. The previous table excluded `EVN` deliberately, reasoning that "real senders omit it freely". That is a statement about senders, not about the standard, and it is exactly what stopped the library telling you an `ADT^A01` was missing a segment the standard marks required. All four published `ADT_A01` variants give `EVN` a minimum of one. | | every recognized pair | `MSH` | 1 | Intended correction. Required in every published structure. Unreachable in practice: an `MSH`-less message is a Tier-3 parse error before this check runs. | | ORU^R01 | `OBR` | 1 | Intended correction. `OBR` sits inside `ORDER_OBSERVATION`, which the publication gives a minimum of one inside `PATIENT_RESULT`, which it also gives a minimum of one. The previous table accepted `OBR` **or** `OBX`, so a report carrying only `OBX` was silent; it now warns. | | OMG^O19 | `OBR` | 1 | Intended correction. The previous table excluded `OBR` from every order message on the reasoning that it is optional in some of them. It is optional in `OML_O21`, `OML_O39` and `OML_O59`, and required in `OMG_O19` and `OMI_O23`. Deriving per structure resolves what one hand-picked rule could not. | | OMI^O23 | `OBR`, `IPC` | 1 | Intended correction. Same reasoning as `OMG^O19`; `IPC` (imaging procedure control) is required in the published imaging order. | | OMP^O09 | `RXO`, `RXR` | 1 | Intended correction. The published pharmacy order requires the order and route segments. | | SIU^S12 through S24, S26 | `RGS` | 1 | Intended correction. The published scheduling structure requires at least one resource group. | | MDM^T02, T06 | `EVN`, `PV1`, `OBX` | 1 | Intended correction. All three sit at a minimum of one along the whole path from the root in all five published `MDM_T02` variants, which is the structure both trigger events reference: `EVN` and `PV1` directly at the top level, `OBX` inside a document content group that is itself required. `EVN` was absent from the old table for every message code, not just for ADT, on the same reasoning that "real senders omit it freely"; a document notification is no more exempt from the published minimum than an admit is. Note that this repo's own document fixtures carry `EVN` but no `PV1`, and so now warn on `PV1`: a true positive against the publication, and the clearest single illustration of the volume change. | | DFT^P03 | `EVN` | 1 | Intended correction. | | ACK | `MSH` | 1 | Intended correction. | ### Expectations removed or narrowed | Pair | Segment | Published minimum | Adjudication | | --- | --- | --- | --- | | ORU^R01 | `OBX` | 0 | Intended correction. Every `OBX` in the published `ORU_R01` sits inside a group with a minimum of zero, so a conformant report can carry none. It used to be accepted as a substitute for `OBR`; it is now neither expected nor a substitute. | | ORU^R01 | `NTE` | 0 (family disagreement) | Intended correction. Variants `ORU_R01-C` and `ORU_R01-D` give the order-observation `NTE` a minimum of one; `ORU_R01-A` and `ORU_R01-B` give it zero. Under the family rule the segment is not expected. | | ADT^A20 | `PID`, `PV1` | 0 | Intended correction. A bed-status update carries no patient: the published `ADT_A20` requires `NPU` and neither `PID` nor `PV1`. The previous table applied one ADT shape to all eight of its trigger events, which is the failure mode that motivated deriving per structure. | | ADT^A24, A37, A60 | `PV1` | 0 | Intended correction. These link, unlink and adverse-reaction events carry no visit in the publication. | | SIU, VXU, ORU^R01 | `PID` | 0 | Unchanged, and re-derived rather than assumed. The previous table excluded `PID` from these on a documented judgement; the publication agrees. | ### Trigger events that became recognized Nothing lost recognition. The registry recognized 32 explicit (message code, trigger event) pairs plus `ACK` on the message code alone; it now recognizes 94 plus `ACK` on the message code alone. | Message | Newly recognized trigger events | | --- | --- | | ADT | A06, A07, A09, A10, A12, A14, A15, A16, A17, A20, A21, A22, A23, A24, A25, A26, A27, A28, A29, A31, A32, A33, A37, A38, A40, A41, A42, A43, A44, A45, A47, A49, A50, A51, A52, A53, A54, A55, A60, A61, A62 | | ORU | R30, R31, R32, R40, R42, R43 | | OML | O33, O35, O39, O59_A | | MDM | T01, T03, T04, T05, T07, T08, T09, T10, T11 | | DFT | P11 | | SIU | S27 | ### One retained transcription `ORM^O01` is the only entry that is not derived, and it is marked as such in the registry: ```ts runnable import { findMessageStructureDefinition } from "@cosyte/hl7"; const orm = findMessageStructureDefinition("ORM", "O01"); orm?.derivation; // => "retained-transcription" orm?.structureId; // => "" orm?.requiredSegments.join(","); // => "ORC" ``` The publication carries no `ORM_O01` at all: it is absent from the 305-entry message-structure manifest and its raw path answers 404. `ORM^O01` is the commonest legacy order message and the registry recognized it before, so dropping it would have been a silent loss of coverage. Its expectation is unchanged from the transcription (`ORC`), and `retainedReason` on the entry carries this paragraph's reason in one sentence. ### Pairs the publication leaves unresolved Eight `ADT` merge and change-identifier messages appear in the publication's message list with no structure named: A18, A30, A34, A35, A36, A39, A46 and A48. They are recorded as unresolved by the generator, and the parser stays silent on them rather than guessing a shape. None of them was recognized before, so nothing regressed. ## Before you upgrade: the volume change Three things changed at once, and all three point the same way: 1. **More trigger events are recognized**, so messages that used to be `recognized: false` and silent are now checked. 2. **More segments are expected per pair**, most visibly `EVN` across the whole ADT family and in `MDM` as well, plus `PV1` in `MDM`. `EVN` was excluded from the old table for every message code, so the widening follows it everywhere the publication requires it, `MDM` and `DFT^P03` included. 3. **The warning names a segment rather than a group label**, so `missingGroups` now holds values like `"EVN"` where it used to hold `"patient"`. The net effect is that **warning volume rises on non-conformant traffic**. If your channel treats a `MISSING_EXPECTED_GROUP` warning as a rejection, sample your traffic before upgrading: an interface that quietly omits `EVN` has been accepted until now and will start being rejected. Nothing else changed. The warning is still Tier-2 and additive, lenient parse still never throws on it, the message is never rewritten, and an unrecognized type is still silent. ## PHI None. The warning message carries only structural identifiers: the message type, the segment name and the published structure id, never a field value. The message type is shape-checked before it is interpolated and withheld wholesale if it does not match, rather than truncated. ## Known limitations - **Coverage is twelve message codes.** Widening it means adding structures to the vendored snapshot and re-running the generator. - **The parse-time net is presence, not cardinality or order.** A message carrying one malformed `OBR` satisfies an `OBR` expectation. Order and cardinality are the opt-in validator's question, and it too stops at the segment: neither surface reads a field. - **The publication is a moving target.** It is vendored at a fixed commit and its publisher states that the guide is in development and not yet production ready. The expectations here are that publication's, and where they differ from a given v2 release the difference is not adjudicated. - **A family disagreement always resolves to silence.** Where variants disagree the segment is not expected, so the net is quieter than the strictest variant would be. - **`OML^O59_A` and other underscore-bearing trigger events** are recognized, but the message type is withheld from the warning text because it does not match the identifier shape the warning layer permits. The segment name and the structure id are still named. ## References - HL7's published, machine-readable v2+ message structures, vendored under `vendor/hl7-v2ig/` with per-file hashes and upstream URLs. - `STRUCTURE_REGISTRY_PROVENANCE`, the same record exported from the package. --- URL: https://docs.cosyte.com/hl7/spec-notes-conformance Markdown: https://docs.cosyte.com/hl7/spec-notes-conformance.md # Spec notes: conformance-profile tooling `validateAgainstProfile(message, profile)` runs a **conformance profile you author** against a parsed message and returns typed findings. It is the functionality-plane answer to "does this feed meet _our_ interface spec?" Where [`msg.structure`](https://docs.cosyte.com/hl7/spec-notes-structure.md) checks only the handful of segment groups the base spec marks Required, a conformance profile checks the rules **your** interface declares: which fields are required, how many times they may repeat, how long they may be, and which coded values you accept. There is a third structural surface beside those two, and it is the one this page is most often confused with. [`validateMessageStructure`](https://docs.cosyte.com/hl7/spec-notes-structure.md) runs **HL7's own published** message structure against a message: segment order, segment occurrence counts, and segments the publication does not name. Nobody authors it and nobody can tune it, because it is read off the vendored publication rather than declared. Reach for it when the question is "does this feed follow the standard's shape for this trigger event?", and for `validateAgainstProfile` when the question is "does this feed meet our spec?". They are complementary: the publication knows nothing about your interface, and your interface rarely restates the publication's message shape. The design boundary is deliberate and load-bearing: - **You bring the profile, and every value set.** hl7 ships **no** vendor / IHE / regulatory profile, **no** bundled code set (no LOINC / SNOMED / ICD / RxNorm), and makes **no** network call. Value-set membership is a literal check against the array of codes _you_ supply, and a condition predicate is decided by reading the message in front of it against values _you_ wrote down. This is what keeps the engine a small, corpus-free tool rather than a validator that needs a code corpus to be useful. - **"No findings" is not an attestation.** An empty result means _nothing this profile checked was violated_: never "this message is conformant." The profile only covers what you declared; everything undeclared is unchecked, and hl7 issues no conformance certification. This is not a substitute for an accredited validator (e.g. NIST GVT). What an empty result _does_ tell you depends on the profile's [level](#profile-levels), which every result echoes: only an implementable profile has removed all its own optionality, so only there is the assessment against that profile complete. ## The four invariants 1. **Never throws.** Any message × any profile × any resolution argument (even a malformed one, or a value forced through `any`) returns a `ConformanceResult`, never an exception. A malformed profile becomes `PROFILE_MALFORMED` findings (never a silent pass). For fail-fast authoring, `defineConformanceProfile(profile)` runs the same shape check and **throws** `ProfileDefinitionError` on a defect. 2. **Valid ⇒ zero findings.** 3. **No PHI in findings.** Every finding names the structural **locus** (segment, field, component, repetition, occurrence) and the rule that fired, never the offending value. A "value not in set" finding reports _where_ and _how many codes_ the set held, never _what the value was_. 4. **Read-only.** Validation never mutates the message. ## Usage codes (HL7 conformance methodology) A rule's usage is one of the seven simple codes below, or a **declared conditional** written `C(t/f)` whose two outcomes are each a simple code (HL7 Conformance Methodology: Message Profiles; IHE ITI TF Vol.2 Appendix C): | Code | Meaning | Engine behaviour | | -------- | ------------------------- | -------------------------------------------------------------------------- | | `R` | Required | absent ⇒ `PROFILE_REQUIRED_ABSENT` | | `RE` | Required, may be Empty | absence is **never** a violation | | `C` | Conditional | with a `condition`: true ⇒ `R`, false ⇒ `X`. Without one: presence **not evaluated** | | `CE` | Conditional, may be Empty | with a `condition`: true ⇒ `RE`, false ⇒ `X`. Without one: presence **not evaluated** | | `O` | Optional | no presence constraint | | `X` | Not permitted | present ⇒ `PROFILE_NOT_PERMITTED` | | `B` | Backward Compatible | presence **not evaluated**; a `condition` is not allowed on it | | `C(t/f)` | Declared conditional | takes the outcome its own `condition` decides, or the one a **caller** resolves; `C` when neither | The outcomes for a bare `C` and `CE` are the ones IHE ITI TF Vol.2 Appendix C states for each: a `C` element shall be populated when the predicate is satisfied and shall not be when it is not; a `CE` element is populated when the predicate is satisfied and the sender knows the value. `B` is deliberately left out: the methodology lists it among the allowable indicators and carries no definition text for it, so nothing says what a predicate on a `B` element would decide, and this engine will not invent an answer. Omitting `usage` on a rule means Optional, and is never refused. ### Usage and cardinality have to agree A segment or field rule may declare both, and the methodology fixes the relationship between them. Three constraints, checked when the profile is **defined**: | The rule declares | Its cardinality must | | ----------------- | ------------------------------------------------------------ | | `R` | have a `min` of 1 or more, if it declares one at all | | anything but `R` | have a `min` of 0, if it declares one at all | | `RE` | be free to carry a `min` above 0: the one documented exception | | `X` | have a `max` of 0, or declare no `max` | The `RE` exception is there so you can say "not always present, but at least three occurrences when it is": that is `RE` with `[3..5]`, which reads as an implied `[0, 3..5]`. Every other non-Required usage takes a minimum of 0, including the conditional codes, whose per-message outcome supplies the rest: the methodology's own worked pairing is `C(R/X)` with `[0..5]`, meaning zero occurrences when the predicate is false and one to five when it is true. A pairing that contradicts itself is `PROFILE_MALFORMED` from `validateAgainstProfile` and a thrown `ProfileDefinitionError` from `defineConformanceProfile`, exactly like any other profile defect. **It is checked against what the rule DECLARES**, so a rule that declares no usage, or no cardinality, is never refused for a pairing it never wrote. `USAGE_CODES` exports that vocabulary as a frozen, ordered listing: `R`, `RE`, `C`, `CE`, `O`, `X`, `C(a/b)`, `B`. **It is a published vocabulary listing, not a membership test for what a rule may declare.** `C(a/b)` is the NOTATION for the declared-conditional family, so it is listed and is refused on a rule (`a` and `b` are placeholders, not usage codes), while `C(RE/X)` is accepted on a rule and is not listed. What a rule may declare is answered by the `UsageCode` type at compile time and by `defineConformanceProfile` at run time. ## Condition predicates: the engine evaluates them against the message A conditionally-used element's usage is decided by a **condition predicate**: a computable test based on other values within the same message. Attach one to a rule's `condition` and the engine evaluates it and applies the outcome it selects. ```ts runnable import { parseHL7, validateAgainstProfile, type ConformanceProfile } from "@cosyte/hl7"; // "PID-8 (Administrative Sex) is Required when a date of birth was sent, and // Not-permitted when one was not." const profile: ConformanceProfile = { name: "conditional-sex", segments: [ { segment: "PID", fields: [ { field: 8, usage: "C(R/X)", condition: { location: { segment: "PID", field: 7 }, presence: "is valued" }, }, ], }, ], }; const head = "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01|MSG00001|P|2.5"; const check = (pid: string) => validateAgainstProfile(parseHL7([head, pid].join("\r")), profile); // A date of birth, no sex: the predicate is true, so `R` applies and fires. check("PID|1||MRN12345^^^HOSP^MR||Doe^John||19800115|").findings[0]?.code; // => "PROFILE_REQUIRED_ABSENT" // No date of birth, a sex: the predicate is false, so `X` applies and fires. check("PID|1||MRN12345^^^HOSP^MR||Doe^John|||M").findings[0]?.code; // => "PROFILE_NOT_PERMITTED" // Both present: the predicate is true, `R` applies, and PID-8 satisfies it. check("PID|1||MRN12345^^^HOSP^MR||Doe^John||19800115|M").findings.length; // => 0 ``` ### The shape of a predicate A predicate is a **presence statement**, a **comparison statement**, or two of those joined by a **connector**. It nests: either side of a connector may itself be a connected predicate. A **location** is a structural coordinate that narrows from the outside in: `segment`, then the 1-indexed `field`, `component` and `subcomponent`. A level may not be skipped, so a `component` needs a `field` and a `subcomponent` needs a `component`. | Statement | Asks | | -------------------------------------- | ------------------------------------------------------------------- | | `{ location, presence: "is valued" }` | does the message carry any content at or below that location? | | `{ location, presence: "is not valued" }` | the complement of the above | | `{ location, verb, values }` | does the content at that location stand in `verb`'s relation to one of `values`? | | `{ connector, left, right }` | `AND` both, `OR` at least one, `XOR` exactly one | The verbs are the published closed set. Each negative is the exact complement of its positive twin **for one value**; how the pair behaves where the location carries more than one is the section below this table, and it is not complementary: | Verb | True when the value | | ------------------------------- | ---------------------------------------------------------- | | `is` / `is not` | equals (does not equal) a listed value, whole and case-sensitive | | `contains` / `does not contain` | carries (does not carry) a listed value as a substring | | `matches` / `does not match` | matches (does not match) a listed value read as a regular expression | `PREDICATE_VERBS` and `PREDICATE_CONNECTORS` export both vocabularies as frozen, ordered listings. A regular-expression value is anchored over the **whole** value (`MR\d{5}` describes `MR83452`, not every string containing one) and is read in Unicode mode; a pattern the platform cannot compile is `PROFILE_MALFORMED`. **Presence and comparison read a location slightly differently, and the split is deliberate.** "Valued" asks whether there is any content at or below the coordinate, which is the same reading the `R` and `X` checks use for a field, so a field-only location is valued when any component of any repetition carries something. A comparison reads the value **at** the coordinate, with an omitted `component` and `subcomponent` both defaulting to `1`, exactly as a field rule's own `component` default does. One asks "is anything there", the other asks "what is the value here". **A repeating field or a segment that occurs more than once needs only one match.** A statement is true when one or more of those repetitions or occurrences satisfies it, which is the language's default occurrence semantics. That existential covers the negative verbs too: `is not` is true when _some_ repetition is not one of the listed values, and not when _every_ repetition is not one of them. **So at a repeating location a verb and its negative can both decide true, and that is the trap to read carefully.** `PID-3.1 is 'MRN1'` asks whether some identifier is `MRN1`; `PID-3.1 is not 'MRN1'` asks whether some identifier is not `MRN1`. For `PID-3 = MRN1~MRN2` the answer to both is yes. A negative verb is therefore not a way to ask whether _no_ repetition is a listed value: the language carries no negation connector, so that question cannot be written in a predicate at all, and `AL1-3.1 is not 'PENICILLIN'` decides true for a patient who has that allergy and one other. Where a location carries exactly one value the pair does behave as an exact complement, and where it carries none neither half decides: the comparison is unevaluatable whichever verb it uses. ```ts runnable import { parseHL7, validateAgainstProfile, type ConformanceProfile } from "@cosyte/hl7"; const raw = [ "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ORU^R01|MSG00002|P|2.5", "PID|1||MRN12345^^^HOSP^MR||Doe^John||19800115|M", "OBX|1|NM|WBC^^LN||7.5", "OBX|2|NM|RBC^^LN||4.2", ].join("\r"); // "A comment is Required but may be empty when this report carries a red-cell // count, and Not-permitted when it does not." const profile: ConformanceProfile = { name: "conditional-comment", segments: [ { segment: "NTE", usage: "C(RE/X)", condition: { connector: "AND", left: { location: { segment: "OBX", field: 3, component: 1 }, verb: "is", values: ["RBC"] }, right: { location: { segment: "PID", field: 7 }, presence: "is valued" }, }, }, ], }; // The SECOND OBX satisfies the left operand, and one is enough. validateAgainstProfile(parseHL7(raw), profile).findings.length; // => 0 ``` ### A predicate the message cannot decide A presence statement is **always** determinate: an element the message does not carry is simply not valued, which is an answer. A comparison against an element the message does not carry is **not**: the language compares content and there is no content to compare. Deciding it false would fabricate an answer, and for a `C(R/X)` rule a fabricated false turns a required element into a permitted absence. So an undecidable predicate produces a `PROFILE_CONDITION_UNEVALUATABLE` finding, the rule's usage outcome is never selected, and the element's presence is **not** assessed. A connected predicate is undecidable exactly when its result depends on an undecidable operand: `AND` is false as soon as one side is false, `OR` is true as soon as one side is true, and `XOR` always needs both. ```ts runnable import { parseHL7, validateAgainstProfile, FINDING_CODES, type ConformanceProfile } from "@cosyte/hl7"; const raw = [ "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01|MSG00003|P|2.5", "PID|1||MRN12345^^^HOSP^MR||Doe^John||19800115|", ].join("\r"); // The predicate reads PV1-2, and this message carries no PV1 at all. const profile: ConformanceProfile = { name: "undecidable", segments: [ { segment: "PID", fields: [ { field: 8, usage: "C(R/X)", condition: { location: { segment: "PV1", field: 2 }, verb: "is", values: ["I"] }, }, ], }, ], }; const { findings } = validateAgainstProfile(parseHL7(raw), profile); // PID-8 is absent and the true outcome would have been `R`, but the engine // neither fires that nor reports the element clean. findings.length; // => 1 findings[0]?.code === FINDING_CODES.PROFILE_CONDITION_UNEVALUATABLE; // => true findings[0]?.locus.field; // => 8 // The finding names the location it could not decide at, and nothing else: no // value read from the message, and no value drawn from the predicate's list. findings[0]?.message.includes("PV1-2"); // => true ``` That code is distinct from every other, so an unassessed conditional can be filtered by `finding.code` rather than by matching a message string. ### What a predicate may not be These are `PROFILE_MALFORMED`: the engine reports the defect, validates no message rule against a profile it cannot trust, and still returns a result rather than throwing. - a verb outside the closed set, or a presence statement that is not one of the two; - a comparison with no value list, an empty one, or one carrying a non-string (a sparse array's holes count as non-strings: a length is not a value list); - a regular-expression value the platform cannot compile; - a location naming no addressable element: a bad segment name, a non-positive index, a component with no field, or a subcomponent with no component; - a comparison whose location stops at the segment, since a segment on its own has no single content to compare (a presence statement may stop there: it asks whether the segment occurs); - a statement declaring none of `presence`, `verb` and `connector`, or more than one of them, and a nesting deeper than the language defines. **A key set to `undefined` still declares it**, so a statement assembled from an options bag has to omit the keys it does not use rather than pass them through empty; the engine will not guess which of two declared discriminants you meant, because guessing decides an element's usage from a question you never asked; - a predicate on a rule whose usage is not conditional, including a rule with no usage at all: there is nothing there for it to decide, and ignoring it would be a silent no-op; - a predicate that arrives as `null`, an array, a string, or any other shape a type check could not have stopped. The fail-fast authoring gate refuses every one of them too: `defineConformanceProfile(profile)` throws `ProfileDefinitionError` at the point the profile is written, rather than at the point it is run. ## Declared conditionals a CALLER resolves instead A rule that declares no predicate can still take its outcome from outside, as a resolution you pass alongside the message and the profile: ```ts runnable import { parseHL7, validateAgainstProfile, type ConformanceProfile } from "@cosyte/hl7"; const raw = [ "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01|MSG00001|P|2.5", "PID|1||MRN12345^^^HOSP^MR||Doe^John^Q||19800115|", ].join("\r"); // "PID-8 is Required when the condition holds, Not-permitted when it does not." const profile: ConformanceProfile = { name: "conditional-sex", segments: [{ segment: "PID", fields: [{ field: 8, usage: "C(R/X)" }] }], }; const msg = parseHL7(raw); // YOU decide the condition holds for this message. PID-8 is absent, so the // true outcome (`R`) fires: const held = validateAgainstProfile(msg, profile, [ { segment: "PID", field: 8, outcome: true }, ]); held.findings[0]?.code; // => "PROFILE_REQUIRED_ABSENT" // Resolve it false and the false outcome (`X`) applies instead; PID-8 is // absent, so nothing fires. const didNotHold = validateAgainstProfile(msg, profile, [ { segment: "PID", field: 8, outcome: false }, ]); didNotHold.findings.length; // => 0 // Resolve nothing and the rule behaves exactly as `C` does: presence is not // evaluated at all. validateAgainstProfile(msg, profile).findings.length; // => 0 ``` A resolution names a **locus**: the segment name, plus the 1-indexed field position when the target is a field rule. It applies to the RULE at that locus, so it covers every occurrence of the segment and every repetition of the field; there is no per-occurrence resolution. Five things are refused as `PROFILE_MALFORMED` rather than ignored: - a locus matching no rule the profile declares; - a locus matching a rule whose usage is not a declared conditional; - a locus matching more than one declared rule (it identifies no single rule); - a locus matching a rule that declares its own condition predicate; - two or more resolutions at one locus. **A rule takes its outcome from the predicate or from a resolution, never from both.** Declaring both at one locus is a defect rather than a precedence question: silently preferring either source would decide a patient-safety question by accident of implementation order. Omitting the argument, or passing `undefined`, means "no resolutions". **Any other value that is not a list of well-formed resolutions is a `PROFILE_MALFORMED` finding**, never read as "none": a mis-typed argument must not return a clean result for elements nothing checked. **`C`, `CE`, `B` and a declared conditional are NOT presence-checked when nothing decides them**, that is, when the rule declares no predicate and no resolution names it. Their length / value-set / cardinality rules still apply when the element _is_ present. That keeps "valid ⇒ zero findings" airtight, and it is exactly why zero findings is not an attestation: an element whose presence nothing evaluated is an element nothing asserted. `B`'s behaviour is this library's own bounded choice rather than a sourced requirement. The methodology's table of allowable usage indicators lists `B` and carries no definition text for it, so the reading taken here is the one that imposes no presence obligation while preserving what the profile author declared, and the same silence is why `B` may not carry a predicate. A declared conditional's outcomes may be any simple code at the constrainable level, which is what a profile claims when it declares no level. The methodology separately restricts an _implementable_ profile to `R`, `RE` or `X` per element, and its conditional outcomes likewise: a profile that claims that level is held to it, which the next section is about. ## Profile levels An empty findings list means two different things at two different levels, and that is the whole reason this exists. The methodology defines three **profile levels**, and draws the completeness line between them: a complete assessment of an interface declaring conformance to an implementable profile can be determined, while for standard-level and constrainable-level profiles not all aspects can be. So "zero findings" from a constrainable profile is consistent with parts of the message never having been assessed at all. | Level | The methodology's definition | What this engine does with it | | ------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | `standard` | the base definitions and constraints as-is; considerable openness still exists | echoes it; imposes no level-derived constraint on any element | | `constrainable` | further constrains a parent, but not all element attributes are fully constrained | echoes it; imposes no level-derived constraint on any element | | `implementable` | defines all elements such that all optionality and openness have been removed | echoes it **only if the claim checks out**, else `PROFILE_MALFORMED` | `PROFILE_LEVELS` exports them as a frozen listing, ordered from the least constrained to the most. Unlike `USAGE_CODES` this listing **is** the accept-set: a `level` outside it is `PROFILE_MALFORMED`, and the `ProfileLevel` type refuses it at compile time. **Every result carries the level in force**, beside the profile name, and it is there on every result including one whose profile was too malformed for even its name to be read: ```ts const { profileName, level, findings } = validateAgainstProfile(msg, profile); ``` **Declaring no level claims `constrainable`.** That is the fail-safe direction: it is the weaker of the two claims a working profile can make, so a profile that says nothing never reads as an implementable one, and every profile written before the declaration existed keeps behaving exactly as it did. ### An implementable claim is checked, not believed The methodology's terminus is that in an implementable profile ultimately only two possibilities are allowed: either a specific element is supported (`R` or `RE`) or it is not (`X`); and for a conditional usage the true and false outcomes must also be defined only as `R`, `RE` or `X`. A profile that claims the level is held to exactly that, when the profile is **defined**. Three things refuse the claim, each `PROFILE_MALFORMED` naming the offending rule's structural locus and the offending declaration: - **A usage the level does not admit**: `C`, `CE`, `O` or `B`. Note that `C` and `CE` are refused even though this engine knows outcomes for them: the methodology requires an implementable profile's conditional outcomes to be _defined_, and a bare code defines them nowhere in the profile. Write `C(R/X)` and say so. - **A declared conditional carrying an outcome the level does not admit**, such as `C(R/O)`. - **A rule that declares no usage at all.** An omitted usage means Optional everywhere else and is never refused there; at this level the profile has asserted that optionality is gone, so silence is a claim it cannot make. It applies to segment, field and component rules alike. A refused claim is **never** the level echoed on the result: it falls back to `constrainable`, because a run that returned diagnostics instead of assessing the message verified no claim of completeness. That holds for any profile defect, not only a level-derived one, since a defect anywhere can stop the gate from reaching the rules below it. **The level changes no message check.** It alters nothing about which findings fire for a given message, their severity, cardinality behaviour, predicate evaluation, or the value-set and coding-system checks. It decides whether the _profile_ is refused, never what the _message_ is found to be. **And it is still not an attestation.** The level is the author's claim, checked for internal consistency against the profile itself: never an external, third-party or accredited statement. Zero findings at `implementable` level means the profile removed its own optionality and nothing it declared was violated. It does not mean anyone certified the interface. ## Binding a value set to the coding system its codes come from A bare `valueSet` answers one question: is this code one we accept? It says nothing about **where the code came from**, and the message does: a coded element carries the sender's claimed coding system right beside the code. So a feed sending a plausible, permitted-looking code labelled with the wrong system validates clean against a bare value set, which is the more dangerous of the two mistakes an interface can make about a code. Add `codingSystem` to the rule and the engine compares that claim too: ```ts runnable import { parseHL7, validateAgainstProfile, FINDING_CODES, type ConformanceProfile, } from "@cosyte/hl7"; // "OBX-3 must carry one of our two result codes, and that code must be a LOINC // code." The value set is still yours; the binding says where it must come from. const profile: ConformanceProfile = { name: "loinc-results", segments: [{ segment: "OBX", fields: [{ field: 3, valueSet: ["WBC", "RBC"], codingSystem: "LN" }] }], }; const head = "MSH|^~\\&|LAB|MAIN|EHR|REF|20260419101500||ORU^R01|MSG00004|P|2.5"; const check = (obx3: string) => validateAgainstProfile(parseHL7([head, `OBX|1|NM|${obx3}||7.5`].join("\r")), profile).findings; // A permitted code, correctly labelled LOINC. Nothing fires. check("WBC^White Blood Cells^LN").length; // => 0 // The same code labelled SNOMED CT. Membership still passes, and the binding // catches exactly what membership cannot see. check("WBC^White Blood Cells^SCT")[0]?.code; // => "PROFILE_CODING_SYSTEM_MISMATCH" // A code you do not accept, correctly labelled. A different answer, on purpose. check("XYZ^Mystery^LN")[0]?.code; // => "PROFILE_VALUE_NOT_IN_SET" // Wrong on both counts: exactly one finding of each, never one merged answer. const both = check("XYZ^Mystery^SCT"); both.length; // => 2 both.map((finding) => finding.code).join(" + "); // => "PROFILE_VALUE_NOT_IN_SET + PROFILE_CODING_SYSTEM_MISMATCH" // Case and the well-known aliases resolve to the same registered acronym, so a // feed sending "loinc" against a binding of "LN" is a match, not a false alarm. check("WBC^White Blood Cells^loinc").length; // => 0 // No coding system at all is a finding, never a pass. check("WBC^White Blood Cells")[0]?.code === FINDING_CODES.PROFILE_CODING_SYSTEM_MISMATCH; // => true // The finding names the expected system and the component it read, and never // the claim the message made. const mismatch = check("WBC^White Blood Cells^SCT")[0]; mismatch?.locus.component; // => 3 mismatch?.message.includes("SCT"); // => false mismatch?.message.includes("LN"); // => true ``` **It is opt-in, and it is a second check rather than a different one.** A rule that declares no `codingSystem` behaves exactly as it always has. A rule that declares one runs both checks independently, so a permitted code under the wrong system is one coding-system finding and no value-set finding. **Comparison is by resolved identity, not by string equality.** `LN`, `LOINC`, `loinc` and ` LN ` all name the same system, and so do `SCT`, `SNOMED` and `SNOMEDCT`. The same map [`codingSystem()`](https://docs.cosyte.com/hl7/spec-notes-coding-system.md) uses resolves both sides, so a correct feed writing a different accepted spelling is never reported. **Which component carries the system.** By default the engine reads **two positions after** the component it checks the code at, which is the coded element's own code-to-system relationship: a code at component 1 puts its system at component 3, and an alternate code at component 4 puts its alternate system at component 6. So `{ valueSet, codingSystem }` checks the primary triplet, and adding `component: 4` moves the whole check to the alternate one. Set `codingSystemComponent` explicitly for a field whose layout does not follow that offset. To check **both** triplets, declare two rules on the same field. **An absent or blank claim is reported, not waved through.** A present, non-empty repetition whose coding-system component is missing, empty, whitespace-only, or names a system that cannot be resolved is the same finding with the same code. Deciding otherwise would let the exact case the binding exists for pass by omitting a component. An **empty** repetition is not reported: it claims no code, so it has no provenance to be wrong about, and an absent or empty **field** is left to its usage code exactly as before. **An identifier this library does not recognize is refused when the profile is defined**, as `PROFILE_MALFORMED`, rather than compared against a system that cannot be resolved. So is a binding that is not a string, is blank, declares a `codingSystemComponent` that is not a positive integer, declares that component without an identifier, or is declared on a rule with no `valueSet` at all: a binding with nothing to bind is a defect, not a system-only check. Two consequences worth stating plainly: - **The recognition set is a deliberate subset, not the whole registry.** It is the same safety-relevant map documented in [coding-system provenance](https://docs.cosyte.com/hl7/spec-notes-coding-system.md), so some identifiers that are genuinely registered are still refused here. That is the fail-safe direction: a refusal at authoring time is visible, a comparison against an unresolvable system is not. - **A local or site-specific coding system cannot be bound.** By construction. If your interface uses one, keep the bare `valueSet` form for that rule. **This checks the sender's claim, never the code.** Nothing here verifies that the code exists in the system the message names, or that the system is current. hl7 still ships no code set and makes no network call; the binding lets you encode the assertion your own interface requires, and asserts nothing of its own. ## Declaring what a field's components are A field rule constrains the field. `components` constrains what is **inside** it: a list of 1-indexed component positions, each with an optional usage code. **A component's cardinality is implied by its usage and is never declared.** The methodology requires an explicit cardinality range for segment groups, segments and field elements, and states that components do not carry one: the range follows from the usage code instead. `R` implies `[1..1]`, `RE` and `O` imply `[0..1]`, and `X` implies `[0..0]`. A component has no repetition construct in HL7 v2, so those reduce to presence: `[1..1]` is "valued here" and `[0..0]` is "not valued here". Declaring a `cardinality` on a component rule is `PROFILE_MALFORMED`, not an extra constraint the engine quietly honours. **Declaring component rules CLOSES the field's component set**, and that is the point. Without it the engine grades only what you wrote down, so a vendor stuffing a fourth component into a field you specified with three passes clean. The methodology counts that as a conformance violation rather than harmless extra content, and its own example is exactly that case: data present for a fourth component where the profile defines three. Content at a component index your rule does not declare is `PROFILE_UNDECLARED_CONTENT`, one finding per undeclared index per repetition. ```ts runnable import { parseHL7, validateAgainstProfile, FINDING_CODES, type ConformanceProfile, } from "@cosyte/hl7"; // "A patient identifier is an ID in component 1, an assigning authority in // component 4 and an identifier type in component 5. Components 2 and 3 are not // supported, and there is nothing past component 5." const profile: ConformanceProfile = { name: "identifier-shape", segments: [ { segment: "PID", fields: [ { field: 3, name: "Patient Identifier List", components: [ { component: 1, name: "ID Number", usage: "R" }, { component: 2, name: "Check Digit", usage: "X" }, { component: 3, name: "Check Digit Scheme", usage: "X" }, { component: 4, name: "Assigning Authority", usage: "RE" }, { component: 5, name: "Identifier Type Code", usage: "R" }, ], }, ], }, ], }; const head = "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01|MSG00001|P|2.5"; const check = (pid3: string) => validateAgainstProfile(parseHL7([head, `PID|1||${pid3}||Doe^John`].join("\r")), profile).findings; // A conforming identifier. Nothing fires. check("MRN12345^^^HOSP^MR").length; // => 0 // No identifier type: component 5 is Required, so its implied [1..1] fires. check("MRN12345^^^HOSP")[0]?.code; // => "PROFILE_REQUIRED_ABSENT" check("MRN12345^^^HOSP")[0]?.locus.component; // => 5 // A check digit where the profile says X: its implied [0..0] fires. check("MRN12345^7^^HOSP^MR")[0]?.code; // => "PROFILE_NOT_PERMITTED" // A SIXTH component, which the profile never declared at all. const extra = check("MRN12345^^^HOSP^MR^SURPRISE"); extra.length; // => 1 extra[0]?.code === FINDING_CODES.PROFILE_UNDECLARED_CONTENT; // => true extra[0]?.locus.component; // => 6 // PHI-safe like every other finding: the surprising value is never named, and // the message carries the locus plus how many components the rule declares. extra[0]?.message.includes("SURPRISE"); // => false extra[0]?.message.includes("component 6"); // => true // An EMPTY list carries no component rule, so it says exactly what leaving the // member out says: no depth declared, nothing closed, nothing reported. const noDepth: ConformanceProfile = { name: "no-depth", segments: [{ segment: "PID", fields: [{ field: 3, components: [] }] }], }; const raw = parseHL7([head, "PID|1||MRN12345^^^HOSP^MR^SURPRISE||Doe^John"].join("\r")); validateAgainstProfile(raw, noDepth).findings.length; // => 0 ``` **Omitting `components` changes nothing, and an empty list is omitting it.** A field rule that declares none declares no depth, is checked exactly as it always has been, and can never emit `PROFILE_UNDECLARED_CONTENT`. The check is additive and opt-in per rule. `components: []` declares no component rule, so it means "no depth declared" and never "this field has no components": a list built by a filter that matched nothing constrains nothing, rather than making every component of every repetition undeclared content. `components` (plural) is not `component` (singular). The singular one says which component the `length` and `valueSet` checks read; it declares no component rule and closes nothing. A few boundaries worth stating plainly: - **Declaring an index is what closes the set, not constraining it.** `{ component: 4 }` with no usage means "component 4 exists and is Optional", which is enough to stop it being undeclared content. - **An empty component is not content.** Trailing separators, an empty component and an empty subcomponent are all absence, so none of them is reported. A component is valued when any of its subcomponents carries something. - **Component rules are evaluated per PRESENT repetition.** An absent or empty field is left to its own usage code exactly as before, and an empty repetition inside a present field reports nothing. - **A conditional usage on a component is never decided.** A component rule carries no `condition` and a caller resolution names a segment plus a field, so `C`, `CE`, `B` and a declared conditional all leave a component's presence unevaluated, on the same terms an undecided conditional field rule gets. - **This goes exactly one level deep.** Sub-component rules are not supported, and a component rule says nothing about what is inside its component. - **The check is only as good as the depth you declare.** A shallow profile still under-reports, which is a property of the profile rather than of the engine: an undeclared field, an undeclared segment and an undeclared sub-component are all still unchecked, and "no findings" is still not an attestation. ## The finding codes Stable, additive codes on `FINDING_CODES`. Segment-level vs field-level is told apart by whether the finding's `locus.field` is set, not by separate codes. - `PROFILE_REQUIRED_ABSENT`: an `R` segment or field is absent / empty. - `PROFILE_NOT_PERMITTED`: an `X` segment or field is present. - `PROFILE_CARDINALITY`: a segment-occurrence or field-repetition count is out of range. - `PROFILE_LENGTH`: a checked component value exceeds the declared max length. - `PROFILE_VALUE_NOT_IN_SET`: a checked component value is not in the supplied value set. - `PROFILE_CONDITION_UNEVALUATABLE`: a rule's condition predicate could not be decided against this message, so its usage outcome was never selected and the element's presence was not assessed. - `PROFILE_CODING_SYSTEM_MISMATCH`: a present repetition does not carry the coding system the rule binds its value set to. - `PROFILE_UNDECLARED_CONTENT`: a present repetition carries content at a component index the field rule does not declare. Only a rule carrying at least one `components` entry can emit it. - `PROFILE_MALFORMED`: the profile itself is structurally malformed (a diagnostic). Length and value-set checks read the value at the rule's `component` (default `1`, a coded element's code), applied per present repetition. `cardinality.min` is checked only when the element is present, so a missing `R` field with `cardinality.min = 1` yields exactly one finding, not two. ## Worked example ```ts runnable import { parseHL7, validateAgainstProfile, type ConformanceProfile } from "@cosyte/hl7"; // A message whose administrative sex (PID-8) is "Q", not a value we accept. const raw = [ "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01|MSG00001|P|2.5", "EVN|A01|20260419101500", "PID|1||MRN12345^^^HOSP^MR||Doe^John^Q||19800115|Q", ].join("\r"); // A profile the CONSUMER authors, an example, NOT an attestation. const profile: ConformanceProfile = { name: "our-adt-intake", segments: [ { segment: "MSH", usage: "R", fields: [{ field: 10, name: "Control ID", usage: "R" }] }, { segment: "PID", usage: "R", cardinality: { min: 1, max: 1 }, fields: [ { field: 3, name: "Patient Identifiers", usage: "R", cardinality: { min: 1, max: 1 } }, { field: 8, name: "Administrative Sex", usage: "RE", valueSet: ["M", "F", "U"] }, ], }, { segment: "ZZZ", usage: "X" }, // no local Z-segments permitted ], }; const result = validateAgainstProfile(parseHL7(raw), profile); result.profileName; // => "our-adt-intake" // The profile declares no level, so it claims the weaker one and is assessed // at it: parts of a message an all-optional rule never constrained were never // asserted either way. result.level; // => "constrainable" result.findings.length; // => 1 const f = result.findings[0]; f?.code; // => "PROFILE_VALUE_NOT_IN_SET" f?.severity; // => "error" f?.locus.segment; // => "PID" f?.locus.field; // => 8 // PHI-safe: the offending value "Q" is NEVER in the finding message. f?.message.includes("Q"); // => false // A message that conforms produces zero findings, which is NOT an attestation, // only "no declared rule was violated". const clean = raw.replace("|Q", "|M"); validateAgainstProfile(parseHL7(clean), profile).findings.length; // => 0 ``` ## Sources - HL7 v2 **Conformance Methodology: Message Profiles** (the usage indicators allowable in a message profile, `C(a/b)` and `B` included, plus cardinality, length and value-set binding), and its **Predicate Definition** requirement that a declared conditional's predicate be testable and based on other values within the message. - HL7 v2 **Conformance Methodology, Appendix A: Predicate Definition Language** (the template pattern, the closed verb set, the content-declarative statements and the `AND` / `OR` / `XOR` connectors this engine's predicate shape mirrors). - **IHE ITI Technical Framework Vol.2 Appendix C**: "HL7 Profiling Conventions" (`R`/`RE`/`C`/`CE`/`O`/`X`, cardinality `[min..max]`, table references). - **NIST IGAMT / GVT**: the public-domain profile-authoring + validation model this engine's bounded subset mirrors. hl7 imports none of their code sets; a NIST-IGAMT XML import adapter is a possible later, separate addition. --- URL: https://docs.cosyte.com/hl7/spec-notes-identity Markdown: https://docs.cosyte.com/hl7/spec-notes-identity.md # Spec notes: patient-identity / merge events `msg.identityEvents()` is the read-side surface for the ADT identity-management trigger events. It exists for one safety-critical reason: **a mis-applied patient merge conflates two patients or orphans clinical data under a retired MRN**, the highest-harm ADT path the library previously left unmodelled. The helper's job is to hand the consumer both sides of the merge, *labelled by role*, so the consumer can never accidentally retire the survivor. ## Spec traceability HL7 v2 Chapter 3 (Patient Administration): - **A18** (merge patient information, backward-compat): *"The PID segment contains the surviving patient ID information. The MRG segment contains the non-surviving information."* The merge direction is therefore **spec-explicit and constant: MRG (prior) → PID (surviving)**. The helper surfaces it as the literal `direction: "MRG_TO_PID"` on every merge/move event and **never infers it from message content**. - **A39/A40** (merge person / merge patient identifier list): the incorrect source identifiers travel in MRG and are *"logically never referenced in future transactions"* once merged into the target identifiers in PID. - Recognized triggers and their classification: - `merge`: A18, A34, A35, A36 (v2.3-era merges), A39, A40, A41 (account), A42 (visit). Two records become one; the prior record is retired. - `move`: A43 (patient identifier list), A44 (account), A45 (visit). Information moves between identifier lists, accounts or visits that both go on existing. - `change`: A47 (patient identifier list), A49 (account number), A50 (visit number), A51 (alternate visit ID). One identifier is replaced by another on the **same** record: the direction is still MRG (prior) to PID (surviving), but nothing is being conflated, so a consumer keying on `merge` must not act on them. - `link` / `unlink`: A24 / A37 (two or more PID groups; the conformant shape carries **no MRG**, a nonconforming MRG is still surfaced, see the fail-safe section) - `add` / `update`: A28 / A31 (person add/update, no MRG) - **The recognized set is floored by the published structures.** Every ADT trigger event whose published structure *requires* MRG is recognized, whether or not it is hand-classified above, so the read side can never fall behind the structures the parser validates against. An MRG-requiring event with no hand-classified kind is surfaced as `change` rather than `merge`: the conservative choice, because a consumer keying on `merge` must not begin conflating records on an event nobody reviewed. - **MRG field map (version-scoped, v2.5.1 §3.4.10):** MRG-1 prior patient identifier list (CX, repeating) → `prior.identifiers`; MRG-3 prior patient account number → `prior.accountNumber`; MRG-4 prior patient ID (backward-compat, **withdrawn as of v2.7** in favour of MRG-1) → `prior.legacyPatientId`; MRG-5 prior visit number → `prior.visitNumber`; MRG-7 prior patient name (XPN) → `prior.name`. - **Version gate:** on a message whose MSH-12 declares v2.7 or later, the withdrawn MRG-4 (and the symmetric withdrawn PID-2 on the surviving side) are **not read** as identity fields. An absent or unparseable MSH-12 falls back to the pre-v2.7 map (real-world ADT traffic is overwhelmingly v2.3 to v2.5.1, where those fields are legal). ## The role-labelling invariant Every party carries `role` **and** `sourceSegment` provenance: - `surviving` / `subject` / `linked` parties are only ever built from **PID (+ the group's PV1)**. - `prior` parties are only ever built from **MRG**. A survivor can never be sourced from MRG and a prior can never be sourced from PID. The invariant is structural (two separate builders) and enforced by a property test over randomized segment layouts. ## Fail-safe behavior A merge/move event with an incomplete MRG→PID pair surfaces **what is present** plus a `MERGE_MISSING_PRIOR_OR_SURVIVOR` warning *on the event* (read-side helpers never mutate `msg.warnings`): - PID group with no MRG → warning (`missing: prior`), survivor still surfaced. - **An orphaned MRG is never dropped**. It yields its own event with the prior surfaced and a `missing: survivor` warning. - A side that is present but carries **no usable identity field** warns too. "Usable" means a CX with an actual ID number (an assigning-authority-only `^^^HOSP` does not count) in the identifier list, the legacy single-ID field, the account number, or the visit number (account/visit count because they are the merge keys of A41/A42); a name alone never counts. This covers a PID with an empty PID-3, and (deliberately) a v2.7+ MRG whose only content was the version-gated MRG-4: the consumer must be able to distinguish "nothing to retire" from "prior gated away". - The direction is **never guessed** on incomplete pairs; consumers should treat any warned event as not-safe-to-apply and route it for review. Warning messages carry only structural facts (trigger code + which role is missing), never an identifier, name, or any other field value (no PHI). ## Known limitations / non-goals - The helper **surfaces** the merge; it does **not apply** it. Re-pointing stored data to the survivor (and maintaining a link graph for A24/A37) is the integration engine's / consumer's responsibility. - No patient matching or probabilistic linkage, ever. - **Not modelled** (these triggers return `[]`): **A30** (merge person information, the v2.3 backward-compat sibling of A34 to A36), **A46** (change patient ID) and **A48** (change alternate patient ID). The published structures this library ships do not name them, so there is no structure to floor them against; a consumer that needs them can read the raw segments meanwhile. - MRG-2 / MRG-6 (prior *alternate* patient/visit IDs, withdrawn v2.7) are deliberately not surfaced. **A51** (change alternate visit ID) is recognized and its parties are surfaced, but its own merge key is MRG-6: an A51 whose only prior content is an alternate visit ID carries no usable prior identity field and therefore warns, exactly as the fail-safe above describes. - `identityEvents()` keys on the trigger event alone (MSH-9.2, falling back to EVN-1). It does not require MSH-9.1 to be `ADT` (lenient toward vendor quirks). --- URL: https://docs.cosyte.com/hl7/spec-notes-batch Markdown: https://docs.cosyte.com/hl7/spec-notes-batch.md # Spec notes: batch / file envelope splitting `splitBatch(raw, optionsOrProfile?)` demarcates the individual `MSH`-led messages inside an HL7 v2 **batch / file** stream and hands each one back already parsed (or, if it hit a fatal, as a typed failure entry). Real lab / ELR / IIS feeds routinely ship many messages wrapped in the batch envelope, and `parseHL7` rejects non-`MSH`-first input outright (correct for a single-message parser). So batch demarcation is a **separate surface**. ## Why a splitter, not a parser option The single-message parser's contract is "first segment is `MSH`". A batch file starts with `FHS` or `BHS`. Folding batch handling into `parseHL7` would either weaken that invariant or silently swallow the envelope. Keeping `splitBatch` separate preserves both contracts: `splitBatch` finds the message boundaries, then delegates each message to `parseHL7` unchanged (forwarding your profile / `strict` / `charset` / `dateFormats`). ## Spec traceability HL7 v2 Chapter 2, the batch protocol: - **§2.10.3 (abstract syntax):** `[FHS] { [BHS] { [MSH …] } [BTS] } [FTS]`. **Every envelope segment is optional**; the braces repeat (a file holds many batches; a batch holds many messages). `splitBatch` models exactly this: an optional file header/trailer and the `MSH`-led messages between them. A **batch** is a run of messages delimited by a `BHS` header **and/or** a `BTS` trailer: both are optional, so a `BTS` closes the preceding run into a batch even with no `BHS`. A run of messages with **neither** a header nor a trailer is not a batch: those messages are still yielded in `messages` but belong to no entry in `batches` (see "What is isolated" below). - **§2.15, BTS (batch trailer):** **BTS-1** batch message count (**optional, `[0..1]`**), BTS-2 comment, **BTS-3** batch totals (repeating, NM). - **§2.15, FTS (file trailer):** **FTS-1** file batch count. - **§2.10.3.3, acknowledging batches:** three modes (ack-all / manual control report / ack-errored-only) on an **exception basis**. Batch ACK is **out of scope** for the splitter (see non-goals). Real-world grounding: - **National CDC/APHL ELR** makes batching **optional**: the Lab-to-EHR profile defines no envelope. A bare message must therefore pass straight through, and a batch with no counts must not be second-guessed. - **IIS file submission** (e.g. Texas ImmTrac2 FTP) **mandates** the full `FHS/BHS…BTS/FTS` frame and rejects a file on any missing header/trailer. `splitBatch` **surfaces** a missing trailer (`BATCH_MISSING_TRAILER`) but does **not** reject: enforcing a profile's mandatory-envelope rule is the caller's decision. ## Count reconciliation (fail-safe) `splitBatch` reconciles the two **well-defined** declared counts against what it actually split: | Declared | Reconciled against | On mismatch | | ----------------------------- | ---------------------- | --------------------------------------- | | **BTS-1** batch message count | messages in that batch | `BATCH_COUNT_MISMATCH` (unit `message`) | | **FTS-1** file batch count | batches in the file | `BATCH_COUNT_MISMATCH` (unit `batch`) | The mismatch is **surfaced, never repaired**: every message is still returned. The splitter never drops the tail to make the numbers agree. A count that is **absent** (BTS-1 is `[0..1]`) or **non-numeric** tolerantly disables reconciliation rather than fabricating a mismatch. Both new warnings live on the returned `BatchSplitResult.warnings` (**never** on `Hl7Message.warnings`) and carry only integers / segment names / positions. **No PHI**: envelope headers carry facility/app identifiers, but the warnings never echo a field value. A few structural edges are handled so they neither drop data nor fabricate a false signal: - **FTS-1 reconciliation is per-file.** A concatenated second `FHS` resets the batch count, so two individually-conformant files do **not** raise a false `BATCH_COUNT_MISMATCH`. - **A lone `BTS` closing nothing** (no messages, no `BHS`) is ignorable noise: it creates no phantom empty batch and no fabricated mismatch. Note the deliberate asymmetry: an **empty `BHS`-opened batch** (a header with no messages) _is_ a batch and counts toward FTS-1: a `BHS` explicitly opens one, whereas a lone trailer opens nothing. - **A trailing blank line** immediately before a boundary is inter-message whitespace, not part of the message (`parseHL7` likewise strips a single trailing CR); interior empty segments are preserved, so each message's `raw` re-parses to exactly the segments the entry reports. - **A missing _trailer_ warns** (`BATCH_MISSING_TRAILER`) because content after a truncation point may be lost; a missing _header_ does **not** warn: nothing is lost by a `BTS`/`FTS` that closes an un-opened scope. ## What is isolated, surfaced: never silently discarded `splitBatch` never _silently_ loses content: anything it cannot place in a well-formed message comes back as a typed **failure entry** in `messages`. - A **malformed message mid-batch** (one that trips a Tier-3 fatal: no `MSH`, truncated `MSH`, bad encoding characters, or, under `strict`, a promoted warning) is returned as `{ ok: false, error, raw, position }`. Its siblings before **and after** it still come back. This is the headline property: _a mid-stream failure never suppresses a later message._ - **Stray content before the first `MSH`**, and the **severed tail** of a message that (malformedly) contained a reserved envelope name mid-body (see the reserved-name limitation below), is preserved as a `NO_MSH_SEGMENT` failure entry: kept and surfaced, never discarded. - **A message delimited by neither a `BHS` nor a `BTS`** (a bare stream, or content before the first `BHS` that is not closed by a `BTS`) is still yielded in `messages`: it simply belongs to no entry in `batches` (which holds each run delimited by a `BHS` and/or a `BTS`). `messages` is the complete surface; `batches` is the envelope structure over it. - A `Buffer` stream is decoded/re-encoded via **`latin1`** (a lossless 1:1 byte↔codepoint mapping) so each message's own **MSH-18 charset** resolution runs on its original bytes, not a stream-wide guess. ## Known limitations / non-goals `splitBatch` is a **splitter**, deliberately narrow. Two of these are structural demarcation limits worth calling out explicitly: - **Reserved envelope names are matched by name (case-sensitive).** A batch stream is a flat `\r`-delimited segment sequence; §2.10.3 disambiguates an envelope segment from a body segment by **position in the nesting**, which in a flat stream reduces to "a line named `FHS`/`BHS`/`BTS`/`FTS` is a boundary." Those names are **reserved** (a conformant message body never contains one) so this is correct for well-formed input. But a (malformed) message whose body literally contains a segment named `FHS`/`BHS`/`BTS`/`FTS` **will be mis-split** at that line; the severed tail surfaces as a `NO_MSH_SEGMENT` failure entry (never dropped). Matching is **case-sensitive** on purpose: a lenient lowercase `bts` in a body is left in the message rather than mistaken for a boundary (a lowercase `msh`-led "message" is not parseable anyway, so declining to treat it as a boundary loses nothing). - **One file envelope per stream is the modelled case.** §2.10.3's abstract syntax is a single `[FHS] … [FTS]`. Concatenated files (a second `FHS`) are handled **best-effort**: batch demarcation, the flattened `messages`, and **per-file** FTS-1 reconciliation are all correct (a second `FHS` resets the batch count, so two individually-conformant files do **not** raise a false `BATCH_COUNT_MISMATCH`), but the single-valued `fileHeader` / `fileTrailer` / `declaredBatchCount` result fields reflect the **first** header / **last** trailer / **last** FTS-1. Inspect `batches` and `warnings` for the full multi-file picture. - **No batch ACK generation.** §2.10.3.3's three acknowledgement modes are not implemented; the splitter only demarcates and reconciles. - **No envelope enforcement.** A profile may _mandate_ the full frame (IIS); the splitter **warns** (`BATCH_MISSING_TRAILER`) and leaves accept/reject to the caller. - **BTS-3 batch totals are surfaced, not reconciled.** BTS-3 is an application-defined repeating total (a checksum/sum whose meaning the sender chooses). There is no generic "actual" to compare it to, so fabricating a comparison would be a confidently-wrong answer. The raw BTS field tokens are available on `batch.trailer.fields`; computing/validating an application total is the consumer's job. - **No typed FHS/BHS field helpers** beyond the raw token array (`envelopeSegment.fields`). First-class file/batch-header accessors are a later candidate. - **No transport de-framing.** Splitting messages out of an MLLP byte stream is `@cosyte/mllp`'s job, not this one. - **Multi-byte charset framing.** A `Buffer` stream is line-ending-normalized at the stream level (its `\r`/`\n` framing bytes are ASCII) before each message is re-decoded with its own MSH-18 charset. This is byte-exact for the single-byte / ASCII-superset charsets HL7 batch framing requires (UTF-8, ISO-8859-x, Shift-JIS), but not for a multi-byte charset (e.g. UTF-16) whose payload embeds raw `0x0A`/`0x0D` bytes: such batch files effectively do not exist, but the framing assumption is worth stating. ## MSH-family field indexing (a gotcha) `BatchEnvelopeSegment.fields` is the segment split on its own field separator, with `fields[0]` holding the segment name. Mind the HL7 MSH-family quirk: - **`FHS` / `BHS`** mirror `MSH`: field 1 is the field separator itself, so `fields[1]` is the **encoding-characters** field (FHS-2/BHS-2). - **`BTS` / `FTS`** are ordinary segments: `fields[1]` is **field 1** (BTS-1 batch message count / FTS-1 file batch count). `declaredMessageCount` (from BTS-1) and `declaredBatchCount` (from FTS-1) are parsed for you, so most callers never touch the raw `fields` at all. --- URL: https://docs.cosyte.com/hl7/spec-notes-stream Markdown: https://docs.cosyte.com/hl7/spec-notes-stream.md # Spec notes: streaming / incremental parse `parseStream(source, optionsOrProfile?)` parses an HL7 v2 byte/text stream **incrementally**: it consumes a chunked source and yields one message per `MSH`-delimited boundary as that boundary completes, holding only **one message** of state, never the whole stream. It is the streaming complement of [`splitBatch`](https://docs.cosyte.com/hl7/spec-notes-batch.md): `splitBatch` demarcates the messages in a buffer you already hold in memory; `parseStream` demarcates them as the bytes arrive, for a feed or file too large to buffer, or a live source. ## Why a separate surface (and what it reuses) `parseHL7` needs a whole message; `splitBatch` needs a whole file. A feed has neither. So streaming is its own surface. But it introduces **no second parse grammar**: once a message's boundaries are known, `parseStream` hands the exact delimited bytes to `parseHL7` unchanged (forwarding your profile / `strict` / `charset` / `dateFormats` per message). A streamed message is therefore byte-for-byte identical to what `splitBatch` produces for the same bytes. The only new logic is **incremental boundary detection**. ## The source `parseStream` accepts any of: - a Node **`Readable`** (a `Readable` is an `AsyncIterable`), - any **async-iterable** of chunks (`async function*`, a web-stream reader adapter, …), - any plain **iterable** of chunks (an array, a `function*`). Chunks are `string` (text stream) or `Buffer`/`Uint8Array` (binary stream). A binary chunk is decoded 1:1 through `latin1` (a lossless byte↔codepoint mapping) so each message's slice re-encodes to its **original bytes** and its own MSH-18 charset resolution runs on them, exactly as `splitBatch` does. A source is expected to be **homogeneous** (all-text or all-binary), which every real `Readable` is. A bare `string` or `Buffer` is **rejected** with a `TypeError`: both are themselves iterable (a string of characters, a Buffer of byte numbers), so passing one would be silently mis-consumed. Use `parseHL7` for a single message, or wrap a whole buffer you want to stream: `parseStream([buffer])`. ## Boundary detection: this is engineering, not spec HL7 v2 Chapter 2 prescribes the message and batch **syntax** but **no incremental / streaming parse**. The rules below are engineering guidance justified by the library's Postel's-Law conventions, grounded in the syntax. They are **not** citable spec mandates. - **`MSH` at segment start is the message boundary.** A message runs from an `MSH` segment up to (but not including) the next `MSH`, the next batch-envelope segment, or end of stream. Identical demarcation to `splitBatch`. So the streamed decomposition of a buffer equals the whole-buffer one. - **Batch-aware (§2.10.3).** The four envelope segments `FHS` / `BHS` / `BTS` / `FTS` act as boundaries (they close the current message) and are **never yielded as messages**. So **`yielded count == MSH count`**. Envelope **count reconciliation** (BTS-1 / FTS-1) is `splitBatch`'s job; `parseStream` streams *messages*, not a batch report. - **Terminator tolerance.** `\r`, `\r\n`, and `\n` are all accepted as segment terminators (real feeds mix them), matching the core parser's `normalize`. ## Chunk-boundary invariance (the hard part) A segment (including the `MSH|^~\&` header) is only demarcated once its **terminator** arrives. Bytes seen before that live in a single bounded partial-segment buffer. So a message split across two chunks (mid-segment, mid-field, even mid-`MSH`) is reassembled correctly, and **feeding the same bytes in 1-byte chunks or one big chunk yields identical messages** (property-tested across randomized chunk boundaries and 1-byte chunks). The one subtle case is a **`\r\n` terminator straddling a chunk boundary**: a `\r` at the very end of the current buffer is *not* yet treated as a terminator: it waits for the next chunk, because that chunk may begin with the `\n` that completes a single `\r\n`. Treating it eagerly would invent a spurious empty segment. (The pathological chunk-split-mid-`MSH|^~\&` and mid-`\r\n` cases are covered by the property and example suites.) ## Bounded memory (O(one message), not O(file)) `parseStream` retains only the in-flight message's segment lines plus the partial-segment buffer. A message is flushed (parsed, **yielded, and released**) the moment the next boundary segment completes; the previous message is gone before the next is read. The source is pulled **lazily**: to close message _i_ the stream must read message _i+1_'s `MSH`, so read-ahead is a **constant one message**, never the whole stream (proven structurally: if it buffered the file, read-ahead would be O(N)). This is what makes a multi-gigabyte ELR file or an unbounded live feed tractable, and it keeps PHI from accumulating in a growing buffer. ## Isolation & fail-safe: never a dropped tail - A **malformed** message (one of the four Tier-3 fatals, or a `strict`-promoted warning) surfaces as a typed **failure entry** carrying the `Hl7ParseError`, and the stream **continues**. A bad message never suppresses the messages after it (property-tested: inject a malformed message at any position, under any chunking → every valid sibling still comes back `ok`, in order). - An **unterminated final message** (the stream ended while its last segment had no terminator) is still yielded **in full**, with an `UNTERMINATED_STREAM_MESSAGE` warning on the entry's `streamWarnings`. Never a throw, never a dropped tail. Only the *final* message can be unterminated; every earlier one is closed by the terminator that precedes the next boundary. ## `streamWarnings` vs `message.warnings` Per-message parse deviations live on `message.warnings`, exactly as a whole-buffer `parseHL7` would report them. Streaming changes nothing there. The entry's separate **`streamWarnings`** carries only *stream-level* diagnostics (today just `UNTERMINATED_STREAM_MESSAGE`), so the per-message surface stays identical to the non-streamed parse. Both carry codes + positions only, **never a field value**, so no PHI is exposed. ## Known limitations - **Consumes an already-de-framed stream.** MLLP `…` framing, ACK timing, reconnect, and TLS are [`@cosyte/mllp`](https://github.com/cosyte/mllp)'s. `parseStream` yields *parsed messages*, it does not own the wire. - **No random access / seek**; ordering is stream order. Not a persistent queue. - **Envelope reconciliation is not re-implemented** here: if you need the BTS-1 / FTS-1 count checks, buffer with `splitBatch`. - Two concatenated messages with **no terminator between them** cannot be split (the second `MSH` is not at a segment start). The parser will not invent a boundary the bytes do not contain. --- URL: https://docs.cosyte.com/hl7/spec-notes-datetime-precision Markdown: https://docs.cosyte.com/hl7/spec-notes-datetime-precision.md # Spec notes: datetime precision + timezone fidelity **Spec:** HL7 v2 Ch. 2A **DTM**: `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`; the number of characters populated (excluding the offset) sets the precision; a missing offset "defaults to that of the local time zone of the sender" (NOT UTC, NOT the parser's zone). TS→DTM moved precision from an explicit degree-of-precision component to the value's length (v2.5/v2.7). ## Why there is no `Date` by default A JavaScript `Date` is an absolute instant, and most HL7 v2 timestamps are not. `|1970|` is a year, `|19880705|` is a calendar day, and a value with no offset is the _sender's_ local time by the standard's own words. Materializing either as a `Date` means inventing a zone, and inventing UTC is how a day-only date of birth `|19880705|` becomes a UTC-midnight instant that reads back as **July 4** through `.getDate()` in any negative-offset zone. So this library parses DTM into typed parts, keeps the precision it was given, and builds a `Date` only when the caller explicitly asks for one. ## The API ### Core: `src/parser/dates.ts` - `type DtmPrecision = "year"|"month"|"day"|"hour"|"minute"|"second"|"fraction"`. - `interface DtmParts { raw; valid; precision?; year?; month?(1 to 12, spec-native); day?; hour?; minute?; second?; fractionalSeconds?(verbatim digits, no dot); hasTimezone; offsetMinutes?(signed, iff tz); matchedFormat?(a declared or built-in format matched); ambiguity?(order refused, see below) }`. - `parseDtm(raw): DtmParts`: pure structural parse of the HL7 DTM shape. **No zero-fill, no `Date`, no UTC.** Precision from populated length. Calendar-range check (month 1 to 12, day 1 to 31, hour 0 to 23, min/sec 0 to 59, offset hours ≤ 24 with minutes ≤ 59) → on bad shape/range `valid:false`, raw kept, parts omitted, never a throw. Empty → `valid:false, hasTimezone:false`. - `formatDtm(parts): string`: reconstruct the DTM string from parts (proves losslessness; round-trip property = `parseDtm(raw)→formatDtm ≡ raw`, exact length, no zero-fill). - `dtmToDate(parts, opts?: { assumeOffsetMinutes?: number }): Date | undefined`: **explicit, opt-in** absolute-instant materialization. `!valid`→undefined. Truncated fields fill to lowest legal value **for instant construction only** (precision still tells the truth). `hasTimezone`→exact instant via the embedded offset. else `assumeOffsetMinutes`→apply it. else→**undefined** (refuse to guess; never silent UTC). - `parseDtmDeclared(raw, formats): DtmParts`: the TS composite path's parse: try `parseDtm`; else the formats the CALLER declared, in order → parts from the matched tokens + `matchedFormat`. **Stops there.** No built-in fallback, deliberately (see below). - `parseDtmCascade(raw, opts): DtmParts`: lenient wrapper for non-composite callers (`meta`): `parseDtmDeclared` first, else `BUILTIN_DATE_FALLBACKS`, whose membership and order are unchanged: `ISO-8601`, `YYYY-MM-DD`, `MM/DD/YYYY`, `MM/DD/YYYY HH:mm:ss`. What a declared format may contain is `SUPPORTED_DATE_TOKENS`, stated in full by the [Date token grammar](https://docs.cosyte.com/hl7/date-token-grammar.md). One exception, below: an order-ambiguous slash date resolves to nothing. ## Order ambiguity: `05/07/1988` is refused, not guessed `BUILTIN_DATE_FALLBACKS` carries `MM/DD/YYYY` and not `DD/MM/YYYY`. A day-first sender's `05/07/1988` therefore used to read as May 7, with no failure and no signal that the field order had been assumed. In a PHI-bearing library that is the worst shape of wrong: plausible, confident and invisible. A slash-separated numeric date whose first two components are **both in 1-12** has two legal readings. When no declared format has matched, the built-ins now resolve **neither**: the result is `valid: false` with an `ambiguity` report (`code: AMBIGUOUS_DATE_ORDER`, the raw value, and both `candidates` as `{ format, month, day, isoDate }`). `msg.meta.timestamp` carries it, so `ambiguity` distinguishes "refused" from "malformed" (no timestamp at all) and from "resolved via a fallback" (`valid: true` + `matchedFormat`). - **Declared formats still win.** `parseHL7(raw, { dateFormats: ["DD/MM/YYYY"] })` and a profile's `dateFormats` are tried ahead of the built-ins, resolve the value, keep the existing `TIMESTAMP_FALLBACK_FORMAT` semantics, and never report ambiguity. Options precede profile. - **One reading still resolves.** `07/25/1988` (no month 25) and `05/05/1988` (both readings agree) are unaffected, as are strict DTM, ISO-8601 and `YYYY-MM-DD`. - **The built-in list did not change.** `DD/MM/YYYY` is used only as a probe for the second reading and never resolves a value: an unambiguous day-first value such as `25/07/1988` still fails loudly. Declaring the order is the route for a day-first feed. - **No warning code was added or renamed.** `Hl7Message.warnings` is frozen at construction and `msg.meta` is built lazily afterwards, so the report travels on the value rather than the warnings collection. - **A typed datetime field never reaches this at all.** The built-ins are the only stage `parseDtmDeclared` omits, so `patient.dateOfBirth` and every other typed datetime answer a day-first `05/07/1988` with a plain `valid: false` and no `ambiguity` report: nothing consulted a built-in, so there was no second reading to refuse. `msg.meta.timestamp` is the value that carries the report. ### Conversion surface: `src/parser/date-conversion.ts` A read layer over `DtmParts`, carrying the three names every `@cosyte` parser exposes. It converts; it never re-parses, and it changes nothing about how a value was parsed. - `interface DateParts { year?; month?(1-12, spec-native); day?; hour?; minute?; second?; millisecond?; offsetMinutes?(signed, iff an explicit offset was stated) }`. Frozen. Names singular. - `toObject(value): DateParts | undefined`: only the stated components, so `Object.keys()` recovers the precision. A component the value did not state is **absent**, not present-with-undefined. No `raw`, `valid`, `precision`, `hasTimezone` or `matchedFormat` key. `millisecond` is the first three fractional digits **verbatim**, right-padded (`.5`=500, `.0500`=50), never a float multiplication. `-0000` surfaces as `offsetMinutes: 0`. `!valid` or no stated component at all -> undefined. - `toISO(value): string | undefined`: ISO-8601 **truncated to the stated precision**, fractional digits verbatim. Offset appended as `Z` when exactly zero (`-0000` included), else `+HH:MM`/ `-HH:MM`; **nothing** appended when no offset was stated. So `toISO` is a rendering and `formatDtm` remains the byte-exact round trip. No year (which HL7 DTM cannot state) -> undefined. - `toDate(value, opts?): Date | undefined`: delegates to `dtmToDate`, so the zone rule and the sub-100-year handling are the existing ones, unchanged. - All three accept `undefined` / `null` and return `undefined`. **None ever throws, for any input.** - **Options bounds, on the second parameter as well as the first:** the bag may be `null` or absent (`opts?.` is read, not defaulted, because a default parameter fires for `undefined` only), and `assumeOffsetMinutes` must be a **finite number** or the answer is `undefined`. `"0"`, `true` and `[]` all multiply to `0` in JS, so an unguarded delegation hands back a UTC instant the caller never asked for; `NaN`, the infinities and an offset past the range a `Date` represents are refused for the same reason, so no answer is ever an `Invalid Date`. A value stating its own offset never reaches the guard: its offset wins and the assumption is ignored, per the Contract. `dtmToDate` is a published name and keeps its own coercing behaviour: the guard is added by the conversion layer, exactly as the component bound below is. - **Component bounds, applied by all three to the WHOLE value:** year 0-9999, month 1-12, day 1 to the last day THAT month has (full 4/100/400 leap rule; an unstated year bounds February at 29), hour 0-23, minute 0-59, second 0-59. `20240230` and `20230229` convert to `undefined` rather than rolling into 1 March, and an in-range prefix is never converted in place of the refused value. The bound reads the components, so a hand-built `DtmParts` is held to it exactly as a parsed one is. `parseDtm` is untouched and stays liberal (day 1-31, month-blind), so `formatDtm` still round-trips those bytes: the refusal is in the conversion, which is where a wrong answer would look right. `new Date("2024-02-30")` is 1 March in V8, so rendering it would shift a date of birth by a day with nothing to notice. - **The stated offset is a bounded component too, when the value claims one:** a whole number of minutes within `+/-23:59` of UTC, which is both what the `+HH:MM` rendering can state and what an ISO-8601 reader accepts (`new Date("2024-02-29T12:00:00+24:00")` is an `Invalid Date`). The same helper does it, so a string, a boolean, an array, `NaN` and a fractional minute are refused with it. Two routes reach that field without a cast: a hand-built `DtmParts`, and `msg.meta.timestamp`, whose fallback read range-checks the calendar components and not the offset, so a wire MSH-7 of `2024-02-29T12:00:00+99:99` states 6039 minutes east. Unbounded, `toObject` reports a non-number in a `number` slot, `toISO` renders `+NaN:NaN` or a three-digit hour, and `toDate` multiplies a `null` or a `"0"` to zero and answers a confident UTC instant. `parseDtm` (up to `+2400`), `formatDtm` and `dtmToDate` are untouched, exactly as for the calendar bound above. An `offsetMinutes` on a value with `hasTimezone: false` states nothing and is ignored, not refused. - The three names are identical in `@cosyte/hl7`, `@cosyte/x12`, `@cosyte/dicom`, `@cosyte/ncpdp`, `@cosyte/astm` and `@cosyte/ccda`, so a consumer importing two of them aliases (`import { toISO as hl7ToISO } from "@cosyte/hl7"`) or namespace-imports. ### TS composite: `src/model/types/ts.ts` - `TS` is the `DtmParts` shape (raw, valid, precision?, parts, hasTimezone, offsetMinutes?). Frozen. **No `.date`.** `parseTs(rep, enc, dateFormats?)` = unescape → `parseDtmDeclared`. - `field.ts::asTs()` returns it, passing the message's merged `dateFormats`. ### Which datetimes honour `dateFormats` `ParseOptions.dateFormats` ++ the applied profile's, deduped first-occurrence-wins, is `msg.dateFormats`, and **every** datetime below honours it: `meta.timestamp`, `patient.dateOfBirth`, `visit.admit/dischargeDateTime`, `observations.observedDateTime` + the `TS|DT` `TypedValue.value`, `allergies.onsetDate`, `diagnoses.dateTime`, `insurance.effectiveDate` / `expirationDate`, `immunizations.administeredDateTime` / `expirationDate`, `charges.transactionDate`, `documents.activityDateTime`, order/medication `timings.start/endDateTime` (`TQ1` and legacy embedded `TQ`), and `appointments.start/endDateTime`. **Built-in fallbacks stop at `meta.timestamp`.** `BUILTIN_DATE_FALLBACKS` carries `MM/DD/YYYY` and no day-first form, so letting it follow the caller's hook onto a typed datetime would read a day-first `05/07/1988` date of birth as a confident May 7. A declared format is the caller stating what their sender means; a built-in fallback is the library guessing, and it does not guess on a clinical datetime. A value matching no declared format stays `valid: false` with `raw` intact. **No warning marks a fallback.** `TIMESTAMP_FALLBACK_FORMAT` exists as a code and `parseDtmCascade` can emit it, but no parse supplies the emit hook it needs, so it does not appear on `msg.warnings`. `matchedFormat` on the `TS` is the caller-visible signal, and it is a structural value rather than a warning for the same reason missing-tz is (no noise on the overwhelmingly common vendor feed). ### Non-goals Fidelity only: **no** localization, timezone conversion, or arithmetic; a missing offset is **flagged sender-local**, never resolved. `HHMM=0000` is preserved (never rolled to the previous day). No new warning code. Missing-tz is the structural `hasTimezone:false`, not a warning (avoids noise on the overwhelmingly common no-offset feed). --- URL: https://docs.cosyte.com/hl7/date-token-grammar Markdown: https://docs.cosyte.com/hl7/date-token-grammar.md # Date token grammar A vendor feed that writes `05-JUL-1988` or `7/5/1988 2:30 PM` is not writing HL7. The `dateFormats` option is how you teach the parser to read it anyway, and this page is the **normative** statement of what a format string may contain: the token table, how a format is split into tokens, how to write a literal, which tokens require which other tokens, when two tokens may sit side by side, how a 12-hour clock becomes a 24-hour value, and the two things this vocabulary deliberately does not have. It is written to be citable. Other parsers in this family adopt the same grammar rather than inventing a second one, and a machine-readable corpus of cases ships beside it so a parser can prove it agrees. ```ts runnable import { SUPPORTED_DATE_TOKENS } from "@cosyte/hl7"; SUPPORTED_DATE_TOKENS.length; // => 15 SUPPORTED_DATE_TOKENS.includes("MMM"); // => true SUPPORTED_DATE_TOKENS.includes("YY"); // => false ``` ## The token table Token spellings are **case-sensitive**: `MM` is month and `mm` is minute. Alphabetic content in the **input** is compared **case-insensitively**, and ASCII only, so `JUL`, `Jul` and `jul` are one value and `PM` and `pm` are one value. | token | means | matches | in-range values | | ------ | ---------------------------- | ----------------- | ------------------------------------------------- | | `YYYY` | year | exactly 4 digits | 0001 to 9999 | | `MM` | month | exactly 2 digits | 01 to 12 | | `M` | month, single-digit tolerant | 1 or 2 digits | 1 to 12 | | `MMM` | month, abbreviated name | exactly 3 letters | `Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec` | | `MMMM` | month, full name | letters | `January` through `December` | | `DD` | day of month | exactly 2 digits | 01 to 31 | | `D` | day, single-digit tolerant | 1 or 2 digits | 1 to 31 | | `HH` | hour, 24-hour clock | exactly 2 digits | 00 to 23 | | `H` | hour, 24-hour, tolerant | 1 or 2 digits | 0 to 23 | | `hh` | hour, 12-hour clock | exactly 2 digits | 01 to 12 | | `h` | hour, 12-hour, tolerant | 1 or 2 digits | 1 to 12 | | `mm` | minute | exactly 2 digits | 00 to 59 | | `ss` | second | exactly 2 digits | 00 to 59 | | `SSSS` | fractional seconds | 1 to 4 digits | any | | `A` | meridiem | exactly 2 letters | `AM` or `PM`, any case | The month names are **English only**. A localized month name is not in this vocabulary and is not expressible; if you need one, normalize the value before it reaches the parser. A value outside a token's range does not match. It is never clamped, wrapped or substituted: `13` is not December and it is not January, it is no match at all. ## Tokenising a format 1. A format is read left to right and split by **longest match** against the table above. `MMMM` wins over `MMM`, which wins over `MM`, which wins over `M`. 2. Every remaining character is a **literal**, and a literal matches itself. 3. An unescaped **ASCII letter or digit** that is not part of a token is an **error at definition time**. There is no such thing as an accidental literal letter: a letter you meant literally must say so. ### Writing a literal letter or digit Put it in square brackets. The bracketed text matches **verbatim**, including its case, and nothing inside brackets is read as a token. ```ts runnable import { defineProfile } from "@cosyte/hl7"; const iso = defineProfile({ name: "iso-style-vendor", dateFormats: ["YYYY-MM-DD[T]HH:mm:ss"], }); iso.dateFormats; // => ["YYYY-MM-DD[T]HH:mm:ss"] ``` Without the brackets that `T` is a bare letter that forms no token, and the profile is refused: ```ts runnable throws import { defineProfile } from "@cosyte/hl7"; defineProfile({ name: "broken", dateFormats: ["YYYY-MM-DDTHH:mm:ss"] }); ``` An escape may hold more than one character (`D [of] MMMM YYYY` reads `5 of July 1988`), and a `[` with no closing `]` is an error at definition time rather than a silent swallow of the rest of the format. ## Rules a format must satisfy All five are checked **at profile definition time**, when `defineProfile()` runs, so a format that cannot be honoured throws where you wrote it instead of sitting in the list quietly matching nothing. Definition time is not the only place they bind. **Rules 2, 3 and 4 bind the matcher as well**, and a format breaking one of them reports **no match** whichever route it arrived by, including the per-parse `dateFormats` option that nothing validates. Each of the three describes a value a parser could otherwise answer with a confident wrong reading: an hour that is not a clock hour, a fraction with no second to hang it on, a digit run with two equally correct splits. Enforcing them only where the format is written would leave the unvalidated route free to produce exactly that, so a parser adopting this grammar implements those three in **both** places. **Rules 1 and 5 are definition-time only.** They are about the format's text rather than about a value, and the matcher is deliberately more tolerant there: a character it does not recognise is matched verbatim, so a format supplied through the parse options keeps the behaviour it had. Their purpose is to tell the author, where the format is written, that it will not read what they meant. ### 1. Every letter and digit belongs to a token or an escape `MM/DD/YYYY hrs` is refused, naming the offending character. So is `YYY/MM`, where three `Y` characters form no token. Punctuation and whitespace need no escape. ### 2. `h`/`hh` and `A` require each other A 12-hour reading is not a clock hour until AM or PM is applied, so `h:mm` is refused. A meridiem beside a 24-hour clock has nothing to convert, so `HH:mm A` is refused too. Use `H`/`HH` for a 24-hour clock and `h`/`hh` with `A` for a 12-hour one. This rule binds the **matcher** as well. An input matched against a format carrying only one half of the pair reports **no match** rather than an hour: `2:30` under `h:mm` is not hour 2, because the feed that wrote it may have meant 14:30, and `14:30 PM` under `HH:mm A` is not hour 14 either. A format may carry a 24-hour token and a 12-hour pair together, and then the two readings must agree once the meridiem is applied, or the input reports no match. ### 3. `SSSS` requires `ss` A fraction is only meaningful at full second precision, which is the same rule the strict parser applies to an HL7 value. This rule binds the **matcher** as well: `1988-07-05 14:30.5` under `YYYY-MM-DD HH:mm.SSSS` reports **no match**, rather than a value carrying a fraction of a minute. ### 4. Two numeric tokens may not sit adjacent when either is variable width `M`, `D`, `H`, `h` and `SSSS` are the variable-width tokens. `MDYYYY` is refused: nothing can decide whether `1231988` starts with month 1 or month 12. Two **fixed**-width numeric tokens may sit adjacent, because the digit run splits deterministically, which is why `YYYYMMDDHHmmss` is fine. `MMM`, `MMMM` and `A` consume letters rather than digits, so a numeric token beside one of them is never ambiguous. **Adjacent means adjacent in the input, not in the format text.** A literal separates two tokens only if it matches at least one character, so the **empty escape `[]` separates nothing** and `M[]D/YYYY` is refused exactly as `MD/YYYY` is. Writing it the long way does not buy a reading of `125` that the short way was denied. ```ts runnable throws import { defineProfile } from "@cosyte/hl7"; defineProfile({ name: "ambiguous", dateFormats: ["M[]D/YYYY"] }); ``` Like rules 2 and 3, this one binds the **matcher** too. A digit run nothing can split has no honest answer, and a parser that picks one of its two readings has produced a confident wrong date. So a format breaking this rule reports **no match**, whichever route it arrived by, including the unvalidated per-parse `dateFormats` option. An empty escape anywhere else is simply invisible: `YYYY[]-MM-DD` reads `1988-07-05` exactly as `YYYY-MM-DD` does. ### 5. A format must carry at least one token An empty format, or one made only of separators, matches nothing. ## Matching an input - A **variable-width** numeric token takes the longest digit run that both stays in range and leaves the rest of the format matchable. So `M/D/YYYY` reads `7/5/1988` and `12/25/1988` with the same format string. - The **whole** input must be consumed. A value that ends early, or that carries trailing characters, is no match. - A format carrying a 12-hour token with no meridiem token, or a meridiem token with no 12-hour token (rule 2), is no match, rather than an hour the meridiem was never applied to. A format carrying both a 24-hour token and a 12-hour pair is no match unless the two readings agree after the conversion. - A format carrying `SSSS` with no `ss` (rule 3) is no match, rather than a fraction of a minute. - A format whose numeric tokens meet with nothing between them (rule 4) is no match, rather than one of the readings its digit run allows. - No match ever raises. It reports no match, and no parts are produced from it. The three bullets naming rules 2, 3 and 4 are those rules holding at match time as well as at definition time, and that is the half of the grammar the corpus cannot carry for you: a `no-match` case there is a **well-formed** format, and a format breaking one of these three is not well formed, so it can only appear in the corpus as a `reject-format` case. A parser that implements them at definition time only answers `2:30` under `h:mm` with hour 2 and passes every corpus case while doing it. This page is where that obligation is written. ```ts runnable import { parseHL7 } from "@cosyte/hl7"; const raw = "MSH|^~\\&|LAB|MAIN|EHR|REF|05-JUL-1988||ORU^R01|1|P|2.5\r" + "PID|1||MRN001^^^HOSP^MR||Testpatient^Casey||19880705|F"; const msg = parseHL7(raw, { dateFormats: ["DD-MMM-YYYY"] }); msg.meta.timestamp?.year; // => 1988 msg.meta.timestamp?.month; // => 7 msg.meta.timestamp?.day; // => 5 msg.meta.timestamp?.precision; // => "day" ``` ### Month names resolve to a month NUMBER `MMM` and `MMMM` produce the spec-native month **1 to 12** that every parsed value uses, never a 0-based index. `05-JUL-1988` under `DD-MMM-YYYY` is year 1988, month **7**, day 5, at day precision. ### The meridiem is converted and then discarded `hh`/`h` plus `A` yields the 24-hour hour, and the meridiem itself never appears in the result. `12 AM` is hour **0**, `12 PM` is hour **12**, and `2 PM` is hour **14**. ```ts runnable import { parseHL7 } from "@cosyte/hl7"; const at = (stamp: string): number | undefined => parseHL7(`MSH|^~\\&|LAB|MAIN|EHR|REF|${stamp}||ORU^R01|1|P|2.5\r`, { dateFormats: ["M/D/YYYY h:mm A"], }).meta.timestamp?.hour; at("7/5/1988 12:00 AM"); // => 0 at("7/5/1988 12:00 PM"); // => 12 at("7/5/1988 2:30 PM"); // => 14 ``` ### Precision follows the fields the format populates A value states the precision it was given and no more. `YYYY` is year precision, `YYYY-MM` is month, `YYYY-MM-DD` is day, and so on through hour, minute, second and fraction. Nothing is zero-filled to reach a level the format did not populate. | the format populates through | stated precision | | ---------------------------- | ---------------- | | year | `year` | | month | `month` | | day | `day` | | hour | `hour` | | minute | `minute` | | second | `second` | | fractional seconds | `fraction` | A format that populates no year describes a time of day rather than a date. The token grammar matches it, but there is no dated value to produce from it, so it yields no timestamp. ## Two exclusions, with their reasons ### No two-digit year There is no `YY` token and there will not be one. Resolving a two-digit year needs a century window, a window is a guess, and a wrong guess moves a date of birth by a hundred years while producing a date that looks entirely plausible. Nothing downstream can detect that. So a format containing `YY` is refused, and the refusal says why: ```ts runnable throws import { defineProfile } from "@cosyte/hl7"; defineProfile({ name: "legacy-lab", dateFormats: ["MM/DD/YY"] }); ``` If a feed really sends two-digit years, widen them to four digits at the ingest boundary, where the century is a decision somebody made on purpose. ### No timezone token An offset cannot be captured by a user format. This is not an oversight: the stance on offsets is that a value carrying none is **flagged** as carrying none, never resolved to UTC, because assuming UTC turns a day-only date of birth into the previous day in every negative-offset zone. See [Datetime precision and timezone fidelity](https://docs.cosyte.com/hl7/spec-notes-datetime-precision.md) for the whole of that argument. A value matched through `dateFormats` therefore never carries an offset, and the consumer decides how to localize it. ## The conformance corpus `test/fixtures/date-tokens/corpus.json` in this repository is the machine- readable form of everything above: pure JSON, no expression language, no syntax belonging to any one parser, and no dependency. It carries - at least one accepted case per token in the table, - at least one refused-at-definition case per rule above, - at least one no-match case per range or name violation, and - at least one accepted case per stated precision level. Each case declares its own expectation, and a runner that meets a case it cannot execute must **fail** rather than skip it: a corpus that quietly ignores what it does not understand proves nothing. A parser adopting this grammar reads the corpus, runs every case, and asserts the stated outcome. That is the whole conformance obligation, and it is the reason the grammar is written here once rather than re-derived per repository. ### The file's shape Top level: | key | meaning | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `grammar` | Which grammar the file describes. Always `date-token-grammar`. | | `revision` | Bumped when a case is added, removed or changed. | | `description` | One sentence, for a reader who arrives at the file before this page. | | `tokens` | The whole token vocabulary. A runner asserts its own published token list equals this, which catches a parser that quietly recognizes more or fewer tokens. | | `refusalRules` | Every reason a format is refused at definition time. | | `precisionLevels` | Every stated precision level a value can carry. | | `noMatchReasons` | Every reason an input fails to match a well-formed format. | | `cases` | The cases themselves, one object each. | Every case carries an `id` (unique, stable, citable from a bug report), a `kind`, and a `note` saying what the case is for. The three kinds: **`kind: "accept"`** - the format is well formed AND the input matches it. `format` is the format string; `tokens` lists every token it uses, so per-token coverage is checkable without a tokenizer; `input` is the value; `expect` is the fields recovered, keyed `year`, `month`, `day`, `hour`, `minute`, `second`, `fractionalSeconds`, where a key that is absent must be absent from the result; and `precision` is one of `precisionLevels`, or `null` for a format that populates no year and so carries no date precision. `month` is spec-native 1 to 12, `hour` is a 24-hour reading with the meridiem already applied and gone, and `fractionalSeconds` is the digit string as written, with no leading dot and no rounding. **`kind: "reject-format"`** - the format is not well formed and must be refused at definition time, before any input is seen. `format` is the offending string and `rule` is one of `refusalRules`. A runner asserts both that the refusal happened and that it named the offending format: an unactionable error is a defect of its own. **`kind: "no-match"`** - the format is well formed and the input does not match it. `format`, `input`, and `reason`, one of `noMatchReasons`. The result is no match: never a substituted or partial value, and never a raised error. Beyond running each case, a runner asserts the corpus still covers what it claims: every token in `tokens` appears in some `accept` case, every rule in `refusalRules` in some `reject-format` case, every level in `precisionLevels` in some `accept` case, every reason in `noMatchReasons` in some `no-match` case, and every `id` is unique. --- URL: https://docs.cosyte.com/hl7/spec-notes-timing Markdown: https://docs.cosyte.com/hl7/spec-notes-timing.md # Spec notes: order / medication timing `order.timings` and `med.timings` surface the **timing structure** of an order or medication (how often, how many, when it starts/ends) from the `TQ1` segment (HL7 v2.5+) or the **legacy embedded TQ** data type in `ORC-7` / `RXE-1` (pre-v2.5). Each entry is an `OrderTiming`. > **hl7 surfaces the timing _structure_; it never _interprets_ it.** It does > **not** compute an administration schedule, resolve "institution-specified > times" (`BID`, `QHS`) to clock times, or interpret sig. The repeat pattern is > surfaced **verbatim**. Reading `Q6H` as "daily", or silently dropping a > `BID`, changes the administered dose count, a transcription-class harm. ## Spec traceability HL7 v2 Chapter 4 (order entry) + Chapter 2A (data types): - **TQ1: Timing/Quantity (§4.5.4), introduced v2.5.** The dedicated timing segment. Modelled fields: - **TQ1-2 Quantity (CQ)** → `quantity`: the service quantity per occurrence (`{ value, units }`). - **TQ1-3 Repeat Pattern (RPT, Table 0335)** → `repeatPattern`: the frequency/SIG field, surfaced **verbatim** (see below). RPT.1 (the code) is read. - **TQ1-4 Explicit Time (TM)** → `explicitTime` (verbatim, first value). - **TQ1-6 Service Duration (CQ)** → `serviceDuration` (verbatim). - **TQ1-7 Start Date/Time (DTM)** → `startDateTime` (precision-preserving `TS`). - **TQ1-8 End Date/Time (DTM)** → `endDateTime` (precision-preserving `TS`). - **TQ1-9 Priority (CWE)** → `priority`. - **TQ1-14 Total Occurrences (NM)** → `totalOccurrences`. **This is TQ1-14, _not_ TQ1-11**. TQ1-11 is the Text Instruction (TX). The load-bearing total is read from field 14; a text instruction in field 11 is never mistaken for the count. - **TQ2: Timing/Quantity Relationship (§4.5.5)** links related orders. **Not modelled** (see non-goals): `TQ2` remains a recognized segment name only. - **Legacy embedded TQ (Ch. 2A §2.A.81)**, carried in **`ORC-7`** (orders) and **`RXE-1`** (encoded medications). Retained backward-compat only; **detail withdrawn as of v2.7**, which redirects to `TQ1`/`TQ2`. The old `TQ` data type packs the whole timing into one field's components: - TQ.1 Quantity (CQ) → `quantity` - TQ.2 Interval (RI): RI.1 repeat pattern → `repeatPattern`, RI.2 explicit time → `explicitTime` - TQ.3 Duration → `serviceDuration` - TQ.4 / TQ.5 Start / End (TS) → `startDateTime` / `endDateTime` - TQ.6 Priority (ID) → `priority` (surfaced as a CWE `{ identifier }`) - TQ.12 Total Occurrences (NM) → `totalOccurrences` Primary sources: HL7 v2.5.1 Ch. 4 (`https://www.hl7.eu/HL7v2x/v251/std251/ch04.html`), HL7 v2.7 Ch. 4 (legacy-`TQ` withdrawal, `https://www.hl7.eu/HL7v2x/v27/std27/ch04.html`), Table 0335 Repeat Pattern (OID `2.16.840.1.113883.18.205`), Caristix TQ1 field reference (`https://hl7-definition.caristix.com/v2/HL7v2.5/Segments/TQ1`). ## Repeat pattern: verbatim, classified for provenance only `repeatPattern` carries the Table-0335 code **exactly as authored** in `code`: never normalized, never resolved to clock times, never mapped to a different frequency. Alongside it, a **provenance-only** `kind` tells a consumer _what kind_ of pattern it is looking at; `code` always remains authoritative: | `kind` | Meaning | `interval`? | | -------------- | ------------------------------------------------------------------------- | ------------------------------------- | | `"parametric"` | a `Q` template: `Q6H`, `Q30M`, `Q2D`, `Q1W`, `Q3J5` | `{ count, unit }`: **never dropped** | | `"named"` | a recognized fixed mnemonic: `BID`, `TID`, `QID`, `QOD`, `QHS`, `PRN`, … | omitted | | `"unknown"` | anything else (a local code, free text, an unrecognized mnemonic) | omitted, surfaced **verbatim** | Table 0335 deliberately **mixes** fixed enumerated codes (scheduled at institution-specified times) with **parametric templates** whose **integer is load-bearing**. `Q6H` (every 6 hours) is a different dose count from `Q8H`. The `interval.count` is the parsed integer and is **never dropped**; `code` still holds the exact authored string (so `Q06H` keeps its leading zero). The `kind` flag is convenience only. It is **never** used to resolve a schedule, and an unrecognized pattern is surfaced verbatim rather than guessed. ## TQ1 vs legacy: chosen by presence (never double-counted, never dropped) The library treats the **presence of a `TQ1` segment** as the v2.5+ structured- timing signal: - Every `TQ1` grouped under the order/medication yields one `OrderTiming` (`source: "TQ1"`). - The **legacy** embedded TQ is read **only when the group carries no `TQ1`** → `source: "legacy"`. The source field is `ORC-7` for an `orders()` group; for a `medications()` group it is `RXE-1` (an encoded RXE order) or, failing that, the preceding `ORC`'s `ORC-7` (so a pre-v2.5 `RXO` pharmacy order whose timing lives in `ORC-7` (and which has no `OBR` to surface it via `orders()`) is not dropped). The `ORC-7` fallback is consumed by only the **first** RX\* of an ORC group, so an `ORC RXO RXE` group never double-surfaces the same `ORC-7` timing. This is deliberately **presence-based, not `MSH-12`-version-based**: it is fail-safe against a message that mis-declares its version, it never **double-counts** the same timing when a modern sender redundantly fills both, and it never **drops** a legacy-only (pre-2.5) timing. A malformed / non-numeric field is tolerantly omitted, never fabricated (HELPERS-07, never throws). ## Positional grouping Timings attach to their order/medication group by document position, the same state machine `orders()` uses for OBX→OBR and `medications()` for RXR→RX\*: - A `TQ1` may appear **either side** of the order detail in a v2.5+ order group (some layouts place the timing group after `OBR`/`RXE`, others between `ORC` and it). A `TQ1` seen **within** an open group attaches to it; a `TQ1` seen before the group's opener is promoted to the next group. - **An intervening `ORC` re-scopes the timing.** Once a new `ORC` begins the next order group, a following `TQ1` binds to that next group, never to the still-open prior order (so `ORC1 OBR1 ORC2 TQ1 OBR2` gives the `TQ1` to the OBR2 order, and OBR1 keeps its own `ORC1`/`ORC-7` timing). - Multiple `TQ1` segments each surface (a tapering schedule of `40 mg BID`, then `20 mg QD`, is two timings). ## Known limitations / non-goals - **No schedule computation.** hl7 surfaces the timing structure; it does not compute administration times, resolve "institution-specified times" to clock times, interpret sig text, or do dose-frequency safety logic. - **`TQ2` (Timing/Quantity Relationship) is not modelled.** It links related orders (sequencing / conjunction across orders); only its segment name is recognized. First-class `TQ2` support is a later candidate. - **First repetition of repeating timing fields.** TQ1-3 (RPT) and TQ1-4 (TM) are repeating; the first repetition (the common case) is surfaced. A message encoding several patterns in a single repeating TQ1-3 field surfaces the first; a tapering schedule authored as **separate `TQ1` segments** (the conformant form) is fully surfaced. - **One legacy TQ per group.** The legacy `ORC-7` / `RXE-1` field is itself repeating; the first repetition is read. Multi-repetition legacy TQ is rare and not surfaced beyond the first. - **CQ units depth.** TQ1-2 units (`CQ.2`, a CE) are surfaced as a `CWE`; the legacy embedded TQ's collapsed units subcomponent is surfaced as `{ identifier }`. - **Numeric parsing follows the library `NM` convention.** `totalOccurrences` and `quantity.value` parse via `Number()` (the same strict-but-permissive rule every `field.asNm()` uses across the library): a non-conformant `NM` string (`1e3`, `0x10`, `10.5`) yields that JS number rather than a warning. This is deliberately uniform with `medications()` amounts and `observations()`, not a timing-specific behavior; conformant integer `NM` values (the norm) are exact. - **No new warning code.** Order and medication timing is a pure additive read surface; it emits no warning of its own. --- URL: https://docs.cosyte.com/hl7/spec-notes-charset Markdown: https://docs.cosyte.com/hl7/spec-notes-charset.md # Spec notes: character-set / encoding decode `parseHL7` accepts a `Buffer` as well as a `string`. When you hand it bytes, it resolves the message's declared character set from **MSH-18** and decodes the byte stream to text **before** delimiter tokenization. So accented and non-Latin names survive intact instead of turning into mojibake or replacement characters. ```ts import { parseHL7 } from "@cosyte/hl7"; import { readFileSync } from "node:fs"; // A message that declares MSH-18 = "UNICODE UTF-8" and carries "Renée". const msg = parseHL7(readFileSync("adt.hl7")); // Buffer, not a string msg.get("PID.5.1"); // "Renée" ✅ (not "Renée") ``` Pass a `string` and nothing here runs. The caller already decoded, and the parser trusts that decision. ## Spec traceability HL7 v2 Chapter 2: - **MSH-18 Character Set** (item 00692), value set **Table 0211** (OID `2.16.840.1.113883.18.116`). MSH-18 is a **repeating** field: the **first** occurrence is the message's default encoding; later occurrences are the alternates a message activates mid-stream. A **blank** MSH-18 means **7-bit ASCII** (the default repertoire, ISO IR6). - **§2.7.4 (charset-switch escapes):** `\Cxxyy\` (single-byte) and `\Mxxyyzz\` (multi-byte; `zz` optional) name an alternate repertoire inline. These are **recognized** (the escape layer preserves them verbatim: no `UNKNOWN_ESCAPE_SEQUENCE`); full stateful ISO-2022 rendering of the switched bytes is a documented non-goal (see limitations). ## What is decoded vs preserved Charset resolution runs through a **frozen Table-0211 registry** (`resolveCharset`), mirroring `KNOWN_SEGMENTS`, one source of truth for how a code is treated: | Table-0211 code | Treatment | | --- | --- | | `8859/1` (and `ISO-8859-1` synonyms) | **decode**: via Node `latin1` (**byte-exact**, never fails) | | `ASCII` / blank / `ISO IR6` | **decode**: strict UTF-8 (a superset; 7-bit is identical) | | `UNICODE UTF-8`, `UNICODE`, `UTF-8` | **decode**: strict UTF-8 | | `8859/2` to `8859/8`, `8859/10`, `8859/13` to `8859/16` (and `ISO-8859-N` synonyms) | **decode**: strict `TextDecoder` (C1 range faithful) | | `8859/9`, `8859/11` | **preserve verbatim**: no faithful Node decoder (windows-1254/874 alias) | | `ISO IR14/87/159`, `GB 18030-2000`, `KS X 1001`, `CNS 11643-1992`, `BIG-5` | **preserve verbatim**: recognized, not decoded | | `UNICODE UTF-16`, `UNICODE UTF-32` | **preserve verbatim**: recognized, not decoded | | anything not in Table 0211 | **preserve verbatim**: unrecognized | `resolveCharset` and `canonicalCharset` are exported for callers that want to inspect this mapping directly. **`8859/1` decodes via `latin1`, not `TextDecoder`.** Node's WHATWG `TextDecoder("iso-8859-1")` is actually **windows-1252**: it remaps the C1 range (byte `0x80` → `€`, `0x9F` → `Ÿ`), which is *not* ISO-8859-1. Node's `latin1` encoding is the true ISO-8859-1 (every byte maps 1:1), so `8859/1` (by far the most common non-UTF-8 HL7 charset) is decoded byte-exactly. The same aliasing affects **`8859/9`** (→ windows-1254) and **`8859/11`** (→ windows-874): Node's ICU remaps their C1 range (`0x80` to `0x9F`) to typographic characters instead of the true ISO code page, and a strict decode does **not** throw on those bytes: it would silently emit a wrong, non-recoverable character. Since Node offers no faithful decoder for them, they are **preserved verbatim** (flagged `UNSUPPORTED_CHARSET`, byte-recoverable) rather than mis-decoded. The other ISO-8859 sets (`8859/2` to `8859/8`, `10`, `13` to `16`) decode the C1 range faithfully (audited) and are decoded normally. **Why default to UTF-8 for the ASCII/blank case.** For conformant 7-bit content an ASCII decode and a UTF-8 decode are byte-identical, so honouring "blank = ASCII" costs nothing, and decoding the default as UTF-8 additionally rescues the ubiquitous real-world feed that ships UTF-8 without declaring MSH-18. A genuine ASCII message is never corrupted, and a **non**-UTF-8 byte under that default does not become `U+FFFD`: it takes the fail-safe below. ## Fail-safe: never guess, never silently corrupt Decoding is **strict** (`fatal: true`) for every set except byte-exact `8859/1`. A byte that is invalid or undefined for the declared set does **not** silently become `U+FFFD` (irreversible loss); instead the raw bytes are read as a `latin1` 1:1 mapping and one warning is raised. The same fail-safe covers a charset the parser does not decode at all: - **`UNSUPPORTED_CHARSET`**: a *recognized* Table-0211 code that was not decoded: either one this parser never decodes (the multibyte / ISO-2022 East-Asian sets, UTF-16/32) **or** a decodable set whose strict decode failed (a byte invalid for it, or an ICU build lacking the label). - **`UNKNOWN_CHARSET`**: a value that is *not* an HL7 Table-0211 code at all. Both read the bytes as `latin1`; the distinction is diagnostic. Neither warning can carry a decoded field value: a label is echoed **only when the Table-0211 registry recognises it**, and anything else is replaced with ``. That bound is membership rather than spelling for a reason. `UNKNOWN_CHARSET` fires precisely *when* a label is absent from the closed table, so at that moment there is no spec-defined spelling left to test against, and a shape test cannot tell `UNICODE UTF-8` from a patient name. It matters because MSH-18 is read as the 18th `|`-token of the text before the first terminator, so on a message carrying no segment terminators that token is a data field. **The cost, stated plainly:** `UNKNOWN_CHARSET` now never reports the label it rejected, because by definition the registry does not recognise it. A sender declaring `WINDOWS-1252` (the classic non-Table-0211 value) yields `Unrecognized character set ""`, and the observed label is on no field of the warning. To identify it, read MSH-18 off the message yourself. That is a deliberate trade of one diagnostic for a leak that reached a downstream report. **Recoverability is exact for single-byte content, best-effort for multibyte.** The HL7 structural bytes (the segment terminator CR (`0x0D`), LF, and the `|^~\&` delimiters) are 7-bit and unambiguous, so a *single-byte* undecoded field's content bytes survive and re-decode downstream via `Buffer.from(value, "latin1")`. A *multibyte* code unit, however, can contain a byte that equals a structural byte (a UTF-16 `0x0D`, say); the tokenizer then frames on it. That is precisely why multibyte **decode** is deferred rather than claimed. See the limitations below. ### `options.charset` override `parseHL7(buf, { charset })` forces the decode charset regardless of MSH-18. When both are present and **disagree** (after canonicalization, so `UNICODE UTF-8` and `UTF-8` are treated as equal), an **`ENCODING_MISMATCH`** warning is raised and the override wins. ## Known limitations (non-goals) - **Multibyte / ISO-2022 East-Asian decode.** The JIS (`ISO IR14/87/159`), GB 18030, KS X 1001, CNS 11643, and BIG-5 sets are **recognized and preserved verbatim**, not decoded. HL7 renders these through stateful `\Mxxyyzz\` switches from an ASCII default; stateless whole-buffer decoding would mis-render switched content, so correctness is preferred over a lossy guess. Opt-in decode, for the case where such a set is the *declared default*, may be added later. - **Multibyte framing on the verbatim path.** A recognized-but-undecoded set is read as `latin1` and still tokenized, so a content byte that equals an HL7 structural byte (the segment terminator CR (`0x0D`) / LF (`0x0A`), or a `|^~\&` delimiter) is framed as structure. This applies to **every** verbatim set (UTF-16/32 code units routinely embed `0x0D`/`0x0A`; the East-Asian DBCS sets less so) and to the `UNKNOWN_CHARSET` path. Byte-recoverability therefore holds for **single-byte** content only; multibyte framing is best-effort, which is why decode is deferred. The `UNSUPPORTED_CHARSET` / `UNKNOWN_CHARSET` warning flags that the charset was not decoded. - **Charset switch escapes are recognized, not applied.** `\Cxxyy\` / `\Mxxyyzz\` pass through preserved; the switched bytes are not re-rendered into the target repertoire. - **Byte-verbatim re-emit of a preserved charset escape** is tracked separately (escape-fidelity work): a preserved `\M…\` decodes losslessly *as text* but the serializer may canonicalize its backslashes. - **No transliteration** between character sets. ## Warning codes | Code | Meaning | | --- | --- | | `UNSUPPORTED_CHARSET` | recognized Table-0211 set not decoded (never-decoded, or a strict-decode failure), bytes read as `latin1` | | `UNKNOWN_CHARSET` | value not in Table 0211, bytes read as `latin1` | | `ENCODING_MISMATCH` | `options.charset` override disagrees with the declared MSH-18 | --- URL: https://docs.cosyte.com/hl7/spec-notes-escapes Markdown: https://docs.cosyte.com/hl7/spec-notes-escapes.md # Spec notes: escape sequences & round-trip fidelity HL7 v2 reserves the delimiter characters plus a small escape grammar (HL7 v2 Chapter 2 **§2.7**). `@cosyte/hl7` follows Postel's Law: it **decodes** escapes on parse so field values read naturally, and it **re-emits spec-clean HL7** on serialize. The guarantee this note pins is that serialize is **byte-verbatim** for parsed content: a parsed field re-emits the sender's _exact_ escape bytes, not a canonicalized equivalent. ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7( "MSH|^~\\&|A|B|C|D|20260101||ORU^R01|ID1|P|2.5\r" + "OBX|1|TX|C^T^L||Critical \\H\\value\\N\\ and \\X41\\||||||F", ); const obx5 = msg.segments("OBX")[0]?.field(5); obx5?.value; // "Critical \H\value\N\ and A" (decode-on-read) obx5?.text; // "Critical \H\value\N\ and \X41\" (byte-verbatim on emit) ``` ## The escape families and how each round-trips | Family | Sequences | On read (`value`) | On emit (`toString`/`text`) | | -------------------- | ------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------ | | **Delimiter** | `\F\ \S\ \T\ \R\ \E\` | decoded to the literal char (`\F\`→`\|`) | re-escaped from the decoded char, **byte-exact** | | **Newline** | `\.br\` | decoded to `\n` | re-escaped to `\.br\`, **byte-exact** | | **Truncation** | `\P\` (v2.7+) | decoded to MSH-2's truncation char (or spec default `#`) | re-escaped to `\P\` only when MSH-2 declared one | | **Hex** | `\X..\` | decoded to the raw byte(s) (`\X41\`→`A`) | **re-encoded to the sender's exact `\X..\` bytes** (overlay) | | **Highlight** | `\H\ \N\` | preserved verbatim (no rendering policy) | **verbatim** (overlay) | | **Formatting** | `\.sp\ \.in\ \.ti\ \.fi\ \.nf\ \.ce\` | preserved verbatim | **verbatim** (overlay) | | **Charset switch** | `\Cxxyy\ \Mxxyyzz\` | preserved verbatim (§2.7.4) | **verbatim** (overlay) | | **Vendor / unknown** | `\Z..\`, unrecognized bodies | preserved verbatim + `UNKNOWN_ESCAPE_SEQUENCE` | **verbatim** (overlay) | ### The `\X..\` hex policy: decode-on-read, re-encode-on-emit A hex escape is **decoded on read** (its byte value is what a consumer wants) but **re-encoded to the sender's original bytes on emit**, including the exact digit **casing** (`\X0d\` is _not_ normalized to `\X0D\`). Decoding away the `\X..\` shape and re-emitting the raw byte would be lossy at the wire level: a byte-level correlation (an ACK's MSA-2 echo against the inbound MSH-10, HL7 v2 §2.9.2.2) would no longer match. The one exception is a decoded **CR** (`\X0D\` → `\r`): a bare CR is the HL7 segment separator, so any decoded CR re-emits as `\X0D\` regardless of how it arrived. Emitting it raw would corrupt the wire framing. ## How it works: the fidelity overlay The raw tree stores **decoded** subcomponent strings: what every reader (`Field.value`, the composite parsers, dot-path, `toJSON`) sees. `reescape` can faithfully invert only the delimiter/newline families, because the preserve-and-hex families decode to ordinary characters that carry no "I was an escape" marker (`\H\` decodes to the literal three characters `\`,`H`,`\`, which `reescape` would double-escape to `\E\H\E\`). So the tokenizer additionally records the **original wire bytes** of exactly those subcomponents in an internal `RawComponent.rawSubcomponents` overlay, and the serializer emits from it verbatim. The overlay is populated **only** for a subcomponent whose decoded form does not re-escape byte-identically. The common, escape-free message carries no overlay at all, so its raw tree is unchanged. `Field.value` (and the whole decoded value/coercion surface) is **unchanged** by this. The overlay is consulted only on emit. Because parse→emit is now byte-verbatim, `parseHL7(x).toString()` is a fixpoint for spec-clean input across the entire escape alphabet, and `buildAck` echoes an escape-bearing MSH-10 into MSA-2 byte-for-byte. ### Cross-encoding emit: the overlay is same-alphabet only The overlay holds the sender's **raw bytes in the sender's delimiter alphabet**, so it is only valid when emit uses that same alphabet. `toString()` and `Field.text` always emit under the message's own encoding characters, so they are always safe. `buildAck` is the one place that **re-encodes**. It emits the ACK with the _default_ alphabet. For a **default-delimiter** inbound (the norm) the overlay applies and MSA-2 is byte-exact; for a **custom-delimiter** inbound the overlay is **bypassed** and the decoded control id is re-escaped under default (spec-clean, and it re-parses back to the same decoded id, correlation-correct per §2.9.2.2). Emitting the sender's raw bytes verbatim under a different alphabet would corrupt MSA-2's structure, so it is not done. ## Known limitations - **Constructed (not parsed) fields have no overlay.** A value set by hand via `setField` / the builder re-escapes its decoded string on emit (the only correct behavior, there are no original wire bytes to preserve). Byte-verbatim fidelity is a property of _parsed_ content. - **Charset-switch bytes are preserved, not rendered.** `\Cxxyy\` / `\Mxxyyzz\` round-trip verbatim, but full stateful ISO-2022 decoding of the switched bytes remains a documented non-goal, see [Character sets (MSH-18)](https://docs.cosyte.com/hl7/spec-notes-charset.md). - **Vendor `\Z..\` escapes are preserved, never interpreted.** They round-trip byte-verbatim and raise `UNKNOWN_ESCAPE_SEQUENCE`; a future profile hook may register vendor handlers. --- URL: https://docs.cosyte.com/hl7/spec-notes-text-rendering Markdown: https://docs.cosyte.com/hl7/spec-notes-text-rendering.md # Spec notes: formatted-text rendering & the text codec The lenient parser **decodes** delimiter, hex, and `\.br\` escapes on read but deliberately **preserves** the presentational ones (`\H\`/`\N\` highlight, the `\.sp\`/`\.in\`/… formatting commands, charset switches, vendor `\Z..\`) as un-rendered sentinels. See [Escapes & round-trip](https://docs.cosyte.com/hl7/spec-notes-escapes.md). That is correct for a tolerant parser, but a clinical narrative surfaced to a human with raw `\.br\` / `\H\` sentinels in it is **misread**. The library adds an opt-in **rendering + encoding layer** on top of the unchanged raw values: - **`renderText`** turns HL7 v2 §2.7 escape/formatting-bearing content into a normalized **display model**: plain text plus highlight-aware runs. - a first-class **text codec** (`decodeText` / `encodeText`, also bundled as the `text` namespace) gets the human string out of a field and, crucially, encodes an arbitrary string **in** without ever injecting a delimiter. Neither changes the raw parse output. Rendering is a **read projection** and the tolerant extraction is byte-for-byte what it always was (Postel's Law). ## Rendering a formatted note ```ts runnable import { renderText } from "@cosyte/hl7"; const r = renderText("Specimen received.\\.br\\Result is \\H\\HIGH\\N\\."); r.text; // => "Specimen received.\nResult is HIGH." r.runs; // => [{ text: "Specimen received.\nResult is ", highlighted: false }, { text: "HIGH", highlighted: true }, { text: ".", highlighted: false }] r.unrenderedSequences; // => [] ``` `Field.render()` is the one-call form on a parsed field: ```ts runnable import { parseHL7 } from "@cosyte/hl7"; const raw = "MSH|^~\\&|LAB|MAIN|EHR|REF|20260701100000||ORU^R01|1|P|2.5\r" + "OBX|1|TX|N^Narrative^L||Gross exam unremarkable.\\.sp\\Microscopic pending.||||||F"; parseHL7(raw).segments("OBX")[0]?.field(5).render().text; // => "Gross exam unremarkable.\nMicroscopic pending." ``` ### Normalization policy (conservative, not a page-layout engine) Confirmed firsthand against HL7 v2 Chapter 2 §2.7 (v2.5.1 + v2.8.2; section numbers shift by version): | Escape | § | Rendered as | | ------------------------- | ------- | ---------------------------------------------------- | | `\F\ \S\ \T\ \R\ \E\ \P\` | 2.7.1 | the literal delimiter / escape / truncation char | | `\Xdddd…\` | 2.7.5/6 | the decoded byte(s) (hex digit **pairs**) | | `\H\` / `\N\` | 2.7.1 | dropped from `text`; toggles a `highlighted` run | | `\.br\` / `\.ce\` | 2.7.6/7 | one line break | | `\.sp\` / `\.sp \` | 2.7.6/7 | `n` line breaks (default 1) | | `\.sk\` / `\.sk \` | 2.7.6/7 | `n` spaces (default 1) | | `\.in \` / `\.ti \` | 2.7.6/7 | dropped (indentation, exact column math is a defer) | | `\.fi\` / `\.nf\` | 2.7.6/7 | dropped (word-wrap mode toggle, no content) | A formatting sentinel is **never** left in the rendered `text`. Pass the wire form (`Field.text`, byte-verbatim for parsed content) for the most faithful result; a pre-decoded `Field.value` also renders correctly (its `\.br\` is already a newline). ## Never fabricates: unrenderable escapes are preserved + flagged A charset switch, a vendor `\Z..\`, or a malformed / unterminated escape cannot be rendered without guessing, so `renderText` keeps its **literal characters** in the output and records it in `unrenderedSequences`. Nothing is ever silently dropped or replaced with an invented glyph. ```ts runnable import { renderText } from "@cosyte/hl7"; const r = renderText("interpret \\Z9\\ per site agreement"); r.text; // => "interpret \\Z9\\ per site agreement" r.unrenderedSequences; // => ["\\Z9\\"] ``` ## The text codec: decode, and encode-safe `decodeText` resolves delimiter/hex/`\.br\` escapes (no warning plumbing to wire up); `encodeText` is the **encode-safe** direction and the primitive the typed emit surface builds on. Every reserved character (the escape char first, then the field / component / subcomponent / repetition separators, the truncation char, and framing-critical CR/LF) is replaced by its escape sequence, so an arbitrary value **cannot break out of its field**. ```ts runnable import { encodeText, decodeText, parseHL7 } from "@cosyte/hl7"; const hostile = "a|b^c~d\\e&f"; encodeText(hostile); // => "a\\F\\b\\S\\c\\R\\d\\E\\e\\T\\f" decodeText(encodeText(hostile)); // => "a|b^c~d\\e&f" // Placed in a field, it re-parses to exactly itself, no forged boundary: const msg = parseHL7(`MSH|^~\\&|A|B|C|D|20260101||ADT^A01|1|P|2.5\rNTE|1||${encodeText(hostile)}`); msg.segments("NTE")[0]?.field(3).value; // => "a|b^c~d\\e&f" ``` The invariant `decodeText(encodeText(s)) === s`, and whole-field re-parse with no delimiter injection, are property-tested over arbitrary strings. Two caveats are inherent to HL7 field encoding (not the codec): the two-character token `""` is HL7's explicit null, and the default parser trims field whitespace, so a value that is exactly `""` or has leading/trailing spaces re-parses null/trimmed at the **field** level. Encode into a component/subcomponent position (or parse with trimming off) to preserve those exactly; the no-injection guarantee is unaffected. ## Known limitations - **Not a page-layout engine.** Highlight is a boundary marker, not a style; indentation is normalized away (exact column math is a defer); no RTF/HTML output. - **Charset switches are preserved, not applied.** `\Cxxyy\` / `\Mxxyyzz\` need stateful ISO-2022 decoding that remains a documented non-goal. They render as their literal characters and are flagged in `unrenderedSequences`. - **Rendering is opt-in.** `Field.value` / `toString()` and the whole raw extraction surface are unchanged; `renderText` never mutates them. --- URL: https://docs.cosyte.com/hl7/spec-notes-nte Markdown: https://docs.cosyte.com/hl7/spec-notes-nte.md # Spec notes: NTE narrative grouping NTE (Notes and Comments) segments are surfaced on the helper output for the **entity they annotate**, resolved **by position**. A lab result note lands on that observation, an order note on the order, a patient note on the patient, and a note with no recognized parent at the message level, never dropped, never mis-attached. > **hl7 attaches notes _positionally_; it never _guesses_.** An NTE inherits its > meaning entirely from the segment it immediately follows (HL7 v2 Ch. 2, the > NTE has **no link field**). A note silently attached to the wrong result, or > dropped, is a clinical-narrative loss. When the nearest preceding non-NTE > segment is not a recognized parent, the note is surfaced **message-level**, > not attached to an unrelated earlier parent. ## Spec traceability HL7 v2 Chapter 2 (control) + Chapter 7 (observation reporting): - **NTE: Notes and Comments (Ch. 2).** A general-purpose segment whose meaning is defined by the **parent context**. Each chapter specifies what an NTE means where it appears. It carries no field linking it to a parent, so attachment is **positional**: an NTE annotates the entity whose segment immediately precedes it. - **NTE-3 Comment (FT), repeating.** The note body. Each non-empty repetition is surfaced as one note line, HL7-unescaped (so `\F\` → `|`, `\T\` → `&`, and the FT line-break escape `\.br\` → newline). The **full** repetition text is preserved: a lenient parser tokenizes a non-conformant raw `^` / `&` in the FT narrative into components/subcomponents, so the library reassembles all of them (unescape each leaf, rejoin with the literal delimiters) rather than reading only the first. A truncated clinical note reads as complete and is worse than a dropped one. Both the conformant (`mass 2\S\3 cm`) and quirky (`mass 2^3 cm`) forms yield the same text. NTE-1 (Set ID), NTE-2 (Source of Comment, Table 0105), and NTE-4 (Comment Type, Table 0364) are **not** modelled. Only NTE-3 text is surfaced. - **ORU_R01 placement (Ch. 7).** The abstract message grammar is `{ [ORC] OBR [{NTE}] [{ [OBX] [{NTE}] }] }`: an NTE after `OBR` is **order-level**, an NTE after `OBX` is **that-result-level**. An NTE after `PID` annotates the **patient**; an NTE after `ORC` annotates the **order** (ORC and OBR both denote the common order). Primary sources: HL7 v2.5.1 Ch. 2 NTE definition (`https://www.hl7.eu/HL7v2x/v251/std251/ch02.html`), HL7 v2.5.1 Ch. 7 ORU (`https://www.hl7.eu/HL7v2x/v251/std251/ch07.html`), v2plus NTE segment reference (`http://v2plus.hl7.org/2021Jan/segment-definition/NTE.html`). ## Positional grouping: nearest preceding non-NTE segment A single walk over `msg.allSegments()` resolves every NTE to a parent: - The **recognized parents** are `PID`, `ORC`, `OBR`, and `OBX`. An NTE attaches to the most recent one it follows. - **Consecutive NTEs chain** to the same parent (`OBX NTE NTE` → both on that observation, in order). - **Any intervening non-NTE, non-parent segment resets the target.** After `PID NTE PV1 NTE`, the first note is the patient's but the second, whose nearest preceding non-NTE segment is `PV1` (not a recognized parent), is surfaced **message-level**, not attached to the earlier `PID`. This is the fail-safe: the library never reaches back past an unrelated segment to guess a parent. - Notes are keyed by **segment reference**. `observations()`, `orders()`, and `buildPatient()` look up a parent's notes from the same referentially-stable `Segment` instances that `msg.segments(type)` / `allSegments()` return from one shared cache, so a note surfaced via `msg.observations()` is identical to the one under `order.observations`. Order-level notes are handled to mirror the `orders()` state machine: notes seen after an `ORC` (before its OBR) are **buffered and flushed onto the OBR** that opens the order, so `order.notes` reads as a single document-ordered list (the ORC-region notes ahead of the OBR-region notes), and **several `ORC`s before one `OBR` all contribute** rather than only the last. Result-level notes live on each `Observation`, not on the order. Message-level orphans are read via `msg.notes()`. **Never dropped, even when a parent's projection is dropped.** A note whose recognized parent has no surfaced helper output is routed to `msg.notes()` rather than vanishing: (1) a **later PID**'s notes in a multi-patient ORU (`msg.patient` is the first PID only), and (2) a **trailing or dangling `ORC`** whose order never opens (no following OBR). Both are surfaced at message level, never lost, never mis-attached to the first patient or a neighbouring order. ## Known limitations / non-goals - **Positional grouping only.** The library does not interpret NTE-2 (source of comment) or NTE-4 (comment type), and does not render FT formatting commands beyond the standard escape set (`\.br\` becomes a newline; other formatting escapes pass through `Field.value` unescaping unchanged). It does not parse note prose. - **Recognized parents are `PID` / `ORC` / `OBR` / `OBX`.** An NTE following any other segment (e.g. `PV1`, `AL1`, `DG1`, `IN1`, a `Z`-segment) is surfaced message-level rather than attached, a conservative "not guessed" choice. First-class notes on those segments are a later candidate. - **First PID only for `patient.notes`.** In a multi-patient ORU the patient view is the first `PID`. A later PID's notes are **not** surfaced through `msg.patient` (consistent with the single-patient helper model), but they are never dropped or mis-attached: they surface at message level via `msg.notes()`. - **Trailing / dangling `ORC`.** A note on an `ORC` that never opens an order (no following `OBR`) is surfaced at message level (`msg.notes()`), not on any order, never dropped. (This differs from `orderControl` (ORC-1), which `orders()` genuinely drops for such an ORC; the note is preserved rather than discarded because losing clinical narrative is the failure the grouping guards against.) - **No new warning code.** Note grouping is a pure additive read surface; it emits no warning of its own. NTE free-text is high-PHI-risk clinical narrative and is treated purely as payload. No warning or log line echoes note text. --- URL: https://docs.cosyte.com/hl7/spec-notes-result-status Markdown: https://docs.cosyte.com/hl7/spec-notes-result-status.md # Spec notes: result status classification Every observation and every order carries `resultStatus`: its own status code, classified by the map HL7 publishes for that code's table. A result posted as wrong (`W`), deleted (`D`) or cancelled (`X`) can be told apart from a current one without keeping a private copy of either table. > **hl7 classifies what HL7's map classifies, and nothing else.** A code the map > leaves unmapped, an empty or absent field, and any value that is not exactly > one code of its table classify as `undetermined`, never as `final`. An > `undetermined` result is not a current result: read the raw `code` and decide. ## What is classified | Helper output | Field read | Table | Map followed | | -------------------------- | -------------------------------- | ----------------------------- | ----------------------------------------------------- | | `observation.resultStatus` | OBX-11 Observation Result Status | HL7 Table 0085, version 3.0.0 | Table 0085 to Observation Status, version 1.0.0 | | `order.resultStatus` | OBR-25 Result Status | HL7 Table 0123, version 3.0.0 | Table 0123 to Diagnostic Report Status, version 1.0.0 | An observation carries it wherever it is returned: `msg.observations()`, `order.observations`, and the observations of `msg.documents()` and `msg.immunizations()`. The same OBX gives the same classification in every view. Both maps come from the HL7 Version 2 to FHIR Implementation Guide (STU 1, standards status Informative). Their canonical URLs are: - `http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70085-to-observation-status` - `http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70123-queries-to-diagnostic-report-status` Every classification names its table and map, each with its version, so a later revision of either map shows up in the output instead of changing it silently. ### The shape `resultStatus` is a frozen object, and everything it nests is frozen too: - `classification`: one of `final`, `corrected`, `amended`, `preliminary`, `entered-in-error`, `cancelled`, `registered`, `partial` or `undetermined`. - `code`: the raw code, byte-identical to the `status` (OBX-11) or `orderStatus` (OBR-25) beside it. It is absent exactly when those are: the field is absent, empty or the HL7 null `""`. - `table`: `{ name, version }`, for example `{ name: "HL7 Table 0085", version: "3.0.0" }`. - `map`: `{ url, version }`, the canonical URL above and `"1.0.0"`. `status` and `orderStatus` are unchanged: still the decoded code, still omitted when the field is empty. ## OBX-11: Table 0085 to Observation Status | OBX-11 | `classification` | | -------------------------------------- | ---------------------------------------- | | `A` | `amended` | | `C` | `corrected` | | `D` | `entered-in-error` | | `F` | `final` | | `P` | `preliminary` | | `X` | `cancelled` | | `W` | `entered-in-error` | | `B`, `I`, `N`, `O`, `R`, `S`, `V`, `U` | `undetermined` (the map: "(not mapped)") | `V` (verified), `U` (status changed to final) and `B` (appended report) read as final to a person, but HL7's map gives them no status. They classify as `undetermined` rather than as a guess at the class HL7 declined to give. ## OBR-25: Table 0123 to Diagnostic Report Status | OBR-25 | `classification` | | ----------------------- | ---------------------------------------- | | `O`, `I`, `S` | `registered` | | `P` | `preliminary` | | `C` | `corrected` | | `R` | `partial` | | `F` | `final` | | `X` | `cancelled` | | `A`, `Y`, `Z`, `M`, `N` | `undetermined` (the map: "(not mapped)") | ## Exactly one code, or `undetermined` A code classifies only when the field arrived as exactly one code of its table, case and all. Everything else is `undetermined`, and the observation or order is still returned: - **Empty, absent or null.** An empty field, a segment that ends before the field, or the HL7 null `""`. - **Not a table code.** A lowercase `f`, a multi-character `FF`, a letter outside the table such as `Q`. - **Whitespace around the code.** `" F"` or `"F "`. With the default field trimming the parser still trims the field and reports `FIELD_WHITESPACE_TRIMMED`, so `status` and `code` read `F`; the classification stays `undetermined` because the field did not arrive as exactly one code. - **A VT or FS byte inside the field.** The parser removes every VT (`0x0B`) and FS (`0x1C`) byte from a message before it reads segments, wherever the byte stood, so a field sent as a VT then `F` has `status` and `code` `F`. The classification stays `undetermined`. Only an MLLP block frame around the whole message, a VT as its first byte together with an FS as its last byte or before one final CR, belongs to no field. - **A code written as an escape sequence.** `\X46\` decodes to `F`, and `status` and `code` read `F`, but the field did not carry the code as itself. - **More than one value.** More than one repetition, component or subcomponent, for example `F~W`, `F^X`, `F&X` or even `F~`, even when the first value is a mapped code. `status` and `code` hold only the first value. No tolerance is added to reach a class: a near-miss is `undetermined`, and the raw code is still carried for the caller to judge. ## Limits - **Reported, never applied.** hl7 holds no store. An `entered-in-error` or `cancelled` observation is still returned; replacing, hiding or deleting an earlier result it refers to, and matching results across messages, is the caller's decision. - **Each segment classifies from itself.** HL7 says an OBX-11 of `D` or `X` applies only to its own OBX, so nothing is carried across OBX segments that share OBX-3 and OBX-4. An order classifies from OBR-25 alone, never from its observations: OBR-25 `F` over an OBX-11 `W` gives a `final` order and an `entered-in-error` observation. - **Two fields only.** OBX-11 and OBR-25 are classified here. ORC-1, ORC-5 and every other status field are surfaced verbatim, as before. RXA-20 and RXA-21 are surfaced verbatim too, and classified separately, per immunization: see the [immunization administration status](https://docs.cosyte.com/hl7/spec-notes-immunization-status.md). - **Not a FHIR element.** The classification is a plain string. No Observation, DiagnosticReport or ConceptMap resource is built and no terminology service is consulted. It is the one place hl7 applies a v2-to-FHIR map; see the [FHIR-bridge IR contract](https://docs.cosyte.com/hl7/spec-notes-fhir-bridge.md). - **Informative maps.** Both maps are STU 1 content with standards status Informative. The classification follows the version it names and does not track later revisions. - **What a field arrived as is recorded when it is parsed.** Whether the parser trimmed a field or removed a VT or FS byte from it stays with that field when other segments are added or removed. A field replaced with `setField` classifies from the value set. A message parsed again from `msg.toString()` classifies from that text, which carries neither the whitespace nor the bytes. - **An MLLP frame reads as a frame.** In a message that starts with a VT and ends with an FS, or with an FS and one CR, those two bytes belong to no field, so a status field that ends the message classifies by its code. Every other VT or FS is read as part of the field it stands in: an FS at the end of a message that does not start with a VT, or an FS followed by anything more than one CR, leaves the field before it `undetermined`. ## Example ```ts runnable import { parseHL7 } from "@cosyte/hl7"; // A synthetic ORU^R01: a final order whose second result was posted in error. const raw = [ "MSH|^~\\&|LAB|FAC|EHR|FAC|20260419101500||ORU^R01|EX00002|P|2.5.1", "PID|||X", "OBR|1|PLACER1|FILLER1|GLU^Glucose^L" + "|".repeat(21) + "F", // OBR-25 is F "OBX|1|NM|GLU^Glucose^L||95|mg/dL|||||F", "OBX|2|NM|GLU^Glucose^L||59|mg/dL|||||W", "OBX|3|NM|GLU^Glucose^L||97|mg/dL|||||V", ].join("\r"); const msg = parseHL7(raw); msg.orders()[0]?.resultStatus.classification; // => "final" const [first, second, third] = msg.observations(); first?.resultStatus.classification; // => "final" second?.resultStatus.classification; // => "entered-in-error" second?.resultStatus.code; // => "W" second?.status; // => "W" third?.resultStatus.classification; // => "undetermined" third?.resultStatus.table.name; // => "HL7 Table 0085" third?.resultStatus.map.version; // => "1.0.0" ``` --- URL: https://docs.cosyte.com/hl7/spec-notes-immunization-status Markdown: https://docs.cosyte.com/hl7/spec-notes-immunization-status.md # Spec notes: immunization administration status Every immunization `msg.immunizations()` returns carries `administrationStatus`: whether its RXA records a dose given, a refused or not-administered dose, a CVX `998` "no vaccine administered" placeholder, or a request to delete an administration sent earlier. A dose count can tell them apart without keeping a private copy of HL7 Table 0322, HL7 Table 0323 or the CDC's CVX rules. > **Only `completed` is a dose given.** A record is `completed` only when RXA-20 > is exactly `CP` or `PA`, RXA-21 is empty or exactly `A`, `U` or `X`, and RXA-5 > carries no CVX `998`. Anything not exactly mapped is `undetermined`, never > `completed`. An `undetermined` record is not a dose: read the raw codes and > decide. ## The classifications | `classification` | What the record says | Decided by | | ------------------------- | ---------------------------------------------------------------------- | ---------- | | `completed` | A dose was given: RXA-20 `CP` (complete) or `PA` (partially given) | RXA-20 | | `not-done` | No dose was given: RXA-20 `RE` (refused) or `NA` (not administered) | RXA-20 | | `no-vaccine-administered` | RXA-5 carries CVX `998`, whatever RXA-20 says | RXA-5 | | `delete-requested` | RXA-21 is `D`: delete an administration sent earlier; it is not a dose | RXA-21 | | `undetermined` | No rule classifies it: a code that is not exactly one mapped code | any | ## Which rule decides Exactly one rule decides each record: the first of these that applies. 1. **RXA-21 exactly `D`**: `delete-requested`. What it deletes is not in this message, so it wins over everything else, including a CVX `998` and an RXA-20 `CP`. 2. **RXA-21 present but not exactly one of `A`, `D`, `U`, `X`** (HL7 Table 0323): `undetermined`, whatever RXA-5 and RXA-20 say. An action code that cannot be read might have been a deletion. 3. **RXA-5 carries CVX `998`**: `no-vaccine-administered`, whatever RXA-20 says, or `undetermined` when the other RXA-5 triplet contradicts it (below). 4. **RXA-20 by HL7's Table 0322 to Event Status map**: `CP` and `PA` are `completed`, `RE` and `NA` are `not-done`, and everything else, including an empty or absent RXA-20, is `undetermined`. RXA-21 `A` (add), `U` (update), `X` (no change) and an empty RXA-21 change nothing: the record classifies exactly as it would with RXA-21 empty. ### RXA-20: Table 0322 to Event Status | RXA-20 | `classification` | | ------------- | ---------------- | | `CP` | `completed` | | `PA` | `completed` | | `RE` | `not-done` | | `NA` | `not-done` | | anything else | `undetermined` | HL7's map says `PA` (partially administered) "is equivalent to" `completed`, and hl7 adopts it as published. The raw `PA` stays beside the classification for a caller that counts a partial dose differently. ### RXA-5: CVX 998 RXA-5 carries CVX `998` when one of its triplets (components 1 to 3, or the alternate components 4 to 6) has the identifier exactly `998` and a coding system that resolves to `CVX` by the same Table 0396 provenance as `codingSystemOf` (so `cvx` counts). The primary triplet also counts when it names no coding system at all. | RXA-5 | Result | | ------------------------------------- | ---------------------------------------------- | | `998^No vaccine administered^CVX` | `no-vaccine-administered` | | `998^No vaccine administered` | `no-vaccine-administered` | | `^^^998^No vaccine administered^CVX` | `no-vaccine-administered` | | `998^No vaccine^CVX^115^Tdap^CVX` | `undetermined`: the two triplets disagree | | `58160-0842-52^Tdap^NDC^998^None^CVX` | `undetermined`: the two triplets disagree | | `998^x^NDC` | not CVX `998`: RXA-20 decides | | `115^Tdap^CVX^998^x` | not CVX `998`: an alternate needs a CVX system | The CDC marks CVX `998` Inactive, but historical messages still carry it. It was added "for use in VXU HL7 messages where the OBX segment is nested with the RXA segment, but the message does not contain information about a vaccine administration", for example to report the vaccines due next. ## Exactly one code, or `undetermined` RXA-20 and RXA-21 count only when the field arrived as exactly one code of its table, case and all. Anything else is outside the table: an RXA-20 falls to `undetermined`, and a present RXA-21 makes the whole record `undetermined`. - **Not a table code.** A lowercase `cp` or `d`, `DEL`, `Z`. - **The HL7 null.** `""` is present and is not a code. - **Whitespace around the code.** `" D"` or `"CP "`. The parser still trims the field and reports `FIELD_WHITESPACE_TRIMMED`, so `actionCode` reads `D`, but the record is `undetermined`, not `delete-requested`. - **A VT or FS byte inside the field.** The parser removes it, as it does for a result status; the field did not arrive as exactly one code. - **A code written as an escape sequence.** `\X44\` decodes to `D`, but the field did not carry the code as itself. - **More than one value.** More than one repetition, component or subcomponent, for example `D~A`, `D^X`, `D&X` or even `D~`, even when the first value is a table code. ## The shape `administrationStatus` is a frozen object, and everything it nests is frozen too: - `classification`: one of `completed`, `not-done`, `no-vaccine-administered`, `delete-requested` or `undetermined`. - `completionStatus`: the raw RXA-20 code, byte-identical to the entry's own `completionStatus`, and absent exactly when that is. - `actionCode`: the raw RXA-21 code, byte-identical to the entry's own `actionCode`, and absent exactly when that is. - `decidedBy`: the field that decided and what it was read against: - `{ field: "RXA-20", table: { name: "HL7 Table 0322", version: "3.0.0" }, map }`, where `map` is `{ url: "http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70322-to-event-status", version: "1.0.0" }`; - `{ field: "RXA-21", table: { name: "HL7 Table 0323", version: "3.0.0" } }`; - `{ field: "RXA-5", codeSet: { name: "CDC CVX", code: "998", version: "2023-03-09" } }`. The CVX code set has no version number, so `version` is the date the CDC last updated the `998` entry. `completionStatus`, `actionCode` and every other key of the immunization are unchanged. ## Limits - **Reported, never applied.** hl7 holds no store. A `delete-requested` record is still returned, in document order, and is never matched against the administration it asks to delete; nothing is removed, merged or updated. Matching records across messages is the caller's decision. - **Each RXA classifies from itself.** A `D`, a `998` or an `undetermined` record never changes the classification of the RXA before or after it. - **`A`, `U` and `X` are not classified.** An `A` is common even for a dose reported before, so it says nothing about whether this dose is new. - **No code set is shipped.** Only the literal CVX code `998` is read. No CVX, MVX or other code is validated. - **An informative map.** The Table 0322 map is STU 1 content with standards status Informative. The classification follows the version it names and does not track later revisions. - **What a field arrived as is recorded when it is parsed.** A field replaced with `setField` classifies from the value set, and a message parsed again from `msg.toString()` classifies from that text. ## Example ```ts runnable import { parseHL7 } from "@cosyte/hl7"; // A synthetic VXU^V04: a dose given, a refusal, a CVX 998 placeholder, // a request to delete the first dose, and a lowercase completion status. const given = "RXA|0|1|20260801||115^Tdap^CVX|0.5|mL^^UCUM" + "|".repeat(13); const raw = [ "MSH|^~\\&|EHR|CLINIC|IIS|STATE|20260801101500||VXU^V04^VXU_V04|EX00003|P|2.5.1", "PID|||X", given + "CP|A", "RXA|0|1|20260801||115^Tdap^CVX|999" + "|".repeat(12) + "00^Parental decision^NIP002||RE|A", "RXA|0|1|20260801||998^No vaccine administered^CVX|999" + "|".repeat(14) + "NA", given + "CP|D", given + "cp", ].join("\r"); const msg = parseHL7(raw); const [dose, refusal, placeholder, deletion, unreadable] = msg.immunizations(); dose?.administrationStatus.classification; // => "completed" refusal?.administrationStatus.classification; // => "not-done" placeholder?.administrationStatus.classification; // => "no-vaccine-administered" placeholder?.administrationStatus.decidedBy.field; // => "RXA-5" deletion?.administrationStatus.classification; // => "delete-requested" deletion?.administrationStatus.actionCode; // => "D" unreadable?.administrationStatus.classification; // => "undetermined" unreadable?.administrationStatus.completionStatus; // => "cp" msg.immunizations().length; // => 5 ``` --- URL: https://docs.cosyte.com/hl7/spec-notes-fhir-bridge Markdown: https://docs.cosyte.com/hl7/spec-notes-fhir-bridge.md # Spec notes: FHIR-bridge IR-stability contract > **IR Contract v1.0.0** · grounds against the HL7 **v2-to-FHIR** Implementation Guide > **v1.0.0** (FHIR **R4**), commit `873b331b3890c8bc5d62ef9b4dabb41801aac70d`. `@cosyte/hl7` is a **mapping source**, not a FHIR converter. With one named exception, it does **not** know about FHIR: it constructs no resource, ships no ConceptMap resource, evaluates no FHIRPath, and translates no terminology. That work lives in a separate package, **`@cosyte/transform`**, which maps hl7's parsed model to FHIR using the public v2-to-FHIR IG. **The one exception is result status classification.** Every observation and order carries `resultStatus`, which classifies OBX-11 and OBR-25 by two maps this IG publishes: Table 0085 to Observation Status and Table 0123 to Diagnostic Report Status, both version 1.0.0. hl7 applies the rows those two maps map, names the map on every classification, and reports every code they leave unmapped as `undetermined`. The result is a plain classification string beside the raw code, not a FHIR element, and no ConceptMap resource ships. See [result status classification](https://docs.cosyte.com/hl7/spec-notes-result-status.md). This document is the **contract between the two**: the set of hl7 access paths `@cosyte/transform` may build against, and the promise that hl7 will not churn them out from under it. It is **versioned** so the bridge can be written once, against a pinned surface, without forcing retro-active hl7 changes. Nothing here adds a v2→FHIR mapping to hl7; the status classification above is the only one hl7 applies, and its own page documents it. This page documents and versions the IR hl7 **already** exposes, and proves (over the IG's public sample corpus) that the source paths the IG references are reachable from that IR (`test/fhir-bridge-coverage.test.ts`). ## Why a contract, and why versioned The v2-to-FHIR IG keys every mapping off a v2 **`segment.field[.component]`** source address and a handful of **datatype maps** (XPN→HumanName, CX→Identifier, CWE→CodeableConcept, DTM→dateTime, …). A mapper is therefore only as stable as the parser's addressing and its typed-datatype surface. If hl7 renamed an accessor or changed an indexing convention, every `transform` map would silently break. The contract pins exactly the surface a mapper needs, and puts a **semver posture** on it (below), so `transform` can pin `@cosyte/hl7` by version and know what a bump can and cannot change. ## The stable mapping surface (the IR) These are the access paths under contract. All are already-shipped, already-tested public API. The contract adds no new runtime surface, it **freezes** the relevant existing one. ### 1. Source-path addressing: `msg.get` / `resolvePath` / `parsePath` The IG's `segment.field[.component][.subcomponent]` addressing maps **one-to-one** onto hl7's dot-path. The indexing conventions are contractual: - **Segment occurrence** `SEG[n]`: **0-based** (omitted ⇒ first occurrence). - **Field** `SEG.N`: **1-based**, HL7 field number. (`MSH.3` is the sending application.) - **Field repetition** `SEG.N[r]`: **0-based** (omitted ⇒ first repetition). - **Component** `.C` and **subcomponent** `.S`: **1-based**. - **MSH offset** is handled internally: `MSH.1` is the field separator, `MSH.2` the encoding characters, `MSH.3` the sending application. The caller always uses HL7 field numbers. `msg.get(path)` returns the decoded leaf (unescaped once at parse) or `undefined` on a missing path: **never throws** on a missing value. `parsePath` validates the address shape without touching a message (useful for a mapper that wants to pre-compile its source paths). ### 2. Segment / field wrappers: `msg.segments(type)`, `Segment.field(n)`, `Field` For anything past a single leaf, the mapper walks wrappers: - `msg.segments(type)` / `msg.allSegments()`: segment wrappers in document order (stable identity). - `Segment.field(n)`: the `Field` at HL7 field number `n` (MSH-offset applied); returns a synthetic empty `Field` (never throws) on a missing field. - `Field.value`: first-subcomponent decoded string. `Field.text`: the **whole field** as canonical, byte-verbatim wire text (repetitions/components included), for a field that must be carried whole. ### 3. Typed composites: the datatype-map inputs The IG's datatype maps consume typed HL7 composites; hl7 surfaces all twelve as first-class shapes, positional and verbatim (it does **not** reorder, default, or invent a component): | Composite | Accessor | IG datatype map (examples) | | --------- | ---------------------------- | ------------------------------------- | | XPN | `Field.asXpn()` / `parseXpn` | `XPN[HumanName]` (PID-5, NK1-2) | | XAD | `Field.asXad()` / `parseXad` | `XAD[Address]` (PID-11) | | CX | `Field.asCx()` / `parseCx` | `CX[Identifier]` (PID-3) | | CWE | `Field.asCwe()` / `parseCwe` | `CWE[CodeableConcept]` (OBX-3, DG1-3) | | CE | `Field.asCe()` / `parseCe` | `CE[CodeableConcept]` | | XTN | `Field.asXtn()` / `parseXtn` | `XTN[ContactPoint]` (PID-13) | | PL | `Field.asPl()` / `parsePl` | `PL[Location]` (PV1-3) | | TS | `Field.asTs()` / `parseTs` | timestamp composites | | NM | `Field.asNm()` / `parseNm` | `NM[Quantity]` (OBX-5) | | SN | `Field.asSn()` / `parseSn` | `SN[Quantity]`/`Range` | | HD | `Field.asHd()` / `parseHd` | `HD[…]` assigning authorities | | XCN | `Field.asXcn()` / `parseXcn` | `XCN[Practitioner]` (OBR-16) | ### 4. Code-system provenance: the CodeableConcept ingredient For CWE/CE the mapper needs the **claimed** code system (which becomes `Coding.system`). hl7 surfaces it verbatim on the composite (`nameOfCodingSystem`, `alternateIdentifier`, `nameOfAlternateCodingSystem`) and via `codingSystem` / `codingSystemOf` (HL7 Table 0396, read-only). hl7 does **not** validate or translate the code. That is `@cosyte/transform` / a terminology service. ### 5. Datetime parts: `parseDtm` → `DtmParts` The IG's DTM→dateTime map needs the **precision** (a `19941201` is a date, not a midnight instant) and the **timezone**. `parseDtm(value)` returns a `DtmParts` with `precision` (`year`/`month`/`day`/`hour`/`minute`/`second`/`fraction`), `hasTimezone`, `offsetMinutes`, and the numeric parts: exactly the inputs the datatype map needs to choose `date` vs `dateTime` and re-format. ### 6. Decoded / rendered text: the narrative input The IG maps FT/TX narrative (OBX-5, NTE) into `.text` / `DocumentReference`. hl7's text codec (`decodeText`, `renderText`, `Field.render()` → `RenderedText`) turns §2.7 highlight/formatting escapes (`\.br\`, `\H\`, …) into a normalized display model. The mapper never has to ship raw sentinels downstream. ### 7. Message metadata: `msg.meta`, `msg.version`, `msg.encodingCharacters` `msg.meta` (message type / trigger / control id / timestamp), `msg.version`, and `msg.encodingCharacters` (the MSH-1 / MSH-2 delimiter inputs) round out the surface. `msg.warnings` lets a mapper decide how to treat a lenient parse. ## The surface in one example ```ts runnable import { parseHL7, resolvePath, parseDtm } from "@cosyte/hl7"; const raw = [ "MSH|^~\\&|HIE|REDDING|||20230814022400||ORU^R01|MSGID|P|2.5.1", "PID|||MRN0003223^^^REDDING&1.1.1.1&GUID^MR||DOE^JANE||19800115|F", "OBX|1|NM|8867-4^Heart rate^LN||72|/min|||||F", ].join("\r"); const msg = parseHL7(raw); const pid = msg.segments("PID")[0]; const obx = msg.segments("OBX")[0]; // 1. Source-path addressing: segment.field.component, one-to-one with the IG. msg.get("PID.3.1"); // => "MRN0003223" resolvePath("PID.5.1", msg.rawSegments, msg.encodingCharacters); // => "DOE" // 3. Typed composites: the datatype-map inputs (positional, verbatim). pid?.field(5).asXpn().familyName; // => "DOE" pid?.field(3).asCx().identifierTypeCode; // => "MR" // 4. Code-system provenance: becomes Coding.system downstream. obx?.field(3).asCwe().nameOfCodingSystem; // => "LN" // 5. Datetime precision: date vs dateTime is a mapping decision. parseDtm(msg.get("PID.7") ?? "").precision; // => "day" ``` ## Semver posture (the promise) The mapping surface above is part of the public surface the package keeps stable. Below 1.0, a breaking change moves the minor version and the changelog says how to migrate. From 1.0, **semver**: - **Additive** (a new composite accessor, a new optional field on a returned shape, a new helper): a **minor** bump. A mapper written against v1 keeps working. - **Breaking** (renaming or removing a contracted accessor, changing an indexing convention, changing a decoded value's meaning, **renaming a warning code**): a **major** bump, announced, with a deprecation cycle where feasible. `transform` pins hl7 by major and reviews majors deliberately. - **Not under contract** (may change without a major): the internal `RawSegment`/`RawField` tree shape reachable via `msg.rawSegments` is an **advanced-only** escape hatch, not the mapping surface. Map through the accessors above, not the raw tree. Warning _message wording_, `prettyPrint` formatting, and helper-view additions are not breaking. ## Coverage proof `test/fhir-bridge-coverage.test.ts` grounds firsthand against the IG (the vendored raw segment-map CSVs + the one public sample message, provenance in `test/fixtures/fhir-bridge/PROVENANCE.md`) and proves three things, gaps recorded honestly: 1. **Addressability.** Every field-level source path the IG references (the `Identifier` column of the vendored segment maps) is addressable by hl7's dot-path model. **No hl7-side gap**. hl7 addresses every v2 field generically. 2. **Sample-corpus resolution.** Over the IG's public sample corpus, every IG-referenced source path the sample **populates** resolves through the IR. hl7 surfaces **100%** of the exercised paths. 3. **Datatype-IR surfacing.** The exact typed shapes the datatype maps consume (XPN, CX with assigning authority, CWE provenance, DTM precision + timezone, rendered narrative) surface from the sample. ### Coverage table: IG source paths over the public sample The public sample corpus is, verified firsthand, **one** v2 message (`MDM^T02`), vendored as a PHI-safe synthetic transcription (field-population, code systems, datetimes, and narrative escapes preserved verbatim; Safe-Harbor identifiers substituted, see `test/fixtures/fhir-bridge/PROVENANCE.md`). Per the segments it exercises (one canonical target-resource map per segment): | Segment (IG map) | IG-referenced fields | Exercised by sample | hl7 surfaces | | ------------------------- | -------------------: | ------------------: | -----------: | | MSH `[MessageHeader]` | 28 | 9 | 9 / 9 | | EVN `[Provenance]` | 7 | 1 | 1 / 1 | | PID `[Patient]` | 40 | 7 | 7 / 7 | | PV1 `[Encounter]` | 54 | 5 | 5 / 5 | | ORC `[ServiceRequest]` | 38 | 9 | 9 / 9 | | TQ1 `[ServiceRequest]` | 14 | 2 | 2 / 2 | | OBR `[DiagnosticReport]` | 54 | 7 | 7 / 7 | | TXA `[DocumentReference]` | 28 | 16 | 16 / 16 | | OBX `[Observation]` | 33 | 7 | 7 / 7 | | **Total** | **296** | **63** | **63 / 63** | ### Recorded gaps (honest) - **233 of 296** IG-referenced field paths are **not exercised** by the single public sample. This is a **corpus-coverage gap**, not a hl7 reachability gap. hl7 addresses all 296 generically; the thin public sample set simply does not populate them. A richer corpus (a private de-identified feed, or the IG adding samples) would raise the _exercised_ number without any hl7 change. - The denominator counts **one canonical target-resource map per segment**. Other target-resource maps exist (e.g. `OBX[DocumentReference]`, `PID[Account]`, `MSH[Bundle]`); including them widens the referenced-path set but does not change the finding. - The IG's machine-readable source addresses (the `Identifier` column) are **field-level** (`SEG-N`). Deeper component references appear in the IG's prose/condition columns, not the computable column; hl7 addresses those too (`SEG.N.C`), but they are outside this pinned count. ## What this is not - **Not a FHIR converter.** hl7 builds no resource, no Bundle, no ConceptMap resource; it evaluates no FHIRPath. It translates no terminology except the one named exception above: `resultStatus` classifies OBX-11 and OBR-25 by the IG's two published status maps, as a plain classification rather than a FHIR element. Everything else is `@cosyte/transform`. - **Not a terminology validator.** Code-system provenance is the **claimed** system, verbatim: never validated against LOINC/SNOMED/RxNorm/ICD. - **Not a full-corpus guarantee.** Coverage is proven over the IG's public sample (one message); reachability is proven for every IG-referenced field-level source path. --- URL: https://docs.cosyte.com/hl7/spec-notes-version-matrix Markdown: https://docs.cosyte.com/hl7/spec-notes-version-matrix.md # Spec notes: version-sensitivity matrix `@cosyte/hl7` parses HL7 v2 messages **structurally across v2.1 to v2.8**. It does **not** transform a message between versions, and it is not a version validator. It reads what the sender sent, tolerantly (Postel's Law), and surfaces a single `VERSION_MISMATCH` warning only when MSH-12 disagrees with an explicitly expected version (profile / `ParseOptions`). The HL7 v2 wire format is deliberately backward-friendly: segments and composites grow **append-only** (new trailing fields / components), delimiters and the segment grammar are stable, and a receiver is expected to ignore what it does not model rather than reject the message. This library leans on that: the parser never assumes a fixed component count, and never silently truncates content it does not yet name. ## What is version-sensitive (and how this library handles it) | Area | Version event | Handling | |---|---|---| | **MSH-2 encoding chars** | v2.7 allows a 5th char (truncation `#`) → MSH-2 may be 5 chars | Accepted; the standard escape set is recognized. | | **Composite component growth** | Composites gain trailing components across versions (CWE: 9 → 22 at v2.7) | Never assumed fixed; extra components preserved (see below), never truncated, never thrown on. | | **CE → CWE** | CE deprecated v2.5, **withdrawn v2.6**; CWE is the successor coded element | Read uniformly: `asCe()` and `asCwe()` accept either shape; neither is lossy (see *CE↔CWE interchange*). | | **CWE version ids** | CWE.7 / CWE.8 (coding-system version id, alternate version id) present from v2.5+ | Surfaced as `codingSystemVersionId` / `alternateCodingSystemVersionId` when present. | | **CWE second-alternate triplet + OIDs** | CWE.10 to CWE.22 added at v2.7 (second-alternate code triplet, coding-system / value-set OIDs) | Preserved verbatim on `extraComponents`, not yet modeled by name (v2 of this library may restore the full shape). | | **TS → DTM** | TS (with a now-deprecated degree-of-precision component) superseded by DTM at v2.5 | One parse path covers both: only the timestamp component is read; the deprecated degree-of-precision component is ignored. Precision is recoverable from `.raw` (its string length encodes the precision). | | **MSH-21** | Renamed **Conformance Statement ID → Message Profile Identifier** at v2.5 (datatype became EI) | No semantic change to parsing: MSH-21 is read positionally like any field. The rename is naming only; this note records it so the field is not mistaken for two different things. | ## Composite growth: the no-loss contract A fixed-shape composite parser has two ways to mishandle a newer, longer value: it can **throw** (assumed a maximum component count) or **silently truncate** (modeled only the components it knew about). This library does neither: - **No throw.** A coded element with any number of trailing components parses cleanly, verified by the `version-growth` property test (arbitrary component counts up to 25). - **No loss.** Components past the modeled set are preserved verbatim, in order, on the composite's `extraComponents` array: - **`CWE.extraComponents`**: HL7 components **10+** (the v2.7 second-alternate triplet, coding-system / value-set OIDs). - **`CE.extraComponents`**: HL7 components **7+** (only populated when a CWE-shaped value is read through the CE accessor). An absent interior component is preserved as `""` so `extraComponents[i]` maps back to HL7 component `(modeled + 1) + i`. Trailing empty components are stripped, and the key is **omitted** entirely when there is nothing past the modeled set. Each entry is the component's first subcomponent (auto-unescaped), the same projection the modeled coded-element components use. Coded-element components beyond the modeled set do not carry subcomponents in any HL7 version, so this is lossless for CWE/CE; a multi-subcomponent extra component (not expected for these datatypes) would surface only its first subcomponent. ## CE↔CWE interchange CE and CWE share their first six components (identifier / text / coding-system trio, repeated for the alternate). CE is the older, withdrawn form; CWE is its superset. Because both accessors preserve everything past the modeled set on `extraComponents`, **neither accessor is lossy on the other's shape**: - `asCwe()` on a CE-shaped value → the 6 shared components, no `extraComponents`. - `asCe()` on a CWE-shaped value → the 6 shared components, with CWE.7+ (version ids, original text, v2.7 extensions) preserved on `extraComponents`. So a downstream that picked the "wrong" accessor for the sender's version still sees all the data; it never silently loses the version-id or alternate-coding information. ## Known limitations - **Structural parse, not transformation.** The library reads v2.1 to v2.8 messages; it does not up-/down-convert between versions. - **No full v2.7 CWE model.** CWE.10 to 22 are preserved (above) but not surfaced by name; a consumer that needs the second-alternate triplet reads it positionally from `extraComponents`. - **Not a conformance validator.** `VERSION_MISMATCH` is the only version-related warning; the parser does not assert that a message conforms to its declared version's segment/field rules. --- URL: https://docs.cosyte.com/hl7/schema-emission Markdown: https://docs.cosyte.com/hl7/schema-emission.md # Spec notes: schema emission for the serialized projection `JSON.stringify(msg)` and `msg.toJSON()` produce a snapshot of a parsed message: a raw-tree mirror carrying the encoding characters, the segments with their fields, repetitions, components and subcomponents, the parse warnings, and the applied profile when there was one. Storing that snapshot, queueing it, or handing it to another service usually means validating it on the way back in, and validating it means having a schema for it. `@cosyte/hl7` writes that schema for you, in two forms: - a **JSON Schema document** at the `2020-12` dialect, ready to hand to any validator you already run; - **Zod schemas as TypeScript source text**, ready to paste into your own codebase where your own `zod` install resolves them. ## Getting the JSON Schema ```ts runnable import { messageJsonSchema, JSON_SCHEMA_DIALECT } from "@cosyte/hl7"; const schema = messageJsonSchema(); schema.$schema; // => "https://json-schema.org/draft/2020-12/schema" schema.$schema === JSON_SCHEMA_DIALECT; // => true // The three members every projection carries, and the one it may omit. schema.required; // => ["encodingCharacters", "segments", "warnings"] Object.keys(schema.properties ?? {}).includes("profile"); // => true // The profile is described down to its two members and nothing else. Object.keys(schema.$defs["SerializedProfile"]?.properties ?? {}); // => ["name", "lineage"] ``` The document uses only the core, applicator and validation vocabularies of the dialect, so a validator that implements the dialect implements all of it. There is no `title`, no `description`, no `default` and no `examples`: nothing on the document carries an illustrative value, and the one place a literal value is constrained is the warning `code` member, whose enumeration is the library's own warning registry. ## Getting the Zod source ```ts runnable import { messageZodSource } from "@cosyte/hl7"; const source = messageZodSource(); source.includes("export const SerializedMessageSchema"); // => true source.includes("export type SerializedMessage"); // => true ``` Write that string to a file in your project and import it like any other module. It declares exactly one import, from `zod`, and `zod` is a dependency of **your** project: `@cosyte/hl7` ships zero runtime dependencies and never imports it. ## Asking by name `emitMessageSchema` takes a target name and returns the artifact as text, which is the form you write to disk. A target it does not support is refused rather than half-answered: ```ts runnable import { emitMessageSchema, SCHEMA_EMIT_TARGETS, SchemaTargetError } from "@cosyte/hl7"; SCHEMA_EMIT_TARGETS; // => ["json-schema-2020-12", "zod"] emitMessageSchema("json-schema-2020-12").startsWith("{"); // => true emitMessageSchema("zod").includes("import { z }"); // => true let refused = ""; try { emitMessageSchema("json-schema-draft-07"); } catch (error) { if (error instanceof SchemaTargetError) { refused = `${error.requestedTarget} -> ${error.supportedTargets.join(", ")}`; } } refused; // => "json-schema-draft-07 -> json-schema-2020-12, zod" ``` ## What the schema describes, and what it does not It describes the **shape of the snapshot**, and only that. It is a mirror of the positional tree, so it says a field has repetitions and an `isNull` flag; it does not say what an `OBX-5` may hold, whether a `PID-3` is well formed, or whether the message conforms to anything. Conformance is a separate question with a separate answer on this package, and the emitted schema is not an input to it. Two consequences worth stating plainly: - **Emission never validates.** Both functions build a description and hand it back. Nothing here reads a message, evaluates a schema against a value, or produces a finding. - **Emission never carries message content.** The output is a function of the library, not of anything it has parsed, so the same library version emits the same bytes whatever has run before. That is what makes it safe to commit the artifact to a repository, which is exactly what this one does. ## The encoding-characters detail The projection's `encodingCharacters` carries five members: `field`, `component`, `repetition`, `escape` and `subcomponent`. It does **not** carry `truncation`, even for a v2.7 message whose MSH-2 declares one and whose parsed `msg.encodingCharacters.truncation` is set. The snapshot is what the serializer writes, and the serializer writes five keys, so the emitted schema declares five and permits no sixth. If you need the truncation character, read it off the message rather than off the snapshot. --- URL: https://docs.cosyte.com/hl7/spec-notes-differential Markdown: https://docs.cosyte.com/hl7/spec-notes-differential.md # Spec notes: differential testing against external oracles `test/differential/differential.test.ts` parses the canonical corpus (`test/fixtures/canonical/*.hl7`, 27 fixtures spanning ADT/ORU/ORM/SIU/MDM/DFT/RDE/RDS/VXU trigger events, TQ1 timing, Z-segments, and nested-subcomponent edge cases) through **both** `@cosyte/hl7` and an external, independently-implemented HL7 v2 parser, then compares segment count and per-segment field extraction. This is a _second, independent read_ of the same corpus. A real bug that both this repo's own fixtures AND its own implementation happen to agree on (a shared blind spot) would never surface from `@cosyte/hl7`'s own test suite alone. An external oracle is the only way to catch that class of bug. ## Why this is oracle-gated, not a hard CI dependency `@cosyte/hl7` ships **zero runtime dependencies** (the standing convention across every `@cosyte/*` parser). The oracle is a **dev-time-only, external, never-vendored** tool: it is never bundled, published, or required to install the package. If no Python interpreter with the oracle package importable is available, `test/differential/differential.test.ts` detects that at runtime and `it.skip()`s every comparison. `scripts/verify.sh` (and CI) stay green with zero Python present. The harness is a **bonus signal when available**, not a merge gate on its own absence. ## The oracle: python-hl7 **Primary oracle: [python-hl7](https://github.com/johnpaulett/python-hl7)** (PyPI distribution name `hl7`: the GitHub repo name and the installable package name differ; `pip install hl7`, not `pip install python-hl7`). Pure-Python, zero C extensions, `import hl7`. **License: BSD** (3-clause), permissive, external-only, safe to invoke as a dev/test tool without any bundling or distribution obligation back onto `@cosyte/hl7`. **Optional secondary oracle: [hl7apy](https://crs4.github.io/hl7apy/)**, MIT-licensed, more structurally strict (validates against HL7 message-structure definitions rather than python-hl7's permissive flat-field model). Not wired into the harness today; flagged here as the natural next oracle if a second, differently-opinionated read becomes valuable. hl7apy's stricter structural validation would likely surface _more_ "divergences" that are actually hl7apy being pickier, not `@cosyte/hl7` being wrong. Expect to spend real time re-deriving the known/justified-divergence list documented below if it's ever added. **Licensing bound.** Both oracles are permissive (BSD / MIT) and used exclusively as external, dev-only test tooling, never imported, vendored, or shipped inside the published `@cosyte/hl7` package. This is the same bound the umbrella's conventions apply to HAPI/nHapi elsewhere in the ecosystem: external-only, never bundled. ## How to run it ```bash cd hl7 # this repo python3 -m venv .difftools .difftools/bin/pip install hl7 pnpm test test/differential/differential.test.ts ``` `.difftools/` is git-ignored (never committed: it's a local dev venv, consistent with the zero-runtime-dep / never-vendored rule above). The harness also falls back to a plain `python3` on `PATH` if `import hl7` succeeds there, so a system-wide install works too. The venv path is just the reproducible default. Detection happens once per run, in `test/differential/differential.test.ts`'s `findOraclePython()`: try `.difftools/bin/python`, then `python3` on `PATH`; the first one where `python3 -c "import hl7"` exits 0 wins. If neither works, every comparison test calls `ctx.skip()` and a one-line console note explains why (pointing back at this doc). ## What is compared, and how For each canonical fixture: 1. **Segment count.** `@cosyte/hl7`'s `msg.rawSegments.length` vs `len(hl7.parse(raw))`. A mismatch here is always a hard failure: it means the two parsers disagree about the message's fundamental shape, never a "known/justified" divergence. 2. **Per-segment name.** Segment name at each index must match exactly. 3. **Per-segment field count**, using the SAME 1-indexed HL7 position convention on both sides (see "Field-indexing alignment" below). 4. **Per-field wire text**: `@cosyte/hl7`'s `Field.text` (the field's canonical re-serialized wire text: repetitions, components, subcomponents, all re-escaped) against python-hl7's `str(field)` (its reconstruction of the field's original delimiter-joined text). ### Field-indexing alignment Both parsers land on the _identical_ 1-indexed convention by construction, which is what makes a byte-level comparison possible at all rather than needing a translation layer: - **Non-MSH segments:** index `0` is the segment name (not a data field, excluded from both sides' comparison); index `N >= 1` is the HL7 N-th field (`PID-5` lives at index 5 on both sides). - **MSH specifically:** index `0`/position `1` is the field separator character itself (MSH-1); index `1`/position `2` is the encoding-characters field (MSH-2); index `N >= 2`/position `N + 1` is MSH-`(N+1)`. `@cosyte/hl7`'s own module JSDoc (`src/parser/tokenize.ts`) documents this convention explicitly. It was written to match the same "unified 1-indexed, MSH-1 lives at position 1" convention any spec-literate HL7 v2 parser (python-hl7 included) uses. MSH-1/MSH-2 are read literally from the raw tokenizer tree on the `@cosyte/hl7` side rather than through `Field.text`, because `Field.text`'s own JSDoc documents that calling it on MSH-1/MSH-2 re-escapes the delimiter-definition characters into garbage: those two positions aren't "escapable content" the way MSH-3 onward is. ## Known / justified divergences The harness classifies every field-text mismatch against a small, explicit, honestly-derived list of known-divergence predicates (`KNOWN_DIVERGENCES` in `differential.test.ts`). A mismatch that matches a known predicate is **reported, not silently dropped, and does not fail the suite**. A mismatch that matches **none** of them **fails**. The point of the allowlist is to keep the harness a real regression detector, not to paper over an actual bug with an overly broad tolerance. ### D-02 trailing-empty-component canonicalization (observed, on `test/fixtures/canonical/tq1-q6h.hl7`) `@cosyte/hl7`'s serializer strips **trailing empty components and subcomponents** when producing canonical wire text (`Field.text`, `src/serialize/emit-field.ts`: this is D-02, the same rule that governs `msg.toString()`'s spec-clean output). python-hl7's `str(field)` instead returns the **verbatim original substring**, trailing empties included. Concretely: the fixture's `TQ1-2` field is `1^tablet^^` (two trailing empty components). hl7 reports `"1^tablet"`; python-hl7 reports `"1^tablet^^"`. This is Postel's-Law-consistent, intended behavior on the `@cosyte/hl7` side: trailing empty components carry no information the HL7 v2 spec treats as significant, and stripping them is exactly what the _serializer_ (the "be conservative in what you emit" side of Postel's Law) is supposed to do. python-hl7 makes the opposite, also-defensible choice: preserve the byte-exact original. Neither is "wrong": they're different design points on the same lenient-parse/canonical-emit spectrum, and this harness exists precisely to make that difference visible and documented rather than silently assumed away. **Detection predicate:** `theirs` (the oracle's text) starts with `ours` (hl7's canonicalized text) and the remainder is composed entirely of trailing `^`/`&` delimiter characters (i.e., empty trailing components/subcomponents), a structural, not content-based, check. ### Divergences NOT yet observed, but plausible (kept here so a future contributor knows to look) These are called out because they're common failure modes when differential-testing two independently-implemented HL7 v2 parsers, but the current 27-fixture canonical corpus doesn't happen to exercise them. If the corpus grows and one of these starts firing, add its predicate to `KNOWN_DIVERGENCES` with the same honesty this file applies to the D-02 case above. Don't broaden an existing predicate to swallow an unrelated divergence. - **Escape-sequence decoding differences.** `@cosyte/hl7`'s tokenizer decodes escape sequences (`unescape()`) into the raw tree and the serializer re-escapes on the way back out (`reescape()`); python-hl7's default parse mode does _not_ auto-decode `\F\`/`\S\`/`\T\`/`\R\`/ `\E\`. See the empty-oracle-parse workaround this harness's `oracle_python_hl7.py` module JSDoc had to account for while probing python-hl7's behavior (its `str(field)` returns the field as found, escape sequences un-expanded, for anything python-hl7 itself doesn't need to touch). None of the 27 canonical fixtures currently embed a `\Xxx\` escape inside a field compared by this harness, so this predicted divergence has not fired yet. - **Explicit-null (`""`) field semantics.** `@cosyte/hl7` distinguishes an _empty_ field (`||`) from HL7's _explicit null_ token (`""`) via `RawField.isNull`; whether python-hl7's `str()` round-trips `""` byte-for-byte or normalizes it differently is untested by the current corpus (no canonical fixture uses an explicit null in a field this harness compares). - **Lenient-mode recovery on malformed input.** The differential harness only runs against the **canonical, spec-clean** corpus: every fixture parses successfully on both sides today. It does _not_ compare lenient-mode recovery behavior (warnings, truncation handling, delimiter quirks) between the two parsers; `@cosyte/hl7`'s Postel's-Law tolerance model (`test/property/lenient.property.test.ts`, `test/property/fuzz.property.test.ts`) is validated in-repo against its own documented fatal/warning contract instead, since a divergence there would almost certainly reflect the two libraries' deliberately different leniency philosophies rather than a correctness bug in either. ## Limitations (read before trusting this harness more than it claims) - **Corpus size.** 27 fixtures is a meaningful spread of real-world message shapes, but it is not exhaustive: the differential harness can only report what the corpus happens to exercise. It complements, not replaces, the spec-traceability tests and property-based fuzz suite (`test/property/fuzz.property.test.ts`) elsewhere in this repo. - **One oracle today.** A single external implementation reduces, but does not eliminate, the risk of a shared blind spot (e.g. both python-hl7 and `@cosyte/hl7` independently under-handling the same obscure HL7 v2 corner case). Adding hl7apy as a second oracle would strengthen this further; it is intentionally left as documented future work rather than built speculatively. - **Field-text comparison, not full semantic comparison.** The harness compares wire text, not parsed/typed values (e.g. it does not compare `@cosyte/hl7`'s typed `XPN`/`CX`/`TS` composites against any equivalent python-hl7 representation, python-hl7 doesn't have typed composites to compare against). This is a deliberate scope boundary: wire-text parity at the segment/field level is what "two parsers agree on how this message is shaped" actually means; composite-type interpretation is `@cosyte/hl7`-specific value-add tested elsewhere (`test/model-types-*.test.ts`). --- URL: https://docs.cosyte.com/hl7/guides-overview Markdown: https://docs.cosyte.com/hl7/guides-overview.md # Guides Task-oriented recipes against the real API. Each stands alone. Jump to the one that matches what you're doing. For the _why_ behind the behavior, follow the links into [Core Concepts](https://docs.cosyte.com/hl7/spec-notes-primer.md). ## Read lab results `msg.observations()` returns every OBX in document order as a typed `Observation`. The coded fields (`identifier`, `units`) are `CWE` composites: `.identifier` is the code, `.text` the human-readable name. ```ts runnable import { parseHL7 } from "@cosyte/hl7"; const raw = [ "MSH|^~\\&|LAB|MAIN|EHR|REF|20260419143000||ORU^R01^ORU_R01|EX00002|P|2.5", "PID|1||MRN67890^^^HOSP^MR||Smith^Alice^B||19750620|F", "OBR|1|ORD-EX-1|FLR-EX-1|CBC^Complete Blood Count^L|||20260419140000", "OBX|1|NM|WBC^White Blood Cells^LN||7.5|10*3/uL|4.5-11.0|N|||F", "OBX|2|NM|HGB^Hemoglobin^LN||14.2|g/dL|13.5-17.5|N|||F", ].join("\r"); const observations = parseHL7(raw).observations(); observations.length; // => 2 const [wbc] = observations; wbc.identifier.identifier; // => "WBC" wbc.identifier.text; // => "White Blood Cells" wbc.value; // => 7.5 wbc.units?.identifier; // => "10*3/uL" wbc.referenceRange; // => "4.5-11.0" ``` Numeric `OBX-5` values arrive typed as `number` (here `7.5`), not strings: a fidelity detail, not a string you have to `parseFloat` yourself. ## Apply a vendor profile Real feeds carry vendor quirks. Apply a built-in profile as the second argument to `parseHL7`, or declare your own with `defineProfile()`: ```ts runnable import { parseHL7, profiles } from "@cosyte/hl7"; const raw = "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01^ADT_A01|EX00001|P|2.5\r" + "PID|1||MRN12345^^^HOSP^MR||Doe^John^Q||19800115|M"; const msg = parseHL7(raw, profiles.epic); msg.patient.mrn; // => "MRN12345" ``` Built-ins ship for Epic, Cerner, Meditech, Athena, a generic lab, and the Visage 7 and Philips Vue PACS imaging systems, each authored through the same public `defineProfile()` API you'd use yourself. Start from the **profile starter kit** in the package's `examples/` to publish your own as a standalone package. ### Custom segment field names are checked by the compiler A profile's `customSegments` declaration names the fields of your Z-segments, and reading one back by name goes through `seg.get(name)`. When the profile is one the compiler can see, the names it declares **for that segment type** are the only ones `get` accepts, so a typo is a build failure rather than a blank column on a report: ```ts runnable import { defineProfile, parseHL7 } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3, departmentName: 4 } }, ZRS: { fields: { resultStatus: 1 } }, }, }); const raw = "MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01^ADT_A01|EX00001|P|2.5\r" + "PID|1||MRN12345^^^HOSP^MR||Doe^John^Q||19800115|M\r" + "ZDP|||CARDIOLOGY|Cardiology Department"; const msg = parseHL7(raw, vendor); msg.part("ZDP")?.get("departmentCode")?.value; // => "CARDIOLOGY" // msg.part("ZDP")?.get("departmentCod"); // does not compile: not a declared name // msg.part("ZDP")?.get("resultStatus"); // does not compile: that name belongs to ZRS ``` Nothing about the read changed: a declared name whose position the message did not carry is still `undefined`, so the result stays `Field | undefined`. And the check only tightens where the declaration is visible. No profile, a value you typed as `Profile`, a profile registered with `setDefaultProfile`, a segment type the profile does not declare, or a walk over `msg.allSegments()` all keep accepting any `string`, exactly as before. ## Build an ACK `buildAck` turns an inbound message into a spec-clean acknowledgement, echoing the correlation id and swapping sender/receiver. `msg.toString()` is always spec-clean regardless of how quirky the input was: ```ts import { parseHL7, buildAck } from "@cosyte/hl7"; const inbound = parseHL7(raw); const ack = buildAck(inbound, { code: "AA" }); // AA | AE | AR ack.toString(); // MSH|^~\&||...|ACK|... + MSA|AA| ``` For MLLP transport framing and ACK correlation over the wire, see the sibling package [`@cosyte/mllp`](https://github.com/cosyte/mllp), which adapts over this same `buildAck` primitive. ## Build a message from scratch `buildMessage` is the outbound counterpart to `parseHL7`: give it the message metadata, append segments with **positional field arrays** (index 0 is the segment name slot, so leading `""`s skip to the field you want), then serialize: ```ts runnable import { buildMessage } from "@cosyte/hl7"; const msg = buildMessage({ type: "ADT^A01", version: "2.5", sendingApp: "CLINIC", sendingFacility: "MAIN", receivingApp: "LAB", receivingFacility: "REF", }).addSegment("PID", ["", "", "MRN12345"]); // PID-3 = MRN12345 msg.get("PID.3"); // => "MRN12345" ``` `msg.toString()` then emits spec-clean HL7: correct delimiters, escaping, and an auto-generated MSH control id. Field array elements are raw field _values_: any delimiter character inside one (e.g. a `^` in a name) is escaped as data, so pass pre-structured composites when you need components. ## Author a message from typed objects The typed builders are the high-level counterparts of the read helpers (`msg.patient`, `msg.observations`, `msg.orders`, …): pass structured values (an `XPN` name, `CX` identifiers, a `TS` timestamp) and the builder assembles the segments the message type requires with correct `^`/`&`/`~` structure. No hand-assembly of delimiters, and any delimiter embedded in a value is **escaped, never injected**. The result is spec-clean and re-parses with **zero warnings**. | builder | message | read it back with | | --- | --- | --- | | `buildAdt(event, init)` | `ADT` admit, discharge, transfer, merge, move and identifier change | `msg.patient`, `msg.visit`, `msg.identityEvents()` | | `buildOru(init)` | `ORU^R01` observation result | `msg.observations()` | | `buildOrm(init)` | `ORM^O01` general order | `msg.orders()` | | `buildSiu(event, init)` | `SIU` scheduling notification | `msg.appointments()` | | `buildMdm(event, init)` | `MDM` clinical document | `msg.documents()` | | `buildDft(event, init)` | `DFT` detail financial transaction | `msg.charges()` | | `buildVxu(init)` | `VXU^V04` vaccination record update | `msg.immunizations()` | | `buildAck(inbound, options)` | `ACK` acknowledgment | `interpretAck(msg)` | ```ts runnable import { buildAdt, parseHL7 } from "@cosyte/hl7"; const msg = buildAdt("A01", { sendingApp: "CLINIC", receivingApp: "LAB", patient: { identifiers: { idNumber: "MRN12345", identifierTypeCode: "MR" }, name: { familyName: "Test", givenName: "Ann" }, // a "^" here would be escaped, not injected birthDateTime: "19880705", administrativeSex: "F", }, visit: { patientClass: "I" }, }); const round = parseHL7(msg.toString()); round.patient?.mrn; // => "MRN12345" round.patient?.familyName; // => "Test" round.warnings.length; // => 0 ``` Every family works the same way. A scheduling notification, for example, takes the appointment and its resource groups and reads back through `msg.appointments()`: ```ts runnable import { buildSiu, parseHL7 } from "@cosyte/hl7"; const msg = buildSiu("S12", { sendingApp: "SCHEDULING", receivingApp: "EHR", appointment: { fillerAppointmentId: "FL-2002", startDateTime: "20260801090000", endDateTime: "20260801093000", fillerStatusCode: { identifier: "Booked" }, }, resourceGroups: [ { setId: "1", resources: [ { kind: "location", code: { identifier: "OR-1" } }, { kind: "personnel", person: { idNumber: "9990", familyName: "Welby" } }, ], }, ], }); const appt = parseHL7(msg.toString()).appointments()[0]; appt?.fillerAppointmentId; // => "FL-2002" appt?.startDateTime?.raw; // => "20260801090000" appt?.resources.length; // => 2 ``` Builders **never fabricate**: only values you supply are emitted, an omitted optional field stays absent, and content the message type requires but the init does not carry is a typed `TypeError`, never a guessed value. ```ts runnable throws import { buildDft } from "@cosyte/hl7"; // A financial transaction message needs at least one transaction: buildDft("P03", { patient: { identifiers: { idNumber: "MRN12345" } }, charges: [], }); ``` ### Which message types can be authored `SUPPORTED_BUILDER_MESSAGES` publishes the (message code, trigger event) pairs the typed builders can author as structurally complete messages, and `supportsBuilderMessage(code, event)` answers for one pair. The set is derived from the same published message structures the parser checks against, so it never claims a pair whose structure needs a segment no typed init can supply. ```ts runnable import { SUPPORTED_BUILDER_MESSAGES, supportsBuilderMessage } from "@cosyte/hl7"; supportsBuilderMessage("SIU", "S12"); // => true supportsBuilderMessage("ADT", "A20"); // => false SUPPORTED_BUILDER_MESSAGES.some((m) => m.messageCode === "VXU"); // => true ``` A `false` answer is not a refusal. A builder that takes a trigger event still emits the content you supply for an event outside the set; the structure summary on the re-parsed message then reports what is missing, exactly as it does for a message that arrived from anywhere else. For a lower-level typed set on an existing message, use `msg.setComposite(path, kind, value)` (e.g. `msg.setComposite("PID.5", "XPN", { familyName: "Doe" })`), or `encodeComposite(kind, value)` to build a field directly. ## Next - [Troubleshooting](https://docs.cosyte.com/hl7/troubleshooting.md): warnings vs. errors, strict mode, charset, and batches. - **API Reference**: every export, generated from source. --- URL: https://docs.cosyte.com/hl7/benchmarks Markdown: https://docs.cosyte.com/hl7/benchmarks.md # Benchmarks & performance `@cosyte/hl7` is the reference Cosyte parser, and interface-engine adoption asks a fair question: **is it fast enough, and does it stay bounded on a large feed?** This page answers both, with a reproducible benchmark suite and a CI guard that stops future changes from silently degrading performance. Two things live here: 1. **A reproducible benchmark suite** (`scripts/bench.ts`, run with `pnpm bench`) that measures parse throughput, per-message memory, and the streaming peak memory that substantiates [`parseStream`](https://docs.cosyte.com/hl7/spec-notes-stream.md)'s O(one-message) claim. Its numbers are **published below as indicative context**. 2. **A performance-regression guard** (`test/perf/perf-regression.test.ts`) that runs in CI and fails if throughput or streaming memory regresses. It is **ratio-based**, not an absolute floor, so it does not flake across CI runners of different speed. > This is a **measurement** layer. It adds no runtime behavior and changes no parse output. The > parser and streamer it measures are exactly the shipped ones. ## Published numbers (indicative) > Indicative run: Node v24.18.0, linux/x64, GC forced (`--expose-gc`), 2026-07-21. **Absolute numbers > are machine- and load-dependent**. Treat them as a directional baseline, not a portable guarantee > (that is what the ratio-based guard is for). | Message class | N | msgs/sec | MB/sec | retained heap / msg | | ---------------- | ------ | -------- | ------ | ------------------- | | ADT^A01 (5 seg) | 20,000 | 25,594 | 7.2 | 25.8 KB | | ORU^R01 (12 seg) | 10,000 | 5,937 | 4.1 | 75.9 KB | Streaming a 20,000-message ADT batch, `parseStream` holds a small fraction of what a whole-buffer parse of the same bytes retains, the headline O(one-message) property: | Streaming (`parseStream`) | N | whole-buffer retained | streaming peak | ratio | | ------------------------- | ------ | --------------------- | -------------- | ------ | | ADT^A01 batch | 20,000 | 523.0 MB | 67.2 MB | 0.129× | The streaming "peak" is transient allocation between garbage collections (chunk slices, intermediate strings), not _retained_ state. The **retained** working set is one message plus the in-flight partial segment, and it does not grow with the file. The whole-buffer figure, by contrast, is O(N): it holds every parsed message at once. The gap is the point. Reproduce any of this: ```bash pnpm bench # human-readable table node --expose-gc node_modules/.bin/tsx scripts/bench.ts # tighter memory numbers pnpm bench --markdown # the results-table body used on this page ``` ## Methodology - **Inputs are synthetic and generated in-process**: a small ADT^A01 (5 segments) and a larger multi-OBX ORU^R01 (12 segments), the low and high ends of everyday message size. No PHI, no fixture files, so a run is self-contained and reproducible anywhere. - **Throughput** is `N / median-elapsed`, after an unmeasured warm-up so the JIT is hot; the parsed segment count is summed into a sink so the parse cannot be optimized away as dead code. - **Per-message retained heap** materializes all N parsed messages and divides the retained `heapUsed` delta by N. - **Streaming peak** feeds the batch in fixed 4 KB chunks and samples `heapUsed` at every yielded message, keeping the peak delta, then compares it to the heap a whole-buffer `splitBatch` of the same bytes retains. ## Why the regression guard is relative, not an absolute floor Absolute throughput and memory swing by an order of magnitude across machines, background load, and (critically) whether v8 **coverage instrumentation** is active (`pnpm test:coverage` instruments `src/**` and inflates every parse several-fold). A committed absolute floor would either be so low it catches nothing or so high it false-alarms on a slow or contended CI runner. So the guard asserts **ratios of two measurements taken in the same process**, where machine speed and instrumentation overhead cancel: - **Throughput linearity, by message count.** Parsing 4× the messages must take about 4× the time, not more: `t(4N)/t(N) ≤ 10` (2.5× the linear ideal of 4, roomy against cold-JIT and scheduler noise, still well below the ~16 a genuine quadratic regression produces). This catches a **super-linear** regression in message count while remaining immune to how fast or instrumented the runner is. - **Throughput linearity, by message size.** The complement, holding count fixed and scaling _length_: one ORU of S OBX lines vs one of 4S, each parsed an equal number of times, `t(4S)/t(S) ≤ 10`. This is the guard that catches an accidental **O(n²)-in-message-length** tokenizer regression. The by-count test, at fixed size, would scale both runs by the same factor and miss it. - **Streaming read-ahead, the memory guard.** `parseStream` must pull its source lazily and retain O(one message), not buffer the file. The guard counts how far the source is pulled _ahead_ of what has been yielded: a pure count (`≤ 2`), immune to GC timing and machine RAM. If `parseStream` ever started buffering the file, read-ahead would grow toward N and trip. This restates the [streaming](https://docs.cosyte.com/hl7/spec-notes-stream.md) invariant as a performance gate. > The gate does **not** assert an absolute retained-heap ratio. A `heapUsed` delta taken without a > forced GC measures uncollected young-gen garbage, not live retained state, so it swings with the > runner's GC budget (a larger `--max-semi-space-size` defers collection and inflates it) and would > false-fail with no regression. The **absolute** memory numbers above are measured in the benchmark, > which forces a collection via `--expose-gc`; the CI gate relies on the count-based read-ahead > invariant, which needs no GC cooperation. Both linearity timings measure the parser's own time: - **The minimum of several passes, after a warm-up.** Benchmark noise is one-sided (a preemption only ever _adds_ time), so the minimum is the cleanest estimator, and taking it on both sides cancels transient stalls. - **Small and large passes interleaved.** The two workloads alternate pass by pass, so a change in machine load (other test files run in parallel) lands on both sides of the ratio rather than on one block of runs. - **Garbage-collection pauses subtracted.** The time the main thread spends in GC pauses during a pass is taken out, because it does not follow the parser's algorithm. A small message's tree mostly dies inside one young-generation cycle; a large one survives the collections that run while it is built and is copied and promoted. On hosted CI runners a pass over the large messages spent 14× to 37× the GC time of a pass over the small ones, for 4× the bytes, which alone pushed an unchanged parser's size ratio past 10. A quadratic tokenizer still trips the guard: its extra work is parser time, not GC time. There is one deliberately **coarse absolute liveness floor** (parse ≥ a few dozen msgs/sec, ~100× below real instrumented throughput). It only trips on a catastrophic hang a ratio could miss, never on ordinary slowness. ## What these numbers are not - **Not a portable guarantee.** The published figures are indicative on one machine and corpus; arbitrary inputs (very large messages, adversarial nesting) are not characterized here. - **Not a cross-library bake-off.** Comparing hl7 to other parsers is a separate exercise; this page measures hl7 against itself over time. - **Not a behavior contract.** Nothing here changes how a message parses. See the [core concepts](https://docs.cosyte.com/hl7/spec-notes-primer.md) for parse semantics. --- URL: https://docs.cosyte.com/hl7/api/@cosyte/namespaces/HL7 Markdown: https://docs.cosyte.com/hl7/api/@cosyte/namespaces/HL7.md # HL7 ## References ### CE Re-exports [CE](https://docs.cosyte.com/hl7/api.md#ce) *** ### CWE Re-exports [CWE](https://docs.cosyte.com/hl7/api.md#cwe-1) *** ### CX Re-exports [CX](https://docs.cosyte.com/hl7/api.md#cx-1) *** ### HD Re-exports [HD](https://docs.cosyte.com/hl7/api.md#hd-1) *** ### NM Re-exports [NM](https://docs.cosyte.com/hl7/api.md#nm-1) *** ### PL Re-exports [PL](https://docs.cosyte.com/hl7/api.md#pl-1) *** ### SN Re-exports [SN](https://docs.cosyte.com/hl7/api.md#sn) *** ### TS Re-exports [TS](https://docs.cosyte.com/hl7/api.md#ts-1) *** ### XAD Re-exports [XAD](https://docs.cosyte.com/hl7/api.md#xad-1) *** ### XCN Re-exports [XCN](https://docs.cosyte.com/hl7/api.md#xcn-1) *** ### XPN Re-exports [XPN](https://docs.cosyte.com/hl7/api.md#xpn-1) *** ### XTN Re-exports [XTN](https://docs.cosyte.com/hl7/api.md#xtn-1) --- URL: https://docs.cosyte.com/hl7/api/@cosyte/namespaces/conformance Markdown: https://docs.cosyte.com/hl7/api/@cosyte/namespaces/conformance.md # conformance ## Interfaces ### ProfileDefect **`Internal`** One structural defect in a profile: the PHI-free `detail` plus a best-effort [FindingLocus](https://docs.cosyte.com/hl7/api.md#findinglocus) (so the engine can attach it to a `PROFILE_MALFORMED` finding). When the defect is above segment level, `locus.segment` is the sentinel `"(profile)"`. #### Properties ##### detail > `readonly` **detail**: `string` ##### locus > `readonly` **locus**: [`FindingLocus`](https://docs.cosyte.com/hl7/api.md#findinglocus) ## Functions ### collectProfileDefects() > **collectProfileDefects**(`profile`): readonly [`ProfileDefect`](#profiledefect)[] **`Internal`** Collect every structural defect in a candidate conformance profile. **Total over `unknown` and never throws**: a caller may pass any value (even one cast through `any`) and get back a defect list rather than an exception. An empty result means the profile is well-formed enough to validate against. #### Parameters ##### profile `unknown` #### Returns readonly [`ProfileDefect`](#profiledefect)[] ## References ### Cardinality Re-exports [Cardinality](https://docs.cosyte.com/hl7/api.md#cardinality) *** ### ComparisonPredicate Re-exports [ComparisonPredicate](https://docs.cosyte.com/hl7/api.md#comparisonpredicate) *** ### ComponentRule Re-exports [ComponentRule](https://docs.cosyte.com/hl7/api.md#componentrule) *** ### ConditionPredicate Re-exports [ConditionPredicate](https://docs.cosyte.com/hl7/api.md#conditionpredicate) *** ### ConformanceFinding Re-exports [ConformanceFinding](https://docs.cosyte.com/hl7/api.md#conformancefinding) *** ### ConformanceProfile Re-exports [ConformanceProfile](https://docs.cosyte.com/hl7/api.md#conformanceprofile) *** ### ConformanceResult Re-exports [ConformanceResult](https://docs.cosyte.com/hl7/api.md#conformanceresult) *** ### ConnectedPredicate Re-exports [ConnectedPredicate](https://docs.cosyte.com/hl7/api.md#connectedpredicate) *** ### DeclaredConditionalUsage Re-exports [DeclaredConditionalUsage](https://docs.cosyte.com/hl7/api.md#declaredconditionalusage) *** ### defineConformanceProfile Re-exports [defineConformanceProfile](https://docs.cosyte.com/hl7/api.md#defineconformanceprofile) *** ### FieldRule Re-exports [FieldRule](https://docs.cosyte.com/hl7/api.md#fieldrule) *** ### FINDING\_CODES Re-exports [FINDING_CODES](https://docs.cosyte.com/hl7/api.md#finding_codes) *** ### FindingCode Re-exports [FindingCode](https://docs.cosyte.com/hl7/api.md#findingcode) *** ### FindingLocus Re-exports [FindingLocus](https://docs.cosyte.com/hl7/api.md#findinglocus) *** ### FindingSeverity Re-exports [FindingSeverity](https://docs.cosyte.com/hl7/api.md#findingseverity) *** ### PREDICATE\_CONNECTORS Re-exports [PREDICATE_CONNECTORS](https://docs.cosyte.com/hl7/api.md#predicate_connectors) *** ### PREDICATE\_VERBS Re-exports [PREDICATE_VERBS](https://docs.cosyte.com/hl7/api.md#predicate_verbs) *** ### PredicateConnector Re-exports [PredicateConnector](https://docs.cosyte.com/hl7/api.md#predicateconnector) *** ### PredicateLocation Re-exports [PredicateLocation](https://docs.cosyte.com/hl7/api.md#predicatelocation) *** ### PredicatePresence Re-exports [PredicatePresence](https://docs.cosyte.com/hl7/api.md#predicatepresence) *** ### PredicateVerb Re-exports [PredicateVerb](https://docs.cosyte.com/hl7/api.md#predicateverb) *** ### PresencePredicate Re-exports [PresencePredicate](https://docs.cosyte.com/hl7/api.md#presencepredicate) *** ### PROFILE\_LEVELS Re-exports [PROFILE_LEVELS](https://docs.cosyte.com/hl7/api.md#profile_levels) *** ### ProfileLevel Re-exports [ProfileLevel](https://docs.cosyte.com/hl7/api.md#profilelevel) *** ### SegmentRule Re-exports [SegmentRule](https://docs.cosyte.com/hl7/api.md#segmentrule) *** ### SimpleUsageCode Re-exports [SimpleUsageCode](https://docs.cosyte.com/hl7/api.md#simpleusagecode) *** ### USAGE\_CODES Re-exports [USAGE_CODES](https://docs.cosyte.com/hl7/api.md#usage_codes) *** ### UsageCode Re-exports [UsageCode](https://docs.cosyte.com/hl7/api.md#usagecode) *** ### UsageCodeRegistryEntry Re-exports [UsageCodeRegistryEntry](https://docs.cosyte.com/hl7/api.md#usagecoderegistryentry) *** ### usageOutcomes Re-exports [usageOutcomes](https://docs.cosyte.com/hl7/api.md#usageoutcomes-1) *** ### UsageOutcomes Re-exports [UsageOutcomes](https://docs.cosyte.com/hl7/api.md#usageoutcomes) *** ### UsageResolution Re-exports [UsageResolution](https://docs.cosyte.com/hl7/api.md#usageresolution) *** ### validateAgainstProfile Re-exports [validateAgainstProfile](https://docs.cosyte.com/hl7/api.md#validateagainstprofile) --- URL: https://docs.cosyte.com/hl7/api/@cosyte/namespaces/text Markdown: https://docs.cosyte.com/hl7/api/@cosyte/namespaces/text.md # text ## References ### decode Renames and re-exports [decodeText](https://docs.cosyte.com/hl7/api.md#decodetext) *** ### encode Renames and re-exports [encodeText](https://docs.cosyte.com/hl7/api.md#encodetext) *** ### render Renames and re-exports [renderText](https://docs.cosyte.com/hl7/api.md#rendertext) --- URL: https://docs.cosyte.com/hl7/api Markdown: https://docs.cosyte.com/hl7/api.md # @cosyte/hl7 ## Namespaces - [conformance](https://docs.cosyte.com/hl7/api/@cosyte/namespaces/conformance.md) - [HL7](https://docs.cosyte.com/hl7/api/@cosyte/namespaces/HL7.md) - [text](https://docs.cosyte.com/hl7/api/@cosyte/namespaces/text.md) ## Classes ### Field Wrapper over a `RawField` exposing HL7 null/empty discrimination (`isNull`), the repetitions tree, and a decoded `value` getter. `seg.field(3) === seg.field(3)`: Field instances are referentially stable per segment position (D-12). #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); const pid5 = msg.segments("PID")[0]?.field(5); console.log(pid5?.value); // "Smith": decoded at parse console.log(pid5?.isNull); // false: HL7 explicit null is "", absent is not console.log(pid5?.repetitions.length); // 1 ``` #### Constructors ##### Constructor > **new Field**(`raw`, `enc`, `position`, `dateFormats?`): [`Field`](#field) **`Internal`** Construct a new `Field`. Called internally by `Segment.field(n)`; user code should obtain `Field` instances via `msg.segments(type)[i].field(n)`. ###### Parameters ###### raw [`RawField`](#rawfield) ###### enc [`EncodingCharacters`](#encodingcharacters-1) ###### position [`Hl7Position`](#hl7position) ###### dateFormats? readonly `string`[] ###### Returns [`Field`](#field) #### Properties ##### dateFormats > `readonly` **dateFormats**: readonly `string`[] **`Internal`** The date formats the caller declared for this message (`ParseOptions.dateFormats` ++ the applied profile's, deduplicated first-occurrence-wins), threaded down from `Hl7Message.dateFormats` so a typed `TS` coercion can honour them. Empty when neither route declared any, which is the strict-only parse. Read by `asTs()` and by the two helper sites that parse a datetime subcomponent directly. ##### enc > `readonly` **enc**: [`EncodingCharacters`](#encodingcharacters-1) **`Internal`** The 5 encoding characters for this message. Exposed for composite parsers. ##### isNull > `readonly` **isNull**: `boolean` HL7 null indicator: `true` iff the underlying field was the two-char literal `""`. ##### position > `readonly` **position**: [`Hl7Position`](#hl7position) **`Internal`** Position of this field in the parent message: used for position-aware error messages. ##### raw > `readonly` **raw**: [`RawField`](#rawfield) **`Internal`** The full `RawField` this wrapper wraps. Exposed for composite parsers. ##### repetitions > `readonly` **repetitions**: readonly [`RawRepetition`](#rawrepetition)[] Reference to the underlying `RawField.repetitions` (no defensive copy). #### Accessors ##### text ###### Get Signature > **get** **text**(): `string` The field's **canonical wire text**: the full field re-serialized with the active delimiters and re-escaped content (repetitions, components, and subcomponents included). Contrast with [value](#value), which returns only the first subcomponent of the first component of the first repetition (decoded once at parse: never re-unescaped). Use this when a field must be compared or echoed as a whole: e.g. correlating an ACK's MSA-2 against the inbound MSH-10, where a vendor-quirk control id containing an unescaped delimiter (`ID^X`) must not be truncated to its first component. **Byte-verbatim for parsed content.** The parse pipeline stores *decoded* content, but also records the original wire bytes of any escape whose decode is not byte-faithful (`RawComponent.rawSubcomponents`), so re-serialization preserves the sender's exact escape bytes: hex escapes stay hex (`A\X41\B`, casing intact), and recognize-and-preserve sequences (`\H\`, `\N`, formatting/charset/vendor escapes) re-emit **verbatim** rather than as escaped literal text. Delimiter/newline escapes and plain content round-trip byte-exact via the re-escape path. The only remaining canonicalization is structural: trailing insignificant empties are stripped (D-02), and a field built by hand (not parsed) re-escapes its decoded value since it has no overlay. **MSH-1/MSH-2 caveat.** Like [value](#value), calling this on MSH-1 or MSH-2 (the delimiter-definition fields) re-escapes the encoding characters themselves and produces garbage: those two fields are only meaningful through `Hl7Message.encodingCharacters`. ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7("MSH|^~\\&|A|B|C|D|20260101||ADT^A01|ID^X|P|2.5\r"); msg.segments("MSH")[0]?.field(10).value; // "ID" (first component only) msg.segments("MSH")[0]?.field(10).text; // "ID^X" (verbatim wire text) ``` ###### Returns `string` ##### value ###### Get Signature > **get** **value**(): `string` First-repetition, first-component, first-subcomponent value as a decoded string: unescaped ONCE by the tokenizer on parse, returned verbatim here (never re-unescaped). Returns `""` when the field is empty or HL7 null. Equivalent to `msg.get('SEG.N')` for a top-level access. ###### Example ```ts const pid5 = msg.segments("PID")[0]?.field(5); // wire "Smith\F\Jr" was decoded at parse → the field separator escape: console.log(pid5?.value); // "Smith|Jr" ``` ###### Returns `string` #### Methods ##### asCe() > **asCe**(): [`CE`](#ce) Coerce this field's first repetition to a typed `CE` (Coded Element). ###### Returns [`CE`](#ce) ###### Example ```ts const code = msg.segments("OBX")[0]?.field(3)?.asCe(); console.log(code?.identifier, code?.text); ``` ##### asCwe() > **asCwe**(): [`CWE`](#cwe-1) Coerce this field's first repetition to a typed `CWE` (Coded With Exceptions). ###### Returns [`CWE`](#cwe-1) ###### Example ```ts const code = msg.segments("OBX")[0]?.field(3)?.asCwe(); console.log(code?.identifier, code?.text); ``` ##### asCx() > **asCx**(): [`CX`](#cx-1) Coerce this field's first repetition to a typed `CX` (Extended Composite ID). `assigningAuthority` is a nested `HD`. ###### Returns [`CX`](#cx-1) ###### Example ```ts const mrn = msg.segments("PID")[0]?.field(3)?.asCx(); console.log(mrn?.idNumber, mrn?.assigningAuthority?.namespaceId); ``` ##### asHd() > **asHd**(): [`HD`](#hd-1) Coerce this field's first repetition to a typed `HD` (Hierarchic Designator). ###### Returns [`HD`](#hd-1) ###### Example ```ts const sending = msg.segments("MSH")[0]?.field(3)?.asHd(); console.log(sending?.namespaceId); ``` ##### asNm() > **asNm**(): [`NM`](#nm-1) Coerce this field's first repetition to a typed `NM` (Numeric). `{ raw, value }`: `value` is `undefined` on non-numeric input. ###### Returns [`NM`](#nm-1) ###### Example ```ts const nm = msg.segments("OBX")[0]?.field(5)?.asNm(); console.log(nm?.value); ``` ##### asPl() > **asPl**(): [`PL`](#pl-1) Coerce this field's first repetition to a typed `PL` (Person Location). `facility` is a nested `HD`. ###### Returns [`PL`](#pl-1) ###### Example ```ts const loc = msg.segments("PV1")[0]?.field(3)?.asPl(); console.log(loc?.pointOfCare, loc?.room, loc?.facility?.namespaceId); ``` ##### asSn() > **asSn**(): [`SN`](#sn) \| `undefined` Coerce this field's first repetition to a typed `SN` (Structured Numeric), or `undefined` when the field carries no usable structured-numeric content. Use for an OBX-5 whose OBX-2 value type is `SN` (a comparator like `>90`, a range like `100-200`, or a ratio like `1:128`). `num1`/`num2` are `number | undefined` (never `NaN`); the comparator is surfaced only when SN.1 is a recognized operator. ###### Returns [`SN`](#sn) \| `undefined` ###### Example ```ts const sn = msg.segments("OBX")[0]?.field(5)?.asSn(); console.log(sn?.comparator, sn?.num1); // ">" 90 ``` ##### asTs() > **asTs**(): [`DtmParts`](#dtmparts) Coerce this field's first repetition to a typed `TS` (Time Stamp): the fidelity `DtmParts` (raw + parts + precision + timezone). `valid` is `false` on unparseable input (no throw). Build an absolute instant only on explicit request via `dtmToDate(ts)`. The canonical HL7 shape is tried first. A value that is not canonical is then matched against the `dateFormats` the caller declared for this message, and a match reports which format won on `matchedFormat`. Nothing beyond those declared formats is tried, so a vendor date the caller has not described stays `valid: false` rather than becoming a plausible wrong date. ###### Returns [`DtmParts`](#dtmparts) ###### Example ```ts const ts = msg.segments("MSH")[0]?.field(7)?.asTs(); console.log(ts?.raw, ts?.precision, ts?.hasTimezone); // parseHL7(raw, { dateFormats: ["MM/DD/YYYY"] }) on a PID-7 of "07/05/1988": const dob = msg.segments("PID")[0]?.field(7)?.asTs(); console.log(dob?.month, dob?.matchedFormat); // 7 "MM/DD/YYYY" ``` ##### asXad() > **asXad**(): [`XAD`](#xad-1) Coerce this field's first repetition to a typed `XAD` (Extended Address). ###### Returns [`XAD`](#xad-1) ###### Example ```ts const addr = msg.segments("PID")[0]?.field(11)?.asXad(); console.log(addr?.street, addr?.city, addr?.stateOrProvince); ``` ##### asXcn() > **asXcn**(): [`XCN`](#xcn-1) Coerce this field's first repetition to a typed `XCN` (Extended Composite ID Number and Name for Persons). `assigningAuthority` is a nested `HD`. Common on OBR-16 (ordering provider), PV1-7 (attending doctor), PV1-8 (referring doctor). Empty field → `{}` (never throws). ###### Returns [`XCN`](#xcn-1) ###### Example ```ts const orderedBy = msg.segments("OBR")[0]?.field(16)?.asXcn(); console.log(orderedBy?.idNumber, orderedBy?.familyName, orderedBy?.identifierTypeCode); ``` ##### asXpn() > **asXpn**(): [`XPN`](#xpn-1) Coerce this field's first repetition to a typed `XPN` (Extended Person Name). Absent components are OMITTED from the result (`exactOptionalPropertyTypes`). Not memoized in v1: each call re-parses (D-09). ###### Returns [`XPN`](#xpn-1) ###### Example ```ts const pid5 = msg.segments("PID")[0]?.field(5); const name = pid5?.asXpn(); console.log(name?.familyName, name?.givenName); ``` ##### asXtn() > **asXtn**(): [`XTN`](#xtn-1) Coerce this field's first repetition to a typed `XTN` (Extended Telecommunication Number). ###### Returns [`XTN`](#xtn-1) ###### Example ```ts const phone = msg.segments("PID")[0]?.field(13)?.asXtn(); console.log(phone?.telephoneNumber); ``` ##### render() > **render**(`opts?`): [`RenderedText`](#renderedtext) Render this field's formatted text (HL7 v2 §2.7 highlight + formatting escapes) into a normalized [RenderedText](#renderedtext) display model: plain text plus highlight-aware runs. A **read projection** over the field's byte-verbatim wire [text](#text): it never mutates the raw value, and it never fabricates (an unrenderable escape is preserved + flagged). Use this to surface a clinical narrative (an NTE / OBX-5 note) to a human without the raw `\.br\` / `\H` sentinels. ###### Parameters ###### opts? [`RenderTextOptions`](#rendertextoptions) see [RenderTextOptions](#rendertextoptions) (e.g. a custom line-break string). ###### Returns [`RenderedText`](#renderedtext) the normalized display model. ###### Example ```ts const note = msg.segments("OBX")[0]?.field(5); note?.render().text; // "Specimen received.\nGross exam normal." ``` ##### empty() > `static` **empty**(`_enc`): [`Field`](#field) **`Internal`** Return a synthetic empty `Field` sentinel: used by `Segment.field(n)` to honor MODEL-05's "never throws on missing" contract. The returned Field has `isNull === false`, `repetitions === []`, and `value === ""`. Referentially stable across calls (same instance returned each time). The `enc` argument is accepted for API symmetry but ignored: the synthetic field carries no content, so unescape would be a no-op regardless of the active encoding characters. For the same reason the sentinel carries no declared `dateFormats`: there is no value to match one against, and `asTs()` on it is the empty invalid `TS` either way. ###### Parameters ###### \_enc [`EncodingCharacters`](#encodingcharacters-1) ###### Returns [`Field`](#field) ###### Example ```ts const empty = Field.empty(msg.encodingCharacters); console.log(empty.value); // "" ``` *** ### Hl7Message Parsed HL7 v2 message. Produced by `parseHL7`. Exposes the raw positional tree (`rawSegments`), delimiter metadata, warnings, and a typed traversal surface: `get(path)` for dot-paths, `getAll(type)` / `segments(type)` / `allSegments()` for wrapper-level iteration. #### Remarks The `warnings` array is frozen at the model boundary so downstream traversal and helpers cannot mutate parser output. The `profile` field is populated when a profile is passed, and is `undefined` otherwise. Segment/Field wrappers are cached per-message and invalidated wholesale by the mutation methods. #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); console.log(msg.get("PID.5.1")); // "Smith" for (const obx of msg.segments("OBX")) { console.log(obx.field(5).value); } for (const w of msg.warnings) console.warn(w.code); ``` #### Extended by - [`ProfiledMessage`](#profiledmessage) - [`TypedMessage`](#typedmessage) #### Constructors ##### Constructor > **new Hl7Message**(`init`): [`Hl7Message`](#hl7message) **`Internal`** Construct a new `Hl7Message`. The constructor takes a plain init object and freezes the warnings array so callers cannot mutate parser output after handoff. ###### Parameters ###### init `Hl7MessageInit` ###### Returns [`Hl7Message`](#hl7message) #### Properties ##### dateFormats > `readonly` **dateFormats**: readonly `string`[] Merged `dateFormats` list: `options.dateFormats ++ profile.dateFormats` deduped first-occurrence per D-21. Empty array when neither source supplied any formats. This is the list every datetime in the message honours, from `meta.timestamp` to any `field.asTs()`, in exactly this order; exposed so a caller can introspect what their options and profile added up to. ##### encodingCharacters > `readonly` **encodingCharacters**: [`EncodingCharacters`](#encodingcharacters-1) ##### profile > `readonly` **profile**: \{ `lineage`: readonly `string`[]; `name`: `string`; \} \| `undefined` ##### rawSegments > `readonly` **rawSegments**: readonly [`RawSegment`](#rawsegment)[] Raw positional tree produced by the parser. 1-indexed per HL7 convention (`fields[0]` is the segment-name / MSH separator placeholder slot). Use `segments(type)` / `allSegments()` for typed wrapper access: this field is exposed for advanced callers that need the raw tree directly. ##### version > `readonly` **version**: `string` HL7 version the model asserts for this message, from MSH-12.1.1, e.g. `"2.5"` or `"2.5.1"`. **Bounded, and it is `""` when MSH-12 does not hold a version.** Same reasoning as [Segment.type](#type): it presents as an identifier and consumers label with it. `meta.version` keeps MSH-12 exactly as it arrived, and is what version-scoped field selection reads, so this bound changes no parsing behaviour. ##### warnings > `readonly` **warnings**: readonly [`Hl7ParseWarning`](#hl7parsewarning)[] #### Accessors ##### meta ###### Get Signature > **get** **meta**(): [`Meta`](#meta-1) MSH-derived message metadata (type, controlId, timestamp, version, etc.). D-01: plain object. D-02: memoized: `msg.meta === msg.meta` across reads until mutation invalidates. D-03: always defined (MSH absence throws `NO_MSH_SEGMENT` at parse time). ###### Example ```ts console.log(msg.meta.type); // "ADT^A01" console.log(msg.meta.timestamp?.raw); // fidelity TS console.log(msg.meta.controlId); // "MSG001" ``` ###### Returns [`Meta`](#meta-1) ##### patient ###### Get Signature > **get** **patient**(): [`Patient`](#patient-8) \| `undefined` PID-derived patient view, or `undefined` when no PID segment exists (D-04). D-02: memoized. HELPERS-07: never throws: absent fields surface as `undefined` on the returned `Patient` object. ###### Example ```ts console.log(msg.patient?.mrn); console.log(msg.patient?.fullName); console.log(msg.patient?.dateOfBirth?.raw); // fidelity TS: e.g. "19800115" ``` ###### Returns [`Patient`](#patient-8) \| `undefined` ##### structure ###### Get Signature > **get** **structure**(): [`MessageStructure`](#messagestructure) Structural-conformance summary for the common message types: a misroute/truncation safety net, NOT a conformance validator. Reports, per the message's recognized (MSH-9.1, MSH-9.2) type, which Required segment groups are present and which are entirely absent (`missingGroups`: the same set the parser flags as `MISSING_EXPECTED_GROUP` warnings). For an unmodelled type, `recognized` is `false` and `missingGroups` is empty. D-02: memoized. ###### Example ```ts console.log(msg.structure.recognized); // true for ORU^R01, ADT^A01, … console.log(msg.structure.missingGroups); // e.g. ["result"] if no OBR/OBX ``` ###### Returns [`MessageStructure`](#messagestructure) ##### visit ###### Get Signature > **get** **visit**(): [`Visit`](#visit-7) \| `undefined` PV1-derived visit view, or `undefined` when no PV1 segment exists (HELPERS-03). D-02: memoized. HELPERS-07: never throws. ###### Example ```ts console.log(msg.visit?.patientClass); // "I" console.log(msg.visit?.admitDateTime?.raw); // fidelity TS console.log(msg.visit?.attendingDoctor?.familyName); ``` ###### Returns [`Visit`](#visit-7) \| `undefined` #### Methods ##### addSegment() > **addSegment**(`name`, `fields`): `this` Append a new segment to the end of the message. `name` must match `/^(?:[A-Z]{3}|Z[A-Z0-9]{2})$/u`: throws `TypeError` otherwise (D-19). `fields` is interpreted in HL7 1-indexed terms: `addSegment("NTE", [a, b, c])` produces a segment whose `NTE-1 = a`, `NTE-2 = b`, `NTE-3 = c`. The internal `RawSegment.fields[0]` name/separator placeholder is synthesized by this method. Each entry may be a plain string (treated as a single-subcomponent single-component single-repetition field) or a full `RawField` object for advanced callers who need structured content. Invalidates caches on return; warnings untouched (D-16). ###### Parameters ###### name `string` ###### fields readonly (`string` \| [`RawField`](#rawfield))[] ###### Returns `this` ###### Example ```ts msg.addSegment("NTE", ["", "note text"]); msg.get("NTE.2"); // "note text" ``` ##### allergies() > **allergies**(): readonly [`Allergy`](#allergy)[] Every AL1 and every IAM as an Allergy, one entry per segment in document order, each naming the segment it was read from (`source`). An ADT^A60 carries its allergies in IAM, so it is read here too. D-05: returns `[]` when neither is present. Limits: the IAR and NTE segments under an IAM are not read, nor IAM-1 or IAM-8 onward (reach them with `msg.segments("IAM")`). IAM-6 is surfaced as `actionCode`, and an exact `D` sets `deleteRequested`, but no action is ever applied: a delete entry is still returned, an update never replaces an earlier entry, and an AL1 and an IAM for the same allergen are two entries. ###### Returns readonly [`Allergy`](#allergy)[] ###### Example ```ts for (const al of msg.allergies()) { const kind = al.deleteRequested === true ? "delete request" : "allergy"; console.log(kind, al.source, al.code?.text, al.severity, al.actionCode); } ``` ##### allSegments() > **allSegments**(): readonly [`Segment`](#segment)\<`string`\>[] Iterate every `Segment` in document order (MSH first, then every subsequent segment). Cached per-message; same array reference and same Segment instances on repeat calls (D-11). Invalidated wholesale by the mutation methods. ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts for (const seg of msg.allSegments()) { console.log(seg.type); } ``` ##### appointments() > **appointments**(): readonly [`Appointment`](#appointment)[] Every SCH of an SIU message as a typed `Appointment`, with the AIS/AIG/AIL/AIP resource segments that follow it grouped positionally under that SCH. Surfaces the placer/filler appointment ids, SCH-25 filler status (Table 0278), SCH-11 start/end timing, and the resource groups (service / general / location / personnel). D-05: returns `[]` when no SCH is present. D-06: not memoized. Never throws (HELPERS-07). Not a scheduling-workflow state machine: see the package known-limitations. ###### Returns readonly [`Appointment`](#appointment)[] ###### Example ```ts for (const appt of msg.appointments()) { console.log(appt.fillerAppointmentId, appt.fillerStatusCode?.identifier); for (const r of appt.resources) console.log(r.kind, r.code?.identifier); } ``` ##### charges() > **charges**(): readonly [`Charge`](#charge)[] Every FT1 of a DFT message as a typed `Charge`, one per FT1 in document order. Surfaces billing-critical fields (FT1-6 transaction type, FT1-7 code, FT1-11/12 extended/unit amount, FT1-19 diagnosis linkage) with **no billing logic and no money-as-float**: amounts are the verbatim CP wire text. D-05: returns `[]` when no FT1 is present. D-06: not memoized. Never throws (HELPERS-07). ###### Returns readonly [`Charge`](#charge)[] ###### Example ```ts for (const charge of msg.charges()) { console.log(charge.transactionType, charge.transactionCode?.identifier); console.log(charge.amountExtended); // verbatim, never a number } ``` ##### diagnoses() > **diagnoses**(): readonly [`Diagnosis`](#diagnosis)[] Every DG1 as a Diagnosis in document order. D-05: returns `[]` when no DG1 present. ###### Returns readonly [`Diagnosis`](#diagnosis)[] ###### Example ```ts for (const dg of msg.diagnoses()) console.log(dg.code?.identifier); ``` ##### documents() > **documents**(): readonly [`ClinicalDocument`](#clinicaldocument)[] Every TXA of an MDM message as a typed `ClinicalDocument`, with the OBX narrative body grouped positionally under that TXA. The completion status (TXA-17) and availability status (TXA-19) are surfaced as **distinct** fields and never conflated: a document can be available before it is authenticated, and reading a preliminary document as final is the harm. D-05: returns `[]` when no TXA is present. D-06: not memoized. Never throws (HELPERS-07). ###### Returns readonly [`ClinicalDocument`](#clinicaldocument)[] ###### Example ```ts for (const doc of msg.documents()) { console.log(doc.documentType, doc.completionStatus, doc.availabilityStatus); for (const obx of doc.observations) console.log(obx.value); // narrative body } ``` ##### get() > **get**(`path`): `string` \| `undefined` Resolve a dot-path (e.g. `PID.5.1`, `OBX[2].5`, `PID.3[0].1`) to its decoded leaf string (unescaped once at parse: never re-unescaped on read). Returns `undefined` when the path doesn't resolve: never throws on missing path (MODEL-05). Throws `TypeError` on malformed path syntax (e.g. `"pid.5"`, empty string). ###### Parameters ###### path `string` ###### Returns `string` \| `undefined` ###### Example ```ts const msg = parseHL7(raw); msg.get("PID.5.1"); // "Smith" msg.get("OBX[2].5"); // third OBX's 5th field msg.get("NOT.9.9"); // undefined msg.get("MSH.12"); // "2.5": HL7 version string ``` ##### getAll() > **getAll**(`segmentType`): readonly [`Segment`](#segment)\<`string`\>[] Return every `Segment` of `segmentType` in document order. Returns `[]` (empty array, NEVER `undefined`) when no segment of that type exists (MODEL-02). Alias for `segments(segmentType)`: shares the same cache, and matches segment names ignoring case exactly as it does. ###### Parameters ###### segmentType `string` ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts for (const obx of msg.getAll("OBX")) { console.log(obx.field(5).value); } ``` ##### identityEvents() > **identityEvents**(): readonly [`IdentityEvent`](#identityevent)[] Every recognized ADT patient-identity event (merge / move / change / link / unlink / person add/update), with the MRG-sourced `prior` and PID/PV1-sourced `surviving` parties labelled by role and the spec-constant `direction: "MRG_TO_PID"` on merge/move/change events. Returns `[]` when the trigger event is not in the identity family. The recognized set is floored by the published message structures: every ADT trigger event whose structure requires MRG is recognized. D-06: not memoized. Never throws; incomplete merge pairs surface a `MERGE_MISSING_PRIOR_OR_SURVIVOR` warning on the event. ###### Returns readonly [`IdentityEvent`](#identityevent)[] ###### Example ```ts for (const ev of msg.identityEvents()) { if (ev.kind === "merge" && ev.prior && ev.surviving) { // retire ev.prior.identifiers in favour of ev.surviving.identifiers } } ``` ##### immunizations() > **immunizations**(): readonly [`Immunization`](#immunization)[] Every RXA of a VXU^V04 as a typed `Immunization`, with RXR (route/site) and OBX children grouped positionally under the RXA and `orderControl` from the preceding ORC of the VXU order group. D-05: returns `[]` when no RXA present. D-06: not memoized. The vaccine code carries its own provenance; the action code (RXA-21) is surfaced verbatim and `recordOrigin` (administered vs historical) is derived only from the well-known NIP001 RXA-9.1 codes: never guessed. ###### Returns readonly [`Immunization`](#immunization)[] ###### Example ```ts for (const imm of msg.immunizations()) { console.log(imm.vaccineCode?.identifier, imm.doseAmount, imm.recordOrigin); console.log(imm.actionCode, imm.completionStatus); } ``` ##### insurance() > **insurance**(): readonly [`Insurance`](#insurance-1)[] Every IN1 as an Insurance entry with positional IN2/IN3 presence flags. D-05: returns `[]` when no IN1 present. ###### Returns readonly [`Insurance`](#insurance-1)[] ###### Example ```ts for (const ins of msg.insurance()) console.log(ins.planId?.text); ``` ##### is() **`Internal`** Single runtime implementation behind both signatures. ###### Call Signature > **is**\<`K`\>(`key`): `this is TypedMessage` Is this message of the type `key` names, and if so, narrow it for the compiler. The check is on the (MSH-9.1, MSH-9.2) pair the parser already extracted, so a message whose MSH-9 carries the three-component form `ADT^A01^ADT_A01` answers `true` to `is("ADT^A01")`. A key is `"^"` (`"ADT^A01"`), or `""` alone for a message type the published structure registry matches on message code alone (`"ACK"`, whose MSH-9.2 carries the acknowledged message's trigger event). `SUPPORTED_OVERLAY_MESSAGES` enumerates every key. **Any other string is `false`, never a throw**: an unrecognized type, the three-component form as a string, an empty string, or a value computed at run time. `is` does not parse its own argument, and a caller who wants a raw comparison has `msg.meta.type`. Read-only: it inspects `meta` and mutates nothing. ###### Type Parameters ###### K `K` *extends* keyof [`OverlayStructures`](#overlaystructures) ###### Parameters ###### key `K` ###### Returns `this is TypedMessage` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); if (msg.is("ORU^R01")) { const code: "ORU" = msg.meta.messageCode; // literal, no cast console.log(msg.part("OBR")?.field(4).value); } console.log(msg.is("ZZZ^Z99")); // false: not a published key ``` ###### Call Signature > **is**(`key`): `boolean` Answer for a key that is not known at compile time. A runtime-computed string cannot narrow anything, so this form returns a plain `boolean`. ###### Parameters ###### key `string` ###### Returns `boolean` ##### medications() > **medications**(): readonly [`Medication`](#medication)[] Every RXO/RXE/RXD/RXA as a typed `Medication`, with RXR (route) and RXC (component) segments grouped positionally under their parent. D-05: returns `[]` when no RX* parent present. D-06: not memoized. The give *amount* and give *strength* are surfaced separately and never reconciled. Each medication also carries its TQ1 / legacy embedded-TQ (RXE-1) `timings` (repeat pattern verbatim, never resolved to a schedule). Each medication also carries `orderControl`: ORC-1 of the ORC that opened its order group (every RX* up to the next ORC shares it), exactly as sent. It is never interpreted into an active, held or discontinued state, and it is omitted when no ORC precedes the RX* or ORC-1 is empty, never carried over from an earlier group. ###### Returns readonly [`Medication`](#medication)[] ###### Example ```ts for (const med of msg.medications()) { console.log(med.orderControl); // e.g. "NW" (new) or "DC" (discontinue), verbatim console.log(med.context, med.giveCode?.identifier, med.giveCode?.nameOfCodingSystem); console.log(med.amount?.minimum, med.strength?.value); for (const t of med.timings) console.log(t.repeatPattern?.code, t.totalOccurrences); } ``` ##### nextOfKin() > **nextOfKin**(): readonly [`NextOfKin`](#nextofkin-1)[] Every NK1 as a NextOfKin entry in document order. D-05: returns `[]` when no NK1 present. ###### Returns readonly [`NextOfKin`](#nextofkin-1)[] ###### Example ```ts for (const nk of msg.nextOfKin()) { console.log(nk.name?.familyName, nk.relationship?.text); } ``` ##### notes() > **notes**(): readonly `string`[] Message-level NTE notes: every NTE segment with no recognized preceding parent (not immediately following a `PID`, `ORC`, `OBR`, or `OBX`), surfaced verbatim in document order so nothing is dropped. Notes that DO attach to a specific patient / order / result are exposed on those helper outputs (`msg.patient?.notes`, `order.notes`, `observation.notes`), not here. D-05: returns `[]` when there are none. D-06: NOT memoized. ###### Returns readonly `string`[] ###### Example ```ts for (const note of msg.notes()) console.log(note); // message-level narrative ``` ##### observations() > **observations**(): readonly [`Observation`](#observation)[] Every OBX segment as a typed Observation in document order. D-05: returns `[]` when no OBX present. D-06: NOT memoized: each call re-walks `rawSegments`. Value type is discriminated per D-13. ###### Returns readonly [`Observation`](#observation)[] ###### Example ```ts for (const obs of msg.observations()) { if (obs.valueType === "NM") console.log(obs.value); // number | undefined } ``` ##### orders() > **orders**(): readonly [`Order`](#order-1)[] Every OBR as an Order with its OBX children grouped positionally (D-12) and its TQ1 / legacy embedded-TQ (ORC-7) `timings` (the repeat pattern is surfaced verbatim, never resolved to a schedule). D-05: returns `[]` when no OBR present. D-06: not memoized. ###### Returns readonly [`Order`](#order-1)[] ###### Example ```ts for (const order of msg.orders()) { console.log(order.placerOrderNumber, order.observations.length); for (const t of order.timings) console.log(t.repeatPattern?.code); // e.g. "Q6H": verbatim } ``` ##### part() > **part**(`segmentType`): [`Segment`](#segment)\<`string`\> \| `undefined` The FIRST `Segment` named `segmentType` in document order, or `undefined` when the message carries none. Shorthand for `segments(segmentType)[0]`: same cache, same case-insensitive matching on both sides, same referential stability. On a message narrowed by [Hl7Message.is](#is), `segmentType` is scoped at compile time to the segment names that message type's published structure marks required. It stays `Segment | undefined` there: the parser tolerates a message that omits a required segment (and warns), so a required name is never a presence guarantee. ###### Parameters ###### segmentType `string` ###### Returns [`Segment`](#segment)\<`string`\> \| `undefined` ###### Example ```ts const pid = msg.part("PID"); console.log(pid?.field(5).value); ``` ##### parts() > **parts**(`segmentType`): readonly [`Segment`](#segment)\<`string`\>[] EVERY `Segment` named `segmentType` in document order, `[]` when there are none. Alias for `segments(segmentType)`: same cache, same array identity, same case-insensitive matching. On a message narrowed by [Hl7Message.is](#is), `segmentType` is scoped at compile time to the segment names that message type's published structure marks required; the returned list can still be empty, for the same reason [Hl7Message.part](#part) can still return `undefined`. ###### Parameters ###### segmentType `string` ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts for (const obx of msg.parts("OBX")) console.log(obx.field(5).value); ``` ##### prettyPrint() > **prettyPrint**(): `string` Emit this message as a human-readable multi-line string for logs and debugging (SER-04). Single opinionated format (D-22 no options): header line with type / controlId / timestamp / segment count, then one line per segment with labeled `[N]=value` fields (D-23). Composite values render as their raw HL7 string: depth stops at field level (D-24). Pure: never warns, never throws (D-26). **Field values render as their raw HL7 string representation.** Embedded delimiters in user data appear as escape sequences: e.g. a patient family name containing `|` renders as `Smith\F\Jones` (NOT `Smith|Jones`). This preserves round-trip fidelity: copy-pasting prettyPrint output into `parseHL7` yields a structurally equivalent message. For un-escaped human display, parse the composite first via typed accessors (e.g. `msg.patient?.familyName`): those return already-decoded strings. ###### Returns `string` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); console.log(msg.prettyPrint()); // HL7 ADT^A01 controlId=MSG001 timestamp=2026-04-19T10:15:00Z (5 segments) // MSH [3]=SENDAPP [4]=SENDFAC ... // PID [1]=1 [3]=MRN123 [5]=Doe^John ``` ##### removeSegment() > **removeSegment**(`segmentType`, `occurrenceOrOptions?`): `this` Remove segments by type + occurrence or by type + all. Call shapes: - `removeSegment("NTE")`: remove the FIRST NTE (occurrence 0). - `removeSegment("OBX", 1)`: remove the SECOND OBX (0-indexed per D-01). - `removeSegment("OBX", { all: true })`: remove ALL OBX segments. `MSH` is protected: `removeSegment("MSH")` throws `TypeError` (every HL7 message must retain its MSH segment). Unknown segment types are a no-op (idempotent; no throw). Segment name must match the D-19 shape regex: invalid shapes throw `TypeError` for symmetry with `addSegment`. Invalidates caches on return; warnings untouched (D-16). ###### Parameters ###### segmentType `string` ###### occurrenceOrOptions? `number` \| \{ `all?`: `boolean`; \} ###### Returns `this` ###### Example ```ts msg.removeSegment("NTE"); // remove first NTE msg.removeSegment("OBX", 1); // remove second OBX msg.removeSegment("OBX", { all: true }); // remove all remaining OBX ``` ##### segments() > **segments**(`segmentType`): readonly [`Segment`](#segment)\<`string`\>[] Return the cached array of `Segment` wrappers for `segmentType` in document order. The returned array identity and the individual Segment instances are both stable across calls (D-11). Invalidated wholesale by the mutation methods. **Segment names are matched ignoring ASCII case, on both sides.** A sender that ships `obx` is returned by `segments("OBX")`, and `segments("obx")` returns the same array (one cache entry, so the D-11 identity guarantee does not split across spellings). A `SEGMENT_CASE` warning records the sender's deviation; `Segment.raw.name` is the spelling that arrived. ###### Parameters ###### segmentType `string` ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts const pid = msg.segments("PID")[0]; if (pid !== undefined) console.log(pid.field(5).value); ``` ##### setComposite() > **setComposite**\<`K`\>(`path`, `kind`, `value`): `this` Set a **typed composite** at a field (or field-repetition) dot-path: the conservative-emit mirror of the typed read accessors (`asXpn`/`asCx`/…). The caller passes a structured value (an XPN name, a CX identifier, a TS timestamp, …) by its [CompositeKind](#compositekind), and the setter encodes it into a spec-clean field using the encode-safe path: any delimiter embedded in a component value is **escaped, never injected**, so a `familyName` of `"Smith^Jr"` re-parses to exactly that string rather than forging a component boundary. No hand-assembly of `^`/`&`/`~`. The path must resolve to a **field** (`"PID.5"`) or a specific **repetition** of a field (`"PID.11[1]"`). A component/subcomponent-level path (`"PID.5.1"`) is rejected with `TypeError`: a composite occupies a whole field, not a single component. Like [setField](#setfield), the target segment must already exist (`addSegment` first); the repetition defaults to index 0 and other repetitions of the field are preserved. Never fabricates: an omitted optional composite field encodes to an empty/absent component, never a defaulted value; an all-empty composite clears the field. Segment/helper caches are invalidated on success; the frozen `warnings` array is untouched. ###### Type Parameters ###### K `K` *extends* [`CompositeKind`](#compositekind) ###### Parameters ###### path `string` ###### kind `K` ###### value [`CompositeValueByKind`](#compositevaluebykind)\[`K`\] ###### Returns `this` ###### Example ```ts const msg = buildMessage({ type: "ADT^A01" }).addSegment("PID", [""]); msg.setComposite("PID.5", "XPN", { familyName: "Smith", givenName: "Ann" }); msg.setComposite("PID.3", "CX", { idNumber: "MRN001", identifierTypeCode: "MR" }); msg.setComposite("PID.7", "TS", "19880705"); msg.get("PID.5.1"); // "Smith" ``` ##### setField() > **setField**(`path`, `value`): `this` Set the string value at a dot-path. Mutates the underlying tree and returns `this` for chaining (D-15). Auto-creates missing repetitions, components, and subcomponents WITHIN an existing field, but does NOT auto-create segments: callers must `addSegment` first (throws `TypeError` with an actionable message otherwise). The value is accepted verbatim: unescaped delimiter characters are NOT rejected on input (D-18). Re-escaping is the serializer's concern. MSH-1 / MSH-2 follow the user-facing HL7 convention: `setField("MSH.3", ...)` targets MSH-3 (sending application), matching `msg.get("MSH.3")`. Segment/Field wrapper caches are invalidated wholesale on success (D-17). The frozen `warnings` array is never touched (D-16). ###### Parameters ###### path `string` ###### value `string` ###### Returns `this` ###### Example ```ts msg.setField("PID.8", "F"); // patient sex → F msg.setField("PID.5.1", "Jones"); // family name msg.setField("PID.4[2].1", "MRN2"); // create third repetition of PID-4 ``` ##### toJSON() > **toJSON**(): [`SerializedMessage`](#serializedmessage) Emit this message as a structured `SerializedMessage` JSON projection (SER-03). Invoked automatically by `JSON.stringify(msg)` (D-18). Re-walks `rawSegments` on every call (D-30 no caching). Mirrors the raw tree one-for-one, preserves `isNull`, always includes `warnings: []`, and includes `profile: { name, lineage }` only when `this.profile` is truthy (D-19/D-20). Pure: never warns, never throws. ###### Returns [`SerializedMessage`](#serializedmessage) ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); const snap = msg.toJSON(); console.log(snap.segments[0]?.name); // "MSH" console.log(JSON.stringify(msg)); // same content, auto-invokes toJSON ``` ##### toString() > **toString**(): `string` Emit this message as spec-clean HL7 (SER-01). Re-walks `rawSegments` on every call (D-30 no caching). Segments are joined with `\r` per D-05; MSH-1 and MSH-2 are inlined verbatim from `this.encodingCharacters` per D-06; every field string passes through `reescape` per D-04. `RawField.isNull === true` is preserved as the HL7 literal `""` (D-02). Pure: never warns, never throws (D-07). ###### Returns `string` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); console.log(msg.toString()); // spec-clean, CR-separated HL7 ``` *** ### Hl7ParseError Thrown by `parseHL7` when the input violates one of the 4 unrecoverable Tier-3 structural rules (missing MSH, truncated MSH, invalid encoding characters, or empty input). Carries positional context plus a short snippet of the offending input so consumers can log actionable errors. #### Remarks `snippet` may contain PHI when parsing real clinical messages, and the library does not redact it: redact at the call site if required by your compliance posture. **It is the only field carrying input verbatim and unfiltered** (capped at 40 characters plus an ellipsis, but not shape-checked in any way), so it is the one to redact first. `message` is bounded but not absolutely content-free, and the difference is worth stating because `message` is what a logger prints by default and what `stack` embeds. A token lifted from the input is echoed only when it matches the form the spec defines for it: a three-character segment identifier, an MSH-9 type, an MSH-12 version, or a charset label the closed Table 0211 actually contains. Anything else becomes ``. So a message cannot carry a field's value, but it can carry a residue of up to three characters when a malformed line happens to look like a segment identifier. The other shapes are narrower in practice than their patterns allow, because the library only ever feeds them registry-matched values, but the patterns themselves admit more (a message type up to 26 characters, a version up to 14), which matters if you construct warnings yourself. Under `{ strict: true }` an escalated Tier-2 warning is thrown as this error, carrying the warning's own bounded message and a `snippet` of the **first 40 characters of the input**. That snippet is the head of the message rather than the deviation's own segment, so it is usually the MSH header; it is deliberately not re-pointed at the offending segment, since doing so would move more clinical content into the unredacted field. #### Example ```ts import { parseHL7, Hl7ParseError } from "@cosyte/hl7"; try { parseHL7(""); } catch (err) { if (err instanceof Hl7ParseError && err.code === "EMPTY_INPUT") { // handle empty input: err.position, err.snippet available } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Hl7ParseError**(`code`, `message`, `position`, `snippet`): [`Hl7ParseError`](#hl7parseerror) **`Internal`** Construct a new `Hl7ParseError`. All four fields are required so every thrower populates full positional context per the TOL-02 requirement. ###### Parameters ###### code [`FatalCode`](#fatalcode) ###### message `string` ###### position [`Hl7Position`](#hl7position) ###### snippet `string` ###### Returns [`Hl7ParseError`](#hl7parseerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`FatalCode`](#fatalcode) ##### position > `readonly` **position**: [`Hl7Position`](#hl7position) ##### snippet > `readonly` **snippet**: `string` *** ### ProfileDefinitionError Thrown by `defineProfile()` and profile-validation code when a profile definition is structurally invalid: e.g. references an undefined parent, declares a malformed custom segment, or includes an unsupported date format. Callers may optionally supply the offending profile name for better diagnostics. #### Example ```ts import { ProfileDefinitionError } from "@cosyte/hl7"; throw new ProfileDefinitionError( "Unknown parent profile: epic-v7", "my-epic-extension", ); ``` #### Extends - `Error` #### Constructors ##### Constructor > **new ProfileDefinitionError**(`message`, `profileName?`): [`ProfileDefinitionError`](#profiledefinitionerror) **`Internal`** Construct a new `ProfileDefinitionError`. `profileName` is optional so callers may omit it when the offending profile cannot be named (e.g. during initial validation before a name is parsed). ###### Parameters ###### message `string` ###### profileName? `string` ###### Returns [`ProfileDefinitionError`](#profiledefinitionerror) ###### Overrides `Error.constructor` #### Properties ##### profileName > `readonly` **profileName**: `string` \| `undefined` *** ### SchemaTargetError Thrown when emission is requested for a target this library does not support. Carries the requested name and the supported set as data as well as in the message, so a caller can react without parsing prose. #### Example ```ts import { emitMessageSchema, SchemaTargetError } from "@cosyte/hl7"; try { emitMessageSchema("openapi-3.1"); } catch (error) { if (error instanceof SchemaTargetError) { error.requestedTarget; // "openapi-3.1" error.supportedTargets; // ["json-schema-2020-12", "zod"] } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new SchemaTargetError**(`requestedTarget`): [`SchemaTargetError`](#schematargeterror) Build the error for one unsupported target name. ###### Parameters ###### requestedTarget `string` ###### Returns [`SchemaTargetError`](#schematargeterror) ###### Overrides `Error.constructor` #### Properties ##### requestedTarget > `readonly` **requestedTarget**: `string` The target name the caller asked for, verbatim. ##### supportedTargets > `readonly` **supportedTargets**: readonly (`"json-schema-2020-12"` \| `"zod"`)[] Every target this library does support. *** ### Segment Wrapper over a `RawSegment` exposing typed per-position `Field` instances. `seg.field(3) === seg.field(3)`: referential stability is guaranteed per segment instance. `FieldName` is the set of names [Segment.get](#get-1) accepts. It is `string` for every segment whose profile-declared field names are not statically known, which is the default and covers every segment reached without a statically known profile; a segment obtained for a declared segment type from a message parsed with one carries that type's declared names instead. #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); const pid = msg.segments("PID")[0]; if (pid !== undefined) console.log(pid.field(5).value); ``` #### Type Parameters ##### FieldName `FieldName` *extends* `string` = `string` the names `get` accepts; `string` when not known. #### Constructors ##### Constructor > **new Segment**\<`FieldName`\>(`raw`, `enc`, `absoluteIndex`, `customFields?`, `dateFormats?`): [`Segment`](#segment)\<`FieldName`\> **`Internal`** Construct a new `Segment`. Called internally by `Hl7Message`; user code should obtain `Segment` instances via `msg.segments(type)` or `msg.allSegments()`. The optional `customFields` parameter is the per-segment portion of the applied profile's merged declarations for this segment type (PROF-07 / D-16). When supplied, `get(name)` resolves names against it; otherwise `get(name)` always returns `undefined`. The optional `dateFormats` parameter is the message's merged date-format list; it is passed straight through to each `Field`. ###### Parameters ###### raw [`RawSegment`](#rawsegment) ###### enc [`EncodingCharacters`](#encodingcharacters-1) ###### absoluteIndex `number` ###### customFields? `Readonly`\<`Record`\<`string`, `number`\>\> ###### dateFormats? readonly `string`[] ###### Returns [`Segment`](#segment)\<`FieldName`\> #### Properties ##### absoluteIndex > `readonly` **absoluteIndex**: `number` **`Internal`** Absolute index of this segment in `Hl7Message.rawSegments[]`. Used for position tracking. ##### customFields > `readonly` **customFields**: `Readonly`\<`Record`\<`string`, `number`\>\> \| `undefined` **`Internal`** Lookup map from profile-declared field name → 1-indexed HL7 position. Absent when no profile was applied to the parent message, or when the applied profile declares no field names for this segment's type in either of its declaration maps (`customSegments` for a Z-segment, `segmentOverrides` for a standard one). Consumed by `get(name)` to resolve named-field access (PROF-07). ##### dateFormats > `readonly` **dateFormats**: readonly `string`[] **`Internal`** The message's merged `dateFormats` (D-21), handed to every `Field` this segment builds so a typed `TS` coercion honours what the caller declared. Empty when neither `ParseOptions.dateFormats` nor the applied profile declared any. ##### enc > `readonly` **enc**: [`EncodingCharacters`](#encodingcharacters-1) **`Internal`** The 5 encoding characters for this message. Exposed for composite parsers. ##### fields > `readonly` **fields**: readonly [`RawField`](#rawfield)[] Reference to the underlying `RawSegment.fields`: 1-indexed per HL7 convention. ##### raw > `readonly` **raw**: [`RawSegment`](#rawsegment) **`Internal`** The full `RawSegment` this wrapper wraps. Exposed for mutation methods. ##### type > `readonly` **type**: `string` Segment identifier: three characters with a leading letter, e.g. `"PID"`, `"OBX"`, `"ZPI"`. **Canonical ASCII uppercase.** A sender that ships `pid` or `Obx`, which no segment identifier in the HL7 v2 standard does but which real feeds do, reports here as `PID` / `OBX`, so `msg.segments("PID")` matches it and a `SEGMENT_CASE` warning records the deviation. Read [Segment.raw](#raw-1)`.name` for the spelling that actually arrived. **Bounded, and it is `""` when the raw name is not that shape.** A line with no field separator has its whole content read as a segment name, so an unescaped line break inside a narrative field forges a "segment" whose name is clinical text. This field presents itself as a structural identifier and consumers interpolate it into labels, loci and reports, so it never carries that text. `""` still means absent, which is a different fact from withheld. Use [Segment.raw](#raw-1)`.name` when you need the verbatim text: it is the unbounded value, and it is what serialization emits, so the byte-verbatim round-trip is unaffected. One consequence worth knowing: a segment whose raw name fails the shape (a 4-character vendor Z-segment, which HL7 v2 Ch. 2 §2.5 does not permit) is not matched by `msg.segments(name)`. #### Methods ##### field() > **field**(`n`): [`Field`](#field) Return the `Field` wrapper at HL7 position `n`. Indexing follows the HL7 1-indexed convention: `seg.field(5)` on a PID segment maps to PID-5. MSH segments use the same user-facing convention: `msh.field(1)` returns the field-separator (MSH-1), `msh.field(2)` returns encoding chars (MSH-2), `msh.field(3)` returns MSH-3, and so on: the internal `fields[N-1]` offset for MSH segments is applied here (mirrors the dot-path resolver in `dot-path.ts`, keeping `msg.segments('MSH')[0].field(3)` and `msg.get('MSH.3')` in agreement). Returns a synthetic empty `Field` (`.isNull === false`, `.value === ""`) when `n` is out of range: never throws (MODEL-05). Successive calls with the same `n` return the same `Field` instance (D-12). ###### Parameters ###### n `number` ###### Returns [`Field`](#field) ###### Example ```ts const pid5 = msg.segments("PID")[0]?.field(5); console.log(pid5?.value); // "Smith" const msh3 = msg.segments("MSH")[0]?.field(3); console.log(msh3?.value); // sending application (HL7 MSH-3) ``` ##### get() > **get**(`name`): [`Field`](#field) \| `undefined` Return the `Field` at the profile-declared position for `name`, or `undefined` when no custom mapping exists (PROF-07). Unlike `field(n)`, missing names return `undefined`, NOT a synthetic empty Field, so typos surface instead of silently resolving to an empty string (D-14). Two declaration maps feed it, and which one a name may come from depends on the segment: `customSegments` names fields on a Z-segment, `segmentOverrides` names fields on a standard one. Both are read-side ALIASES. A name resolves to whatever `field(n)` returns for its declared position and nothing else moves: the positional accessor, dot-paths, the typed clinical accessors (`msg.patient`, `msg.allergies()`, ...), the warning list and both serializations are unaffected by a declaration. For segments the profile in force declares nothing for (and for every segment when no profile was applied), this method always returns `undefined`. When the declared position is out of range for the underlying `RawSegment.fields`, `get(name)` returns `undefined` (NOT a synthetic-empty Field) so callers can distinguish "name not declared" from "name declared but position missing in the raw message" only at the presence level (both collapse to `undefined` per D-14). **`name` is checked at compile time when the declaration is statically known.** On a segment obtained by type from a message parsed with a statically known profile, `name` is scoped to the names that profile declares FOR THAT SEGMENT TYPE, so a typo is a type error rather than a silent `undefined`. Everywhere the declaration is not statically known (no profile, a value typed as the general `Profile` interface, the process-wide default profile, an undeclared segment type, an empty field map, or the undifferentiated `allSegments()` walk) `name` stays `string`. The return type is unchanged either way: a declared name is not a promise that the wire message carried it. ###### Parameters ###### name `FieldName` ###### Returns [`Field`](#field) \| `undefined` ###### Example ```ts import { defineProfile, parseHL7 } from "@cosyte/hl7"; const profile = defineProfile({ name: "vendor", customSegments: { ZPI: { fields: { encounterId: 3 } } }, }); const msg = parseHL7(raw, profile); console.log(msg.part("ZPI")?.get("encounterId")?.value); // narrowed, no cast // msg.part("ZPI")?.get("encounterld"); // does not compile: not declared const walked = msg.allSegments().find((s) => s.type === "ZPI"); console.log(walked?.get("encounterId")?.value); // a walk takes any name ``` ## Interfaces ### AckErrorDetail One ERR segment's worth of error detail for `buildAck`. Carries **codes and locations only: never echoed PHI values** (the phi-redaction-review gate binds this contract). #### Example ```ts import { buildAck } from "@cosyte/hl7"; const ack = buildAck(inbound, { code: "AE", error: { conditionCode: "101", severity: "E", location: "PID^1^5" }, }); ``` #### Properties ##### conditionCode? > `readonly` `optional` **conditionCode?**: `string` HL7 Table 0357 message error condition code (ERR-3.1). Defaults to `"207"` (Application internal error). The standard display text is looked up from Table 0357 and emitted in ERR-3.2; unknown codes emit with empty text (the code is preserved verbatim: never dropped). ##### location? > `readonly` `optional` **location?**: `string` Error location (ERR-2, an HL7 ERL). A structural path such as `"PID^1^5"` (segment id ^ segment sequence ^ field position). **Must not contain a patient data value**: locations point at *where*, never *what*. ##### severity? > `readonly` `optional` **severity?**: [`ErrSeverity`](#errseverity) HL7 Table 0516 severity (ERR-4). Defaults to `"E"` (Error). *** ### AckErrorEntry One ERR segment interpreted from an ACK. All fields are surfaced verbatim (no table lookup) and OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Codes/locations only: never PHI. #### Properties ##### conditionCode? > `readonly` `optional` **conditionCode?**: `string` ERR-3.1: HL7 Table 0357 condition code. ##### conditionCodeSystem? > `readonly` `optional` **conditionCodeSystem?**: `string` ERR-3.3: condition code system name (e.g. `HL70357`). ##### conditionText? > `readonly` `optional` **conditionText?**: `string` ERR-3.2: condition code display text. ##### location? > `readonly` `optional` **location?**: `string` ERR-2: error location (an HL7 ERL), surfaced verbatim. ##### severity? > `readonly` `optional` **severity?**: `string` ERR-4: HL7 Table 0516 severity (`I`/`W`/`E`). *** ### Acknowledgment Typed view of an inbound ACK. `accepted` / `error` / `rejected` are derived from MSA-1 against HL7 Table 0008 and are **mutually exclusive**; all three are `false` when MSA-1 is absent or not a recognized code (fail-safe). #### Example ```ts import { interpretAck, parseHL7 } from "@cosyte/hl7"; const ack = interpretAck(parseHL7(rawAck)); if (ack.accepted) { // safe to consider the message acknowledged } else if (ack.rejected) { for (const e of ack.errors) console.error(e.conditionCode, e.severity); } ``` #### Properties ##### accepted > `readonly` **accepted**: `boolean` True iff MSA-1 is a positive accept (`AA`/`CA`). ##### code? > `readonly` `optional` **code?**: `string` MSA-1 acknowledgment code (HL7 Table 0008), verbatim. Omitted when absent. ##### controlId? > `readonly` `optional` **controlId?**: `string` MSA-2 message control id (the correlated inbound MSH-10), surfaced as the field's **canonical wire text** (`Field.text`): the whole field, delimiters included, never truncated to the first component. Note this is the *re-escaped* form: a properly-escaped id arrives as its wire bytes (`ID\S\X`), and an HL7 explicit-null MSA-2 surfaces as the literal two-character `""`. Omitted when absent/empty. ##### error > `readonly` **error**: `boolean` True iff MSA-1 is an error acknowledgment (`AE`/`CE`). ##### errors > `readonly` **errors**: readonly [`AckErrorEntry`](#ackerrorentry)[] Every ERR segment in document order ( `[]` when none ). ##### rejected > `readonly` **rejected**: `boolean` True iff MSA-1 is a reject acknowledgment (`AR`/`CR`). *** ### AdtEvent Typed EVN (event type) content for [buildAdt](#buildadt). #### Properties ##### eventOccurred? > `readonly` `optional` **eventOccurred?**: `string` \| [`DtmParts`](#dtmparts) EVN-6 Event Occurred. ##### recordedDateTime? > `readonly` `optional` **recordedDateTime?**: `string` \| [`DtmParts`](#dtmparts) EVN-2 Recorded Date/Time. *** ### AdtPatient Typed PID (patient identification) content for [buildAdt](#buildadt). #### Properties ##### accountNumber? > `readonly` `optional` **accountNumber?**: [`CX`](#cx-1) PID-18 Patient Account Number. ##### address? > `readonly` `optional` **address?**: [`XAD`](#xad-1) \| readonly [`XAD`](#xad-1)[] PID-11 Patient Address: one or more XAD addresses. ##### administrativeSex? > `readonly` `optional` **administrativeSex?**: `string` PID-8 Administrative Sex (e.g. `"F"`, `"M"`, `"U"`). ##### birthDateTime? > `readonly` `optional` **birthDateTime?**: `string` \| [`DtmParts`](#dtmparts) PID-7 Date/Time of Birth. ##### identifiers? > `readonly` `optional` **identifiers?**: [`CX`](#cx-1) \| readonly [`CX`](#cx-1)[] PID-3 Patient Identifier List: one or more CX identifiers (MRN, SSN, …). ##### mothersMaidenName? > `readonly` `optional` **mothersMaidenName?**: [`XPN`](#xpn-1) PID-6 Mother's Maiden Name. ##### name? > `readonly` `optional` **name?**: [`XPN`](#xpn-1) \| readonly [`XPN`](#xpn-1)[] PID-5 Patient Name. ##### phoneHome? > `readonly` `optional` **phoneHome?**: [`XTN`](#xtn-1) \| readonly [`XTN`](#xtn-1)[] PID-13 Phone Number - Home: one or more XTN telecoms. ##### setId? > `readonly` `optional` **setId?**: `string` PID-1 Set ID. *** ### AdtPriorIdentity Typed MRG (merge patient information) content for [buildAdt](#buildadt): the **prior**, non-surviving identity that a merge or move event retires in favour of the `patient` identity. The direction is HL7's and is constant: the identifiers here merge INTO the ones on PID, never the reverse. #### Example ```ts import type { AdtPriorIdentity } from "@cosyte/hl7"; const prior: AdtPriorIdentity = { identifiers: { idNumber: "MRN-OLD", identifierTypeCode: "MR" }, accountNumber: { idNumber: "ACCT-OLD" }, }; ``` #### Properties ##### accountNumber? > `readonly` `optional` **accountNumber?**: [`CX`](#cx-1) MRG-3 Prior Patient Account Number: the merge key of an account merge. ##### identifiers? > `readonly` `optional` **identifiers?**: [`CX`](#cx-1) \| readonly [`CX`](#cx-1)[] MRG-1 Prior Patient Identifier List: one or more CX identifiers. ##### legacyPatientId? > `readonly` `optional` **legacyPatientId?**: [`CX`](#cx-1) MRG-4 Prior Patient ID. Backward compatibility only: withdrawn as of HL7 v2.7 in favour of MRG-1, and the read side stops reading it on a message whose MSH-12 declares v2.7 or later. ##### name? > `readonly` `optional` **name?**: [`XPN`](#xpn-1) MRG-7 Prior Patient Name. ##### visitNumber? > `readonly` `optional` **visitNumber?**: [`CX`](#cx-1) MRG-5 Prior Visit Number: the merge key of a visit merge. *** ### AdtVisit Typed PV1 (patient visit) content for [buildAdt](#buildadt). #### Properties ##### admitDateTime? > `readonly` `optional` **admitDateTime?**: `string` \| [`DtmParts`](#dtmparts) PV1-44 Admit Date/Time. ##### assignedLocation? > `readonly` `optional` **assignedLocation?**: [`PL`](#pl-1) PV1-3 Assigned Patient Location. ##### attendingDoctor? > `readonly` `optional` **attendingDoctor?**: [`XCN`](#xcn-1) \| readonly [`XCN`](#xcn-1)[] PV1-7 Attending Doctor. ##### patientClass? > `readonly` `optional` **patientClass?**: `string` PV1-2 Patient Class (e.g. `"I"` inpatient, `"O"` outpatient, `"E"` emergency). ##### referringDoctor? > `readonly` `optional` **referringDoctor?**: [`XCN`](#xcn-1) \| readonly [`XCN`](#xcn-1)[] PV1-8 Referring Doctor. ##### setId? > `readonly` `optional` **setId?**: `string` PV1-1 Set ID. ##### visitNumber? > `readonly` `optional` **visitNumber?**: [`CX`](#cx-1) PV1-19 Visit Number. *** ### Allergy One allergy entry, read from an AL1 or an IAM segment (see `source`). Both segments fill `type`, `code`, `severity` and `reaction` in the same shapes; `onsetDate` comes from AL1 only, and `actionCode`, `deleteRequested` and `uniqueIdentifier` from IAM only. Every key but `source` is OMITTED when the field it reads is empty: none is defaulted. An entry is a report of what one segment said, never the result of applying it. An IAM entry whose action code is `D` is a request to delete an allergy sent earlier, and it is still returned: it is marked by `deleteRequested`, and nothing is removed, merged or updated on the caller's behalf. #### Example ```ts import type { Allergy } from "@cosyte/hl7"; const al: Allergy = { source: "AL1", type: "DA", code: { identifier: "PEN", text: "Penicillin" }, severity: "SV", reaction: "Hives", }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` IAM-6 allergy action code (Table 0206), component 1 exactly as sent: no case-folding, no mapping, and an unlisted code surfaced as it arrived. OMITTED when IAM-6 is empty, never defaulted to `A`. Absent on AL1 entries. IAM-6 does not repeat: a malformed repeated IAM-6 surfaces every repetition's component 1, joined by the message's repetition separator (`D~A`), rather than its first piece. ###### Example ```ts for (const al of msg.allergies()) { if (al.actionCode === "U") console.log("update to", al.uniqueIdentifier?.entityIdentifier); } ``` ##### code? > `readonly` `optional` **code?**: [`CWE`](#cwe-1) AL1-3 or IAM-3 allergen code. ##### deleteRequested? > `readonly` `optional` **deleteRequested?**: `true` `true` when `actionCode` is exactly `D`: this entry asks the receiver to delete an allergy sent earlier, identified by `uniqueIdentifier`. OMITTED on every other entry, including a lowercase `d`, an unknown or malformed code, an empty IAM-6 and every AL1 entry. The entry itself is still returned, with every field it carries; the delete is never applied here. ###### Example ```ts const current = msg.allergies().filter((al) => al.deleteRequested !== true); ``` ##### onsetDate? > `readonly` `optional` **onsetDate?**: [`DtmParts`](#dtmparts) AL1-6 onset date as the fidelity `TS`. ##### reaction? > `readonly` `optional` **reaction?**: `string` AL1-5 or IAM-5 allergy reaction (first repetition). ##### severity? > `readonly` `optional` **severity?**: `string` AL1-4 or IAM-4 severity, component 1 (SV=severe, MO=moderate, MI=mild). ##### source > `readonly` **source**: [`AllergySource`](#allergysource-1) The segment this entry was read from. Always present. ###### Example ```ts const fromIam = msg.allergies().filter((al) => al.source === "IAM"); ``` ##### type? > `readonly` `optional` **type?**: `string` AL1-2 or IAM-2 allergen type, component 1 (DA=drug, FA=food, EA=environmental, ...). ##### uniqueIdentifier? > `readonly` `optional` **uniqueIdentifier?**: [`AllergyUniqueIdentifier`](#allergyuniqueidentifier-1) IAM-7 allergy unique identifier, components 1 and 2. OMITTED when IAM-7 is empty; never filled from IAM-3. Absent on AL1 entries. ###### Example ```ts msg.allergies()[0]?.uniqueIdentifier?.entityIdentifier; // "ALG-0001" ``` *** ### AllergyUniqueIdentifier IAM-7 allergy unique identifier (EI): the sender's identifier for one allergy, which an update or delete for that allergy repeats. Components 1 and 2 only; each key is OMITTED when its component is empty. It is never filled from IAM-3 or any other field when IAM-7 is absent. #### Example ```ts import type { AllergyUniqueIdentifier } from "@cosyte/hl7"; const id: AllergyUniqueIdentifier = { entityIdentifier: "ALG-0001", namespaceId: "LAB" }; ``` #### Properties ##### entityIdentifier? > `readonly` `optional` **entityIdentifier?**: `string` EI-1 entity identifier, verbatim. ###### Example ```ts msg.allergies()[0]?.uniqueIdentifier?.entityIdentifier; // "ALG-0001" ``` ##### namespaceId? > `readonly` `optional` **namespaceId?**: `string` EI-2 namespace ID of the application that assigned the identifier, verbatim. ###### Example ```ts msg.allergies()[0]?.uniqueIdentifier?.namespaceId; // "LAB" ``` *** ### Appointment SCH-derived appointment entry (SIU scheduling breadth). Surfaces the appointment identifiers, filler status (SCH-25, Table 0278), SCH-11 start/end timing, and the AI* resource groups. NOT a scheduling-workflow state machine. #### Example ```ts import type { Appointment } from "@cosyte/hl7"; const appt: Appointment = { fillerAppointmentId: "A1001", fillerStatusCode: { identifier: "Booked" }, resources: [], }; ``` #### Properties ##### endDateTime? > `readonly` `optional` **endDateTime?**: [`DtmParts`](#dtmparts) Appointment end date/time: SCH-11 TQ.5 (fidelity `TS`). ##### fillerAppointmentId? > `readonly` `optional` **fillerAppointmentId?**: `string` SCH-2 filler appointment ID (EI first component, verbatim). ##### fillerStatusCode? > `readonly` `optional` **fillerStatusCode?**: [`CWE`](#cwe-1) SCH-25 filler status code (HL7 Table 0278): the appointment status, verbatim/provenance-only. ##### placerAppointmentId? > `readonly` `optional` **placerAppointmentId?**: `string` SCH-1 placer appointment ID (EI first component, verbatim). ##### resources > `readonly` **resources**: readonly [`AppointmentResource`](#appointmentresource)[] AIS/AIG/AIL/AIP resources grouped under this SCH. Always present (possibly empty). ##### startDateTime? > `readonly` `optional` **startDateTime?**: [`DtmParts`](#dtmparts) Appointment start date/time: SCH-11 TQ.4 (fidelity `TS`). *** ### AppointmentResource One appointment resource grouped under a `SCH`: an AIS (service), AIG (general resource), AIL (location), or AIP (personnel / provider) segment. The resource identifier lives at position 3 of every AI* segment; for the personnel resource (AIP) it is additionally surfaced as a typed `person` (XCN), while the coded `code` (first component verbatim) is always available. #### Example ```ts import type { AppointmentResource } from "@cosyte/hl7"; const r: AppointmentResource = { kind: "location", code: { identifier: "OR-1" } }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CWE`](#cwe-1) The AI*-3 resource identifier surfaced as a coded element: `code.identifier` is the resource id (first component, verbatim). AIS-3 / AIG-3 are coded elements, so `code.text` / `code.nameOfCodingSystem` are meaningful there; AIL-3 is a **PL** (location) rather than a coded element, so only `code.identifier` (the location id, PL.1) is meaningful and the other CWE fields are positional provenance, not a coding system. Provenance-only. ##### kind > `readonly` **kind**: `"service"` \| `"general"` \| `"location"` \| `"personnel"` Which AI* segment sourced this resource: AIS→service, AIG→general, AIL→location, AIP→personnel. ##### person? > `readonly` `optional` **person?**: [`XCN`](#xcn-1) AIP-3 personnel resource as a typed `XCN` (personnel resources only): the appointment provider. *** ### Batch One batch within a stream: a run of messages delimited by a `BHS` header and/or a `BTS` trailer (both optional in §2.10.3, so a `BTS` closes a preceding run into a batch even with no `BHS`). A run of messages with **neither** a header nor a trailer is *not* a batch: those messages live in [BatchSplitResult.messages](#messages-1) only, so they never inflate a batch count. `declaredMessageCount` is BTS-1 when the trailer declared a usable non-negative integer (it is **optional [0..1]** in the spec, so it may be absent); `actualMessageCount` is always the real count. #### Example ```ts import { splitBatch } from "@cosyte/hl7"; const [batch] = splitBatch(raw).batches; batch?.header?.name; // "BHS" (or undefined for a headerless BTS-closed run) batch?.actualMessageCount; // messages actually in the batch ``` #### Type Parameters ##### M `M` *extends* [`Hl7Message`](#hl7message) = [`Hl7Message`](#hl7message) the message type an `ok` entry carries. #### Properties ##### actualMessageCount > `readonly` **actualMessageCount**: `number` The actual number of messages split out of this batch. ##### declaredMessageCount? > `readonly` `optional` **declaredMessageCount?**: `number` BTS-1 batch message count, when declared as a non-negative integer. ##### header? > `readonly` `optional` **header?**: [`BatchEnvelopeSegment`](#batchenvelopesegment) The `BHS` header, when this batch was opened by one. ##### messages > `readonly` **messages**: readonly [`BatchMessageEntry`](#batchmessageentry)\<`M`\>[] The messages in this batch, in stream order. ##### trailer? > `readonly` `optional` **trailer?**: [`BatchEnvelopeSegment`](#batchenvelopesegment) The `BTS` trailer, when this batch was closed by one. *** ### BatchEnvelopeSegment A raw batch-envelope segment (`FHS`/`BHS`/`BTS`/`FTS`) surfaced by [splitBatch](#splitbatch). `fields` is the segment split on its own field separator with `fields[0]` holding the segment name: deliberately the raw token array, not a typed model: typed FHS/BHS field helpers beyond the raw fields are not part of this surface. Note the HL7 MSH-family indexing quirk: for `FHS`/`BHS`, `fields[1]` is the encoding-characters field (FHS-2/BHS-2); for `BTS`/`FTS`, `fields[1]` is field 1 (BTS-1 batch message count / FTS-1 file batch count). #### Example ```ts import { splitBatch } from "@cosyte/hl7"; const { fileHeader } = splitBatch("FHS|^~\\&|SENDER\r..."); fileHeader?.name; // "FHS" fileHeader?.fields[2]; // "SENDER" (FHS-3, the File Sending Application) ``` #### Properties ##### fields > `readonly` **fields**: readonly `string`[] Raw field tokens; `fields[0]` is the segment name. ##### name > `readonly` **name**: [`BatchEnvelopeName`](#batchenvelopename) ##### position > `readonly` **position**: [`Hl7Position`](#hl7position) Position of this envelope segment in the split stream. ##### raw > `readonly` **raw**: `string` The verbatim segment string (line-ending normalized). *** ### BatchSplitResult The result of [splitBatch](#splitbatch): the flattened `messages` (every message across every batch, in stream order: the primary surface), the nested `batches`, the raw file envelope segments, and the batch-level `warnings` (count-mismatch / missing-trailer). `hadEnvelope` is `false` for a bare passthrough (no FHS/BHS/BTS/FTS seen). #### Example ```ts import { splitBatch } from "@cosyte/hl7"; const result = splitBatch(rawBatchFile); result.messages.length; // every message, batched or not result.batches.length; // === result.actualBatchCount result.warnings; // BATCH_COUNT_MISMATCH / BATCH_MISSING_TRAILER (counts only) ``` #### Type Parameters ##### M `M` *extends* [`Hl7Message`](#hl7message) = [`Hl7Message`](#hl7message) the message type an `ok` entry carries. #### Properties ##### actualBatchCount > `readonly` **actualBatchCount**: `number` The number of explicit batches split out of the stream. ##### batches > `readonly` **batches**: readonly [`Batch`](#batch)\<`M`\>[] The explicit (`BHS`-delimited) batches, in stream order. ##### declaredBatchCount? > `readonly` `optional` **declaredBatchCount?**: `number` The last `FTS-1` file batch count, when declared as a non-negative integer. ##### fileHeader? > `readonly` `optional` **fileHeader?**: [`BatchEnvelopeSegment`](#batchenvelopesegment) The **first** `FHS` file header, when present. ##### fileTrailer? > `readonly` `optional` **fileTrailer?**: [`BatchEnvelopeSegment`](#batchenvelopesegment) The **last** `FTS` file trailer, when present. ##### hadEnvelope > `readonly` **hadEnvelope**: `boolean` `false` when no envelope segment was seen (bare passthrough). ##### messages > `readonly` **messages**: readonly [`BatchMessageEntry`](#batchmessageentry)\<`M`\>[] Every message split out of the stream, in order: the primary surface. This includes messages that belong to no explicit batch (a bare stream, or content outside any `BHS`/`BTS`), so it is a superset of the messages reachable via `batches`. ##### warnings > `readonly` **warnings**: readonly [`Hl7ParseWarning`](#hl7parsewarning)[] Batch-level warnings (count mismatch, missing trailer); counts/positions only, never PHI. *** ### BuildAckOptions Options for [buildAck](#buildack). #### Example ```ts buildAck(inbound, { code: "AA" }); // bare accept buildAck(inbound, { code: "AR", error: { conditionCode: "200" } }); ``` #### Properties ##### code > `readonly` **code**: [`AckCode`](#ackcode) The acknowledgment disposition to emit in MSA-1 (HL7 Table 0008). One of `AA`/`AE`/`AR` (original) or `CA`/`CE`/`CR` (enhanced accept-level). Required: `buildAck` builds the disposition it is told. An unknown code is a programming error and throws `TypeError`. ##### error? > `readonly` `optional` **error?**: [`AckErrorDetail`](#ackerrordetail) \| readonly [`AckErrorDetail`](#ackerrordetail)[] Optional error detail. A single [AckErrorDetail](#ackerrordetail) or an array → one ERR segment each. Typically supplied for `AE`/`AR`/`CE`/`CR`. ##### mode? > `readonly` `optional` **mode?**: [`AckMode`](#ackmode) Optional explicit acknowledgment mode. When omitted it is derived from the inbound MSH-15/16 via [detectAckMode](#detectackmode). `buildAck` emits `code` verbatim regardless of mode: this field is advisory metadata for adapters (e.g. `@cosyte/mllp`'s commit-policy layer) that need the detected mode. *** ### BuildAdtInit Input for [buildAdt](#buildadt): the MSH envelope plus the typed segment bodies. #### Extends - `MessageEnvelope` #### Properties ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted. ###### Inherited from `MessageEnvelope.controlId` ##### event? > `readonly` `optional` **event?**: [`AdtEvent`](#adtevent) EVN content (EVN-1 is the trigger event; EVN-2/6 optional). ##### patient > `readonly` **patient**: [`AdtPatient`](#adtpatient) PID content. **Required**: never fabricated. ##### priorIdentity? > `readonly` `optional` **priorIdentity?**: [`AdtPriorIdentity`](#adtprioridentity) MRG content: the prior identity a merge, move or identifier-change event retires. Optional, and emitted only when supplied. **Required for a trigger event whose published structure requires MRG**, where an init without it is a typed error rather than a message missing the identity it retires. ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ###### Inherited from `MessageEnvelope.processingId` ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ###### Inherited from `MessageEnvelope.receivingApp` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ###### Inherited from `MessageEnvelope.receivingFacility` ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ###### Inherited from `MessageEnvelope.sendingApp` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ###### Inherited from `MessageEnvelope.sendingFacility` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` → HL7 `YYYYMMDDHHmmss` (UTC, seconds); a pre-formatted HL7 TS string passes through verbatim. Defaults to `new Date()`. ###### Inherited from `MessageEnvelope.timestamp` ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. ###### Inherited from `MessageEnvelope.version` ##### visit? > `readonly` `optional` **visit?**: [`AdtVisit`](#adtvisit) PV1 content. Optional; an (empty) PV1 is emitted regardless so the visit group is present. *** ### BuildDftInit Input for [buildDft](#builddft): the MSH envelope plus the typed segment bodies. #### Example ```ts import type { BuildDftInit } from "@cosyte/hl7"; const init: BuildDftInit = { sendingApp: "CLINIC", receivingApp: "BILLING", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }, charges: [{ transactionType: "CG", amountExtended: { price: "150.00", denomination: "USD" } }], }; ``` #### Extends - `MessageEnvelope` #### Properties ##### charges > `readonly` **charges**: readonly [`DftCharge`](#dftcharge)[] FT1 content. **Required, non-empty**: a DFT with no transaction is a typed error. ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted. ###### Inherited from `MessageEnvelope.controlId` ##### event? > `readonly` `optional` **event?**: [`AdtEvent`](#adtevent) EVN content (EVN-1 is the trigger event; EVN-2/6 optional). ##### patient > `readonly` **patient**: [`AdtPatient`](#adtpatient) PID content. **Required**: never fabricated. ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ###### Inherited from `MessageEnvelope.processingId` ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ###### Inherited from `MessageEnvelope.receivingApp` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ###### Inherited from `MessageEnvelope.receivingFacility` ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ###### Inherited from `MessageEnvelope.sendingApp` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ###### Inherited from `MessageEnvelope.sendingFacility` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` → HL7 `YYYYMMDDHHmmss` (UTC, seconds); a pre-formatted HL7 TS string passes through verbatim. Defaults to `new Date()`. ###### Inherited from `MessageEnvelope.timestamp` ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. ###### Inherited from `MessageEnvelope.version` ##### visit? > `readonly` `optional` **visit?**: [`AdtVisit`](#adtvisit) PV1 content. Optional; a PV1 is emitted only when supplied. *** ### BuildMdmInit Input for [buildMdm](#buildmdm): the MSH envelope plus the typed segment bodies. #### Example ```ts import type { BuildMdmInit } from "@cosyte/hl7"; const init: BuildMdmInit = { sendingApp: "TRANSCRIPTION", receivingApp: "EHR", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }, document: { documentType: "DS", body: [{ valueType: "TX", value: "Report text." }] }, }; ``` #### Extends - `MessageEnvelope` #### Properties ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted. ###### Inherited from `MessageEnvelope.controlId` ##### document > `readonly` **document**: [`MdmDocument`](#mdmdocument) TXA content. **Required**: a document notification with no document is a typed error. ##### event? > `readonly` `optional` **event?**: [`AdtEvent`](#adtevent) EVN content (EVN-1 is the trigger event; EVN-2/6 optional). ##### patient > `readonly` **patient**: [`AdtPatient`](#adtpatient) PID content. **Required**: never fabricated. ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ###### Inherited from `MessageEnvelope.processingId` ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ###### Inherited from `MessageEnvelope.receivingApp` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ###### Inherited from `MessageEnvelope.receivingFacility` ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ###### Inherited from `MessageEnvelope.sendingApp` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ###### Inherited from `MessageEnvelope.sendingFacility` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` → HL7 `YYYYMMDDHHmmss` (UTC, seconds); a pre-formatted HL7 TS string passes through verbatim. Defaults to `new Date()`. ###### Inherited from `MessageEnvelope.timestamp` ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. ###### Inherited from `MessageEnvelope.version` ##### visit? > `readonly` `optional` **visit?**: [`AdtVisit`](#adtvisit) PV1 content. Optional; an (empty) PV1 is emitted regardless so the visit group is present. *** ### BuildMessageInit Input shape for `buildMessage` (SER-06). Mirrors `msg.meta` 1-for-1 so read and write surfaces share field names (`sendingApp`, `sendingFacility`, `receivingApp`, `receivingFacility`, `controlId`, `timestamp`, `version`, `processingId`). `type` is the only required field. **Empty string vs. undefined semantics:** omitting a field and passing an empty string produce IDENTICAL wire output (both emit as an absent positional field). To emit an HL7 explicit null (`""`) at a specific position in an outbound message, build the message first and then call `.setField(path, '""')`: the mutation method sets `isNull=true` on the underlying `RawField`, and the emitter preserves that as the literal two-char output per D-02. #### Example ```ts import { buildMessage } from "@cosyte/hl7"; const msg = buildMessage({ type: "ADT^A01", sendingApp: "CLINIC", sendingFacility: "MAIN", receivingApp: "LAB", receivingFacility: "REF", timestamp: new Date("2026-04-19T10:15:00Z"), }) .addSegment("PID", ["", "", "MRN123", "", "Doe^John"]); console.log(msg.toString()); // To emit HL7 explicit null ("") instead of absent: // msg.setField("PID.2", '""'); // distinct from empty/omitted ``` #### Properties ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted (D-12). ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` formatted to HL7 `YYYYMMDDHHmmss` (UTC, seconds) when supplied; pre-formatted HL7 TS string passed through verbatim (D-13). Defaults to `new Date()`. ##### type > `readonly` **type**: `string` HL7 message type, e.g. `"ADT^A01"` (code + trigger) or `"ORU^R01^ORU_R01"` (code + trigger + structure). Required (D-16). The string is split on `^` into MSH-9 components; each component is emitted verbatim. Literal `^` characters in a component are NOT representable via this field: splitting is unconditional. Callers needing that edge case should build the message and then use `.setField("MSH.9.1", ...)` etc. after construction. Rejected at runtime (D-16 / WR-04): - empty string `""` or whitespace-only `" "`; - strings whose every `^`-split component is empty/whitespace (e.g. `"^"`, `"^^"`, `" ^ "`). ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. *** ### BuildOrmInit Input for [buildOrm](#buildorm): the MSH envelope plus the typed segment bodies. #### Example ```ts import type { BuildOrmInit } from "@cosyte/hl7"; const init: BuildOrmInit = { sendingApp: "EHR", receivingApp: "LAB", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }, orders: [{ orderControl: "NW", universalServiceId: { identifier: "CBC" } }], }; ``` #### Extends - `MessageEnvelope` #### Properties ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted. ###### Inherited from `MessageEnvelope.controlId` ##### orders > `readonly` **orders**: readonly [`OrmOrder`](#ormorder)[] ORC/OBR content. **Required, non-empty**: an ORM with no order is a typed error. ##### patient > `readonly` **patient**: [`AdtPatient`](#adtpatient) PID content. **Required**: never fabricated. ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ###### Inherited from `MessageEnvelope.processingId` ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ###### Inherited from `MessageEnvelope.receivingApp` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ###### Inherited from `MessageEnvelope.receivingFacility` ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ###### Inherited from `MessageEnvelope.sendingApp` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ###### Inherited from `MessageEnvelope.sendingFacility` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` → HL7 `YYYYMMDDHHmmss` (UTC, seconds); a pre-formatted HL7 TS string passes through verbatim. Defaults to `new Date()`. ###### Inherited from `MessageEnvelope.timestamp` ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. ###### Inherited from `MessageEnvelope.version` *** ### BuildOruInit Input for [buildOru](#buildoru): the MSH envelope plus the typed segment bodies. #### Extends - `MessageEnvelope` #### Properties ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted. ###### Inherited from `MessageEnvelope.controlId` ##### observations > `readonly` **observations**: readonly [`OruObservation`](#oruobservation)[] OBX content. **Required, non-empty**: an ORU with no result is a typed error. ##### order? > `readonly` `optional` **order?**: [`OruOrder`](#oruorder) OBR content. Optional; an (empty) OBR is emitted regardless so the result group is well-formed. ##### patient > `readonly` **patient**: [`AdtPatient`](#adtpatient) PID content. **Required**: never fabricated. ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ###### Inherited from `MessageEnvelope.processingId` ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ###### Inherited from `MessageEnvelope.receivingApp` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ###### Inherited from `MessageEnvelope.receivingFacility` ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ###### Inherited from `MessageEnvelope.sendingApp` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ###### Inherited from `MessageEnvelope.sendingFacility` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` → HL7 `YYYYMMDDHHmmss` (UTC, seconds); a pre-formatted HL7 TS string passes through verbatim. Defaults to `new Date()`. ###### Inherited from `MessageEnvelope.timestamp` ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. ###### Inherited from `MessageEnvelope.version` *** ### BuildSiuInit Input for [buildSiu](#buildsiu): the MSH envelope plus the typed segment bodies. #### Example ```ts import type { BuildSiuInit } from "@cosyte/hl7"; const init: BuildSiuInit = { sendingApp: "SCHEDULING", receivingApp: "EHR", appointment: { fillerAppointmentId: "FL-2002", startDateTime: "20260801090000" }, resourceGroups: [{ setId: "1", resources: [{ kind: "location", code: { identifier: "OR-1" } }] }], }; ``` #### Extends - `MessageEnvelope` #### Properties ##### appointment > `readonly` **appointment**: [`SiuAppointment`](#siuappointment) SCH content. **Required**: never fabricated. ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted. ###### Inherited from `MessageEnvelope.controlId` ##### patient? > `readonly` `optional` **patient?**: [`AdtPatient`](#adtpatient) PID content. Optional: the patient group of the published SIU structure is optional, so a PID is emitted only when supplied. ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ###### Inherited from `MessageEnvelope.processingId` ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ###### Inherited from `MessageEnvelope.receivingApp` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ###### Inherited from `MessageEnvelope.receivingFacility` ##### resourceGroups > `readonly` **resourceGroups**: readonly [`SiuResourceGroup`](#siuresourcegroup)[] RGS content. **Required, non-empty**: the published structure needs a resource group. ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ###### Inherited from `MessageEnvelope.sendingApp` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ###### Inherited from `MessageEnvelope.sendingFacility` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` → HL7 `YYYYMMDDHHmmss` (UTC, seconds); a pre-formatted HL7 TS string passes through verbatim. Defaults to `new Date()`. ###### Inherited from `MessageEnvelope.timestamp` ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. ###### Inherited from `MessageEnvelope.version` ##### visit? > `readonly` `optional` **visit?**: [`AdtVisit`](#adtvisit) PV1 content. Optional; emitted only when supplied, and only alongside a patient. *** ### BuildVxuInit Input for [buildVxu](#buildvxu): the MSH envelope plus the typed segment bodies. #### Example ```ts import type { BuildVxuInit } from "@cosyte/hl7"; const init: BuildVxuInit = { sendingApp: "CLINIC", receivingApp: "IIS", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }, immunizations: [{ vaccineCode: { identifier: "115", nameOfCodingSystem: "CVX" } }], }; ``` #### Extends - `MessageEnvelope` #### Properties ##### controlId? > `readonly` `optional` **controlId?**: `string` Auto-generated via `generateControlId()` when omitted. ###### Inherited from `MessageEnvelope.controlId` ##### immunizations > `readonly` **immunizations**: readonly [`VxuImmunization`](#vxuimmunization)[] RXA content. **Required, non-empty**: a vaccination update with no dose is a typed error. ##### patient > `readonly` **patient**: [`AdtPatient`](#adtpatient) PID content. **Required**: never fabricated. ##### processingId? > `readonly` `optional` **processingId?**: `string` Defaults to `"P"` (production). ###### Inherited from `MessageEnvelope.processingId` ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` ###### Inherited from `MessageEnvelope.receivingApp` ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` ###### Inherited from `MessageEnvelope.receivingFacility` ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` ###### Inherited from `MessageEnvelope.sendingApp` ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` ###### Inherited from `MessageEnvelope.sendingFacility` ##### timestamp? > `readonly` `optional` **timestamp?**: `string` \| `Date` `Date` → HL7 `YYYYMMDDHHmmss` (UTC, seconds); a pre-formatted HL7 TS string passes through verbatim. Defaults to `new Date()`. ###### Inherited from `MessageEnvelope.timestamp` ##### version? > `readonly` `optional` **version?**: `string` Defaults to `"2.5"`. ###### Inherited from `MessageEnvelope.version` *** ### Cardinality A repetition-count constraint. `min` / `max` are inclusive bounds on the number of repetitions (for a field rule) or occurrences (for a segment rule). `max` may be the literal `"*"` for "unbounded". Omitted bounds are unconstrained on that side. **Cardinality `min` is checked only when the element is present.** An absent **Required** element is reported as [FINDING\_CODES.PROFILE\_REQUIRED\_ABSENT](#profile_required_absent) (a usage finding), not a cardinality finding: so a missing `R` field with `cardinality.min = 1` yields exactly one finding, never two. #### Example ```ts import type { Cardinality } from "@cosyte/hl7"; const once: Cardinality = { min: 1, max: 1 }; const many: Cardinality = { min: 1, max: "*" }; ``` #### Properties ##### max? > `readonly` `optional` **max?**: `number` \| `"*"` ##### min? > `readonly` `optional` **min?**: `number` *** ### CE HL7 v2 Coded Element (CE): coded element per HL7 Chapter 2. All 6 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. identifier (e.g. "GLU") 2. text (human-readable, e.g. "Glucose") 3. nameOfCodingSystem (e.g. "LN" for LOINC) 4. alternateIdentifier 5. alternateText 6. nameOfAlternateCodingSystem Components 7+ (present when a CWE-shaped value is read through the CE accessor: e.g. version ids, originalText) are surfaced verbatim on `extraComponents` rather than dropped. #### Example ```ts import type { CE } from "@cosyte/hl7"; const code: CE = { identifier: "GLU", text: "Glucose", nameOfCodingSystem: "LN" }; ``` #### Properties ##### alternateIdentifier? > `readonly` `optional` **alternateIdentifier?**: `string` ##### alternateText? > `readonly` `optional` **alternateText?**: `string` ##### extraComponents? > `readonly` `optional` **extraComponents?**: readonly `string`[] Components beyond the modeled 6 (HL7 component 7 onward), preserved verbatim and in order. Non-empty only when a CWE-shaped value is read through the CE accessor; OMITTED otherwise. An absent interior component is preserved as `""` so `extraComponents[i]` maps to HL7 component `7 + i`. ##### identifier? > `readonly` `optional` **identifier?**: `string` ##### nameOfAlternateCodingSystem? > `readonly` `optional` **nameOfAlternateCodingSystem?**: `string` ##### nameOfCodingSystem? > `readonly` `optional` **nameOfCodingSystem?**: `string` ##### text? > `readonly` `optional` **text?**: `string` *** ### Charge FT1-derived charge entry (DFT financial breadth). Billing-critical fields surfaced with **no billing logic and no money-as-float**: the extended/unit amounts are the verbatim CP wire text, never parsed to a number. #### Example ```ts import type { Charge } from "@cosyte/hl7"; const c: Charge = { transactionType: "CG", transactionCode: { identifier: "80053", text: "Metabolic panel" }, amountExtended: "150.00^USD", diagnoses: [{ identifier: "E11.9" }], }; ``` #### Properties ##### amountExtended? > `readonly` `optional` **amountExtended?**: `string` FT1-11 transaction amount, extended (CP): canonical wire text (e.g. `150.00^USD`, byte-exact for a plain amount); never parsed to a number. ##### amountUnit? > `readonly` `optional` **amountUnit?**: `string` FT1-12 transaction amount, unit (CP): canonical wire text; never parsed to a number. ##### diagnoses > `readonly` **diagnoses**: readonly [`CWE`](#cwe-1)[] FT1-19 diagnosis code(s) linked to this charge (CE, repeating): billing diagnosis linkage. Always present (possibly empty). ##### quantity? > `readonly` `optional` **quantity?**: `number` FT1-10 transaction quantity (NM; strict-parsed, never `NaN`). ##### transactionCode? > `readonly` `optional` **transactionCode?**: [`CWE`](#cwe-1) FT1-7 transaction code: the institution charge/procedure code (CWE, provenance-only, never validated). ##### transactionDate? > `readonly` `optional` **transactionDate?**: [`DtmParts`](#dtmparts) FT1-4 transaction date (fidelity `TS`). ##### transactionType? > `readonly` `optional` **transactionType?**: `string` FT1-6 transaction type (HL7 Table 0017: `CG` charge, `CD` credit, `PY` payment, `AJ` adjustment). Verbatim. *** ### CharsetResolution The outcome of resolving an MSH-18 label (or `options.charset` override) against Table 0211. #### Properties ##### canonical > `readonly` **canonical**: `string` The canonical Table-0211 code the label resolved to (e.g. `"UTF-8"`, `"8859/1"`, `"ISO IR87"`). For an unrecognized label this is the input trimmed + upper-cased, so it is still a stable key for comparison. ##### decoder > `readonly` **decoder**: `string` The WHATWG `TextDecoder` label to decode with when `treatment === "decode"`. Empty when `treatment === "verbatim"`. ##### recognized > `readonly` **recognized**: `boolean` `true` when the label is a recognized HL7 Table-0211 code (whether or not the parser decodes it); `false` when the label is not in Table 0211. Drives the warning code on the verbatim path: recognized-but-`verbatim` → `UNSUPPORTED_CHARSET`, unrecognized → `UNKNOWN_CHARSET`. ##### treatment > `readonly` **treatment**: [`CharsetTreatment`](#charsettreatment) Whether the parser decodes this set or preserves its bytes verbatim. *** ### ClinicalDocument TXA-derived clinical-document entry (MDM document breadth). The load-bearing safety property: **completion status (TXA-17) and availability status (TXA-19) are DISTINCT fields and are never conflated**: a document can be *available* before it is *authenticated*, and reading a preliminary document as final is the clinical harm. Both are verbatim / provenance-only. #### Example ```ts import type { ClinicalDocument } from "@cosyte/hl7"; const doc: ClinicalDocument = { documentType: "DS", completionStatus: "IP", // in progress: NOT yet authenticated availabilityStatus: "AV", // available: a different axis observations: [], }; ``` #### Properties ##### activityDateTime? > `readonly` `optional` **activityDateTime?**: [`DtmParts`](#dtmparts) TXA-4 activity date/time (fidelity `TS`). ##### availabilityStatus? > `readonly` `optional` **availabilityStatus?**: `string` TXA-19 document **availability** status (HL7 Table 0273: `AV` available, `CA` cancelled, `OB` obsolete, `UN` unavailable). Surfaced DISTINCT from [completionStatus](#completionstatus); verbatim, never validated, never merged. ##### completionStatus? > `readonly` `optional` **completionStatus?**: `string` TXA-17 document **completion** status (HL7 Table 0271: e.g. `DO` documented, `IP` in progress, `AU` authenticated, `LA` legally authenticated, `IN` incomplete). Surfaced DISTINCT from [availabilityStatus](#availabilitystatus); verbatim, never validated, never merged. ##### documentType? > `readonly` `optional` **documentType?**: `string` TXA-2 document type (HL7 Table 0270), verbatim. ##### observations > `readonly` **observations**: readonly [`Observation`](#observation)[] OBX narrative body grouped under this TXA. Always present (possibly empty). ##### parentDocumentNumber? > `readonly` `optional` **parentDocumentNumber?**: `string` TXA-13 parent document number (EI first component): addendum / replacement link. ##### uniqueDocumentNumber? > `readonly` `optional` **uniqueDocumentNumber?**: `string` TXA-12 unique document number (EI first component, verbatim). *** ### CodedSystemFields Structural shape of any coded element that carries a primary + alternate coding system: both [CWE](#cwe-1) and [CE](#ce) satisfy it. Kept structural (not a union) so callers can pass a `dg.code` / `obs.code` directly. #### Properties ##### nameOfAlternateCodingSystem? > `readonly` `optional` **nameOfAlternateCodingSystem?**: `string` ##### nameOfCodingSystem? > `readonly` `optional` **nameOfCodingSystem?**: `string` *** ### CodingSystemInfo A coding-system provenance answer: the system a code CLAIMS, never validated. `claimed` is always present and verbatim (never dropped); the resolved `id` / `name` are present only when the claim maps to a registered Table 0396 entry. #### Properties ##### claimed > `readonly` **claimed**: `string` The coding-system id exactly as it appeared in CWE.3 / CE.3: preserved verbatim (original case and spelling), never altered, never dropped. ##### id? > `readonly` `optional` **id?**: `string` Registered Table 0396 acronym (alias-normalized). Present only when `known`. ##### known > `readonly` **known**: `boolean` `true` when `claimed` resolved (directly or via alias) to a registered Table 0396 entry. ##### name? > `readonly` `optional` **name?**: `string` Canonical human-readable name. Present only when `known`. *** ### ComparisonPredicate A comparison statement: does the content at `location` stand in the `verb` relation to one of the `values`? The value read at a location is the subcomponent at the coordinate, with an omitted `component` and `subcomponent` both defaulting to `1` (a coded element's code), exactly as a field rule's own `component` default does. `values` must carry at least one entry: a comparison with no value list is `PROFILE_MALFORMED`, and is a compile error where the author writes TypeScript. **A comparison against an element the message does not carry is unevaluatable**, not false: there is no content to compare. It is reported as [FINDING\_CODES.PROFILE\_CONDITION\_UNEVALUATABLE](#profile_condition_unevaluatable) rather than silently deciding the conditional. **The statement is quantified over repetitions and occurrences.** Where the location names a repeating field, or a segment that occurs more than once, the comparison reads one value per repetition per occurrence and is true when ONE OR MORE of them stands in the verb's relation to the value list. That is the language's default occurrence semantics, and it covers the negative verbs too: `is not` is true when SOME value is not a listed one, not when every value is not. So at a repeating location a verb and its negative can both decide true against the same value list. [PredicateVerb](#predicateverb) works the case through. #### Example ```ts import type { ComparisonPredicate } from "@cosyte/hl7"; // "IF RXA-20 (Completion Status) contains one of the values in the list: {'CP', 'PA'}" const predicate: ComparisonPredicate = { location: { segment: "RXA", field: 20 }, verb: "contains", values: ["CP", "PA"], }; ``` #### Properties ##### location > `readonly` **location**: [`PredicateLocation`](#predicatelocation) The element whose content the statement compares. ##### values > `readonly` **values**: readonly `string`[] The value list, at least one entry. For `matches` / `does not match` each entry is a regular-expression source; anything the platform cannot compile is `PROFILE_MALFORMED`. ##### verb > `readonly` **verb**: [`PredicateVerb`](#predicateverb) The relation to test. *** ### ComponentRule A rule for one **component position** inside a field, declared on a field rule's [FieldRule.components](#components). **A component's cardinality is IMPLIED by its usage and may not be declared.** The HL7 conformance methodology (section 5.2, Cardinality) states it outright: an explicit cardinality range is required for segment groups, segments and field elements, while component and sub-component elements do not explicitly include one, and the range implicitly associated with each depends on its usage code: `[1..1]` for `R`, `[0..1]` for `RE` and `O`, `[0..0]` for `X`. A component carries no repetition construct in HL7 v2, so `[1..1]` reduces to "present with a value exactly once" and `[0..0]` to "absent or empty": the observable is a presence finding, never a count finding. Declaring a `cardinality` here is [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed), not a silently-honoured extra constraint. **Declaring component rules CLOSES the field's component set.** A field rule that declares them says what that field's components are, so a present repetition carrying a non-empty value at an index the rule does not declare is [FINDING\_CODES.PROFILE\_UNDECLARED\_CONTENT](#profile_undeclared_content). A field rule that declares NONE declares no depth and is checked exactly as it always was: the undeclared-content check can never fire for it. "None" means no component rule reaches the engine, so an EMPTY list says exactly what omitting the member says; it is never read as a declaration that the field has no components. #### Example ```ts import type { ComponentRule } from "@cosyte/hl7"; const identifier: ComponentRule = { component: 1, usage: "R" }; const checkDigit: ComponentRule = { component: 2, usage: "X" }; ``` #### Properties ##### component > `readonly` **component**: `number` 1-indexed component position within the field (e.g. `4` for `PID-3.4`). ##### name? > `readonly` `optional` **name?**: `string` Optional human label for the component (e.g. `"Assigning Authority"`). Structural documentation for the profile author only: findings identify a component by its PHI-free structural locus (segment + field + index), never by this label, so the label is never echoed into a finding message. ##### severity? > `readonly` `optional` **severity?**: [`FindingSeverity`](#findingseverity) Severity for findings this component rule produces. Defaults to the severity of the field rule that carries it (itself `"error"` by default), so downgrading a field rule downgrades its components with it. ##### usage? > `readonly` `optional` **usage?**: [`UsageCode`](#usagecode) Usage constraint for this component, from the same vocabulary a field rule declares (see [UsageCode](#usagecode)). Its cardinality follows from it and is never declared beside it. - **`R`**: a present repetition SHALL carry a non-empty value here. Absent ⇒ [FINDING\_CODES.PROFILE\_REQUIRED\_ABSENT](#profile_required_absent). - **`X`**: a present repetition SHALL NOT carry a value here. Present ⇒ [FINDING\_CODES.PROFILE\_NOT\_PERMITTED](#profile_not_permitted). - **`RE`** / **`O`**: no presence constraint. - **`C`** / **`CE`** / **`B`** / a declared conditional: presence is **not evaluated**, on exactly the terms an undecided conditional field rule gets. A component rule declares no condition predicate and no caller resolution can name one, so nothing ever decides it. Omitted ⇒ Optional, and an omitted usage is never refused. The component still counts as DECLARED for the undeclared-content check: declaring the index is what closes the set, not constraining it. *** ### CompositeValueByKind Maps each [CompositeKind](#compositekind) to the typed value [encodeComposite](#encodecomposite) (and `setComposite`) accept for it. `TS` also accepts a pre-formatted HL7 timestamp string; `NM` also accepts a `number` or a raw numeric string, both are emitted verbatim (the serializer never re-formats a claimed value). #### Properties ##### CE > `readonly` **CE**: [`CE`](#ce) ##### CWE > `readonly` **CWE**: [`CWE`](#cwe-1) ##### CX > `readonly` **CX**: [`CX`](#cx-1) ##### HD > `readonly` **HD**: [`HD`](#hd-1) ##### NM > `readonly` **NM**: `string` \| `number` \| [`NM`](#nm-1) ##### PL > `readonly` **PL**: [`PL`](#pl-1) ##### TS > `readonly` **TS**: `string` \| [`DtmParts`](#dtmparts) ##### XAD > `readonly` **XAD**: [`XAD`](#xad-1) ##### XCN > `readonly` **XCN**: [`XCN`](#xcn-1) ##### XPN > `readonly` **XPN**: [`XPN`](#xpn-1) ##### XTN > `readonly` **XTN**: [`XTN`](#xtn-1) *** ### ConformanceFinding One typed conformance finding. Carries the [FindingCode](#findingcode), a [FindingSeverity](#findingseverity), the structural [FindingLocus](#findinglocus), and a human-readable `message` describing the rule that fired. **The `message` is PHI-safe by construction**: it names the locus, the rule, and (for a value-set miss) the SIZE of the value set, but **never** the offending field value. #### Example ```ts import type { ConformanceFinding } from "@cosyte/hl7"; const f: ConformanceFinding = { code: "PROFILE_VALUE_NOT_IN_SET", severity: "error", locus: { segment: "PID", field: 8, component: 1 }, message: 'PID-8 component 1 value is not in the profile value set (3 permitted codes).', }; ``` #### Properties ##### code > `readonly` **code**: [`FindingCode`](#findingcode) ##### locus > `readonly` **locus**: [`FindingLocus`](#findinglocus) ##### message > `readonly` **message**: `string` ##### severity > `readonly` **severity**: [`FindingSeverity`](#findingseverity) *** ### ConformanceProfile A **user-authored, declarative** conformance profile. The consumer supplies this; hl7 ships none. It is a bounded subset of the HL7 v2 Message-Profile model: usage / condition predicate / cardinality / length / consumer-supplied value set: with **no** bundled code set and **no** network binding (both deliberate scope boundaries). A condition predicate reads the message in front of it and nothing else. #### Example ```ts import type { ConformanceProfile } from "@cosyte/hl7"; // A minimal ADT profile the CONSUMER authors: example, NOT an attestation. const profile: ConformanceProfile = { name: "example-adt-min", segments: [ { segment: "MSH", usage: "R", fields: [{ field: 10, name: "Control ID", usage: "R" }] }, { segment: "PID", usage: "R", cardinality: { min: 1, max: 1 }, fields: [ { field: 3, name: "Patient Identifiers", usage: "R", cardinality: { min: 1, max: 1 } }, { field: 8, name: "Administrative Sex", usage: "RE", valueSet: ["M", "F", "U"] }, ] }, { segment: "ZZZ", usage: "X" }, ], }; ``` #### Properties ##### level? > `readonly` `optional` **level?**: [`ProfileLevel`](#profilelevel) The [ProfileLevel](#profilelevel) this profile CLAIMS, echoed into [ConformanceResult.level](#level-1). **Omitting it claims `constrainable`**, the weaker of the two claims a working profile can make, so a profile that says nothing never reads as an implementable one. Declaring `standard` or `constrainable` imposes no level-derived constraint on any element: both levels are defined as leaving optionality in place, so there is nothing for the engine to check. **Declaring `implementable` is a claim the engine CHECKS**, because the level asserts that all optionality and openness have been removed. Every segment, field and component rule must declare a usage, and that usage must be `R`, `RE` or `X`, or a declared conditional both of whose outcomes are drawn from those three. A rule left at `C`, `CE`, `O` or `B`, a conditional whose outcomes are not, and a rule that declares no usage at all each refuse the claim as [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed) rather than being accepted on the author's word. The level changes NO message check: it alters which findings can fire for a given message not at all. It is a declaration plus its gate. ##### name > `readonly` **name**: `string` A name for provenance: echoed into [ConformanceResult.profileName](#profilename-1). ##### segments > `readonly` **segments**: readonly [`SegmentRule`](#segmentrule)[] The segment rules, evaluated in array order (stable finding order). *** ### ConformanceResult The result of [validateAgainstProfile](#validateagainstprofile): the profile's `name`, the [ProfileLevel](#profilelevel) in force, and the ordered list of `findings`. **`findings.length === 0` is NOT a conformance attestation.** It means every rule the profile declared was satisfied: nothing about the parts of the message the profile did not cover, and nothing about clinical correctness. Read it as "no declared rule was violated," never as "conformant." That holds at every level, `implementable` included: the level says the profile removed its own optionality, not that anyone accredited the result. **What the level DOES tell you** is how to read an empty findings list. At `implementable` level every element the profile declares was reduced to a supported / not-supported decision, so the assessment against that profile is complete. At `standard` or `constrainable` level it is not: elements the profile left optional were never asserted either way, so an empty list is consistent with parts of the message having gone unassessed. #### Properties ##### findings > `readonly` **findings**: readonly [`ConformanceFinding`](#conformancefinding)[] ##### level > `readonly` **level**: [`ProfileLevel`](#profilelevel) The [ProfileLevel](#profilelevel) the assessment was actually made at. Present on EVERY result, including one for a profile so malformed its name could not be read. A profile that declares no level is assessed and echoed as `constrainable`; a profile whose declaration is not one of the three levels is echoed the same way, alongside the [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed) finding that names it. **A REFUSED `implementable` claim is never echoed here.** Where the engine returned `PROFILE_MALFORMED` findings it did not complete the assessment the claim asserts, so the level in force falls back to `constrainable` rather than repeating a claim nothing verified. ##### profileName > `readonly` **profileName**: `string` *** ### ConnectedPredicate Two statements joined by a connector. Nests: either side may itself be a connected predicate. A connected predicate is unevaluatable exactly when its result DEPENDS on an unevaluatable operand. So `AND` is false as soon as one side is false however unevaluatable the other is, `OR` is true as soon as one side is true, and `XOR` needs both sides. #### Example ```ts import type { ConnectedPredicate } from "@cosyte/hl7"; // "IF RXA-9.1 contains '00' AND RXA-20 contains one of the values in the list: {'CP','PA'}" const predicate: ConnectedPredicate = { connector: "AND", left: { location: { segment: "RXA", field: 9, component: 1 }, verb: "contains", values: ["00"] }, right: { location: { segment: "RXA", field: 20 }, verb: "contains", values: ["CP", "PA"] }, }; ``` #### Properties ##### connector > `readonly` **connector**: [`PredicateConnector`](#predicateconnector) Which connector joins the two operands. ##### left > `readonly` **left**: [`ConditionPredicate`](#conditionpredicate) The left-hand operand. ##### right > `readonly` **right**: [`ConditionPredicate`](#conditionpredicate) The right-hand operand. *** ### CustomSegmentDefinition Shape of a single segment field-name declaration used by profile authoring (`defineProfile()`). `fields` maps a caller-visible field NAME to its 1-indexed HL7 position within the segment. Declared here (alongside `Profile`) to keep the parser's type module the single source of truth; `src/profiles/define.ts` re-exports this type, so consumers can write `import type { CustomSegmentDefinition } from "@cosyte/hl7"`. It is the value shape of BOTH declaration maps, and which map a declaration sits in is what decides where the name may be used: `customSegments` names fields on a custom Z-segment, `segmentOverrides` names fields on a standard HL7 v2 segment. The shape is identical because the meaning of a position is: a 1-indexed HL7 field position, read exactly as `Segment.field(n)` reads it. #### Example ```ts import type { CustomSegmentDefinition } from "@cosyte/hl7"; const zdp: CustomSegmentDefinition = { fields: { departmentCode: 3, departmentName: 4 }, }; ``` #### Properties ##### fields > `readonly` **fields**: `Readonly`\<`Record`\<`string`, `number`\>\> *** ### CWE HL7 v2 Coded with Exceptions (CWE): coded element per HL7 Chapter 2. All 9 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. identifier (e.g. "GLU") 2. text (human-readable, e.g. "Glucose") 3. nameOfCodingSystem (e.g. "LN" for LOINC, "SCT" for SNOMED CT) 4. alternateIdentifier 5. alternateText 6. nameOfAlternateCodingSystem 7. codingSystemVersionId 8. alternateCodingSystemVersionId 9. originalText Components 10+ (present only on v2.7+ senders) are surfaced verbatim, in order, on `extraComponents`: never silently truncated. #### Example ```ts import type { CWE } from "@cosyte/hl7"; const code: CWE = { identifier: "GLU", text: "Glucose", nameOfCodingSystem: "LN" }; ``` #### Properties ##### alternateCodingSystemVersionId? > `readonly` `optional` **alternateCodingSystemVersionId?**: `string` ##### alternateIdentifier? > `readonly` `optional` **alternateIdentifier?**: `string` ##### alternateText? > `readonly` `optional` **alternateText?**: `string` ##### codingSystemVersionId? > `readonly` `optional` **codingSystemVersionId?**: `string` ##### extraComponents? > `readonly` `optional` **extraComponents?**: readonly `string`[] Components beyond the modeled 9 (HL7 component 10 onward), preserved verbatim and in order for forward-compatibility with v2.7+ senders that carry the second-alternate triplet or coding-system / value-set OIDs. OMITTED when the element has no components past the 9th. An absent interior component is preserved as `""` so `extraComponents[i]` maps to HL7 component `10 + i`. ##### identifier? > `readonly` `optional` **identifier?**: `string` ##### nameOfAlternateCodingSystem? > `readonly` `optional` **nameOfAlternateCodingSystem?**: `string` ##### nameOfCodingSystem? > `readonly` `optional` **nameOfCodingSystem?**: `string` ##### originalText? > `readonly` `optional` **originalText?**: `string` ##### text? > `readonly` `optional` **text?**: `string` *** ### CX HL7 v2 Extended Composite ID (CX): structured identifier per HL7 Chapter 2. All 10 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). `assigningAuthority` uses the nested `HD` shape; `assigningFacility` is flattened to a plain string in v1. Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. idNumber 2. checkDigit 3. checkDigitScheme (ISO 7064, M10, M11, NPI) 4. assigningAuthority (nested HD: 3 subcomponents form a HD composite) 5. identifierTypeCode (MR, SSN, DL, MC, ...) 6. assigningFacility (v1: flattened to string; spec is HD-shaped) 7. effectiveDate (raw HL7 TS string) 8. expirationDate (raw HL7 TS string) 9. assigningJurisdiction (v1: flattened to string) 10. assigningAgencyOrDepartment (v1: flattened to string) #### Example ```ts import type { CX } from "@cosyte/hl7"; const mrn: CX = { idNumber: "123456", assigningAuthority: { namespaceId: "EPIC", universalId: "1.2.840.114350", universalIdType: "ISO" }, identifierTypeCode: "MR", }; ``` #### Properties ##### assigningAgencyOrDepartment? > `readonly` `optional` **assigningAgencyOrDepartment?**: `string` ##### assigningAuthority? > `readonly` `optional` **assigningAuthority?**: [`HD`](#hd-1) ##### assigningFacility? > `readonly` `optional` **assigningFacility?**: `string` ##### assigningJurisdiction? > `readonly` `optional` **assigningJurisdiction?**: `string` ##### checkDigit? > `readonly` `optional` **checkDigit?**: `string` ##### checkDigitScheme? > `readonly` `optional` **checkDigitScheme?**: `string` ##### effectiveDate? > `readonly` `optional` **effectiveDate?**: `string` ##### expirationDate? > `readonly` `optional` **expirationDate?**: `string` ##### identifierTypeCode? > `readonly` `optional` **identifierTypeCode?**: `string` ##### idNumber? > `readonly` `optional` **idNumber?**: `string` *** ### DateParts The calendar components a datetime actually STATED, as a frozen plain object. A component the value did not state is ABSENT: the key is not present at all rather than present with `undefined`, so `Object.keys()` is exactly the set of stated components and the value's precision is recoverable from it. There is no `raw`, `valid` or `precision` key here: that fidelity metadata stays on [DtmParts](#dtmparts). `month` is SPEC-NATIVE 1 to 12, never the JS `Date` 0 to 11. Delete `offsetMinutes` and what is left is accepted as-is by `Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject`, with no key rename and no value adjustment. That compatibility is the reason for the shape; neither library is a dependency here, and neither needs to be. #### Example ```ts import { parseDtm, toObject } from "@cosyte/hl7"; toObject(parseDtm("19880705")); // { year: 1988, month: 7, day: 5 }: three keys, no zero-fill Object.keys(toObject(parseDtm("1970")) ?? {}); // ["year"]: a year-precision value states one component ``` #### Properties ##### day? > `readonly` `optional` **day?**: `number` Day of month, 1 to 31. ##### hour? > `readonly` `optional` **hour?**: `number` Hour, 0 to 23. ##### millisecond? > `readonly` `optional` **millisecond?**: `number` Milliseconds, derived from the stated fractional second by taking its first three digits VERBATIM and right-padding with zeroes (`"5"` is 500, `"0500"` is 50, `"123456"` is 123). Never computed by multiplying a floating-point fraction by 1000, which loses the last digit on values such as 0.123. Absent when the value stated no fraction. ##### minute? > `readonly` `optional` **minute?**: `number` Minute, 0 to 59. ##### month? > `readonly` `optional` **month?**: `number` Month, 1 to 12 (spec-native, NOT the JS `Date` 0 to 11). ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` Signed minutes east of UTC, present IF AND ONLY IF the value carried an explicit offset. A stated zero offset (including HL7's `-0000`) is present as `0`. Never synthesised from the host machine's zone. ##### second? > `readonly` `optional` **second?**: `number` Second, 0 to 59. ##### year? > `readonly` `optional` **year?**: `number` Four-digit year, exactly as stated. A year below 100 stays below 100. *** ### DefinedProfile The profile [defineProfile](#defineprofile) produces for one options object: every member of the general [Profile](#profile-2) interface, with `customSegments` at the declarations that options object actually carries (its own, merged with every parent's). It stays assignable to [Profile](#profile-2) with no cast, so a profile built by the factory still passes to every API that accepts the general interface, and a caller who annotates a variable with `Profile` simply gives the narrowing up rather than hitting a type error. #### Example ```ts import { defineProfile, type Profile } from "@cosyte/hl7"; const epic = defineProfile({ name: "epic", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); const asGeneral: Profile = epic; // no cast: the narrowing is additive console.log(asGeneral.name); // "epic" ``` #### Extends - `Omit`\<[`Profile`](#profile-2), `"customSegments"`\> #### Type Parameters ##### O `O` #### Properties ##### customSegments > `readonly` **customSegments**: `DeclaredSegments`\<`O`\> The merged declarations, at the literal segment types and field names. ##### dateFormats? > `readonly` `optional` **dateFormats?**: readonly `string`[] Date formats this vendor writes. Merged after `ParseOptions.dateFormats` and honoured on every datetime the library returns, exactly as the option form is. ###### Inherited from [`Profile`](#profile-2).[`dateFormats`](#dateformats-6) ##### describe? > `readonly` `optional` **describe?**: () => `string` ###### Returns `string` ###### Inherited from [`Profile`](#profile-2).[`describe`](#describe-1) ##### description? > `readonly` `optional` **description?**: `string` ###### Inherited from [`Profile`](#profile-2).[`description`](#description-3) ##### lineage? > `readonly` `optional` **lineage?**: readonly `string`[] ###### Inherited from [`Profile`](#profile-2).[`lineage`](#lineage-1) ##### name > `readonly` **name**: `string` ###### Inherited from [`Profile`](#profile-2).[`name`](#name-16) ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) ###### Inherited from [`Profile`](#profile-2).[`onWarning`](#onwarning-3) ##### segmentOverrides? > `readonly` `optional` **segmentOverrides?**: `Readonly`\<`Record`\<`string`, [`CustomSegmentDefinition`](#customsegmentdefinition)\>\> Site-specific field NAMES bound to positions on STANDARD HL7 v2 segments, keyed by canonical segment name (`PID`, `AL1`, `MSH`, ...). A separate map from [Profile.customSegments](#customsegments-2), which stays Z-segment-only: a standard name appearing there would change which segments the parser treats as profile-claimed, and an author skimming a profile literal can see at a glance which declarations touch standard clinical fields. Read-side aliases, and ADDITIVE ONLY. A declaration here creates a new name for `Segment.get(name)` to resolve on segments of that type, and changes no existing read: positional access, dot-paths, the typed clinical accessors, the warning list and both serializations are what they were without it. ###### Inherited from [`Profile`](#profile-2).[`segmentOverrides`](#segmentoverrides-2) *** ### DefineProfileOptions Options accepted by `defineProfile()` (D-02). Mirrors the locked `Profile` shape plus the `extends` input key. Every field except `name` is optional. #### Example ```ts import { defineProfile, type DefineProfileOptions } from "@cosyte/hl7"; const opts: DefineProfileOptions = { name: "my-lab", dateFormats: ["MM/DD/YYYY"], customSegments: { ZLB: { fields: { noteText: 3 } } }, }; const profile = defineProfile(opts); ``` #### Properties ##### customSegments? > `readonly` `optional` **customSegments?**: `Readonly`\<`Record`\<`string`, [`CustomSegmentDefinition`](#customsegmentdefinition)\>\> ##### dateFormats? > `readonly` `optional` **dateFormats?**: readonly `string`[] ##### description? > `readonly` `optional` **description?**: `string` ##### extends? > `readonly` `optional` **extends?**: [`Profile`](#profile-2) \| readonly [`Profile`](#profile-2)[] ##### name > `readonly` **name**: `string` ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) ##### segmentOverrides? > `readonly` `optional` **segmentOverrides?**: `Readonly`\<`Record`\<`string`, [`CustomSegmentDefinition`](#customsegmentdefinition)\>\> Field names bound to positions on STANDARD HL7 v2 segments, keyed by canonical segment name. Sibling of `customSegments`, never a relaxation of it: a Z-segment key here is refused and pointed back at `customSegments`, and a key that is not a standard segment name is refused outright. Every binding is a NEW read. Declaring one changes nothing an existing caller sees; see [Profile.segmentOverrides](#segmentoverrides-2). *** ### DftAmount A transaction amount (CP, composite price) for [buildDft](#builddft), supplied as its typed parts. Every part is emitted **verbatim**: the price keeps the precision the caller wrote, and the denomination is never inferred. #### Example ```ts import type { DftAmount } from "@cosyte/hl7"; const amount: DftAmount = { price: "150.00", denomination: "USD" }; ``` #### Properties ##### denomination? > `readonly` `optional` **denomination?**: `string` CP.1.2 Price denomination, e.g. `"USD"` (ISO 4217). Never inferred. ##### price? > `readonly` `optional` **price?**: `string` CP.1.1 Price quantity, e.g. `"150.00"`. Emitted verbatim, never a float. ##### priceType? > `readonly` `optional` **priceType?**: `string` CP.2 Price type (HL7 Table 0205, e.g. `"AP"` administrative price). *** ### DftCharge Typed FT1 (financial transaction) content for [buildDft](#builddft). #### Example ```ts import type { DftCharge } from "@cosyte/hl7"; const charge: DftCharge = { setId: "1", transactionDate: "20260801", transactionType: "CG", transactionCode: { identifier: "80053", text: "Metabolic panel" }, quantity: "1", amountExtended: { price: "150.00", denomination: "USD" }, diagnoses: [{ identifier: "E11.9" }], }; ``` #### Properties ##### amountExtended? > `readonly` `optional` **amountExtended?**: [`DftAmount`](#dftamount) FT1-11 Transaction Amount, Extended. ##### amountUnit? > `readonly` `optional` **amountUnit?**: [`DftAmount`](#dftamount) FT1-12 Transaction Amount, Unit. ##### diagnoses? > `readonly` `optional` **diagnoses?**: [`CWE`](#cwe-1) \| readonly [`CWE`](#cwe-1)[] FT1-19 Diagnosis Code(s): the billing diagnosis linkage, repeating. ##### postingDate? > `readonly` `optional` **postingDate?**: `string` \| [`DtmParts`](#dtmparts) FT1-5 Transaction Posting Date. ##### quantity? > `readonly` `optional` **quantity?**: `string` \| `number` \| [`NM`](#nm-1) FT1-10 Transaction Quantity (emitted verbatim: the caller owns its precision). ##### setId? > `readonly` `optional` **setId?**: `string` FT1-1 Set ID. ##### transactionCode? > `readonly` `optional` **transactionCode?**: [`CWE`](#cwe-1) FT1-7 Transaction Code: the institution charge or procedure code. ##### transactionDate? > `readonly` `optional` **transactionDate?**: `string` \| [`DtmParts`](#dtmparts) FT1-4 Transaction Date. ##### transactionId? > `readonly` `optional` **transactionId?**: `string` FT1-2 Transaction ID. ##### transactionType? > `readonly` `optional` **transactionType?**: `string` FT1-6 Transaction Type (HL7 Table 0017: `CG`, `CD`, `PY`, `AJ`). *** ### Diagnosis DG1-derived diagnosis entry (HELPERS-06). `dateTime` is the fidelity `TS`. #### Example ```ts import type { Diagnosis } from "@cosyte/hl7"; const dg: Diagnosis = { code: { identifier: "E11.9", text: "Type 2 diabetes" }, description: "Type 2 diabetes mellitus without complications", type: "F", }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CWE`](#cwe-1) DG1-3 diagnosis code. ##### dateTime? > `readonly` `optional` **dateTime?**: [`DtmParts`](#dtmparts) DG1-5 diagnosis date/time as the fidelity `TS`. ##### description? > `readonly` `optional` **description?**: `string` DG1-4 diagnosis description. ##### type? > `readonly` `optional` **type?**: `string` DG1-6 diagnosis type (A=admitting, W=working, F=final). *** ### DotPath Parsed representation of a dot-path string. Produced by `parsePath`, consumed by `resolvePath`. All numeric indices are normalized to the internal convention (segmentIndex = 0-based occurrence, fieldIndex = 1-based HL7 field number, repetitionIndex = 0-based rep, componentIndex and subcomponentIndex = 1-based HL7 positions). #### Example ```ts import { parsePath } from "@cosyte/hl7"; parsePath("OBX[2].5.1"); // { segmentType: "OBX", segmentIndex: 2, fieldIndex: 5, // repetitionIndex: 0, componentIndex: 1 } ``` #### Properties ##### componentIndex? > `readonly` `optional` **componentIndex?**: `number` 1-based HL7 component position (within a repetition). ##### fieldIndex > `readonly` **fieldIndex**: `number` 1-based HL7 field number; maps to `RawSegment.fields[fieldIndex]`. ##### repetitionIndex? > `readonly` `optional` **repetitionIndex?**: `number` 0-based repetition index; defaults to 0 when `[N]` is omitted. ##### segmentIndex > `readonly` **segmentIndex**: `number` 0-based occurrence of this segment type in the message. ##### segmentType > `readonly` **segmentType**: `string` 3-char segment identifier (e.g. `"PID"`, `"OBX"`, `"ZPI"`). ##### subcomponentIndex? > `readonly` `optional` **subcomponentIndex?**: `number` 1-based HL7 subcomponent position (within a component). *** ### DtmAmbiguity Report attached to a timestamp the parser REFUSED to resolve because the value is order-ambiguous: a slash-separated numeric date whose first two components are both in 1-12, so a month-first and a day-first reading are each a legal calendar date and no evidence chooses between them. The parser reports the two readings rather than picking one, because picking one produces a plausible, wrong calendar date and nothing downstream can tell that it was invented. Supply the vendor's order through `parseHL7(raw, { dateFormats: ["DD/MM/YYYY"] })` or a profile's `dateFormats`, which are both tried ahead of the built-ins, and the value resolves with no report at all. #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const ts = parseHL7(raw).meta.timestamp; // MSH-7 was "05/07/1988" console.log(ts?.valid); // false: nothing was resolved console.log(ts?.ambiguity?.raw); // "05/07/1988" console.log(ts?.ambiguity?.candidates[0]); // MM/DD/YYYY -> 1988-05-07 console.log(ts?.ambiguity?.candidates[1]); // DD/MM/YYYY -> 1988-07-05 ``` #### Properties ##### candidates > `readonly` **candidates**: readonly \[[`DtmAmbiguityCandidate`](#dtmambiguitycandidate), [`DtmAmbiguityCandidate`](#dtmambiguitycandidate)\] Both readings, month-first then day-first. Exactly two, and they always disagree: a value whose readings coincide is resolved, not refused. ##### code > `readonly` **code**: `"AMBIGUOUS_DATE_ORDER"` Always [AMBIGUOUS\_DATE\_ORDER](#ambiguous_date_order): the stable identifier to compare on. ##### message > `readonly` **message**: `string` A one-line explanation naming the value, both readings, and the remedy. ##### raw > `readonly` **raw**: `string` The value exactly as it arrived, so the report names what was declined. *** ### DtmAmbiguityCandidate One of the two calendar dates an order-ambiguous slash value could denote: the format that would produce it, its spec-native `month` (1-12) and `day`, and the ISO `YYYY-MM-DD` rendering of the date part. Declaring `format` through `parseHL7(raw, { dateFormats: [...] })` or a profile is what makes the parser return that reading. #### Example ```ts import type { DtmAmbiguityCandidate } from "@cosyte/hl7"; const dayFirst: DtmAmbiguityCandidate = { format: "DD/MM/YYYY", month: 7, day: 5, isoDate: "1988-07-05", }; ``` #### Properties ##### day > `readonly` **day**: `number` Day of month under this reading, 1-31. ##### format > `readonly` **format**: `string` The date format that yields this reading, e.g. `"DD/MM/YYYY"`. ##### isoDate > `readonly` **isoDate**: `string` The date part of this reading as ISO `YYYY-MM-DD`, e.g. `"1988-07-05"`. ##### month > `readonly` **month**: `number` Month under this reading, 1-12 (spec-native, NOT JS 0-11). *** ### DtmParts Parsed HL7 v2 TS/DTM value: the raw string plus its structural parts, with the stated **precision** and **timezone fidelity** preserved. This is the shape of the `TS` composite (`field.asTs()`) and every helper datetime. The parts are only populated when `valid` is `true`. `month`/`day`/… are **spec-native** (`month` is 1 to 12, NOT the JS `Date` 0 to 11). `offsetMinutes` is present iff `hasTimezone` is `true` and is signed minutes east of UTC (`+0500` → `300`, `-0430` → `-270`). A missing offset is **flagged** (`hasTimezone: false`), never resolved to UTC: the consumer decides how to localize with [dtmToDate](#dtmtodate). #### Example ```ts import type { DtmParts } from "@cosyte/hl7"; const dob: DtmParts = { raw: "19880705", valid: true, precision: "day", year: 1988, month: 7, day: 5, hasTimezone: false, }; ``` #### Properties ##### ambiguity? > `readonly` `optional` **ambiguity?**: [`DtmAmbiguity`](#dtmambiguity) Why an order-ambiguous slash date was refused, naming the raw value and both readings it could have had. Present ONLY when `valid` is `false` because the field order could not be established; absent on every other result, including a plain malformed one. See [DtmAmbiguity](#dtmambiguity). ##### day? > `readonly` `optional` **day?**: `number` Day of month, 1 to 31. ##### fractionalSeconds? > `readonly` `optional` **fractionalSeconds?**: `string` Fractional-second digits exactly as populated (no leading dot), e.g. `"5"` (0.5 s), `"0500"` (0.05 s). Preserved verbatim: never rounded. ##### hasTimezone > `readonly` **hasTimezone**: `boolean` `true` iff an explicit `+/-ZZZZ` offset was present. ##### hour? > `readonly` `optional` **hour?**: `number` Hour, 0 to 23. ##### matchedFormat? > `readonly` `optional` **matchedFormat?**: `string` The non-canonical format that matched, e.g. `"MM/DD/YYYY"`: normally one the caller declared in `dateFormats`, or `"ISO-8601"` and the other [BUILTIN\_DATE\_FALLBACKS](#builtin_date_fallbacks) entries, which only `msg.meta.timestamp` can reach. Absent for a strict HL7 DTM parse. ##### minute? > `readonly` `optional` **minute?**: `number` Minute, 0 to 59. ##### month? > `readonly` `optional` **month?**: `number` Month, 1 to 12 (spec-native, NOT JS 0 to 11). ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` Signed minutes east of UTC; present iff `hasTimezone` is `true`. ##### precision? > `readonly` `optional` **precision?**: [`DtmPrecision`](#dtmprecision) Stated precision; absent when `valid` is `false`. ##### raw > `readonly` **raw**: `string` The original HL7 string, exactly as it appeared (already unescaped). ##### second? > `readonly` `optional` **second?**: `number` Second, 0 to 59. ##### valid > `readonly` **valid**: `boolean` `true` when `raw` is a well-formed, in-range HL7 DTM (or a matched fallback format). `false` for empty, malformed, calendar-out-of-range, or order-ambiguous input: in which case only `raw`, `hasTimezone: false` and (for the ambiguous case) [DtmParts.ambiguity](#ambiguity) are meaningful. ##### year? > `readonly` `optional` **year?**: `number` Four-digit year. *** ### DtmToDateOptions Options controlling how [dtmToDate](#dtmtodate) resolves a missing timezone. #### Example ```ts import { parseDtm, dtmToDate } from "@cosyte/hl7"; // Treat an offset-less value as UTC (an explicit caller choice): dtmToDate(parseDtm("20250102"), { assumeOffsetMinutes: 0 }); // ...or as US Eastern standard time (UTC-05:00): dtmToDate(parseDtm("20250102"), { assumeOffsetMinutes: -300 }); ``` #### Properties ##### assumeOffsetMinutes? > `readonly` `optional` **assumeOffsetMinutes?**: `number` Offset (signed minutes east of UTC) to assume when the value carries **no** timezone. Without it, an offset-less value resolves to `undefined`, [dtmToDate](#dtmtodate) never guesses a zone. Ignored when the value already has an offset. Pass `0` to explicitly treat a naive value as UTC. *** ### EncodingCharacters The HL7 delimiter characters discovered from MSH-1 (field separator) and MSH-2 (encoding characters). The first four: component, repetition, escape, subcomponent: are mandatory across all HL7 v2 versions. The fifth, `truncation`, is the v2.7+ truncation character (default `#` per spec §2.5.5.2): only present when MSH-2 actually carries 5 encoding characters, so messages that pre-date v2.7 round-trip with a 4-char MSH-2 unchanged. #### Example ```ts import type { EncodingCharacters } from "@cosyte/hl7"; const v25: EncodingCharacters = { field: "|", component: "^", repetition: "~", escape: "\\", subcomponent: "&", }; const v27: EncodingCharacters = { ...v25, truncation: "#" }; ``` #### Properties ##### component > `readonly` **component**: `string` ##### escape > `readonly` **escape**: `string` ##### field > `readonly` **field**: `string` ##### repetition > `readonly` **repetition**: `string` ##### subcomponent > `readonly` **subcomponent**: `string` ##### truncation? > `readonly` `optional` **truncation?**: `string` *** ### ExpectedSegmentGroup One expected segment for a recognized message type. The published structure definition gives `requiredSegment` a minimum of one along the whole path from the structure root, in every variant of the referenced structure's family, so a structurally conformant message always carries it. `name` and `anchorSegments` carry the same segment: they are the long-standing shape of this type and a group is now exactly one required segment, because the derivation works segment by segment rather than by hand-picked bundle. #### Properties ##### anchorSegments > `readonly` **anchorSegments**: readonly `string`[] The segment name(s) whose presence satisfies this expectation. ##### name > `readonly` **name**: `string` The expectation's label, which is the required segment's name. ##### requiredSegment > `readonly` **requiredSegment**: `string` The segment the published structure gives a minimum of one. *** ### FieldRule A rule for one **field position** within a segment. Every constraint is optional; a rule with only a `field` index is a no-op. #### Remarks `field` is the 1-indexed HL7 position (MSH offset handled internally, exactly like `Segment.field(n)`: `{ field: 9 }` on `MSH` targets MSH-9). Length and value-set checks read the value at `component` (default **1**), so a coded field's code (`CWE.1` / `CE.1`) is checked by default; set `component` to check a different component. Both are applied **per present repetition**. #### Properties ##### cardinality? > `readonly` `optional` **cardinality?**: [`Cardinality`](#cardinality) Repetition-count constraint for this field. ##### codingSystem? > `readonly` `optional` **codingSystem?**: `string` Bind [valueSet](#valueset) to the coding system its codes must be drawn from: a coding-system identifier this library recognizes, or any alias it recognizes for one. Declaring it turns "the code must be one of these" into "and it must be drawn from THIS system", so a feed sending a plausible code labelled with the wrong system stops validating clean. The message's own coding-system component is compared by RESOLVED IDENTITY, not by string equality: a feed sending `loinc`, `LOINC` or `LN` all match a binding of `LN`, because each resolves to the same registered acronym. A repetition whose coding-system component resolves to anything else, including nothing at all, is [FINDING\_CODES.PROFILE\_CODING\_SYSTEM\_MISMATCH](#profile_coding_system_mismatch). **Opt-in and independent.** Omitting it leaves a `valueSet` rule behaving exactly as it always has. Declaring it adds a SECOND check rather than changing the first: a permitted code carried under the wrong system yields one coding-system finding and no value-set finding, and a code that is wrong on both counts yields exactly one of each. **Recognition is this library's own frozen subset, not the whole registry.** An identifier it does not recognize is refused when the profile is defined ([FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed)), rather than silently compared against a system that cannot be resolved: so a local or site-specific system cannot be bound, and a consumer with one keeps the bare `valueSet`. A binding declared without a `valueSet` is refused the same way: there is nothing there for it to bind. **This checks the sender's CLAIM, never the code.** Nothing here verifies that the code exists in the system the message names: hl7 still ships no code set and makes no network call. ##### codingSystemComponent? > `readonly` `optional` **codingSystemComponent?**: `number` 1-indexed component carrying the coding-system identifier that a declared [FieldRule.codingSystem](#codingsystem) is compared against. Defaults to **two positions after [component](#component-2)**, which is the coded element's own code-to-system relationship: a code at component 1 puts its system at component 3, and an alternate code at component 4 puts its alternate system at component 6. So a rule checking the primary triplet needs neither key, and a rule checking the alternate triplet sets `component` alone. Set it explicitly for a field whose layout does not follow that offset. Checking BOTH triplets from one rule is not supported: declare two rules on the same field. ##### component? > `readonly` `optional` **component?**: `number` 1-indexed component whose value the `length` / `valueSet` checks read. Defaults to `1` (the first component: a coded element's code). **Not to be confused with [FieldRule.components](#components)** (plural), which declares per-component RULES and closes the field's component set. This one (singular) only says which component the length / value-set checks read; declaring it neither declares a component rule nor closes anything. ##### components? > `readonly` `optional` **components?**: readonly [`ComponentRule`](#componentrule)[] Per-component rules for this field: what its components ARE and what each one's usage is. Applied **per present repetition**. **Declaring them closes the field's component set**, which is what makes [FINDING\_CODES.PROFILE\_UNDECLARED\_CONTENT](#profile_undeclared_content) decidable: a present repetition carrying a non-empty value at an index no rule here declares is content the profile never specified, and the HL7 conformance methodology counts that as a conformance violation rather than harmless extra content (its own worked example is data in a fourth component where the profile defines three). **Omitting them changes nothing, and an empty list is omitting them.** A field rule that declares no component rules declares no depth: it is checked exactly as it always has been, and the undeclared-content check can never fire for it. `components: []` carries no component rule, so it says precisely that and is never read as "this field has no components" (which would make every component of every repetition undeclared content). This is additive and opt-in, per field rule. Each component's cardinality is IMPLIED by its usage and may not be declared: see [ComponentRule](#componentrule). ##### condition? > `readonly` `optional` **condition?**: [`ConditionPredicate`](#conditionpredicate) The [ConditionPredicate](#conditionpredicate) that decides this rule's usage, evaluated against the message. Only a rule whose `usage` is conditional (`C`, `CE`, or a declared conditional) may declare one. ##### field > `readonly` **field**: `number` 1-indexed HL7 field position (e.g. `3` for PID-3, `9` for MSH-9). ##### length? > `readonly` `optional` **length?**: `number` Maximum character length of the checked component value (inclusive). ##### name? > `readonly` `optional` **name?**: `string` Optional human label for the field (e.g. `"Patient Identifier List"`). Structural documentation for the profile author only: findings identify a field by its PHI-free structural locus (segment + index), never by this label, so the label is never echoed into a finding message. ##### severity? > `readonly` `optional` **severity?**: [`FindingSeverity`](#findingseverity) Severity for findings this rule produces. Defaults to `"error"`. A profile author can downgrade a data-quality rule (e.g. a length or value-set check) to `"warning"` or `"info"` without changing the check itself. ##### usage? > `readonly` `optional` **usage?**: [`UsageCode`](#usagecode) Usage constraint: a simple code, or a declared conditional such as `C(RE/X)` (see [UsageCode](#usagecode)). Omitted ⇒ Optional, and an omitted usage is never refused. ##### valueSet? > `readonly` `optional` **valueSet?**: readonly `string`[] Consumer-supplied permitted-value list. The checked component value must be a member (case-sensitive exact match). **hl7 ships no code set**: this is BYO terminology; membership is a literal string check, never a LOINC / SNOMED / ICD / RxNorm lookup and never a network call. *** ### FindingLocus The **structural** locus a finding refers to: segment name plus, where applicable, the field position, component, repetition, and segment occurrence. Every member is a name or an index: a locus is **inherently PHI-free** and never carries a field value. A finding with no [field](#field-4) is **segment-level** (presence / cardinality of the segment itself); a finding with a `field` is **field-level**. For a [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed) diagnostic (a defect in the profile, not the message) `segment` may be the sentinel `"(profile)"`. #### Properties ##### component? > `readonly` `optional` **component?**: `number` 1-indexed component, when a component-scoped check fired: length, value-set, a component rule's own usage, or undeclared content. ##### field? > `readonly` `optional` **field?**: `number` 1-indexed field position, when the finding is field-level. ##### occurrence? > `readonly` `optional` **occurrence?**: `number` 0-indexed segment occurrence, when the segment type repeats. ##### repetition? > `readonly` `optional` **repetition?**: `number` 0-indexed field repetition, when the finding is repetition-scoped. ##### segment > `readonly` **segment**: `string` Segment name (e.g. `"PID"`), or `"(profile)"` for a profile-shape defect. *** ### HD HL7 v2 Hierarchic Designator (HD): per HL7 Chapter 2 data type. All 3 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. namespaceId: application- or facility-scoped identifier (e.g. "EPIC"). 2. universalId: globally-unique id (e.g. an OID or UUID string). 3. universalIdType: classifier for `universalId` (ISO, GUID, UUID, DNS, URI, HL7, HCD, Random, etc.). #### Example ```ts import type { HD } from "@cosyte/hl7"; const authority: HD = { namespaceId: "EPIC", universalId: "1.2.840.114350", universalIdType: "ISO" }; ``` #### Properties ##### namespaceId? > `readonly` `optional` **namespaceId?**: `string` ##### universalId? > `readonly` `optional` **universalId?**: `string` ##### universalIdType? > `readonly` `optional` **universalIdType?**: `string` *** ### Hl7ParseWarning Data shape for every Tier-2 warning emitted by the parser. Warnings are plain data (distinct from `Hl7ParseError`, which is a thrown `Error` subclass) so they can be safely accumulated into `Hl7Message.warnings` and passed to `onWarning` callbacks. #### Example ```ts import type { Hl7ParseWarning } from "@cosyte/hl7"; const w: Hl7ParseWarning = { code: "UNKNOWN_SEGMENT", message: "Unknown segment: ZZZ", position: { segmentIndex: 4 }, }; ``` #### Properties ##### code > `readonly` **code**: [`WarningCode`](#warningcode) ##### message > `readonly` **message**: `string` ##### position > `readonly` **position**: [`Hl7Position`](#hl7position) *** ### Hl7Position Positional context attached to every warning and fatal error. Fields are 1-indexed against the HL7 spec convention (see `RawSegment.fields` for the index 0 slot convention). All fields past `segmentIndex` are optional, for a top-level fatal like `EMPTY_INPUT` only `segmentIndex: 0` is populated; for a tokenizer warning deep inside a subcomponent all five indices may be set. #### Remarks With `exactOptionalPropertyTypes: true`, do not pass `fieldIndex: undefined` explicitly: omit the key instead. #### Example ```ts import type { Hl7Position } from "@cosyte/hl7"; const pos: Hl7Position = { segmentIndex: 2, fieldIndex: 5 }; ``` #### Properties ##### componentIndex? > `readonly` `optional` **componentIndex?**: `number` ##### fieldIndex? > `readonly` `optional` **fieldIndex?**: `number` ##### repetitionIndex? > `readonly` `optional` **repetitionIndex?**: `number` ##### segmentIndex > `readonly` **segmentIndex**: `number` ##### subcomponentIndex? > `readonly` `optional` **subcomponentIndex?**: `number` *** ### IdentityEvent One recognized patient-identity event. For `merge` / `move` / `change` kinds the `surviving` (PID/PV1-sourced) and `prior` (MRG-sourced) parties are also exposed directly, with the spec-constant `direction: "MRG_TO_PID"`: the prior identifiers are the ones being retired in favour of the surviving ones, never the reverse (HL7 v2 Ch. 3, A18/A39/A40). `parties` is the complete role-labelled surface in document order: nothing present in the message is dropped, including a nonconforming MRG in a link/add message (surfaced as a `prior`-role party). `warnings` carries the event's own fail-safe warnings (currently `MERGE_MISSING_PRIOR_OR_SURVIVOR`); they are scoped to the event and are NOT appended to `Hl7Message.warnings` (a read-side helper never mutates the message). #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; for (const ev of parseHL7(raw).identityEvents()) { if (ev.kind === "merge" && ev.prior && ev.surviving) { // retire ev.prior.identifiers in favour of ev.surviving.identifiers } else if (ev.warnings.length > 0) { // incomplete pair: do NOT apply; route for review } } ``` #### Properties ##### direction? > `readonly` `optional` **direction?**: `"MRG_TO_PID"` Spec-constant direction: the MRG (prior) identifiers are retired INTO the PID (surviving) identifiers. Present on `merge` / `move` / `change` events only; never inferred from content. ##### eventType > `readonly` **eventType**: `string` Trigger event code (MSH-9.2, falling back to EVN-1), e.g. `"A40"`. ##### kind > `readonly` **kind**: [`IdentityEventKind`](#identityeventkind-1) Classification of the trigger event. ##### parties > `readonly` **parties**: readonly [`IdentityParty`](#identityparty)[] Every party in document order, role-labelled: the complete surface. ##### prior? > `readonly` `optional` **prior?**: [`IdentityParty`](#identityparty) The prior (non-surviving) party (merge/move/change): ONLY ever sourced from MRG. ##### surviving? > `readonly` `optional` **surviving?**: [`IdentityParty`](#identityparty) The surviving party (merge/move/change): ONLY ever sourced from PID/PV1. ##### warnings > `readonly` **warnings**: readonly [`Hl7ParseWarning`](#hl7parsewarning)[] Event-scoped fail-safe warnings (never PHI-bearing). *** ### IdentityParty One party (one patient identity) in an identity event, labelled by role with its source segment recorded as provenance. Absent fields are OMITTED (exactOptionalPropertyTypes). All arrays and the object itself are frozen. Field sources by `sourceSegment`: - `"PID"`: `identifiers` = PID-3 repetitions, `legacyPatientId` = PID-2 (pre-v2.7 only), `accountNumber` = PID-18, `visitNumber` = PV1-19 (from the group's PV1, when present), `name` = PID-5 (first repetition). - `"MRG"`: `identifiers` = MRG-1 repetitions, `legacyPatientId` = MRG-4 (pre-v2.7 only), `accountNumber` = MRG-3, `visitNumber` = MRG-5, `name` = MRG-7 (first repetition). #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const ev = parseHL7(raw).identityEvents()[0]; if (ev?.prior) { console.log(ev.prior.role); // "prior" console.log(ev.prior.sourceSegment); // "MRG" for (const cx of ev.prior.identifiers) console.log(cx.idNumber); } ``` #### Properties ##### accountNumber? > `readonly` `optional` **accountNumber?**: [`CX`](#cx-1) Patient account number (PID-18 / MRG-3). ##### identifiers > `readonly` **identifiers**: readonly [`CX`](#cx-1)[] Identifier list (PID-3 / MRG-1), every non-empty CX repetition. ##### legacyPatientId? > `readonly` `optional` **legacyPatientId?**: [`CX`](#cx-1) Legacy single patient ID (PID-2 / MRG-4). Backward-compat only; withdrawn as of HL7 v2.7: OMITTED (not read) when MSH-12 declares v2.7 or later. ##### name? > `readonly` `optional` **name?**: [`XPN`](#xpn-1) Patient name (PID-5 / MRG-7, first repetition). ##### role > `readonly` **role**: [`IdentityRole`](#identityrole) Role of this party in the event: the safety-critical label. ##### sourceSegment > `readonly` **sourceSegment**: `"PID"` \| `"MRG"` Segment this party was sourced from: provenance for the role label. ##### visitNumber? > `readonly` `optional` **visitNumber?**: [`CX`](#cx-1) Visit number (PV1-19 for a PID party / MRG-5 for a prior party). *** ### Immunization A vaccine dose extracted from one RXA (Pharmacy/Treatment Administration) segment of a VXU^V04 immunization message, with its RXR (route/site) and OBX (e.g. VFC eligibility / funding source) children grouped positionally under the RXA, and `orderControl` from the preceding ORC of the VXU order group (`ORC`→`RXA`→`[RXR]`→`[{OBX}]`). **Safety contract.** A wrong vaccine, dose, or mis-keyed action code can harm a patient or corrupt an IIS (Immunization Information System) registry, so this view is deliberately conservative: - `vaccineCode` carries its own coding-system provenance via the CWE (`vaccineCode.nameOfCodingSystem`: `CVX` HL7 Table 0292; live IIS feeds frequently *dual-code* RXA-5 with an alternate CVX/NDC in CWE.4-6, surfaced as `vaccineCode.alternateIdentifier`/`alternateText`/`nameOfAlternateCodingSystem`). The helper reports the *claim*; it never validates or looks the code up. - `actionCode` (RXA-21, `A`/`D`/`U`) is surfaced **verbatim** and never defaulted: mis-keying it corrupts a registry's add/delete/update dedup. - `doseAmount` is strict-`Number()` parsed; the IIS "unknown dose" sentinel `999` is surfaced **as the number `999`**, never specially coerced. - `recordOrigin` (administered vs historical) is derived only from the well-known NIP001 RXA-9.1 codes and OMITTED otherwise: see [ImmunizationRecordOrigin](#immunizationrecordorigin-1). - `administrationStatus` tells a dose given (`"completed"`) apart from a refused or not-administered record, a CVX `998` "no vaccine administered" placeholder and a delete request, and is `"undetermined"` rather than a guess: see [ImmunizationAdministrationStatus](#immunizationadministrationstatus-1). Every record is still returned, whatever it classifies as. - Malformed RXA segments never throw: absent fields are omitted keys. `routes`, `observations` and `administrationStatus` are ALWAYS present (`routes` and `observations` possibly empty). Deferred (not v1): IIS-specific state profile constraints; CVX/MVX validity checks; the 2nd+ repetition of the repeating RXA-15/16/17 lot/expiry/manufacturer fields (only the first repetition is surfaced). #### Example ```ts import type { Immunization } from "@cosyte/hl7"; const imm: Immunization = { vaccineCode: { identifier: "115", text: "Tdap", nameOfCodingSystem: "CVX" }, doseAmount: 0.5, doseUnits: { identifier: "mL", nameOfCodingSystem: "UCUM" }, doseUnitsAreUcum: true, recordOrigin: "administered", manufacturer: { identifier: "PMC", text: "Sanofi Pasteur", nameOfCodingSystem: "MVX" }, completionStatus: "CP", actionCode: "A", administrationStatus: { classification: "completed", completionStatus: "CP", actionCode: "A", decidedBy: { field: "RXA-20", table: { name: "HL7 Table 0322", version: "3.0.0" }, map: { url: "http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70322-to-event-status", version: "1.0.0", }, }, }, routes: [{ route: { identifier: "IM", text: "Intramuscular" } }], observations: [], }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` RXA-21 action code (`A`=add, `D`=delete, `U`=update): preserved verbatim, NEVER defaulted. ##### administeredDateTime? > `readonly` `optional` **administeredDateTime?**: [`DtmParts`](#dtmparts) RXA-3 date/time start of administration as the fidelity `TS`. ##### administrationStatus > `readonly` **administrationStatus**: [`ImmunizationAdministrationStatus`](#immunizationadministrationstatus-1) The record's administration status, derived from this RXA's own RXA-5, RXA-20 and RXA-21, with the raw RXA-20 and RXA-21 codes beside it and the field and table, map or code set that decided it. Always present: `"undetermined"` when nothing classifies it. See [ImmunizationAdministrationStatus](#immunizationadministrationstatus-1). ##### completionStatus? > `readonly` `optional` **completionStatus?**: `string` RXA-20 completion status (`CP`=complete, `RE`=refused, `NA`=not administered, `PA`=partially administered). ##### doseAmount? > `readonly` `optional` **doseAmount?**: `number` RXA-6 administered dose amount (strict-parsed; never `NaN`). `999` = IIS "unknown", surfaced as-is. ##### doseUnits? > `readonly` `optional` **doseUnits?**: [`CWE`](#cwe-1) RXA-7 administered dose units (UCUM). ##### doseUnitsAreUcum? > `readonly` `optional` **doseUnitsAreUcum?**: `boolean` `true` iff RXA-7's coding system (CWE.3) is exactly `UCUM` (HL7 Table 0396) : i.e. the dose unit is declared UCUM and safe to interpret as computable. `false` means a unit IS present but is NOT declared UCUM (surfaced as-is, never coerced). OMITTED when RXA-7 is absent. A *claim* check only: UCUM grammar is not validated here. ##### expirationDate? > `readonly` `optional` **expirationDate?**: [`DtmParts`](#dtmparts) RXA-16 substance expiration date (first repetition) as the fidelity `TS`. ##### informationSource? > `readonly` `optional` **informationSource?**: [`CWE`](#cwe-1) RXA-9 immunization information source (HL7 Table NIP001), preserved verbatim. ##### lotNumber? > `readonly` `optional` **lotNumber?**: `string` RXA-15 substance lot number (first repetition). ##### manufacturer? > `readonly` `optional` **manufacturer?**: [`CWE`](#cwe-1) RXA-17 substance manufacturer (MVX, HL7 Table 0227; first repetition). ##### observations > `readonly` **observations**: readonly [`Observation`](#observation)[] OBX children grouped under this RXA (VFC eligibility, funding source, …). Always present (possibly empty). ##### orderControl? > `readonly` `optional` **orderControl?**: `string` ORC-1 order control when an ORC precedes this RXA in the VXU order group. ##### recordOrigin? > `readonly` `optional` **recordOrigin?**: [`ImmunizationRecordOrigin`](#immunizationrecordorigin-1) Derived administered-vs-historical classification from RXA-9.1. See [ImmunizationRecordOrigin](#immunizationrecordorigin-1). ##### refusalReason? > `readonly` `optional` **refusalReason?**: [`CWE`](#cwe-1) RXA-18 substance/treatment refusal reason (first repetition). ##### routes > `readonly` **routes**: readonly [`MedicationRoute`](#medicationroute)[] RXR children grouped under this RXA (Table 0162 route / Table 0163 site). Always present (possibly empty). ##### vaccineCode? > `readonly` `optional` **vaccineCode?**: [`CWE`](#cwe-1) RXA-5 administered vaccine code (CVX, HL7 Table 0292) with provenance + any alternate coding. *** ### ImmunizationAdministrationStatus An immunization record's administration status, derived from that RXA's own RXA-5, RXA-20 and RXA-21: carried as `administrationStatus` on every [Immunization](#immunization). It tells a dose given apart from a refused or not-administered record, a CVX `998` "no vaccine administered" placeholder, and a request to delete an administration sent earlier. **Which rule decides.** Exactly one rule decides, the first that applies: 1. RXA-21 exactly `D`: `"delete-requested"`. 2. RXA-21 present but not exactly one of `A`, `D`, `U`, `X` (HL7 Table 0323): `"undetermined"`. RXA-21 `A`, `U`, `X` or empty change nothing. 3. RXA-5 carries CVX `998` in a triplet whose coding system resolves to `CVX` (or, in the primary triplet, that names no coding system): `"no-vaccine-administered"`, or `"undetermined"` when the other triplet carries a different, non-empty identifier. 4. RXA-20 by HL7's Table 0322 to Event Status map: `CP` and `PA` `"completed"`, `RE` and `NA` `"not-done"`, anything else `"undetermined"`. **Safety contract.** `"completed"` only when RXA-20 is exactly `CP` or `PA`, RXA-21 is empty or exactly `A`, `U` or `X`, and RXA-5 carries no CVX `998`. Each RXA classifies from its own fields alone: nothing is carried across segments. A status is reported, never applied: a `"delete-requested"` record is still returned, is never matched against an earlier message, and removes nothing. The object is plain data, not a FHIR element. #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); const given = msg .immunizations() .filter((imm) => imm.administrationStatus.classification === "completed"); for (const imm of msg.immunizations()) { const { classification, completionStatus, actionCode, decidedBy } = imm.administrationStatus; if (classification === "undetermined") { console.log("not classifiable:", decidedBy.field, completionStatus, actionCode); } } ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` The raw RXA-21 code, byte-identical to the `actionCode` of the same immunization: the field's first value, decoded. OMITTED exactly when `actionCode` is (the field is absent, empty or `""`). ##### classification > `readonly` **classification**: [`ImmunizationStatusClass`](#immunizationstatusclass) The classification the deciding rule gives the record. ##### completionStatus? > `readonly` `optional` **completionStatus?**: `string` The raw RXA-20 code, byte-identical to the `completionStatus` of the same immunization: the field's first value, decoded. OMITTED exactly when `completionStatus` is (the field is absent, empty or `""`). ##### decidedBy > `readonly` **decidedBy**: [`ImmunizationStatusBasis`](#immunizationstatusbasis) The field that decided, and the table, map or code set it was read against. *** ### ImmunizationStatusCodeSet The code set an RXA-5 classification read: the CDC's CVX (Vaccines Administered) code set, and the one code of it this library reads, `998` "no vaccine administered". The CVX code set carries no version number of its own; `version` is the date the CDC last updated the `998` entry, so a later change to that entry shows up here as a changed `version`. #### Example ```ts import type { ImmunizationStatusCodeSet } from "@cosyte/hl7"; const codeSet: ImmunizationStatusCodeSet = { name: "CDC CVX", code: "998", version: "2023-03-09", }; ``` #### Properties ##### code > `readonly` **code**: `"998"` The CVX code read: `998`, "no vaccine administered". ##### name > `readonly` **name**: `"CDC CVX"` The code set: the CDC's CVX (Vaccines Administered) codes. ##### version > `readonly` **version**: `"2023-03-09"` The date the CDC last updated the `998` entry, as `YYYY-MM-DD`. *** ### ImmunizationStatusMap The published HL7 v2-to-FHIR map an RXA-20 classification followed, by canonical URL and version. The map comes from the HL7 Version 2 to FHIR Implementation Guide (STU 1, standards status Informative); a later map revision shows up here as a changed `version`. #### Example ```ts import type { ImmunizationStatusMap } from "@cosyte/hl7"; const map: ImmunizationStatusMap = { url: "http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70322-to-event-status", version: "1.0.0", }; ``` #### Properties ##### url > `readonly` **url**: `"http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70322-to-event-status"` Canonical URL of the ConceptMap whose rows the classification follows. ##### version > `readonly` **version**: `"1.0.0"` The ConceptMap version the classification follows. *** ### ImmunizationStatusTable The HL7 v2 code table an [ImmunizationAdministrationStatus](#immunizationadministrationstatus-1) read: Table 0322 (Completion Status, RXA-20) or Table 0323 (Action Code, RXA-21), each at the code-system version its codes were taken from. #### Example ```ts import type { ImmunizationStatusTable } from "@cosyte/hl7"; const table: ImmunizationStatusTable = { name: "HL7 Table 0322", version: "3.0.0" }; ``` #### Properties ##### name > `readonly` **name**: `"HL7 Table 0322"` \| `"HL7 Table 0323"` `"HL7 Table 0322"` for RXA-20, `"HL7 Table 0323"` for RXA-21. ##### version > `readonly` **version**: `"3.0.0"` The code-system version the table's codes were taken from. *** ### Insurance IN1-derived insurance entry (HELPERS-06) with positional IN2/IN3 presence flags (D-05 extension). `hasIn2` / `hasIn3` are ALWAYS present booleans; callers who need the full IN2/IN3 surface can walk `msg.segments("IN2")`. #### Example ```ts import type { Insurance } from "@cosyte/hl7"; const ins: Insurance = { planId: { identifier: "PLAN1", text: "Aetna PPO" }, policyNumber: "POL123", groupNumber: "GRP1", hasIn2: false, hasIn3: false, }; ``` #### Properties ##### companyId? > `readonly` `optional` **companyId?**: [`CX`](#cx-1) IN1-3 insurance company id. ##### companyName? > `readonly` `optional` **companyName?**: `string` IN1-4 insurance company name (first repetition, first component). ##### effectiveDate? > `readonly` `optional` **effectiveDate?**: [`DtmParts`](#dtmparts) IN1-12 plan effective date as the fidelity `TS`. ##### expirationDate? > `readonly` `optional` **expirationDate?**: [`DtmParts`](#dtmparts) IN1-13 plan expiration date as the fidelity `TS`. ##### groupNumber? > `readonly` `optional` **groupNumber?**: `string` IN1-8 group number. ##### hasIn2 > `readonly` **hasIn2**: `boolean` `true` iff an IN2 segment follows this IN1 before the next IN1. ##### hasIn3 > `readonly` **hasIn3**: `boolean` `true` iff an IN3 segment follows this IN1 before the next IN1. ##### insuredName? > `readonly` `optional` **insuredName?**: [`XPN`](#xpn-1) IN1-16 insured's name. ##### planId? > `readonly` `optional` **planId?**: [`CWE`](#cwe-1) IN1-2 insurance plan id. ##### policyNumber? > `readonly` `optional` **policyNumber?**: `string` IN1-36 policy number. *** ### JsonSchemaDocument The emitted JSON Schema document: the root node of the projection, plus the reusable definitions its members reference. #### Example ```ts import { messageJsonSchema, type JsonSchemaDocument } from "@cosyte/hl7"; const doc: JsonSchemaDocument = messageJsonSchema(); Object.keys(doc.$defs).includes("SerializedWarning"); // true ``` #### Extends - [`JsonSchemaNode`](#jsonschemanode) #### Properties ##### $defs > `readonly` **$defs**: `Readonly`\<`Record`\<`string`, [`JsonSchemaNode`](#jsonschemanode)\>\> ##### $ref? > `readonly` `optional` **$ref?**: `string` ###### Inherited from [`JsonSchemaNode`](#jsonschemanode).[`$ref`](#ref-1) ##### $schema > `readonly` **$schema**: `string` ##### additionalProperties? > `readonly` `optional` **additionalProperties?**: `false` ###### Inherited from [`JsonSchemaNode`](#jsonschemanode).[`additionalProperties`](#additionalproperties-1) ##### enum? > `readonly` `optional` **enum?**: readonly `string`[] ###### Inherited from [`JsonSchemaNode`](#jsonschemanode).[`enum`](#enum-1) ##### items? > `readonly` `optional` **items?**: [`JsonSchemaNode`](#jsonschemanode) ###### Inherited from [`JsonSchemaNode`](#jsonschemanode).[`items`](#items-1) ##### properties? > `readonly` `optional` **properties?**: `Readonly`\<`Record`\<`string`, [`JsonSchemaNode`](#jsonschemanode)\>\> ###### Inherited from [`JsonSchemaNode`](#jsonschemanode).[`properties`](#properties-1) ##### required? > `readonly` `optional` **required?**: readonly `string`[] ###### Inherited from [`JsonSchemaNode`](#jsonschemanode).[`required`](#required-1) ##### type? > `readonly` `optional` **type?**: `"string"` \| `"boolean"` \| `"object"` \| `"array"` \| `"integer"` ###### Inherited from [`JsonSchemaNode`](#jsonschemanode).[`type`](#type-5) *** ### JsonSchemaNode One node of the emitted JSON Schema. Every member is optional because a node carries only the keywords its own shape needs: a reference carries `$ref` alone, a scalar carries `type`, an object carries `type`, `properties`, `required` and `additionalProperties`. #### Example ```ts import type { JsonSchemaNode } from "@cosyte/hl7"; const subcomponents: JsonSchemaNode = { type: "array", items: { type: "string" } }; ``` #### Extended by - [`JsonSchemaDocument`](#jsonschemadocument) #### Properties ##### $ref? > `readonly` `optional` **$ref?**: `string` ##### additionalProperties? > `readonly` `optional` **additionalProperties?**: `false` ##### enum? > `readonly` `optional` **enum?**: readonly `string`[] ##### items? > `readonly` `optional` **items?**: [`JsonSchemaNode`](#jsonschemanode) ##### properties? > `readonly` `optional` **properties?**: `Readonly`\<`Record`\<`string`, [`JsonSchemaNode`](#jsonschemanode)\>\> ##### required? > `readonly` `optional` **required?**: readonly `string`[] ##### type? > `readonly` `optional` **type?**: `"string"` \| `"boolean"` \| `"object"` \| `"array"` \| `"integer"` *** ### KnownCodingSystem One recognized HL7 Table 0396 coding system: its registered acronym (the value expected in CWE.3 / CE.3), a canonical human-readable name, and the widely-used aliases tolerated for it. Aliases are matched case-insensitively. #### Properties ##### aliases > `readonly` **aliases**: readonly `string`[] Widely-used alternative spellings that claim the same system (matched case-insensitively), e.g. `"LOINC"` for `LN`. The registered `id` itself is always recognized and need not be repeated here. ##### id > `readonly` **id**: `string` Registered Table 0396 acronym, e.g. `"LN"`. ##### name > `readonly` **name**: `string` Canonical human-readable name, e.g. `"LOINC"`. *** ### MdmDocument Typed TXA (transcription document header) content for [buildMdm](#buildmdm). #### Example ```ts import type { MdmDocument } from "@cosyte/hl7"; const document: MdmDocument = { documentType: "DS", uniqueDocumentNumber: "DOC-1001", completionStatus: "AU", // authenticated availabilityStatus: "AV", // a different axis: available body: [{ setId: "1", valueType: "TX", value: "Discharge summary text." }], }; ``` #### Properties ##### activityDateTime? > `readonly` `optional` **activityDateTime?**: `string` \| [`DtmParts`](#dtmparts) TXA-4 Activity Date/Time. ##### availabilityStatus? > `readonly` `optional` **availabilityStatus?**: `string` TXA-19 Document **Availability** Status (HL7 Table 0273: `AV`, `CA`, `OB`, `UN`). A different axis from [completionStatus](#completionstatus-3): never merged, never defaulted. ##### body? > `readonly` `optional` **body?**: readonly [`OruObservation`](#oruobservation)[] The transcribed narrative body, one OBX per entry. **Required for the content events** (`T02`, `T04`, `T06`, `T08`, `T10`), whose published structure gives OBX a minimum of one. ##### completionStatus? > `readonly` `optional` **completionStatus?**: `string` TXA-17 Document **Completion** Status (HL7 Table 0271: `DO`, `IP`, `AU`, `LA`, `IN`). A different axis from [availabilityStatus](#availabilitystatus-1): never merged, never defaulted. ##### documentType? > `readonly` `optional` **documentType?**: `string` TXA-2 Document Type (HL7 Table 0270), verbatim. ##### parentDocumentNumber? > `readonly` `optional` **parentDocumentNumber?**: `string` TXA-13 Parent Document Number: the addendum or replacement link. ##### setId? > `readonly` `optional` **setId?**: `string` TXA-1 Set ID. ##### uniqueDocumentNumber? > `readonly` `optional` **uniqueDocumentNumber?**: `string` TXA-12 Unique Document Number. *** ### Medication A medication extracted from one RXO/RXE/RXD/RXA segment, with its RXR (route) and RXC (component) children grouped positionally. `context` records which RX* segment this came from (give vs dispense vs administered). **Safety contract.** A wrong drug, strength, or route can harm a real patient, so this view is deliberately conservative: - `giveCode` carries its own coding-system provenance via the CWE (`giveCode.nameOfCodingSystem`: e.g. `RXN` RxNorm, `NDC`). The helper surfaces the *claim*; it never validates or looks the code up. - `amount` (how much) and `strength` (concentration) are SEPARATE fields and are never reconciled: including against any strength a coded drug implies. A disagreement is preserved for the consumer to see. - Malformed RX* segments never throw: absent fields are omitted keys. `routes` and `components` are ALWAYS present (possibly empty). So is `timings`: empty when no TQ1 / legacy embedded TQ (RXE-1) accompanies the medication; the repeat pattern is surfaced **verbatim, never normalized to a schedule**. Deferred (not v1): sig/frequency *interpretation*, dose-range or interaction checking, pharmacologic resolution of compounds. `orderControl` is ORC-1 of the ORC that opened this medication's order group, surfaced **verbatim** (`NW`, `DC`, `HD`, an unlisted code, any letter case) and never classified into an active, held or ended state. #### Example ```ts import type { Medication } from "@cosyte/hl7"; const med: Medication = { orderControl: "NW", context: "encoded", giveCode: { identifier: "1049630", text: "Acetaminophen 325 MG", nameOfCodingSystem: "RXN" }, amount: { minimum: 2, units: { identifier: "TAB" } }, strength: { value: 325, units: { identifier: "mg", nameOfCodingSystem: "UCUM" } }, routes: [{ route: { identifier: "PO", text: "Oral" } }], components: [], timings: [{ source: "TQ1", repeatPattern: { code: "Q6H", kind: "parametric", interval: { count: 6, unit: "H" } } }], }; ``` #### Properties ##### amount? > `readonly` `optional` **amount?**: [`MedicationAmount`](#medicationamount-1) Give/dispense/administered amount (+ units). See [MedicationAmount](#medicationamount-1). ##### components > `readonly` **components**: readonly [`MedicationComponent`](#medicationcomponent)[] RXC children grouped under this RX* (compound components). Always present (possibly empty). ##### context > `readonly` **context**: [`MedicationContext`](#medicationcontext-1) Which RX* segment this medication came from (give/dispense/administered). ##### dosageForm? > `readonly` `optional` **dosageForm?**: [`CWE`](#cwe-1) RXO-5 requested dosage form (order context). ##### giveCode? > `readonly` `optional` **giveCode?**: [`CWE`](#cwe-1) RXO-1 / RXE-2 / RXD-2 / RXA-5 give/dispense/administered drug code, with provenance. ##### orderControl? > `readonly` `optional` **orderControl?**: `string` ORC-1 order control of the ORC that opened this medication's order group (the run of segments from that ORC to the next ORC), exactly as sent: never trimmed, case-folded, looked up, or classified into an active, held or ended state. Every RX* segment in one group carries the same value. Omitted when no ORC precedes the RX* segment in the message or that ORC's ORC-1 is empty; it is never carried over from an earlier group. ##### routes > `readonly` **routes**: readonly [`MedicationRoute`](#medicationroute)[] RXR children grouped under this RX* (Table 0162 route). Always present (possibly empty). ##### strength? > `readonly` `optional` **strength?**: [`MedicationStrength`](#medicationstrength-1) RXE-25/26 give strength: ENCODED context only; never reconciled with `giveCode`. ##### timings > `readonly` **timings**: readonly [`OrderTiming`](#ordertiming)[] TQ1 / legacy embedded-TQ (RXE-1) timing(s) grouped under this medication. Always present: empty when the medication carries no timing. See [OrderTiming](#ordertiming). *** ### MedicationAmount The give / dispense / administered amount of a `Medication`. Carries the HL7 min/max amount pair and its units. - For an **order** (RXO-2/3) or **encoded** order (RXE-3/4) the amount is a genuine min..max range: both keys may be present. - For a **dispense** (RXD-4) or **administration** (RXA-6) there is a SINGLE amount; it is surfaced as `minimum` with `maximum` OMITTED. This is a single value, not a range: do not read the absent `maximum` as "no upper bound on a range". `minimum`/`maximum` are strict-`Number()` parsed (`undefined`, never `NaN`). `units` is the give/dispense/administered units CWE (RXO-4 / RXE-5 / RXD-5 / RXA-7); check `units.nameOfCodingSystem === "UCUM"` for computable units. #### Example ```ts import type { MedicationAmount } from "@cosyte/hl7"; const amount: MedicationAmount = { minimum: 250, units: { identifier: "mg", nameOfCodingSystem: "UCUM" } }; ``` #### Properties ##### maximum? > `readonly` `optional` **maximum?**: `number` RXO-3 / RXE-4 maximum. OMITTED for single-amount (dispense/administration) contexts. ##### minimum? > `readonly` `optional` **minimum?**: `number` RXO-2 / RXE-3 minimum, or the single dispense (RXD-4) / administered (RXA-6) amount. ##### units? > `readonly` `optional` **units?**: [`CWE`](#cwe-1) RXO-4 / RXE-5 / RXD-5 / RXA-7 give/dispense/administered units. *** ### MedicationComponent One RXC (Pharmacy/Treatment Component Order) grouped under its parent RX* segment: a component of a compound/IV. Surfaced STRUCTURALLY (the component list as authored), NOT pharmacologically resolved. #### Example ```ts import type { MedicationComponent } from "@cosyte/hl7"; const c: MedicationComponent = { type: "B", code: { identifier: "D5W", text: "Dextrose 5%" }, amount: 1000 }; ``` #### Properties ##### amount? > `readonly` `optional` **amount?**: `number` RXC-3 component amount (strict-parsed; never `NaN`). ##### code? > `readonly` `optional` **code?**: [`CWE`](#cwe-1) RXC-2 component code. ##### type? > `readonly` `optional` **type?**: `string` RXC-1 component type (e.g. "B"=base, "A"=additive: HL7 Table 0166). ##### units? > `readonly` `optional` **units?**: [`CWE`](#cwe-1) RXC-4 component units. *** ### MedicationRoute One RXR (Pharmacy/Treatment Route) grouped under its parent RX* segment. `route` is HL7 Table 0162 (CWE); `site` is Table 0163 (CWE). Provenance travels on the CWE (`route.nameOfCodingSystem`): a "PO" route is only safe to act on when you know the system it was coded against. #### Example ```ts import type { MedicationRoute } from "@cosyte/hl7"; const r: MedicationRoute = { route: { identifier: "PO", text: "Oral" } }; ``` #### Properties ##### route? > `readonly` `optional` **route?**: [`CWE`](#cwe-1) RXR-1 route of administration (HL7 Table 0162). ##### site? > `readonly` `optional` **site?**: [`CWE`](#cwe-1) RXR-2 administration site (HL7 Table 0163). *** ### MedicationStrength The give *strength* of an **encoded** `Medication` (RXE-25 value + RXE-26 units). Strength is the concentration of active ingredient (e.g. "250 mg"), distinct from the give *amount* (how much is administered, e.g. "2 tablets"). Only the `"encoded"` (RXE) context carries strength. **Fail-safe:** strength is surfaced exactly as the explicit RXE-25/26 fields declare it, and is NEVER reconciled against any strength *implied* by the give code (e.g. an NDC that encodes "250 mg"). A consumer that sees both an explicit strength here and a coded drug in `giveCode` must treat a disagreement as a real signal: the library does not silently pick a winner. `value` is strict-`Number()` parsed (`undefined`, never `NaN`). #### Example ```ts import type { MedicationStrength } from "@cosyte/hl7"; const strength: MedicationStrength = { value: 250, units: { identifier: "mg", nameOfCodingSystem: "UCUM" } }; ``` #### Properties ##### units? > `readonly` `optional` **units?**: [`CWE`](#cwe-1) RXE-26 give strength units. ##### value? > `readonly` `optional` **value?**: `number` RXE-25 give strength numeric value (strict-parsed; never `NaN`). *** ### MessageStructure The structure summary for a parsed message: the data behind `Hl7Message.structure`. For an unrecognized type, `recognized` is `false` and every list is empty (the safety net is deliberately silent on types it does not model). #### Properties ##### expectedGroups > `readonly` **expectedGroups**: readonly [`StructureGroup`](#structuregroup)[] Per-expected-segment presence verdicts (empty when unrecognized). ##### messageCode > `readonly` **messageCode**: `string` MSH-9.1 message code observed on the message. `""` when absent, and `""` when `recognized` is `false` and the observed value is not identifier-shaped (MSH-9 can hold a data field on a malformed message). ##### missingGroups > `readonly` **missingGroups**: readonly `string`[] Names of the expectations that are absent: the warnings. ##### missingSegments > `readonly` **missingSegments**: readonly `string`[] The required segments the message does not carry. ##### recognized > `readonly` **recognized**: `boolean` `true` when a `MESSAGE_STRUCTURE_DEFINITIONS` entry matched the type. ##### requiredSegments > `readonly` **requiredSegments**: readonly `string`[] Every segment the published structure requires (empty when unrecognized). ##### structureIds > `readonly` **structureIds**: readonly `string`[] The published structure ids the verdict was derived from. Empty when the type is unrecognized or the entry is a retained transcription. ##### triggerEvent > `readonly` **triggerEvent**: `string` MSH-9.2 trigger event observed on the message. `""` when absent, and `""` when `recognized` is `false` and the observed value is not identifier-shaped. Note that on the `recognized` branch this is echoed verbatim, which for a definition matching on message code alone (`ACK`) means an arbitrary MSH-9.2. *** ### MessageStructureDefinition The expected structure of one recognized message type. An entry covers one message code and the trigger events the publication maps to a single structure. An empty `triggerEvents` list means "match on message code alone" (used for `ACK`, whose MSH-9.2 carries the acknowledged message's trigger event rather than one of its own). #### Properties ##### derivation > `readonly` **derivation**: [`StructureDerivation`](#structurederivation) Whether the expectations were derived or retained. ##### expectedGroups > `readonly` **expectedGroups**: readonly [`ExpectedSegmentGroup`](#expectedsegmentgroup)[] One entry per segment the published structure requires. ##### messageCode > `readonly` **messageCode**: `string` MSH-9.1 message code, e.g. `"ORU"`. ##### requiredSegments > `readonly` **requiredSegments**: readonly `string`[] The required segment names, sorted: the same set, flattened. ##### retainedReason > `readonly` **retainedReason**: `string` Why a retained transcription could not be derived from the publication. Empty string for a derived entry. ##### structureId > `readonly` **structureId**: `string` The published structure id these expectations were derived from, e.g. `"ADT_A01"`. Empty for a retained transcription. ##### structureIds > `readonly` **structureIds**: readonly `string`[] Every member of that structure's variant family that was actually read, e.g. `["ADT_A01-A", "ADT_A01-B", "ADT_A01-C", "ADT_A01-D"]`. Empty for a retained transcription. ##### triggerEvents > `readonly` **triggerEvents**: readonly `string`[] The MSH-9.2 trigger events this definition applies to, e.g. `["R01"]`. An empty list means "match on message code alone" (e.g. `ACK`). *** ### Meta MSH-derived message metadata (HELPERS-01). D-03: always defined on `Hl7Message.meta` (MSH absence throws `NO_MSH_SEGMENT` at parse time); individual fields are optional because vendor-quirky messages routinely omit pieces of MSH. `timestamp` is the fidelity `TS` (precision + timezone preserved), not an eager UTC-assuming `Date`. #### Example ```ts import type { Meta } from "@cosyte/hl7"; const meta: Meta = { type: "ADT^A01", messageCode: "ADT", triggerEvent: "A01", controlId: "MSG001", version: "2.5", }; console.log(meta.timestamp?.raw, meta.timestamp?.precision); ``` #### Properties ##### controlId? > `readonly` `optional` **controlId?**: `string` MSH-10 message control ID: unique per message per sender. ##### messageCode? > `readonly` `optional` **messageCode?**: `string` MSH-9.1 message code (e.g. "ADT", "ORU"). ##### messageStructure? > `readonly` `optional` **messageStructure?**: `string` MSH-9.3 message structure (e.g. "ADT_A01"). ##### processingId? > `readonly` `optional` **processingId?**: `string` MSH-11.1 processing id (P=production, T=test, D=debug). ##### receivingApp? > `readonly` `optional` **receivingApp?**: `string` MSH-5.1 receiving application namespace id. ##### receivingFacility? > `readonly` `optional` **receivingFacility?**: `string` MSH-6.1 receiving facility namespace id. ##### sendingApp? > `readonly` `optional` **sendingApp?**: `string` MSH-3.1 sending application namespace id. ##### sendingFacility? > `readonly` `optional` **sendingFacility?**: `string` MSH-4.1 sending facility namespace id. ##### timestamp? > `readonly` `optional` **timestamp?**: [`DtmParts`](#dtmparts) MSH-7 message date/time as the fidelity `TS`. Absent when MSH-7 is empty or unparseable. Present with `valid: false` and an `ambiguity` report when the value is a slash date whose field order cannot be established: check `valid` before reading the parts. ##### triggerEvent? > `readonly` `optional` **triggerEvent?**: `string` MSH-9.2 trigger event (e.g. "A01", "R01"). ##### type? > `readonly` `optional` **type?**: `string` MSH-9 full message type string, e.g. "ADT^A01" or "ORU^R01^ORU_R01". ##### version? > `readonly` `optional` **version?**: `string` MSH-12 HL7 version string (e.g. "2.5", "2.5.1"). *** ### NextOfKin NK1-derived next-of-kin entry (HELPERS-06). Lean subset: callers can reach for `msg.segments("NK1")` when they need the full NK1 surface. #### Example ```ts import type { NextOfKin } from "@cosyte/hl7"; const nk: NextOfKin = { name: { familyName: "Doe", givenName: "John" }, relationship: { identifier: "FTH", text: "Father" }, }; ``` #### Properties ##### address? > `readonly` `optional` **address?**: [`XAD`](#xad-1) NK1-4 address. ##### contactRole? > `readonly` `optional` **contactRole?**: [`CWE`](#cwe-1) NK1-7 contact role. ##### name? > `readonly` `optional` **name?**: [`XPN`](#xpn-1) NK1-2 next-of-kin name. ##### phone? > `readonly` `optional` **phone?**: [`XTN`](#xtn-1) NK1-5 phone (first repetition). ##### relationship? > `readonly` `optional` **relationship?**: [`CWE`](#cwe-1) NK1-3 relationship to patient (FTH=father, MTH=mother, SPO=spouse, ...). *** ### NM HL7 v2 Numeric (NM) composite. Carries both the raw HL7 numeric string and the parsed JS `number`. `.value` is `undefined` when the raw string is empty or not fully numeric: NEVER throws. Unlike most composites in this phase, both `raw` and `value` are ALWAYS-PRESENT keys (not optional): `value` is explicitly typed as `number | undefined` so callers can destructure uniformly. #### Example ```ts import type { NM } from "@cosyte/hl7"; const glucose: NM = { raw: "120", value: 120 }; const bad: NM = { raw: "N/A", value: undefined }; ``` #### Properties ##### raw > `readonly` **raw**: `string` ##### value > `readonly` **value**: `number` \| `undefined` *** ### ObservationBase Fields shared by every `Observation` variant, regardless of the OBX-2 value type. Split from the discriminated union to keep the union declaration readable (D-15 locked field list). #### Example ```ts import type { ObservationBase } from "@cosyte/hl7"; const base: ObservationBase = { setId: "1", identifier: { identifier: "GLU", text: "Glucose" }, resultStatus: { classification: "undetermined", table: { name: "HL7 Table 0085", version: "3.0.0" }, map: { url: "http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70085-to-observation-status", version: "1.0.0", }, }, }; ``` #### Properties ##### abnormalFlags? > `readonly` `optional` **abnormalFlags?**: `string` OBX-8 abnormal flags (e.g. "H", "HH", "L", "LL"). ##### identifier > `readonly` **identifier**: [`CWE`](#cwe-1) OBX-3 observation identifier. Always present (may be `{}` if OBX-3 absent). ##### notes? > `readonly` `optional` **notes?**: readonly `string`[] NTE note lines positionally attached to this OBX: each non-empty NTE-3 (Comment, FT) repetition of every NTE immediately following this observation, HL7-unescaped, in document order. OMITTED when the observation carries no notes. High-PHI-risk clinical narrative. ##### observedDateTime? > `readonly` `optional` **observedDateTime?**: [`DtmParts`](#dtmparts) OBX-14 date/time of observation as the fidelity `TS`. ##### referenceRange? > `readonly` `optional` **referenceRange?**: `string` OBX-7 reference range (e.g. "80-110"). ##### resultStatus > `readonly` **resultStatus**: [`ResultStatusClassification`](#resultstatusclassification) OBX-11 read against HL7 Table 0085 and classified by HL7's Table 0085 to Observation Status map. Always present: `"undetermined"` when OBX-11 is absent, unmapped or not exactly one code. See [ResultStatusClassification](#resultstatusclassification). ##### setId? > `readonly` `optional` **setId?**: `string` OBX-1 set id (string: typically sequential "1", "2", ...). ##### status? > `readonly` `optional` **status?**: `string` OBX-11 observation result status (e.g. "F"=final, "P"=preliminary). ##### units? > `readonly` `optional` **units?**: [`CWE`](#cwe-1) OBX-6 units. ##### unitsAreUcum? > `readonly` `optional` **unitsAreUcum?**: `boolean` `true` iff OBX-6's coding system (CWE.3, "name of coding system") is exactly `UCUM` (HL7 Table 0396): i.e. the unit is declared UCUM and is safe to interpret as a computable unit. `false` means a unit IS present but is NOT declared UCUM (e.g. a local code or free text) and is surfaced as-is, never coerced. OMITTED when OBX-6 is absent. This is a *claim* check only: the library does not validate UCUM grammar or check the alternate coding system (CWE.6). *** ### Order OBR-derived order (HELPERS-05, D-16) with positionally-grouped OBX children (D-12). `observations` is ALWAYS present: empty when no OBX follows this OBR before the next OBR or end-of-message. `timings` is ALWAYS present, empty when the order carries no TQ1 / legacy embedded TQ. #### Example ```ts import type { Order } from "@cosyte/hl7"; const order: Order = { placerOrderNumber: "PLACER1", fillerOrderNumber: "FILLER1", universalServiceId: { identifier: "GLU", text: "Glucose" }, orderStatus: "F", resultStatus: { classification: "final", code: "F", table: { name: "HL7 Table 0123", version: "3.0.0" }, map: { url: "http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70123-queries-to-diagnostic-report-status", version: "1.0.0", }, }, observations: [], timings: [], }; ``` #### Properties ##### fillerOrderNumber? > `readonly` `optional` **fillerOrderNumber?**: `string` OBR-3 filler order number. ##### notes? > `readonly` `optional` **notes?**: readonly `string`[] NTE note lines positionally attached to this order: the ORC-region notes (before the OBR) followed by the OBR-region notes, in document order. Several ORCs before one OBR all contribute here; nothing is dropped. OMITTED when the order carries no notes. A note on a trailing or dangling ORC that never opens an order is surfaced at message level (`msg.notes()`), not here: still never dropped. High-PHI-risk clinical narrative. ##### observations > `readonly` **observations**: readonly [`Observation`](#observation)[] OBX children grouped under this OBR (D-12 positional grouping). Always present. ##### orderControl? > `readonly` `optional` **orderControl?**: `string` ORC-1 order control when an ORC precedes this OBR. ##### orderedBy? > `readonly` `optional` **orderedBy?**: [`XCN`](#xcn-1) OBR-16 ordering provider (D-24a XCN). ##### orderStatus? > `readonly` `optional` **orderStatus?**: `string` OBR-25 result status (HL7 Table 0123, e.g. `"F"` final, `"P"` preliminary). ##### placerOrderNumber? > `readonly` `optional` **placerOrderNumber?**: `string` OBR-2 placer order number. ##### resultStatus > `readonly` **resultStatus**: [`ResultStatusClassification`](#resultstatusclassification) OBR-25 read against HL7 Table 0123 and classified by HL7's Table 0123 to Diagnostic Report Status map. Always present: `"undetermined"` when OBR-25 is absent, unmapped or not exactly one code. Classified from OBR-25 alone, never from the statuses of the order's observations, each of which carries its own. See [ResultStatusClassification](#resultstatusclassification). ##### timings > `readonly` **timings**: readonly [`OrderTiming`](#ordertiming)[] TQ1 / legacy embedded-TQ timing(s) grouped under this order. Always present: empty when the order carries no timing. See [OrderTiming](#ordertiming). ##### universalServiceId? > `readonly` `optional` **universalServiceId?**: [`CWE`](#cwe-1) OBR-4 universal service identifier (test code + description). *** ### OrderTiming The order/medication **timing** structure: one TQ1 segment (v2.5+) or the legacy embedded TQ in ORC-7 / RXE-1 (pre-v2.5). Attached to [Order.timings](#timings-1) and [Medication.timings](#timings). **Safety contract.** hl7 surfaces the timing **structure**; it does **not** compute administration schedules, resolve "institution-specified times" to clock times, or interpret sig. The load-bearing [repeatPattern](#repeatpattern) and [totalOccurrences](#totaloccurrences) are preserved verbatim (see [RepeatPattern](#repeatpattern-1)); `startDateTime`/`endDateTime` keep the `TS` precision + timezone fidelity. A malformed timing never throws: absent pieces are omitted keys. #### Example ```ts import type { OrderTiming } from "@cosyte/hl7"; const t: OrderTiming = { source: "TQ1", quantity: { value: 1 }, repeatPattern: { code: "Q6H", kind: "parametric", interval: { count: 6, unit: "H" } }, totalOccurrences: 20, }; ``` #### Properties ##### endDateTime? > `readonly` `optional` **endDateTime?**: [`DtmParts`](#dtmparts) TQ1-8 / legacy TQ.5 end date/time as the fidelity `TS`. ##### explicitTime? > `readonly` `optional` **explicitTime?**: `string` TQ1-4 / legacy TQ.2 interval RI.2 explicit time(s): surfaced verbatim (first repetition/value). ##### priority? > `readonly` `optional` **priority?**: [`CWE`](#cwe-1) TQ1-9 priority (CWE) / legacy TQ.6 priority (surfaced as a CWE `{ identifier }`). ##### quantity? > `readonly` `optional` **quantity?**: [`TimingQuantity`](#timingquantity) TQ1-2 / legacy TQ.1 service quantity (CQ). ##### repeatPattern? > `readonly` `optional` **repeatPattern?**: [`RepeatPattern`](#repeatpattern-1) TQ1-3 / legacy TQ.2 interval RI.1 repeat pattern (Table 0335): verbatim. See [RepeatPattern](#repeatpattern-1). ##### serviceDuration? > `readonly` `optional` **serviceDuration?**: `string` TQ1-6 / legacy TQ.3 service duration: surfaced verbatim. ##### source > `readonly` **source**: `"TQ1"` \| `"legacy"` Which structure this timing was read from: the dedicated **TQ1** segment (v2.5+) or the **legacy** embedded TQ data type in ORC-7 (orders) / RXE-1 (encoded medications, pre-v2.5). The library treats the presence of a TQ1 segment as the v2.5+ signal: the legacy embedded TQ is surfaced only when no TQ1 accompanies the order, so the same timing is never double-counted and a legacy-only timing is never dropped. ##### startDateTime? > `readonly` `optional` **startDateTime?**: [`DtmParts`](#dtmparts) TQ1-7 / legacy TQ.4 start date/time as the fidelity `TS`. ##### totalOccurrences? > `readonly` `optional` **totalOccurrences?**: `number` TQ1-14 / legacy TQ.12 total occurrences (NM): how many times the service is to be performed (strict-parsed; never `NaN`). **TQ1-14, not TQ1-11** (TQ1-11 is Text Instruction). Load-bearing: losing it drops the total administered count. *** ### OrmOrder Typed order content for [buildOrm](#buildorm): the OBR order detail every typed builder shares, plus the ORC control information and any OBX children. #### Example ```ts import type { OrmOrder } from "@cosyte/hl7"; const order: OrmOrder = { orderControl: "NW", setId: "1", placerOrderNumber: "PL-1001", universalServiceId: { identifier: "CBC", text: "Complete Blood Count" }, orderingProvider: { idNumber: "9990", familyName: "Welby" }, }; ``` #### Extends - [`OruOrder`](#oruorder) #### Properties ##### fillerOrderNumber? > `readonly` `optional` **fillerOrderNumber?**: `string` OBR-3 Filler Order Number. ###### Inherited from [`OruOrder`](#oruorder).[`fillerOrderNumber`](#fillerordernumber-2) ##### observationDateTime? > `readonly` `optional` **observationDateTime?**: `string` \| [`DtmParts`](#dtmparts) OBR-7 Observation Date/Time. ###### Inherited from [`OruOrder`](#oruorder).[`observationDateTime`](#observationdatetime-2) ##### observations? > `readonly` `optional` **observations?**: readonly [`OruObservation`](#oruobservation)[] OBX children of this order, in the order supplied. ##### orcFillerOrderNumber? > `readonly` `optional` **orcFillerOrderNumber?**: `string` ORC-3 Filler Order Number. Defaults to nothing: never copied from OBR-3. ##### orcPlacerOrderNumber? > `readonly` `optional` **orcPlacerOrderNumber?**: `string` ORC-2 Placer Order Number. Defaults to nothing: never copied from OBR-2. ##### orderControl? > `readonly` `optional` **orderControl?**: `string` ORC-1 Order Control (HL7 Table 0119: `NW` new order, `CA` cancel, `XO` change, …). An ORC is emitted for every order regardless, because the published expectation for this message type is exactly that segment; this value fills ORC-1 when supplied and is never defaulted. ##### orderingProvider? > `readonly` `optional` **orderingProvider?**: [`XCN`](#xcn-1) \| readonly [`XCN`](#xcn-1)[] OBR-16 Ordering Provider. ###### Inherited from [`OruOrder`](#oruorder).[`orderingProvider`](#orderingprovider-1) ##### placerOrderNumber? > `readonly` `optional` **placerOrderNumber?**: `string` OBR-2 Placer Order Number. ###### Inherited from [`OruOrder`](#oruorder).[`placerOrderNumber`](#placerordernumber-2) ##### resultStatus? > `readonly` `optional` **resultStatus?**: `string` OBR-25 Result Status (e.g. `"F"` final, `"P"` preliminary, `"C"` corrected). ###### Inherited from [`OruOrder`](#oruorder).[`resultStatus`](#resultstatus-3) ##### setId? > `readonly` `optional` **setId?**: `string` OBR-1 Set ID. ###### Inherited from [`OruOrder`](#oruorder).[`setId`](#setid-7) ##### universalServiceId? > `readonly` `optional` **universalServiceId?**: [`CWE`](#cwe-1) OBR-4 Universal Service Identifier. ###### Inherited from [`OruOrder`](#oruorder).[`universalServiceId`](#universalserviceid-2) *** ### OruObservation Typed OBX (observation / result) content for [buildOru](#buildoru). #### Properties ##### abnormalFlags? > `readonly` `optional` **abnormalFlags?**: `string` OBX-8 Abnormal Flags (e.g. `"H"`, `"L"`, `"N"`). ##### identifier? > `readonly` `optional` **identifier?**: [`CWE`](#cwe-1) OBX-3 Observation Identifier. ##### observationDateTime? > `readonly` `optional` **observationDateTime?**: `string` \| [`DtmParts`](#dtmparts) OBX-14 Date/Time of the Observation. ##### observationResultStatus? > `readonly` `optional` **observationResultStatus?**: `string` OBX-11 Observation Result Status (e.g. `"F"` final, `"P"` preliminary). ##### referenceRange? > `readonly` `optional` **referenceRange?**: `string` OBX-7 References Range. ##### setId? > `readonly` `optional` **setId?**: `string` OBX-1 Set ID. ##### units? > `readonly` `optional` **units?**: [`CWE`](#cwe-1) OBX-6 Units. ##### value? > `readonly` `optional` **value?**: `string` OBX-5 Observation Value (emitted verbatim: the caller owns its formatting). ##### valueType? > `readonly` `optional` **valueType?**: `string` OBX-2 Value Type (e.g. `"NM"`, `"ST"`, `"CE"`, `"TX"`). *** ### OruOrder Typed OBR (observation request / order) content for [buildOru](#buildoru). #### Extended by - [`OrmOrder`](#ormorder) #### Properties ##### fillerOrderNumber? > `readonly` `optional` **fillerOrderNumber?**: `string` OBR-3 Filler Order Number. ##### observationDateTime? > `readonly` `optional` **observationDateTime?**: `string` \| [`DtmParts`](#dtmparts) OBR-7 Observation Date/Time. ##### orderingProvider? > `readonly` `optional` **orderingProvider?**: [`XCN`](#xcn-1) \| readonly [`XCN`](#xcn-1)[] OBR-16 Ordering Provider. ##### placerOrderNumber? > `readonly` `optional` **placerOrderNumber?**: `string` OBR-2 Placer Order Number. ##### resultStatus? > `readonly` `optional` **resultStatus?**: `string` OBR-25 Result Status (e.g. `"F"` final, `"P"` preliminary, `"C"` corrected). ##### setId? > `readonly` `optional` **setId?**: `string` OBR-1 Set ID. ##### universalServiceId? > `readonly` `optional` **universalServiceId?**: [`CWE`](#cwe-1) OBR-4 Universal Service Identifier. *** ### OverlayStructures Every published overlay key, mapped to the segment names the pair's published structure marks required. This is the compile-time face of the derived structure registry: the runtime support claim ([SUPPORTED\_OVERLAY\_MESSAGES](#supported_overlay_messages)) is computed from that registry, and a test compares the two in both directions so this table can never claim a pair the registry does not recognize. A key is `"^"` (`"ADT^A01"`), or `""` alone for a registry entry that matches on message code alone (`"ACK"`, whose MSH-9.2 carries the acknowledged message's trigger event rather than one of its own). #### Example ```ts import type { OverlayStructures } from "@cosyte/hl7"; // The required segments for one pair, at the type level: type A01 = OverlayStructures["ADT^A01"]; // readonly ["EVN", "MSH", "PID", "PV1"] ``` #### Properties ##### ACK > `readonly` **ACK**: readonly \[`"MSA"`, `"MSH"`\] ##### ADT^A01 > `readonly` **ADT^A01**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A02 > `readonly` **ADT^A02**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A03 > `readonly` **ADT^A03**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A04 > `readonly` **ADT^A04**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A05 > `readonly` **ADT^A05**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A06 > `readonly` **ADT^A06**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A07 > `readonly` **ADT^A07**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A08 > `readonly` **ADT^A08**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A09 > `readonly` **ADT^A09**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A10 > `readonly` **ADT^A10**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A11 > `readonly` **ADT^A11**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A12 > `readonly` **ADT^A12**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A13 > `readonly` **ADT^A13**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A14 > `readonly` **ADT^A14**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A15 > `readonly` **ADT^A15**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PRT"`, `"PV1"`\] ##### ADT^A16 > `readonly` **ADT^A16**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A17 > `readonly` **ADT^A17**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A20 > `readonly` **ADT^A20**: readonly \[`"EVN"`, `"MSH"`, `"NPU"`\] ##### ADT^A21 > `readonly` **ADT^A21**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A22 > `readonly` **ADT^A22**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A23 > `readonly` **ADT^A23**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A24 > `readonly` **ADT^A24**: readonly \[`"EVN"`, `"MSH"`, `"PID"`\] ##### ADT^A25 > `readonly` **ADT^A25**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A26 > `readonly` **ADT^A26**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A27 > `readonly` **ADT^A27**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A28 > `readonly` **ADT^A28**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A29 > `readonly` **ADT^A29**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A31 > `readonly` **ADT^A31**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A32 > `readonly` **ADT^A32**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A33 > `readonly` **ADT^A33**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A37 > `readonly` **ADT^A37**: readonly \[`"EVN"`, `"MSH"`, `"PID"`\] ##### ADT^A38 > `readonly` **ADT^A38**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A40 > `readonly` **ADT^A40**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`\] ##### ADT^A41 > `readonly` **ADT^A41**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`\] ##### ADT^A42 > `readonly` **ADT^A42**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`\] ##### ADT^A43 > `readonly` **ADT^A43**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`\] ##### ADT^A44 > `readonly` **ADT^A44**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`\] ##### ADT^A45 > `readonly` **ADT^A45**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A47 > `readonly` **ADT^A47**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`\] ##### ADT^A49 > `readonly` **ADT^A49**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`\] ##### ADT^A50 > `readonly` **ADT^A50**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A51 > `readonly` **ADT^A51**: readonly \[`"EVN"`, `"MRG"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A52 > `readonly` **ADT^A52**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A53 > `readonly` **ADT^A53**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A54 > `readonly` **ADT^A54**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A55 > `readonly` **ADT^A55**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A60 > `readonly` **ADT^A60**: readonly \[`"EVN"`, `"MSH"`, `"PID"`\] ##### ADT^A61 > `readonly` **ADT^A61**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### ADT^A62 > `readonly` **ADT^A62**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`\] ##### DFT^P03 > `readonly` **DFT^P03**: readonly \[`"EVN"`, `"FT1"`, `"MSH"`, `"PID"`\] ##### DFT^P11 > `readonly` **DFT^P11**: readonly \[`"EVN"`, `"FT1"`, `"MSH"`, `"PID"`\] ##### MDM^T01 > `readonly` **MDM^T01**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T02 > `readonly` **MDM^T02**: readonly \[`"EVN"`, `"MSH"`, `"OBX"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T03 > `readonly` **MDM^T03**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T04 > `readonly` **MDM^T04**: readonly \[`"EVN"`, `"MSH"`, `"OBX"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T05 > `readonly` **MDM^T05**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T06 > `readonly` **MDM^T06**: readonly \[`"EVN"`, `"MSH"`, `"OBX"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T07 > `readonly` **MDM^T07**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T08 > `readonly` **MDM^T08**: readonly \[`"EVN"`, `"MSH"`, `"OBX"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T09 > `readonly` **MDM^T09**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T10 > `readonly` **MDM^T10**: readonly \[`"EVN"`, `"MSH"`, `"OBX"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### MDM^T11 > `readonly` **MDM^T11**: readonly \[`"EVN"`, `"MSH"`, `"PID"`, `"PV1"`, `"TXA"`\] ##### OMG^O19 > `readonly` **OMG^O19**: readonly \[`"MSH"`, `"OBR"`, `"ORC"`\] ##### OMI^O23 > `readonly` **OMI^O23**: readonly \[`"IPC"`, `"MSH"`, `"OBR"`, `"ORC"`\] ##### OML^O21 > `readonly` **OML^O21**: readonly \[`"MSH"`, `"ORC"`\] ##### OML^O33 > `readonly` **OML^O33**: readonly \[`"MSH"`, `"ORC"`, `"SPM"`\] ##### OML^O35 > `readonly` **OML^O35**: readonly \[`"MSH"`, `"ORC"`, `"SAC"`, `"SPM"`\] ##### OML^O39 > `readonly` **OML^O39**: readonly \[`"MSH"`, `"ORC"`\] ##### OML^O59\_A > `readonly` **OML^O59\_A**: readonly \[`"MSH"`, `"ORC"`\] ##### OMP^O09 > `readonly` **OMP^O09**: readonly \[`"MSH"`, `"ORC"`, `"RXO"`, `"RXR"`\] ##### ORM^O01 > `readonly` **ORM^O01**: readonly \[`"ORC"`\] ##### ORU^R01 > `readonly` **ORU^R01**: readonly \[`"MSH"`, `"OBR"`\] ##### ORU^R30 > `readonly` **ORU^R30**: readonly \[`"MSH"`, `"OBR"`, `"OBX"`, `"ORC"`, `"PID"`\] ##### ORU^R31 > `readonly` **ORU^R31**: readonly \[`"MSH"`, `"OBR"`, `"OBX"`, `"ORC"`, `"PID"`\] ##### ORU^R32 > `readonly` **ORU^R32**: readonly \[`"MSH"`, `"OBR"`, `"OBX"`, `"ORC"`, `"PID"`\] ##### ORU^R40 > `readonly` **ORU^R40**: readonly \[`"MSH"`, `"OBR"`\] ##### ORU^R42 > `readonly` **ORU^R42**: readonly \[`"MSH"`, `"OBR"`\] ##### ORU^R43 > `readonly` **ORU^R43**: readonly \[`"MSH"`, `"OBR"`\] ##### SIU^S12 > `readonly` **SIU^S12**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S13 > `readonly` **SIU^S13**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S14 > `readonly` **SIU^S14**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S15 > `readonly` **SIU^S15**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S16 > `readonly` **SIU^S16**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S17 > `readonly` **SIU^S17**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S18 > `readonly` **SIU^S18**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S19 > `readonly` **SIU^S19**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S20 > `readonly` **SIU^S20**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S21 > `readonly` **SIU^S21**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S22 > `readonly` **SIU^S22**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S23 > `readonly` **SIU^S23**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S24 > `readonly` **SIU^S24**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S26 > `readonly` **SIU^S26**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### SIU^S27 > `readonly` **SIU^S27**: readonly \[`"MSH"`, `"RGS"`, `"SCH"`\] ##### VXU^V04 > `readonly` **VXU^V04**: readonly \[`"MSH"`, `"PID"`\] *** ### ParseOptions Options accepted by `parseHL7` to tune lenient/strict behaviour, inject a profile, and configure optional preprocessing steps. Every field is optional; `parseHL7(raw, {})` is valid and produces the library defaults. #### Remarks With `exactOptionalPropertyTypes: true`, callers cannot pass `{ strict: undefined }`: either omit the key or pass a boolean. The `profile: null` form is the explicit opt-out from the process-scoped default profile (PROF-08 semantics); `profile` omitted means "use the default if one is registered". #### Example ```ts import { parseHL7, type ParseOptions } from "@cosyte/hl7"; const opts: ParseOptions = { strict: true, onWarning: (w) => console.warn(w.code), dateFormats: ["YYYY-MM-DD"], }; parseHL7(raw, opts); ``` #### Properties ##### charset? > `readonly` `optional` **charset?**: `string` Override the character set used to decode `Buffer` input. When supplied this wins over MSH-18 auto-discovery. When both are supplied and they disagree (after alias normalization) the parser emits `ENCODING_MISMATCH` and honours this override. Ignored for `string` input. ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; parseHL7(buf, { charset: "ISO-8859-1" }); ``` ##### dateFormats? > `readonly` `optional` **dateFormats?**: readonly `string`[] Date formats your sender writes, tried in order after the canonical HL7 shape fails. They reach **every** datetime the library returns, from `msg.meta.timestamp` to `patient.dateOfBirth` to an appointment's start time, and a `TS` that matched one names it on `matchedFormat`. These are tried ahead of an applied profile's own `dateFormats`, and they are the ONLY non-canonical formats a typed datetime field accepts: a value you have not described stays `valid: false` rather than being guessed at. Tokens come from `SUPPORTED_DATE_TOKENS`; an entry with no supported token matches nothing rather than throwing. ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) ##### profile? > `readonly` `optional` **profile?**: [`Profile`](#profile-2) \| `null` ##### strict? > `readonly` `optional` **strict?**: `boolean` ##### stripMllpFraming? > `readonly` `optional` **stripMllpFraming?**: `boolean` ##### trimFields? > `readonly` `optional` **trimFields?**: `boolean` *** ### Patient PID-derived patient view (HELPERS-02). `msg.patient` is `undefined` (D-04) when no PID segment exists; this interface describes the shape when present. `identifiers` and `phoneNumbers` are ALWAYS present as arrays (D-09 / D-20): empty when the underlying field is absent. `name` is ALWAYS present (D-19) even if `{}` when PID-5 is empty. #### Example ```ts import type { Patient } from "@cosyte/hl7"; const p: Patient = { mrn: "MRN123", identifiers: [{ idNumber: "MRN123", identifierTypeCode: "MR" }], name: { familyName: "Smith", givenName: "Jane" }, familyName: "Smith", givenName: "Jane", fullName: "Jane Smith", phoneNumbers: [], }; console.log(p.dateOfBirth?.raw, p.dateOfBirth?.precision); ``` #### Properties ##### address? > `readonly` `optional` **address?**: [`XAD`](#xad-1) PID-11 home address parsed as XAD. ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: [`DtmParts`](#dtmparts) PID-7 date of birth as the fidelity `TS`. A day-only DOB keeps `precision: "day"`: never coerced to a UTC-midnight instant that would read as the previous day in a negative-offset zone. ##### ethnicity? > `readonly` `optional` **ethnicity?**: [`CWE`](#cwe-1) PID-22 ethnic group. ##### familyName? > `readonly` `optional` **familyName?**: `string` PID-5.1 flat family name convenience (D-19). ##### fullName? > `readonly` `optional` **fullName?**: `string` Composed Western-order name "Given Middle Family, Suffix" (D-17). ##### givenName? > `readonly` `optional` **givenName?**: `string` PID-5.2 flat given name convenience (D-19). ##### identifiers > `readonly` **identifiers**: readonly [`CX`](#cx-1)[] Full PID-3 identifier list, each parsed as a CX. Always present (D-09). ##### language? > `readonly` `optional` **language?**: [`CE`](#ce) PID-15 primary language. ##### middleName? > `readonly` `optional` **middleName?**: `string` PID-5.3 mapped from `XPN.secondName` (D-19). ##### mrn? > `readonly` `optional` **mrn?**: `string` Medical record number picked via `pickMrn` (D-07 / D-08). ##### name > `readonly` **name**: [`XPN`](#xpn-1) Full PID-5 parsed name (first repetition). Always present as `{}` when empty (D-19). ##### notes? > `readonly` `optional` **notes?**: readonly `string`[] NTE note lines positionally attached to the (first) PID: notes immediately following the patient's PID segment, HL7-unescaped, in document order. OMITTED when the patient carries no notes. High-PHI-risk clinical narrative. ##### phoneNumbers > `readonly` **phoneNumbers**: readonly [`XTN`](#xtn-1)[] PID-13 (home) + PID-14 (business) repetitions concatenated. Always present (D-20). ##### race? > `readonly` `optional` **race?**: [`CWE`](#cwe-1) PID-10 race. ##### sex? > `readonly` `optional` **sex?**: `string` PID-8 administrative sex code. *** ### PL HL7 v2 Person Location (PL): structured location per HL7 Chapter 2. All 11 v1 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). `facility` uses the nested `HD` shape; `assigningAuthorityForLocation` is flattened to a plain string in v1 (HL7 spec treats it as HD-shaped). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. pointOfCare: e.g. "ICU", "ED" 2. room 3. bed 4. facility: nested HD (3 subcomponents form an HD composite) 5. locationStatus: O=Occupied, U=Unoccupied, K=Contaminated, C=Closed, H=Housekeeping, I=Isolated 6. personLocationType: C=Clinic, D=Department, H=Home, N=Nursing Unit, O=Office, R=Revenue Location 7. building 8. floor 9. locationDescription: free-text 10. comprehensiveLocationId 11. assigningAuthorityForLocation (v1: flattened to string) #### Example ```ts import type { PL } from "@cosyte/hl7"; const bed: PL = { pointOfCare: "ICU", room: "101", bed: "A", facility: { namespaceId: "HOSP", universalId: "1.2.3", universalIdType: "UUID" }, }; ``` #### Properties ##### assigningAuthorityForLocation? > `readonly` `optional` **assigningAuthorityForLocation?**: `string` ##### bed? > `readonly` `optional` **bed?**: `string` ##### building? > `readonly` `optional` **building?**: `string` ##### comprehensiveLocationId? > `readonly` `optional` **comprehensiveLocationId?**: `string` ##### facility? > `readonly` `optional` **facility?**: [`HD`](#hd-1) ##### floor? > `readonly` `optional` **floor?**: `string` ##### locationDescription? > `readonly` `optional` **locationDescription?**: `string` ##### locationStatus? > `readonly` `optional` **locationStatus?**: `string` ##### personLocationType? > `readonly` `optional` **personLocationType?**: `string` ##### pointOfCare? > `readonly` `optional` **pointOfCare?**: `string` ##### room? > `readonly` `optional` **room?**: `string` *** ### PredicateLocation Where in the message a condition predicate reads: a structural coordinate, a segment name plus the 1-indexed field, component and subcomponent positions that narrow it. Every member is a name or an index, so a location is inherently PHI-free and is safe to name in a finding. The coordinate narrows from the outside in: a `component` needs a `field`, and a `subcomponent` needs a `component`. A location that skips a level names no addressable element and is refused as [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed), as is a location whose `segment` is not a valid segment name. **A comparison statement needs a `field`.** A segment on its own has no single content to compare, so a [ComparisonPredicate](#comparisonpredicate) whose location stops at the segment is also `PROFILE_MALFORMED`. A [PresencePredicate](#presencepredicate) may stop there: it asks whether the segment occurs. #### Example ```ts import type { PredicateLocation } from "@cosyte/hl7"; const completionStatus: PredicateLocation = { segment: "RXA", field: 20 }; const identifier: PredicateLocation = { segment: "RXA", field: 9, component: 1 }; ``` #### Properties ##### component? > `readonly` `optional` **component?**: `number` 1-indexed component within the field. Requires `field`. ##### field? > `readonly` `optional` **field?**: `number` 1-indexed HL7 field position (e.g. `20` for RXA-20). Omitted targets the segment itself. ##### segment > `readonly` **segment**: `string` Segment name: 3 chars, `[A-Z][A-Z0-9]{2}` (standard or `Z…` segment). ##### subcomponent? > `readonly` `optional` **subcomponent?**: `number` 1-indexed subcomponent within the component. Requires `component`. *** ### PresencePredicate A presence statement: does the element at `location` carry content? Content is read at or below the location, so a field-only location is valued when any subcomponent of any component of any repetition is non-empty, and a segment-only location is valued when the segment occurs at all. This is the same reading of "present" the engine's `R` and `X` checks use. #### Example ```ts import type { PresencePredicate } from "@cosyte/hl7"; // "IF RXA-9.1 (Identifier) is valued" const predicate: PresencePredicate = { location: { segment: "RXA", field: 9, component: 1 }, presence: "is valued", }; ``` #### Properties ##### location > `readonly` **location**: [`PredicateLocation`](#predicatelocation) The element the statement asks about. ##### presence > `readonly` **presence**: [`PredicatePresence`](#predicatepresence) Which of the two presence statements this is. *** ### Profile Structural placeholder for HL7 profiles. A profile bundles vendor-specific tolerances, date formats, custom segment definitions, and optional callbacks. Profiles are built with the `defineProfile()` factory. `customSegments` is narrowed to the locked `CustomSegmentDefinition` shape, and `Profile` carries an optional `describe?` method so `defineProfile()`-produced profiles can be introspected without consumers needing to narrow away from the `Profile` type. The `describe` method is only populated by `defineProfile()`: hand-authored `Profile` objects may omit it. #### Example ```ts import type { Profile } from "@cosyte/hl7"; const epic: Profile = { name: "epic", description: "Epic-specific quirks and date formats", dateFormats: ["YYYYMMDDHHmmss", "YYYYMMDD"], customSegments: { ZDP: { fields: { departmentCode: 3, departmentName: 4 } }, }, segmentOverrides: { PID: { fields: { siteMrn: 19 } }, }, }; ``` #### Properties ##### customSegments? > `readonly` `optional` **customSegments?**: `Readonly`\<`Record`\<`string`, [`CustomSegmentDefinition`](#customsegmentdefinition)\>\> ##### dateFormats? > `readonly` `optional` **dateFormats?**: readonly `string`[] Date formats this vendor writes. Merged after `ParseOptions.dateFormats` and honoured on every datetime the library returns, exactly as the option form is. ##### describe? > `readonly` `optional` **describe?**: () => `string` ###### Returns `string` ##### description? > `readonly` `optional` **description?**: `string` ##### lineage? > `readonly` `optional` **lineage?**: readonly `string`[] ##### name > `readonly` **name**: `string` ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) ##### segmentOverrides? > `readonly` `optional` **segmentOverrides?**: `Readonly`\<`Record`\<`string`, [`CustomSegmentDefinition`](#customsegmentdefinition)\>\> Site-specific field NAMES bound to positions on STANDARD HL7 v2 segments, keyed by canonical segment name (`PID`, `AL1`, `MSH`, ...). A separate map from [Profile.customSegments](#customsegments-2), which stays Z-segment-only: a standard name appearing there would change which segments the parser treats as profile-claimed, and an author skimming a profile literal can see at a glance which declarations touch standard clinical fields. Read-side aliases, and ADDITIVE ONLY. A declaration here creates a new name for `Segment.get(name)` to resolve on segments of that type, and changes no existing read: positional access, dot-paths, the typed clinical accessors, the warning list and both serializations are what they were without it. *** ### ProfiledMessage A parsed message whose applied profile is statically known: every [Hl7Message](#hl7message) member unchanged, with the segment-type-keyed accessors scoped so a segment of a declared type exposes that type's declared field names to [Segment.get](#get-1). It is a view, not a subclass: the parse allocates nothing extra and behaves identically. Segments reached any other way (the undifferentiated [Hl7Message.allSegments](#allsegments) walk) keep the `string`-accepting reader, because the segment type is not knowable from a walk. #### Example ```ts import { defineProfile, parseHL7 } from "@cosyte/hl7"; const epic = defineProfile({ name: "epic", customSegments: { ZDP: { fields: { departmentCode: 3, departmentName: 4 } } }, }); const msg = parseHL7(raw, epic); const zdp = msg.part("ZDP"); console.log(zdp?.get("departmentCode")?.value); // narrowed, no cast // zdp?.get("departmentCod"); // does not compile: not a declared ZDP name console.log(msg.part("PID")?.field(5).value); // undeclared type: any name compiles ``` #### Extends - [`Hl7Message`](#hl7message) #### Type Parameters ##### P `P` #### Properties ##### dateFormats > `readonly` **dateFormats**: readonly `string`[] Merged `dateFormats` list: `options.dateFormats ++ profile.dateFormats` deduped first-occurrence per D-21. Empty array when neither source supplied any formats. This is the list every datetime in the message honours, from `meta.timestamp` to any `field.asTs()`, in exactly this order; exposed so a caller can introspect what their options and profile added up to. ###### Inherited from [`ProfiledMessage`](#profiledmessage).[`dateFormats`](#dateformats-7) ##### encodingCharacters > `readonly` **encodingCharacters**: [`EncodingCharacters`](#encodingcharacters-1) ###### Inherited from [`Hl7Message`](#hl7message).[`encodingCharacters`](#encodingcharacters) ##### profile > `readonly` **profile**: \{ `lineage`: readonly `string`[]; `name`: `string`; \} \| `undefined` ###### Inherited from [`Hl7Message`](#hl7message).[`profile`](#profile) ##### rawSegments > `readonly` **rawSegments**: readonly [`RawSegment`](#rawsegment)[] Raw positional tree produced by the parser. 1-indexed per HL7 convention (`fields[0]` is the segment-name / MSH separator placeholder slot). Use `segments(type)` / `allSegments()` for typed wrapper access: this field is exposed for advanced callers that need the raw tree directly. ###### Inherited from [`ProfiledMessage`](#profiledmessage).[`rawSegments`](#rawsegments-1) ##### version > `readonly` **version**: `string` HL7 version the model asserts for this message, from MSH-12.1.1, e.g. `"2.5"` or `"2.5.1"`. **Bounded, and it is `""` when MSH-12 does not hold a version.** Same reasoning as [Segment.type](#type): it presents as an identifier and consumers label with it. `meta.version` keeps MSH-12 exactly as it arrived, and is what version-scoped field selection reads, so this bound changes no parsing behaviour. ###### Inherited from [`Hl7Message`](#hl7message).[`version`](#version) ##### warnings > `readonly` **warnings**: readonly [`Hl7ParseWarning`](#hl7parsewarning)[] ###### Inherited from [`ProfiledMessage`](#profiledmessage).[`warnings`](#warnings-3) #### Accessors ##### meta ###### Get Signature > **get** **meta**(): [`Meta`](#meta-1) MSH-derived message metadata (type, controlId, timestamp, version, etc.). D-01: plain object. D-02: memoized: `msg.meta === msg.meta` across reads until mutation invalidates. D-03: always defined (MSH absence throws `NO_MSH_SEGMENT` at parse time). ###### Example ```ts console.log(msg.meta.type); // "ADT^A01" console.log(msg.meta.timestamp?.raw); // fidelity TS console.log(msg.meta.controlId); // "MSG001" ``` ###### Returns [`Meta`](#meta-1) ###### Inherited from [`Hl7Message`](#hl7message).[`meta`](#meta) ##### patient ###### Get Signature > **get** **patient**(): [`Patient`](#patient-8) \| `undefined` PID-derived patient view, or `undefined` when no PID segment exists (D-04). D-02: memoized. HELPERS-07: never throws: absent fields surface as `undefined` on the returned `Patient` object. ###### Example ```ts console.log(msg.patient?.mrn); console.log(msg.patient?.fullName); console.log(msg.patient?.dateOfBirth?.raw); // fidelity TS: e.g. "19800115" ``` ###### Returns [`Patient`](#patient-8) \| `undefined` ###### Inherited from [`Hl7Message`](#hl7message).[`patient`](#patient) ##### structure ###### Get Signature > **get** **structure**(): [`MessageStructure`](#messagestructure) Structural-conformance summary for the common message types: a misroute/truncation safety net, NOT a conformance validator. Reports, per the message's recognized (MSH-9.1, MSH-9.2) type, which Required segment groups are present and which are entirely absent (`missingGroups`: the same set the parser flags as `MISSING_EXPECTED_GROUP` warnings). For an unmodelled type, `recognized` is `false` and `missingGroups` is empty. D-02: memoized. ###### Example ```ts console.log(msg.structure.recognized); // true for ORU^R01, ADT^A01, … console.log(msg.structure.missingGroups); // e.g. ["result"] if no OBR/OBX ``` ###### Returns [`MessageStructure`](#messagestructure) ###### Inherited from [`Hl7Message`](#hl7message).[`structure`](#structure) ##### visit ###### Get Signature > **get** **visit**(): [`Visit`](#visit-7) \| `undefined` PV1-derived visit view, or `undefined` when no PV1 segment exists (HELPERS-03). D-02: memoized. HELPERS-07: never throws. ###### Example ```ts console.log(msg.visit?.patientClass); // "I" console.log(msg.visit?.admitDateTime?.raw); // fidelity TS console.log(msg.visit?.attendingDoctor?.familyName); ``` ###### Returns [`Visit`](#visit-7) \| `undefined` ###### Inherited from [`Hl7Message`](#hl7message).[`visit`](#visit) #### Methods ##### addSegment() > **addSegment**(`name`, `fields`): `this` Append a new segment to the end of the message. `name` must match `/^(?:[A-Z]{3}|Z[A-Z0-9]{2})$/u`: throws `TypeError` otherwise (D-19). `fields` is interpreted in HL7 1-indexed terms: `addSegment("NTE", [a, b, c])` produces a segment whose `NTE-1 = a`, `NTE-2 = b`, `NTE-3 = c`. The internal `RawSegment.fields[0]` name/separator placeholder is synthesized by this method. Each entry may be a plain string (treated as a single-subcomponent single-component single-repetition field) or a full `RawField` object for advanced callers who need structured content. Invalidates caches on return; warnings untouched (D-16). ###### Parameters ###### name `string` ###### fields readonly (`string` \| [`RawField`](#rawfield))[] ###### Returns `this` ###### Example ```ts msg.addSegment("NTE", ["", "note text"]); msg.get("NTE.2"); // "note text" ``` ###### Inherited from [`Hl7Message`](#hl7message).[`addSegment`](#addsegment) ##### allergies() > **allergies**(): readonly [`Allergy`](#allergy)[] Every AL1 and every IAM as an Allergy, one entry per segment in document order, each naming the segment it was read from (`source`). An ADT^A60 carries its allergies in IAM, so it is read here too. D-05: returns `[]` when neither is present. Limits: the IAR and NTE segments under an IAM are not read, nor IAM-1 or IAM-8 onward (reach them with `msg.segments("IAM")`). IAM-6 is surfaced as `actionCode`, and an exact `D` sets `deleteRequested`, but no action is ever applied: a delete entry is still returned, an update never replaces an earlier entry, and an AL1 and an IAM for the same allergen are two entries. ###### Returns readonly [`Allergy`](#allergy)[] ###### Example ```ts for (const al of msg.allergies()) { const kind = al.deleteRequested === true ? "delete request" : "allergy"; console.log(kind, al.source, al.code?.text, al.severity, al.actionCode); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`allergies`](#allergies) ##### allSegments() > **allSegments**(): readonly [`Segment`](#segment)\<`string`\>[] Iterate every `Segment` in document order (MSH first, then every subsequent segment). Cached per-message; same array reference and same Segment instances on repeat calls (D-11). Invalidated wholesale by the mutation methods. ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts for (const seg of msg.allSegments()) { console.log(seg.type); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`allSegments`](#allsegments) ##### appointments() > **appointments**(): readonly [`Appointment`](#appointment)[] Every SCH of an SIU message as a typed `Appointment`, with the AIS/AIG/AIL/AIP resource segments that follow it grouped positionally under that SCH. Surfaces the placer/filler appointment ids, SCH-25 filler status (Table 0278), SCH-11 start/end timing, and the resource groups (service / general / location / personnel). D-05: returns `[]` when no SCH is present. D-06: not memoized. Never throws (HELPERS-07). Not a scheduling-workflow state machine: see the package known-limitations. ###### Returns readonly [`Appointment`](#appointment)[] ###### Example ```ts for (const appt of msg.appointments()) { console.log(appt.fillerAppointmentId, appt.fillerStatusCode?.identifier); for (const r of appt.resources) console.log(r.kind, r.code?.identifier); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`appointments`](#appointments) ##### charges() > **charges**(): readonly [`Charge`](#charge)[] Every FT1 of a DFT message as a typed `Charge`, one per FT1 in document order. Surfaces billing-critical fields (FT1-6 transaction type, FT1-7 code, FT1-11/12 extended/unit amount, FT1-19 diagnosis linkage) with **no billing logic and no money-as-float**: amounts are the verbatim CP wire text. D-05: returns `[]` when no FT1 is present. D-06: not memoized. Never throws (HELPERS-07). ###### Returns readonly [`Charge`](#charge)[] ###### Example ```ts for (const charge of msg.charges()) { console.log(charge.transactionType, charge.transactionCode?.identifier); console.log(charge.amountExtended); // verbatim, never a number } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`charges`](#charges) ##### diagnoses() > **diagnoses**(): readonly [`Diagnosis`](#diagnosis)[] Every DG1 as a Diagnosis in document order. D-05: returns `[]` when no DG1 present. ###### Returns readonly [`Diagnosis`](#diagnosis)[] ###### Example ```ts for (const dg of msg.diagnoses()) console.log(dg.code?.identifier); ``` ###### Inherited from [`Hl7Message`](#hl7message).[`diagnoses`](#diagnoses) ##### documents() > **documents**(): readonly [`ClinicalDocument`](#clinicaldocument)[] Every TXA of an MDM message as a typed `ClinicalDocument`, with the OBX narrative body grouped positionally under that TXA. The completion status (TXA-17) and availability status (TXA-19) are surfaced as **distinct** fields and never conflated: a document can be available before it is authenticated, and reading a preliminary document as final is the harm. D-05: returns `[]` when no TXA is present. D-06: not memoized. Never throws (HELPERS-07). ###### Returns readonly [`ClinicalDocument`](#clinicaldocument)[] ###### Example ```ts for (const doc of msg.documents()) { console.log(doc.documentType, doc.completionStatus, doc.availabilityStatus); for (const obx of doc.observations) console.log(obx.value); // narrative body } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`documents`](#documents) ##### get() > **get**(`path`): `string` \| `undefined` Resolve a dot-path (e.g. `PID.5.1`, `OBX[2].5`, `PID.3[0].1`) to its decoded leaf string (unescaped once at parse: never re-unescaped on read). Returns `undefined` when the path doesn't resolve: never throws on missing path (MODEL-05). Throws `TypeError` on malformed path syntax (e.g. `"pid.5"`, empty string). ###### Parameters ###### path `string` ###### Returns `string` \| `undefined` ###### Example ```ts const msg = parseHL7(raw); msg.get("PID.5.1"); // "Smith" msg.get("OBX[2].5"); // third OBX's 5th field msg.get("NOT.9.9"); // undefined msg.get("MSH.12"); // "2.5": HL7 version string ``` ###### Inherited from [`Hl7Message`](#hl7message).[`get`](#get) ##### getAll() > **getAll**\<`S`\>(`segmentType`): readonly [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\>[] Alias for [ProfiledMessage.segments](#segments-2), with the same narrowing. ###### Type Parameters ###### S `S` *extends* `string` ###### Parameters ###### segmentType `S` ###### Returns readonly [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\>[] ###### Overrides [`Hl7Message`](#hl7message).[`getAll`](#getall) ##### identityEvents() > **identityEvents**(): readonly [`IdentityEvent`](#identityevent)[] Every recognized ADT patient-identity event (merge / move / change / link / unlink / person add/update), with the MRG-sourced `prior` and PID/PV1-sourced `surviving` parties labelled by role and the spec-constant `direction: "MRG_TO_PID"` on merge/move/change events. Returns `[]` when the trigger event is not in the identity family. The recognized set is floored by the published message structures: every ADT trigger event whose structure requires MRG is recognized. D-06: not memoized. Never throws; incomplete merge pairs surface a `MERGE_MISSING_PRIOR_OR_SURVIVOR` warning on the event. ###### Returns readonly [`IdentityEvent`](#identityevent)[] ###### Example ```ts for (const ev of msg.identityEvents()) { if (ev.kind === "merge" && ev.prior && ev.surviving) { // retire ev.prior.identifiers in favour of ev.surviving.identifiers } } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`identityEvents`](#identityevents) ##### immunizations() > **immunizations**(): readonly [`Immunization`](#immunization)[] Every RXA of a VXU^V04 as a typed `Immunization`, with RXR (route/site) and OBX children grouped positionally under the RXA and `orderControl` from the preceding ORC of the VXU order group. D-05: returns `[]` when no RXA present. D-06: not memoized. The vaccine code carries its own provenance; the action code (RXA-21) is surfaced verbatim and `recordOrigin` (administered vs historical) is derived only from the well-known NIP001 RXA-9.1 codes: never guessed. ###### Returns readonly [`Immunization`](#immunization)[] ###### Example ```ts for (const imm of msg.immunizations()) { console.log(imm.vaccineCode?.identifier, imm.doseAmount, imm.recordOrigin); console.log(imm.actionCode, imm.completionStatus); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`immunizations`](#immunizations) ##### insurance() > **insurance**(): readonly [`Insurance`](#insurance-1)[] Every IN1 as an Insurance entry with positional IN2/IN3 presence flags. D-05: returns `[]` when no IN1 present. ###### Returns readonly [`Insurance`](#insurance-1)[] ###### Example ```ts for (const ins of msg.insurance()) console.log(ins.planId?.text); ``` ###### Inherited from [`Hl7Message`](#hl7message).[`insurance`](#insurance) ##### is() **`Internal`** Single runtime implementation behind both signatures. ###### Call Signature > **is**\<`K`\>(`key`): `this is TypedMessage` Is this message of the type `key` names, and if so, narrow it for the compiler. The check is on the (MSH-9.1, MSH-9.2) pair the parser already extracted, so a message whose MSH-9 carries the three-component form `ADT^A01^ADT_A01` answers `true` to `is("ADT^A01")`. A key is `"^"` (`"ADT^A01"`), or `""` alone for a message type the published structure registry matches on message code alone (`"ACK"`, whose MSH-9.2 carries the acknowledged message's trigger event). `SUPPORTED_OVERLAY_MESSAGES` enumerates every key. **Any other string is `false`, never a throw**: an unrecognized type, the three-component form as a string, an empty string, or a value computed at run time. `is` does not parse its own argument, and a caller who wants a raw comparison has `msg.meta.type`. Read-only: it inspects `meta` and mutates nothing. ###### Type Parameters ###### K `K` *extends* keyof [`OverlayStructures`](#overlaystructures) ###### Parameters ###### key `K` ###### Returns `this is TypedMessage` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); if (msg.is("ORU^R01")) { const code: "ORU" = msg.meta.messageCode; // literal, no cast console.log(msg.part("OBR")?.field(4).value); } console.log(msg.is("ZZZ^Z99")); // false: not a published key ``` ###### Inherited from [`Hl7Message`](#hl7message).[`is`](#is) ###### Call Signature > **is**(`key`): `boolean` Answer for a key that is not known at compile time. A runtime-computed string cannot narrow anything, so this form returns a plain `boolean`. ###### Parameters ###### key `string` ###### Returns `boolean` ###### Inherited from [`Hl7Message`](#hl7message).[`is`](#is) ##### medications() > **medications**(): readonly [`Medication`](#medication)[] Every RXO/RXE/RXD/RXA as a typed `Medication`, with RXR (route) and RXC (component) segments grouped positionally under their parent. D-05: returns `[]` when no RX* parent present. D-06: not memoized. The give *amount* and give *strength* are surfaced separately and never reconciled. Each medication also carries its TQ1 / legacy embedded-TQ (RXE-1) `timings` (repeat pattern verbatim, never resolved to a schedule). Each medication also carries `orderControl`: ORC-1 of the ORC that opened its order group (every RX* up to the next ORC shares it), exactly as sent. It is never interpreted into an active, held or discontinued state, and it is omitted when no ORC precedes the RX* or ORC-1 is empty, never carried over from an earlier group. ###### Returns readonly [`Medication`](#medication)[] ###### Example ```ts for (const med of msg.medications()) { console.log(med.orderControl); // e.g. "NW" (new) or "DC" (discontinue), verbatim console.log(med.context, med.giveCode?.identifier, med.giveCode?.nameOfCodingSystem); console.log(med.amount?.minimum, med.strength?.value); for (const t of med.timings) console.log(t.repeatPattern?.code, t.totalOccurrences); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`medications`](#medications) ##### nextOfKin() > **nextOfKin**(): readonly [`NextOfKin`](#nextofkin-1)[] Every NK1 as a NextOfKin entry in document order. D-05: returns `[]` when no NK1 present. ###### Returns readonly [`NextOfKin`](#nextofkin-1)[] ###### Example ```ts for (const nk of msg.nextOfKin()) { console.log(nk.name?.familyName, nk.relationship?.text); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`nextOfKin`](#nextofkin) ##### notes() > **notes**(): readonly `string`[] Message-level NTE notes: every NTE segment with no recognized preceding parent (not immediately following a `PID`, `ORC`, `OBR`, or `OBX`), surfaced verbatim in document order so nothing is dropped. Notes that DO attach to a specific patient / order / result are exposed on those helper outputs (`msg.patient?.notes`, `order.notes`, `observation.notes`), not here. D-05: returns `[]` when there are none. D-06: NOT memoized. ###### Returns readonly `string`[] ###### Example ```ts for (const note of msg.notes()) console.log(note); // message-level narrative ``` ###### Inherited from [`Hl7Message`](#hl7message).[`notes`](#notes) ##### observations() > **observations**(): readonly [`Observation`](#observation)[] Every OBX segment as a typed Observation in document order. D-05: returns `[]` when no OBX present. D-06: NOT memoized: each call re-walks `rawSegments`. Value type is discriminated per D-13. ###### Returns readonly [`Observation`](#observation)[] ###### Example ```ts for (const obs of msg.observations()) { if (obs.valueType === "NM") console.log(obs.value); // number | undefined } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`observations`](#observations) ##### orders() > **orders**(): readonly [`Order`](#order-1)[] Every OBR as an Order with its OBX children grouped positionally (D-12) and its TQ1 / legacy embedded-TQ (ORC-7) `timings` (the repeat pattern is surfaced verbatim, never resolved to a schedule). D-05: returns `[]` when no OBR present. D-06: not memoized. ###### Returns readonly [`Order`](#order-1)[] ###### Example ```ts for (const order of msg.orders()) { console.log(order.placerOrderNumber, order.observations.length); for (const t of order.timings) console.log(t.repeatPattern?.code); // e.g. "Q6H": verbatim } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`orders`](#orders) ##### part() > **part**\<`S`\>(`segmentType`): [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\> \| `undefined` The first segment of `segmentType`, with that type's declared field names. ###### Type Parameters ###### S `S` *extends* `string` ###### Parameters ###### segmentType `S` ###### Returns [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\> \| `undefined` ###### Overrides [`Hl7Message`](#hl7message).[`part`](#part) ##### parts() > **parts**\<`S`\>(`segmentType`): readonly [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\>[] Every segment of `segmentType`, with that type's declared field names. ###### Type Parameters ###### S `S` *extends* `string` ###### Parameters ###### segmentType `S` ###### Returns readonly [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\>[] ###### Overrides [`Hl7Message`](#hl7message).[`parts`](#parts) ##### prettyPrint() > **prettyPrint**(): `string` Emit this message as a human-readable multi-line string for logs and debugging (SER-04). Single opinionated format (D-22 no options): header line with type / controlId / timestamp / segment count, then one line per segment with labeled `[N]=value` fields (D-23). Composite values render as their raw HL7 string: depth stops at field level (D-24). Pure: never warns, never throws (D-26). **Field values render as their raw HL7 string representation.** Embedded delimiters in user data appear as escape sequences: e.g. a patient family name containing `|` renders as `Smith\F\Jones` (NOT `Smith|Jones`). This preserves round-trip fidelity: copy-pasting prettyPrint output into `parseHL7` yields a structurally equivalent message. For un-escaped human display, parse the composite first via typed accessors (e.g. `msg.patient?.familyName`): those return already-decoded strings. ###### Returns `string` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); console.log(msg.prettyPrint()); // HL7 ADT^A01 controlId=MSG001 timestamp=2026-04-19T10:15:00Z (5 segments) // MSH [3]=SENDAPP [4]=SENDFAC ... // PID [1]=1 [3]=MRN123 [5]=Doe^John ``` ###### Inherited from [`Hl7Message`](#hl7message).[`prettyPrint`](#prettyprint) ##### removeSegment() > **removeSegment**(`segmentType`, `occurrenceOrOptions?`): `this` Remove segments by type + occurrence or by type + all. Call shapes: - `removeSegment("NTE")`: remove the FIRST NTE (occurrence 0). - `removeSegment("OBX", 1)`: remove the SECOND OBX (0-indexed per D-01). - `removeSegment("OBX", { all: true })`: remove ALL OBX segments. `MSH` is protected: `removeSegment("MSH")` throws `TypeError` (every HL7 message must retain its MSH segment). Unknown segment types are a no-op (idempotent; no throw). Segment name must match the D-19 shape regex: invalid shapes throw `TypeError` for symmetry with `addSegment`. Invalidates caches on return; warnings untouched (D-16). ###### Parameters ###### segmentType `string` ###### occurrenceOrOptions? `number` \| \{ `all?`: `boolean`; \} ###### Returns `this` ###### Example ```ts msg.removeSegment("NTE"); // remove first NTE msg.removeSegment("OBX", 1); // remove second OBX msg.removeSegment("OBX", { all: true }); // remove all remaining OBX ``` ###### Inherited from [`Hl7Message`](#hl7message).[`removeSegment`](#removesegment) ##### segments() > **segments**\<`S`\>(`segmentType`): readonly [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\>[] Every segment of `segmentType`, with that type's declared field names. ###### Type Parameters ###### S `S` *extends* `string` ###### Parameters ###### segmentType `S` ###### Returns readonly [`Segment`](#segment)\<[`ProfileFieldName`](#profilefieldname)\<`P`, `S`\>\>[] ###### Overrides [`Hl7Message`](#hl7message).[`segments`](#segments) ##### setComposite() > **setComposite**\<`K`\>(`path`, `kind`, `value`): `this` Set a **typed composite** at a field (or field-repetition) dot-path: the conservative-emit mirror of the typed read accessors (`asXpn`/`asCx`/…). The caller passes a structured value (an XPN name, a CX identifier, a TS timestamp, …) by its [CompositeKind](#compositekind), and the setter encodes it into a spec-clean field using the encode-safe path: any delimiter embedded in a component value is **escaped, never injected**, so a `familyName` of `"Smith^Jr"` re-parses to exactly that string rather than forging a component boundary. No hand-assembly of `^`/`&`/`~`. The path must resolve to a **field** (`"PID.5"`) or a specific **repetition** of a field (`"PID.11[1]"`). A component/subcomponent-level path (`"PID.5.1"`) is rejected with `TypeError`: a composite occupies a whole field, not a single component. Like [setField](#setfield), the target segment must already exist (`addSegment` first); the repetition defaults to index 0 and other repetitions of the field are preserved. Never fabricates: an omitted optional composite field encodes to an empty/absent component, never a defaulted value; an all-empty composite clears the field. Segment/helper caches are invalidated on success; the frozen `warnings` array is untouched. ###### Type Parameters ###### K `K` *extends* [`CompositeKind`](#compositekind) ###### Parameters ###### path `string` ###### kind `K` ###### value [`CompositeValueByKind`](#compositevaluebykind)\[`K`\] ###### Returns `this` ###### Example ```ts const msg = buildMessage({ type: "ADT^A01" }).addSegment("PID", [""]); msg.setComposite("PID.5", "XPN", { familyName: "Smith", givenName: "Ann" }); msg.setComposite("PID.3", "CX", { idNumber: "MRN001", identifierTypeCode: "MR" }); msg.setComposite("PID.7", "TS", "19880705"); msg.get("PID.5.1"); // "Smith" ``` ###### Inherited from [`Hl7Message`](#hl7message).[`setComposite`](#setcomposite) ##### setField() > **setField**(`path`, `value`): `this` Set the string value at a dot-path. Mutates the underlying tree and returns `this` for chaining (D-15). Auto-creates missing repetitions, components, and subcomponents WITHIN an existing field, but does NOT auto-create segments: callers must `addSegment` first (throws `TypeError` with an actionable message otherwise). The value is accepted verbatim: unescaped delimiter characters are NOT rejected on input (D-18). Re-escaping is the serializer's concern. MSH-1 / MSH-2 follow the user-facing HL7 convention: `setField("MSH.3", ...)` targets MSH-3 (sending application), matching `msg.get("MSH.3")`. Segment/Field wrapper caches are invalidated wholesale on success (D-17). The frozen `warnings` array is never touched (D-16). ###### Parameters ###### path `string` ###### value `string` ###### Returns `this` ###### Example ```ts msg.setField("PID.8", "F"); // patient sex → F msg.setField("PID.5.1", "Jones"); // family name msg.setField("PID.4[2].1", "MRN2"); // create third repetition of PID-4 ``` ###### Inherited from [`Hl7Message`](#hl7message).[`setField`](#setfield) ##### toJSON() > **toJSON**(): [`SerializedMessage`](#serializedmessage) Emit this message as a structured `SerializedMessage` JSON projection (SER-03). Invoked automatically by `JSON.stringify(msg)` (D-18). Re-walks `rawSegments` on every call (D-30 no caching). Mirrors the raw tree one-for-one, preserves `isNull`, always includes `warnings: []`, and includes `profile: { name, lineage }` only when `this.profile` is truthy (D-19/D-20). Pure: never warns, never throws. ###### Returns [`SerializedMessage`](#serializedmessage) ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); const snap = msg.toJSON(); console.log(snap.segments[0]?.name); // "MSH" console.log(JSON.stringify(msg)); // same content, auto-invokes toJSON ``` ###### Inherited from [`Hl7Message`](#hl7message).[`toJSON`](#tojson) ##### toString() > **toString**(): `string` Emit this message as spec-clean HL7 (SER-01). Re-walks `rawSegments` on every call (D-30 no caching). Segments are joined with `\r` per D-05; MSH-1 and MSH-2 are inlined verbatim from `this.encodingCharacters` per D-06; every field string passes through `reescape` per D-04. `RawField.isNull === true` is preserved as the HL7 literal `""` (D-02). Pure: never warns, never throws (D-07). ###### Returns `string` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); console.log(msg.toString()); // spec-clean, CR-separated HL7 ``` ###### Inherited from [`Hl7Message`](#hl7message).[`toString`](#tostring) *** ### RawComponent A single component inside a repetition: the most deeply nested data layer in the HL7 positional tree. A component is an ordered list of subcomponent strings (subcomponent separator `&` by default). #### Example ```ts import type { RawComponent } from "@cosyte/hl7"; const comp: RawComponent = { subcomponents: ["Smith", "John"] }; ``` #### Properties ##### rawSubcomponents? > `readonly` `optional` **rawSubcomponents?**: readonly (`string` \| `undefined`)[] **`Internal`** Escape-fidelity overlay, positionally aligned with [subcomponents](#subcomponents): for each index, the subcomponent's **original wire bytes** to emit verbatim instead of re-escaping the decoded form. An entry is present (non-`undefined`) only when a subcomponent carried an escape whose decoded form does not re-escape back to the exact wire bytes: i.e. a recognize- and-preserve escape (`\H\`, `\Z..`, charset/formatting) or a hex escape (`\X41\`, or non-canonical hex casing). Delimiter escapes (`\F`→`|`) round- trip through `reescape` unchanged and get no overlay. The whole field is absent when no subcomponent needs it (the overwhelming common case), so a plain message's raw tree is byte-for-byte the shape it was before this overlay existed. Consumed only by the serializer ("src/serialize/emit-field.ts"); never read by the value/coercion surface, so it changes no decoded value. ##### subcomponents > `readonly` **subcomponents**: readonly `string`[] The component's subcomponents, **HL7-decoded**: the tokenizer expands escape sequences on parse so consumers read literal values (`Smith|Jones`, not `Smith\F\Jones`). This is the value surface every reader uses (`Field.value`, the composite parsers, dot-path, `toJSON`). *** ### RawField A positional field inside a segment. Carries its repetitions plus an `isNull` discriminant that distinguishes the HL7 explicit null (`""`, a two-character literal double quote) from an empty field (no content between delimiters). #### Example ```ts import type { RawField } from "@cosyte/hl7"; const nullField: RawField = { repetitions: [], isNull: true }; const emptyField: RawField = { repetitions: [], isNull: false }; ``` #### Properties ##### isNull > `readonly` **isNull**: `boolean` ##### repetitions > `readonly` **repetitions**: readonly [`RawRepetition`](#rawrepetition)[] *** ### RawRepetition A single repetition inside a field: HL7 fields may repeat using the repetition separator (`~` by default). Each repetition is an ordered list of components. #### Example ```ts import type { RawRepetition } from "@cosyte/hl7"; const rep: RawRepetition = { components: [{ subcomponents: ["Smith"] }] }; ``` #### Properties ##### components > `readonly` **components**: readonly [`RawComponent`](#rawcomponent)[] *** ### RawSegment A parsed HL7 segment: the top level of the positional tree. The `name` is the three-character segment identifier (`MSH`, `PID`, `ZPI`, ...) and `fields` is the 1-indexed positional field array (see the JSDoc on `fields` for the index 0 slot convention). #### Example ```ts import type { RawSegment } from "@cosyte/hl7"; const pid: RawSegment = { name: "PID", fields: [ { repetitions: [], isNull: false }, ], }; ``` #### Properties ##### fields > `readonly` **fields**: readonly [`RawField`](#rawfield)[] Positional fields array using HL7 1-indexed convention for ALL segments. - `fields[0]` is the segment name / separator placeholder slot (never a data field). - `fields[N]` for N >= 1 is the HL7 N-th field. Examples: - MSH: `fields[0]` = field-separator char, `fields[1]` = MSH-2 (encoding chars), `fields[2]` = MSH-3, ..., `fields[11]` = MSH-12. - PID: `fields[0]` = "PID" name placeholder, `fields[1]` = PID-1, `fields[2]` = PID-2, .... ##### name > `readonly` **name**: `string` *** ### RenderedText The normalized display model produced by [renderText](#rendertext): a flat plain-text string plus the structured highlight-aware runs, plus an honesty list of the escape sequences that were preserved rather than rendered. #### Properties ##### runs > `readonly` **runs**: readonly [`TextRun`](#textrun)[] The structured, highlight-aware form: `{ text, highlighted }` runs in document order. Empty runs are elided. Use this to preserve emphasis (bold / reverse-video / etc.) that the flat [text](#text-3) drops. ##### text > `readonly` **text**: `string` The full plain-text normalization: formatting commands become whitespace / line breaks and highlight boundaries are dropped. This is the string to show a human or feed a downstream `.text`. Equal to the concatenation of every [runs](#runs) entry's `text`. ##### unrenderedSequences > `readonly` **unrenderedSequences**: readonly `string`[] The escape sequences `renderText` **preserved verbatim instead of rendering**: vendor `\Zdddd…\`, charset switches `\Cxxyy`/`\Mxxyyzz`, and any malformed / unterminated sequence. Their literal characters ALSO appear in [text](#text-3)/[runs](#runs) (never silently dropped); this list exists so a consumer can detect that a non-render decision was made and surface or route those sequences deliberately. Empty when everything rendered cleanly. *** ### RenderTextOptions Options for [renderText](#rendertext). #### Properties ##### newline? > `readonly` `optional` **newline?**: `string` The string emitted for each line break (`\.br\`, `\.sp`, `\.ce\`, and a raw CR/LF/CRLF in the input). Defaults to `"\n"`. Set `"\r\n"` for Windows-style display, or `" "` to flatten a note to a single line. *** ### RepeatPattern An order/medication timing **repeat pattern** (HL7 Table 0335): the frequency/SIG field (TQ1-3, or the legacy embedded TQ interval RI.1). **Safety contract.** `code` is the **decoded field value** (HL7 escapes are unescaped as with every field read) and is **never resolved to clock times, normalized, or mapped to a different frequency**: reading `Q6H` as "daily" or silently dropping a `BID` changes the administered dose count, a transcription-class harm. `kind`/`interval` are convenience provenance ONLY; `code` is the authoritative value. #### Example ```ts import type { RepeatPattern } from "@cosyte/hl7"; const q6h: RepeatPattern = { code: "Q6H", kind: "parametric", interval: { count: 6, unit: "H" } }; const bid: RepeatPattern = { code: "BID", kind: "named" }; ``` #### Properties ##### code > `readonly` **code**: `string` The Table-0335 repeat-pattern code exactly as authored (e.g. `"Q6H"`, `"BID"`). Never normalized. ##### interval? > `readonly` `optional` **interval?**: `object` For a `"parametric"` `Q` template only: the load-bearing integer and its unit letter (`S`/`M`/`H`/`D`/`W`/`L`, or `J` for day-of-week). OMITTED for `"named"`/`"unknown"` patterns. Informational, `code` remains authoritative. ###### count > `readonly` **count**: `number` ###### unit > `readonly` **unit**: `string` ##### kind > `readonly` **kind**: [`RepeatPatternKind`](#repeatpatternkind-1) Provenance classification of `code`: never used to resolve a schedule. See [RepeatPatternKind](#repeatpatternkind-1). *** ### ResultStatusClassification A result status read against its HL7 table and classified by HL7's published v2-to-FHIR status map: carried as `resultStatus` on every [Observation](#observation) (from OBX-11) and every [Order](#order-1) (from OBR-25). **Safety contract.** Each OBX classifies from its own OBX-11 alone and each order from its own OBR-25 alone: nothing is inherited, propagated or reconciled across segments. A status is reported, never applied: an `"entered-in-error"` observation is still returned, and replacing or deleting an earlier result is the caller's decision. `classification` is `"final"` only when the field is exactly `F`; every value HL7's map does not map is `"undetermined"` (see [ResultStatusClass](#resultstatusclass)), never a guess. The object is plain data, not a FHIR element: no resource is built and no terminology service is consulted. #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); for (const obs of msg.observations()) { const { classification, code } = obs.resultStatus; if (classification === "entered-in-error") console.log("posted in error:", code); else if (classification === "undetermined") console.log("not classifiable:", code); } ``` #### Properties ##### classification > `readonly` **classification**: [`ResultStatusClass`](#resultstatusclass) The classification HL7's map gives the code, or `"undetermined"`. ##### code? > `readonly` `optional` **code?**: `string` The raw code, byte-identical to the `status` (OBX-11) or `orderStatus` (OBR-25) the same observation or order surfaces: the field's first value, decoded. OMITTED when that field is absent, empty or `""`, exactly when `status` / `orderStatus` is. When the field carries more than one repetition, component or subcomponent this is still only the first value, and `classification` is `"undetermined"`. ##### map > `readonly` **map**: [`ResultStatusMap`](#resultstatusmap) The HL7 v2-to-FHIR map the classification followed. ##### table > `readonly` **table**: [`ResultStatusTable`](#resultstatustable) The HL7 table the code was read against. *** ### ResultStatusMap The published HL7 v2-to-FHIR map a [ResultStatusClassification](#resultstatusclassification) followed, by canonical URL and version. The maps come from the HL7 Version 2 to FHIR Implementation Guide (STU 1, standards status Informative); a later map revision shows up here as a changed `version`. #### Example ```ts import type { ResultStatusMap } from "@cosyte/hl7"; const map: ResultStatusMap = { url: "http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70085-to-observation-status", version: "1.0.0", }; ``` #### Properties ##### url > `readonly` **url**: `"http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70085-to-observation-status"` \| `"http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70123-queries-to-diagnostic-report-status"` Canonical URL of the ConceptMap whose rows the classification follows. ##### version > `readonly` **version**: `"1.0.0"` The ConceptMap version the classification follows. *** ### ResultStatusTable The HL7 v2 code table a [ResultStatusClassification](#resultstatusclassification) read: Table 0085 (Observation Result Status, OBX-11) or Table 0123 (Result Status, OBR-25), each at the code-system version the map was checked against. #### Example ```ts import type { ResultStatusTable } from "@cosyte/hl7"; const table: ResultStatusTable = { name: "HL7 Table 0085", version: "3.0.0" }; ``` #### Properties ##### name > `readonly` **name**: `"HL7 Table 0085"` \| `"HL7 Table 0123"` `"HL7 Table 0085"` for OBX-11, `"HL7 Table 0123"` for OBR-25. ##### version > `readonly` **version**: `"3.0.0"` The code-system version the table's codes were taken from. *** ### SegmentRule A rule for one **segment type**. `usage` constrains whether the segment must / must not appear; `cardinality` constrains how many times; `fields` are the per-field rules, applied to **every** occurrence of the segment. #### Properties ##### cardinality? > `readonly` `optional` **cardinality?**: [`Cardinality`](#cardinality) Occurrence-count constraint for this segment across the message. ##### condition? > `readonly` `optional` **condition?**: [`ConditionPredicate`](#conditionpredicate) The [ConditionPredicate](#conditionpredicate) that decides this rule's usage, evaluated against the message. Only a rule whose `usage` is conditional (`C`, `CE`, or a declared conditional) may declare one. ##### fields? > `readonly` `optional` **fields?**: readonly [`FieldRule`](#fieldrule)[] Per-field rules, applied to each occurrence of this segment. ##### segment > `readonly` **segment**: `string` Segment name: 3 chars, `[A-Z][A-Z0-9]{2}` (standard or `Z…` segment). ##### severity? > `readonly` `optional` **severity?**: [`FindingSeverity`](#findingseverity) Severity for the segment-level presence / cardinality findings. Default `"error"`. ##### usage? > `readonly` `optional` **usage?**: [`UsageCode`](#usagecode) Usage for the segment as a whole. `R` ⇒ at least one occurrence required; `X` ⇒ none permitted; `RE` / `O` ⇒ no presence constraint; `C` / `CE` / `B` ⇒ presence not evaluated (no predicate language). A declared conditional such as `C(R/X)` takes the outcome a caller resolves, and is evaluated exactly as `C` when nothing resolves it. Omitted ⇒ Optional. *** ### SerializedMessage Snapshot-stable JSON projection of an `Hl7Message` (SER-03). Every field is readonly; segment order is preserved; `isNull` flags are preserved. Runtime immutability: the top-level object is `Object.freeze`d (boundary-frozen). Inner arrays are readonly at the TypeScript type level but mutable at runtime: treat as immutable, do not mutate. #### Example ```ts import { parseHL7, type SerializedMessage } from "@cosyte/hl7"; const msg = parseHL7(raw); const snap: SerializedMessage = msg.toJSON(); console.log(snap.segments[0]?.name); // "MSH" console.log(snap.warnings.length); // 0 when clean console.log(JSON.stringify(msg) === JSON.stringify(snap)); // true console.log(Object.isFrozen(snap)); // true (D-30 boundary freeze) ``` #### Properties ##### encodingCharacters > `readonly` **encodingCharacters**: [`EncodingCharacters`](#encodingcharacters-1) ##### profile? > `readonly` `optional` **profile?**: `object` ###### lineage > `readonly` **lineage**: readonly `string`[] ###### name > `readonly` **name**: `string` ##### segments > `readonly` **segments**: readonly `object`[] ##### warnings > `readonly` **warnings**: readonly [`Hl7ParseWarning`](#hl7parsewarning)[] *** ### SiuAppointment Typed SCH (scheduling activity information) content for [buildSiu](#buildsiu). #### Example ```ts import type { SiuAppointment } from "@cosyte/hl7"; const appointment: SiuAppointment = { placerAppointmentId: "PL-1001", fillerAppointmentId: "FL-2002", startDateTime: "20260801090000", endDateTime: "20260801093000", fillerStatusCode: { identifier: "Booked" }, }; ``` #### Properties ##### endDateTime? > `readonly` `optional` **endDateTime?**: `string` \| [`DtmParts`](#dtmparts) SCH-11.5 End Date/Time of the appointment timing quantity. ##### fillerAppointmentId? > `readonly` `optional` **fillerAppointmentId?**: `string` SCH-2 Filler Appointment ID. ##### fillerStatusCode? > `readonly` `optional` **fillerStatusCode?**: [`CWE`](#cwe-1) SCH-25 Filler Status Code (HL7 Table 0278). Verbatim; never defaulted. ##### placerAppointmentId? > `readonly` `optional` **placerAppointmentId?**: `string` SCH-1 Placer Appointment ID. ##### startDateTime? > `readonly` `optional` **startDateTime?**: `string` \| [`DtmParts`](#dtmparts) SCH-11.4 Start Date/Time of the appointment timing quantity. *** ### SiuResource Typed AI* (appointment resource) content for [buildSiu](#buildsiu). #### Example ```ts import type { SiuResource } from "@cosyte/hl7"; const theatre: SiuResource = { kind: "location", code: { identifier: "OR-1" } }; const surgeon: SiuResource = { kind: "personnel", person: { idNumber: "9990", familyName: "Welby", identifierTypeCode: "NPI" }, }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CWE`](#cwe-1) AI*-3 resource identifier for a service, general resource or location (AIS-3 / AIG-3 are coded elements; AIL-3 is a location, whose first component is the location id). Ignored for a `personnel` resource, which carries [person](#person-1) instead. ##### kind > `readonly` **kind**: [`SiuResourceKind`](#siuresourcekind-1) Which resource segment to emit: AIS, AIG, AIL or AIP. ##### person? > `readonly` `optional` **person?**: [`XCN`](#xcn-1) AIP-3 personnel resource: the appointment provider. Personnel resources only. ##### setId? > `readonly` `optional` **setId?**: `string` AI*-1 Set ID. *** ### SiuResourceGroup Typed RGS (resource group) content for [buildSiu](#buildsiu). #### Example ```ts import type { SiuResourceGroup } from "@cosyte/hl7"; const group: SiuResourceGroup = { setId: "1", resourceGroupId: { identifier: "RG-1", text: "Theatre" }, resources: [{ kind: "location", code: { identifier: "OR-1" } }], }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` RGS-2 Segment Action Code. ##### resourceGroupId? > `readonly` `optional` **resourceGroupId?**: [`CWE`](#cwe-1) RGS-3 Resource Group ID. ##### resources? > `readonly` `optional` **resources?**: readonly [`SiuResource`](#siuresource)[] The AI* resources belonging to this group, in the order supplied. ##### setId? > `readonly` `optional` **setId?**: `string` RGS-1 Set ID. *** ### SN HL7 v2 Structured Numeric (SN) composite. `num1` and `num2` are ALWAYS-PRESENT keys (typed `number | undefined`) so callers can destructure uniformly, mirroring `NM`; `comparator` and `separatorOrSuffix` are OMITTED when absent (`exactOptionalPropertyTypes`). An absent `comparator` means the default `=` relation per HL7 Chapter 2A. This library surfaces the structure only: it does not evaluate the inequality, validate the unit, or convert values. #### Example ```ts import type { SN } from "@cosyte/hl7"; const gfr: SN = { comparator: ">", num1: 90, num2: undefined }; // >90 const range: SN = { num1: 100, separatorOrSuffix: "-", num2: 200 }; // 100-200 ``` #### Properties ##### comparator? > `readonly` `optional` **comparator?**: `string` SN.1 comparator: one of `>` `<` `>=` `<=` `=` `<>`. Omitted ⇒ default `=`. ##### num1 > `readonly` **num1**: `number` \| `undefined` SN.2 first numeric value. `undefined` when absent or non-numeric (never `NaN`). ##### num2 > `readonly` **num2**: `number` \| `undefined` SN.4 second numeric value. `undefined` when absent or non-numeric (never `NaN`). ##### separatorOrSuffix? > `readonly` `optional` **separatorOrSuffix?**: `string` SN.3 separator/suffix: `-` (range), `:`/`/` (ratio), `+` (suffix), `.`. Omitted when absent. *** ### StructureFamily A referenced structure id and the variant family actually read for it. #### Properties ##### members > `readonly` **members**: readonly `string`[] The family members present in the snapshot, sorted. ##### structureId > `readonly` **structureId**: `string` The structure id the publication's message list references. *** ### StructureFinding One typed published-structure finding: the [StructureFindingCode](#structurefindingcode-1), a severity, the structural [StructureFindingLocus](#structurefindinglocus-1), and a human-readable `message`. **The `message` is PHI-safe by construction**: it names the segment, the occurrence, the published structure and the counts involved, and never a value read out of the message. #### Example ```ts import type { StructureFinding } from "@cosyte/hl7"; const f: StructureFinding = { code: "STRUCTURE_SEGMENT_CARDINALITY", severity: "error", locus: { segment: "EVN", structureId: "ADT_A01-A" }, message: 'Segment "EVN" occurs 0 times; published structure ADT_A01-A requires at least 1.', }; ``` #### Properties ##### code > `readonly` **code**: [`StructureFindingCode`](#structurefindingcode-1) Which check fired. ##### locus > `readonly` **locus**: [`StructureFindingLocus`](#structurefindinglocus-1) Where in the message, structurally. ##### message > `readonly` **message**: `string` Human-readable description of the rule that fired. Carries no field value. ##### severity > `readonly` **severity**: [`FindingSeverity`](#findingseverity) How hard a finding this is. An ordering or cardinality finding is an `error`: the publication states a constraint and the message breaks it. An unexpected segment is a `warning`, because HL7 reserves `Z` segments for exactly this and a site-defined segment in a live feed is normal traffic rather than a broken message. The check still fires, under its own code, so a consumer that wants it to be an error can make it one. *** ### StructureFindingLocus The **structural** locus a published-structure finding refers to: a segment name, the 0-indexed occurrence of that segment where one occurrence is responsible, and the published structure the finding was raised against. Every member is a name or an index, so a locus is **inherently PHI-free** and never carries a field value. The occurrence index counts occurrences of that segment name, the same way a conformance finding's does. #### Example ```ts import type { StructureFindingLocus } from "@cosyte/hl7"; const locus: StructureFindingLocus = { segment: "PID", occurrence: 1, structureId: "ADT_A01-A" }; ``` #### Properties ##### occurrence? > `readonly` `optional` **occurrence?**: `number` 0-indexed occurrence of that segment name, present when one occurrence is responsible. A minimum-cardinality finding names no occurrence: the responsible occurrence is the one that is not there. ##### segment > `readonly` **segment**: `string` Segment name (e.g. `"PID"`), bounded to the shape a segment id may take. ##### structureId > `readonly` **structureId**: `string` The published structure variant the finding was raised against. *** ### StructureGroup The presence verdict for one expected segment of a recognized message type. #### Properties ##### anchorSegments > `readonly` **anchorSegments**: readonly `string`[] The anchor segment name(s) whose presence would satisfy this group. ##### name > `readonly` **name**: `string` The expectation's label from its `ExpectedSegmentGroup`, e.g. `"OBR"`. ##### present > `readonly` **present**: `boolean` `true` when the required segment is present in the message. ##### requiredSegment > `readonly` **requiredSegment**: `string` The segment the published structure gives a minimum of one. *** ### StructurePublicationRef The publication a snapshot was taken from. #### Properties ##### commit > `readonly` **commit**: `string` The commit the snapshot was taken at. ##### commitDate > `readonly` **commitDate**: `string` That commit's date. ##### name > `readonly` **name**: `string` Human-readable name of the publication. ##### repository > `readonly` **repository**: `string` The publishing repository, e.g. `"HL7/v2ig"`. ##### repositoryUrl > `readonly` **repositoryUrl**: `string` Its web URL. ##### tree > `readonly` **tree**: `string` The subtree the structure definitions live in. *** ### StructureRegistryProvenance Everything needed to audit where a structural expectation came from: the publication, the exact bytes, and the structure behind every recognized pair. #### Properties ##### families > `readonly` **families**: readonly [`StructureFamily`](#structurefamily)[] Each referenced structure id and the variant family read for it. ##### files > `readonly` **files**: readonly [`StructureSnapshotFile`](#structuresnapshotfile)[] Every vendored file the registry was derived from, with its sha256. ##### pairs > `readonly` **pairs**: readonly [`StructureSourcePair`](#structuresourcepair)[] Each recognized pair and the structure id it came from. ##### publication > `readonly` **publication**: [`StructurePublicationRef`](#structurepublicationref) The publication the registry was derived from. ##### snapshotTakenAt > `readonly` **snapshotTakenAt**: `string` ISO date the snapshot was taken. *** ### StructureSnapshotFile One vendored publication file the shipped registry was derived from. #### Properties ##### bytes > `readonly` **bytes**: `number` Byte length of the vendored file. ##### path > `readonly` **path**: `string` Path relative to the vendored snapshot directory. ##### sha256 > `readonly` **sha256**: `string` Lowercase hex sha256 of the vendored bytes. ##### url > `readonly` **url**: `string` The upstream URL the bytes were fetched from. *** ### StructureSourcePair One (message code, trigger event) pair and the structure it came from. An empty `triggerEvent` marks an entry matched on message code alone, and an empty `structureId` marks a retained transcription. #### Properties ##### messageCode > `readonly` **messageCode**: `string` MSH-9.1 message code. ##### structureId > `readonly` **structureId**: `string` The published structure id, or `""` for a retained transcription. ##### triggerEvent > `readonly` **triggerEvent**: `string` MSH-9.2 trigger event, or `""` for a code-alone match. *** ### StructureValidationResult The result of [validateMessageStructure](#validatemessagestructure). **Read `validated` first.** `false` means the publication could not answer for this message and `reason` says why; `findings` is empty and no structure was selected. It is NOT "the message is fine". **`validated: true` with `findings.length === 0` is not a conformance attestation either.** It means the message did not break the published structure in the three ways checked here: segment order, segment occurrence counts, and segments the publication does not name. Field content, datatypes, value sets, tables and clinical correctness are all unchecked, and the publication itself is vendored at a fixed commit. #### Example ```ts import { parseHL7, validateMessageStructure } from "@cosyte/hl7"; const result = validateMessageStructure(parseHL7(raw)); if (result.validated && result.findings.length === 0) { // nothing checked here was violated, against result.structureId } ``` #### Properties ##### findings > `readonly` **findings**: readonly [`StructureFinding`](#structurefinding)[] The findings, in a stable order: the ordering finding first, then cardinality findings by segment name, then unexpected segments in the order they appear in the message. Always empty when `validated` is `false`. ##### reason > `readonly` **reason**: `""` \| [`StructureNotValidatedReason`](#structurenotvalidatedreason) Why validation did not run. Empty string when it did. ##### reasonMessage > `readonly` **reasonMessage**: `string` Human-readable form of `reason`. Empty string when validation ran. ##### structureId > `readonly` **structureId**: `string` The published structure variant the findings were raised against, e.g. `"ADT_A01-A"`. Empty string when `validated` is `false`. ##### structureIds > `readonly` **structureIds**: readonly `string`[] Every variant of the family that was considered, sorted. Empty when `validated` is `false`. ##### validated > `readonly` **validated**: `boolean` Whether a published structure was actually validated against. *** ### SupportedBuilderMessage One (message code, trigger event) pair the typed builders can author, with the builder that authors it and the segments the published structure requires for it. `triggerEvent` is the empty string for a pair the registry matches on message code alone (`ACK`, whose MSH-9.2 carries the acknowledged message's trigger event); [supportsBuilderMessage](#supportsbuildermessage) treats such an entry as matching any trigger event for that code. #### Example ```ts import { SUPPORTED_BUILDER_MESSAGES } from "@cosyte/hl7"; const vxu = SUPPORTED_BUILDER_MESSAGES.find((m) => m.messageCode === "VXU"); console.log(vxu?.triggerEvent, vxu?.builder); // "V04" "buildVxu" ``` #### Properties ##### builder > `readonly` **builder**: `string` The exported builder that authors this pair. ##### messageCode > `readonly` **messageCode**: `string` MSH-9.1 message code. ##### requiredSegments > `readonly` **requiredSegments**: readonly `string`[] The segments the derived structure registry marks required for this pair. ##### triggerEvent > `readonly` **triggerEvent**: `string` MSH-9.2 trigger event; `""` when the registry matches on message code alone. *** ### SupportedOverlayMessage One published overlay key, with the message type it matches and the segments the published structure requires for it. `triggerEvent` is the empty string for a key the registry matches on message code alone (`ACK`), and [Hl7Message.is](#is) then answers `true` for that key whatever MSH-9.2 carries, in the same way `supportsBuilderMessage` treats a code-only registry entry. #### Example ```ts import { SUPPORTED_OVERLAY_MESSAGES } from "@cosyte/hl7"; const oru = SUPPORTED_OVERLAY_MESSAGES.find((m) => m.key === "ORU^R01"); console.log(oru?.messageCode, oru?.triggerEvent); // "ORU" "R01" console.log(oru?.requiredSegments); // ["MSH", "OBR"] ``` #### Properties ##### key > `readonly` **key**: `string` The overlay key, e.g. `"ORU^R01"` or the code-only `"ACK"`. ##### messageCode > `readonly` **messageCode**: `string` MSH-9.1 message code. ##### requiredSegments > `readonly` **requiredSegments**: readonly `string`[] The segments the derived structure registry marks required for this key. ##### triggerEvent > `readonly` **triggerEvent**: `string` MSH-9.2 trigger event; `""` when the registry matches on message code alone. *** ### TextRun One contiguous run of rendered display text, tagged with whether it fell inside a `\H`…`\N` highlight span. Runs preserve emphasis boundaries that the flat [RenderedText.text](#text-3) intentionally drops. #### Properties ##### highlighted > `readonly` **highlighted**: `boolean` `true` when this run is inside a `\H\`…`\N` highlight span. ##### text > `readonly` **text**: `string` The literal display text of this run: escape sentinels already resolved (delimiter/hex decoded, formatting → whitespace/line breaks). Never contains a formatting sentinel; may contain the newline used for breaks. *** ### TimingQuantity A composite-quantity (CQ) value on an order/medication timing: the TQ1-2 service quantity. `value` is strict-`Number()` parsed (`undefined`, never `NaN`); `units` carries any CQ.2 units. Both keys OMITTED when absent. #### Example ```ts import type { TimingQuantity } from "@cosyte/hl7"; const q: TimingQuantity = { value: 1, units: { identifier: "tablet" } }; ``` #### Properties ##### units? > `readonly` `optional` **units?**: [`CWE`](#cwe-1) CQ.2 units. ##### value? > `readonly` `optional` **value?**: `number` CQ.1 quantity numeric value (strict-parsed; never `NaN`). *** ### TypedBuilderCoverage One typed builder's emit capability: the message code it authors and the segment names its typed init can populate. The published support set is the intersection of this with the derived structure registry. #### Example ```ts import { TYPED_BUILDER_COVERAGE } from "@cosyte/hl7"; const adt = TYPED_BUILDER_COVERAGE.find((c) => c.builder === "buildAdt"); console.log(adt?.segments.includes("MRG")); // true: prior identity is supported ``` #### Properties ##### builder > `readonly` **builder**: `string` The exported builder function that authors this message code. ##### fixedTriggerEvents > `readonly` **fixedTriggerEvents**: readonly `string`[] The trigger events this builder can author, when it authors a fixed set. Empty when the builder authors every trigger event the registry records for its message code (the builders that take a trigger-event argument). ##### messageCode > `readonly` **messageCode**: `string` MSH-9.1 message code this builder authors. ##### segments > `readonly` **segments**: readonly `string`[] Every segment name the builder's typed init can populate. *** ### TypedMessage A parsed message narrowed to one overlay key: every `Hl7Message` member unchanged, the message code and trigger event at literal types, and `part` / `parts` scoped at compile time to the segment names that pair's published structure marks required. It is a view, not a subclass: `msg.is(key)` narrows the message you already have, allocates nothing, and changes no runtime behaviour. Nothing here is typed as guaranteed-present, because the parser tolerates a message that omits a required segment (and warns); `part` returns `Segment | undefined` and `parts` a possibly-empty list, exactly as `segments` does. #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); if (msg.is("ORU^R01")) { const obr = msg.part("OBR"); // Segment | undefined, no cast console.log(obr?.field(4).value); // msg.part("PID"); // does not compile: PID is not required for ORU^R01 console.log(msg.segments("PID").length); // the base accessor takes any name } ``` #### Extends - [`Hl7Message`](#hl7message) #### Type Parameters ##### K `K` *extends* [`OverlayKey`](#overlaykey) #### Properties ##### dateFormats > `readonly` **dateFormats**: readonly `string`[] Merged `dateFormats` list: `options.dateFormats ++ profile.dateFormats` deduped first-occurrence per D-21. Empty array when neither source supplied any formats. This is the list every datetime in the message honours, from `meta.timestamp` to any `field.asTs()`, in exactly this order; exposed so a caller can introspect what their options and profile added up to. ###### Inherited from [`ProfiledMessage`](#profiledmessage).[`dateFormats`](#dateformats-7) ##### encodingCharacters > `readonly` **encodingCharacters**: [`EncodingCharacters`](#encodingcharacters-1) ###### Inherited from [`Hl7Message`](#hl7message).[`encodingCharacters`](#encodingcharacters) ##### meta > `readonly` **meta**: [`TypedMeta`](#typedmeta)\<`K`\> MSH-derived metadata, with the message type at the key's literal types. ###### Overrides [`Hl7Message`](#hl7message).[`meta`](#meta) ##### profile > `readonly` **profile**: \{ `lineage`: readonly `string`[]; `name`: `string`; \} \| `undefined` ###### Inherited from [`Hl7Message`](#hl7message).[`profile`](#profile) ##### rawSegments > `readonly` **rawSegments**: readonly [`RawSegment`](#rawsegment)[] Raw positional tree produced by the parser. 1-indexed per HL7 convention (`fields[0]` is the segment-name / MSH separator placeholder slot). Use `segments(type)` / `allSegments()` for typed wrapper access: this field is exposed for advanced callers that need the raw tree directly. ###### Inherited from [`ProfiledMessage`](#profiledmessage).[`rawSegments`](#rawsegments-1) ##### version > `readonly` **version**: `string` HL7 version the model asserts for this message, from MSH-12.1.1, e.g. `"2.5"` or `"2.5.1"`. **Bounded, and it is `""` when MSH-12 does not hold a version.** Same reasoning as [Segment.type](#type): it presents as an identifier and consumers label with it. `meta.version` keeps MSH-12 exactly as it arrived, and is what version-scoped field selection reads, so this bound changes no parsing behaviour. ###### Inherited from [`Hl7Message`](#hl7message).[`version`](#version) ##### warnings > `readonly` **warnings**: readonly [`Hl7ParseWarning`](#hl7parsewarning)[] ###### Inherited from [`ProfiledMessage`](#profiledmessage).[`warnings`](#warnings-3) #### Accessors ##### patient ###### Get Signature > **get** **patient**(): [`Patient`](#patient-8) \| `undefined` PID-derived patient view, or `undefined` when no PID segment exists (D-04). D-02: memoized. HELPERS-07: never throws: absent fields surface as `undefined` on the returned `Patient` object. ###### Example ```ts console.log(msg.patient?.mrn); console.log(msg.patient?.fullName); console.log(msg.patient?.dateOfBirth?.raw); // fidelity TS: e.g. "19800115" ``` ###### Returns [`Patient`](#patient-8) \| `undefined` ###### Inherited from [`Hl7Message`](#hl7message).[`patient`](#patient) ##### structure ###### Get Signature > **get** **structure**(): [`MessageStructure`](#messagestructure) Structural-conformance summary for the common message types: a misroute/truncation safety net, NOT a conformance validator. Reports, per the message's recognized (MSH-9.1, MSH-9.2) type, which Required segment groups are present and which are entirely absent (`missingGroups`: the same set the parser flags as `MISSING_EXPECTED_GROUP` warnings). For an unmodelled type, `recognized` is `false` and `missingGroups` is empty. D-02: memoized. ###### Example ```ts console.log(msg.structure.recognized); // true for ORU^R01, ADT^A01, … console.log(msg.structure.missingGroups); // e.g. ["result"] if no OBR/OBX ``` ###### Returns [`MessageStructure`](#messagestructure) ###### Inherited from [`Hl7Message`](#hl7message).[`structure`](#structure) ##### visit ###### Get Signature > **get** **visit**(): [`Visit`](#visit-7) \| `undefined` PV1-derived visit view, or `undefined` when no PV1 segment exists (HELPERS-03). D-02: memoized. HELPERS-07: never throws. ###### Example ```ts console.log(msg.visit?.patientClass); // "I" console.log(msg.visit?.admitDateTime?.raw); // fidelity TS console.log(msg.visit?.attendingDoctor?.familyName); ``` ###### Returns [`Visit`](#visit-7) \| `undefined` ###### Inherited from [`Hl7Message`](#hl7message).[`visit`](#visit) #### Methods ##### addSegment() > **addSegment**(`name`, `fields`): `this` Append a new segment to the end of the message. `name` must match `/^(?:[A-Z]{3}|Z[A-Z0-9]{2})$/u`: throws `TypeError` otherwise (D-19). `fields` is interpreted in HL7 1-indexed terms: `addSegment("NTE", [a, b, c])` produces a segment whose `NTE-1 = a`, `NTE-2 = b`, `NTE-3 = c`. The internal `RawSegment.fields[0]` name/separator placeholder is synthesized by this method. Each entry may be a plain string (treated as a single-subcomponent single-component single-repetition field) or a full `RawField` object for advanced callers who need structured content. Invalidates caches on return; warnings untouched (D-16). ###### Parameters ###### name `string` ###### fields readonly (`string` \| [`RawField`](#rawfield))[] ###### Returns `this` ###### Example ```ts msg.addSegment("NTE", ["", "note text"]); msg.get("NTE.2"); // "note text" ``` ###### Inherited from [`Hl7Message`](#hl7message).[`addSegment`](#addsegment) ##### allergies() > **allergies**(): readonly [`Allergy`](#allergy)[] Every AL1 and every IAM as an Allergy, one entry per segment in document order, each naming the segment it was read from (`source`). An ADT^A60 carries its allergies in IAM, so it is read here too. D-05: returns `[]` when neither is present. Limits: the IAR and NTE segments under an IAM are not read, nor IAM-1 or IAM-8 onward (reach them with `msg.segments("IAM")`). IAM-6 is surfaced as `actionCode`, and an exact `D` sets `deleteRequested`, but no action is ever applied: a delete entry is still returned, an update never replaces an earlier entry, and an AL1 and an IAM for the same allergen are two entries. ###### Returns readonly [`Allergy`](#allergy)[] ###### Example ```ts for (const al of msg.allergies()) { const kind = al.deleteRequested === true ? "delete request" : "allergy"; console.log(kind, al.source, al.code?.text, al.severity, al.actionCode); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`allergies`](#allergies) ##### allSegments() > **allSegments**(): readonly [`Segment`](#segment)\<`string`\>[] Iterate every `Segment` in document order (MSH first, then every subsequent segment). Cached per-message; same array reference and same Segment instances on repeat calls (D-11). Invalidated wholesale by the mutation methods. ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts for (const seg of msg.allSegments()) { console.log(seg.type); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`allSegments`](#allsegments) ##### appointments() > **appointments**(): readonly [`Appointment`](#appointment)[] Every SCH of an SIU message as a typed `Appointment`, with the AIS/AIG/AIL/AIP resource segments that follow it grouped positionally under that SCH. Surfaces the placer/filler appointment ids, SCH-25 filler status (Table 0278), SCH-11 start/end timing, and the resource groups (service / general / location / personnel). D-05: returns `[]` when no SCH is present. D-06: not memoized. Never throws (HELPERS-07). Not a scheduling-workflow state machine: see the package known-limitations. ###### Returns readonly [`Appointment`](#appointment)[] ###### Example ```ts for (const appt of msg.appointments()) { console.log(appt.fillerAppointmentId, appt.fillerStatusCode?.identifier); for (const r of appt.resources) console.log(r.kind, r.code?.identifier); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`appointments`](#appointments) ##### charges() > **charges**(): readonly [`Charge`](#charge)[] Every FT1 of a DFT message as a typed `Charge`, one per FT1 in document order. Surfaces billing-critical fields (FT1-6 transaction type, FT1-7 code, FT1-11/12 extended/unit amount, FT1-19 diagnosis linkage) with **no billing logic and no money-as-float**: amounts are the verbatim CP wire text. D-05: returns `[]` when no FT1 is present. D-06: not memoized. Never throws (HELPERS-07). ###### Returns readonly [`Charge`](#charge)[] ###### Example ```ts for (const charge of msg.charges()) { console.log(charge.transactionType, charge.transactionCode?.identifier); console.log(charge.amountExtended); // verbatim, never a number } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`charges`](#charges) ##### diagnoses() > **diagnoses**(): readonly [`Diagnosis`](#diagnosis)[] Every DG1 as a Diagnosis in document order. D-05: returns `[]` when no DG1 present. ###### Returns readonly [`Diagnosis`](#diagnosis)[] ###### Example ```ts for (const dg of msg.diagnoses()) console.log(dg.code?.identifier); ``` ###### Inherited from [`Hl7Message`](#hl7message).[`diagnoses`](#diagnoses) ##### documents() > **documents**(): readonly [`ClinicalDocument`](#clinicaldocument)[] Every TXA of an MDM message as a typed `ClinicalDocument`, with the OBX narrative body grouped positionally under that TXA. The completion status (TXA-17) and availability status (TXA-19) are surfaced as **distinct** fields and never conflated: a document can be available before it is authenticated, and reading a preliminary document as final is the harm. D-05: returns `[]` when no TXA is present. D-06: not memoized. Never throws (HELPERS-07). ###### Returns readonly [`ClinicalDocument`](#clinicaldocument)[] ###### Example ```ts for (const doc of msg.documents()) { console.log(doc.documentType, doc.completionStatus, doc.availabilityStatus); for (const obx of doc.observations) console.log(obx.value); // narrative body } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`documents`](#documents) ##### get() > **get**(`path`): `string` \| `undefined` Resolve a dot-path (e.g. `PID.5.1`, `OBX[2].5`, `PID.3[0].1`) to its decoded leaf string (unescaped once at parse: never re-unescaped on read). Returns `undefined` when the path doesn't resolve: never throws on missing path (MODEL-05). Throws `TypeError` on malformed path syntax (e.g. `"pid.5"`, empty string). ###### Parameters ###### path `string` ###### Returns `string` \| `undefined` ###### Example ```ts const msg = parseHL7(raw); msg.get("PID.5.1"); // "Smith" msg.get("OBX[2].5"); // third OBX's 5th field msg.get("NOT.9.9"); // undefined msg.get("MSH.12"); // "2.5": HL7 version string ``` ###### Inherited from [`Hl7Message`](#hl7message).[`get`](#get) ##### getAll() > **getAll**(`segmentType`): readonly [`Segment`](#segment)\<`string`\>[] Return every `Segment` of `segmentType` in document order. Returns `[]` (empty array, NEVER `undefined`) when no segment of that type exists (MODEL-02). Alias for `segments(segmentType)`: shares the same cache, and matches segment names ignoring case exactly as it does. ###### Parameters ###### segmentType `string` ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts for (const obx of msg.getAll("OBX")) { console.log(obx.field(5).value); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`getAll`](#getall) ##### identityEvents() > **identityEvents**(): readonly [`IdentityEvent`](#identityevent)[] Every recognized ADT patient-identity event (merge / move / change / link / unlink / person add/update), with the MRG-sourced `prior` and PID/PV1-sourced `surviving` parties labelled by role and the spec-constant `direction: "MRG_TO_PID"` on merge/move/change events. Returns `[]` when the trigger event is not in the identity family. The recognized set is floored by the published message structures: every ADT trigger event whose structure requires MRG is recognized. D-06: not memoized. Never throws; incomplete merge pairs surface a `MERGE_MISSING_PRIOR_OR_SURVIVOR` warning on the event. ###### Returns readonly [`IdentityEvent`](#identityevent)[] ###### Example ```ts for (const ev of msg.identityEvents()) { if (ev.kind === "merge" && ev.prior && ev.surviving) { // retire ev.prior.identifiers in favour of ev.surviving.identifiers } } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`identityEvents`](#identityevents) ##### immunizations() > **immunizations**(): readonly [`Immunization`](#immunization)[] Every RXA of a VXU^V04 as a typed `Immunization`, with RXR (route/site) and OBX children grouped positionally under the RXA and `orderControl` from the preceding ORC of the VXU order group. D-05: returns `[]` when no RXA present. D-06: not memoized. The vaccine code carries its own provenance; the action code (RXA-21) is surfaced verbatim and `recordOrigin` (administered vs historical) is derived only from the well-known NIP001 RXA-9.1 codes: never guessed. ###### Returns readonly [`Immunization`](#immunization)[] ###### Example ```ts for (const imm of msg.immunizations()) { console.log(imm.vaccineCode?.identifier, imm.doseAmount, imm.recordOrigin); console.log(imm.actionCode, imm.completionStatus); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`immunizations`](#immunizations) ##### insurance() > **insurance**(): readonly [`Insurance`](#insurance-1)[] Every IN1 as an Insurance entry with positional IN2/IN3 presence flags. D-05: returns `[]` when no IN1 present. ###### Returns readonly [`Insurance`](#insurance-1)[] ###### Example ```ts for (const ins of msg.insurance()) console.log(ins.planId?.text); ``` ###### Inherited from [`Hl7Message`](#hl7message).[`insurance`](#insurance) ##### is() **`Internal`** Single runtime implementation behind both signatures. ###### Call Signature > **is**\<`K`\>(`key`): `this is TypedMessage` Is this message of the type `key` names, and if so, narrow it for the compiler. The check is on the (MSH-9.1, MSH-9.2) pair the parser already extracted, so a message whose MSH-9 carries the three-component form `ADT^A01^ADT_A01` answers `true` to `is("ADT^A01")`. A key is `"^"` (`"ADT^A01"`), or `""` alone for a message type the published structure registry matches on message code alone (`"ACK"`, whose MSH-9.2 carries the acknowledged message's trigger event). `SUPPORTED_OVERLAY_MESSAGES` enumerates every key. **Any other string is `false`, never a throw**: an unrecognized type, the three-component form as a string, an empty string, or a value computed at run time. `is` does not parse its own argument, and a caller who wants a raw comparison has `msg.meta.type`. Read-only: it inspects `meta` and mutates nothing. ###### Type Parameters ###### K `K` *extends* keyof [`OverlayStructures`](#overlaystructures) ###### Parameters ###### key `K` ###### Returns `this is TypedMessage` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); if (msg.is("ORU^R01")) { const code: "ORU" = msg.meta.messageCode; // literal, no cast console.log(msg.part("OBR")?.field(4).value); } console.log(msg.is("ZZZ^Z99")); // false: not a published key ``` ###### Inherited from [`Hl7Message`](#hl7message).[`is`](#is) ###### Call Signature > **is**(`key`): `boolean` Answer for a key that is not known at compile time. A runtime-computed string cannot narrow anything, so this form returns a plain `boolean`. ###### Parameters ###### key `string` ###### Returns `boolean` ###### Inherited from [`Hl7Message`](#hl7message).[`is`](#is) ##### medications() > **medications**(): readonly [`Medication`](#medication)[] Every RXO/RXE/RXD/RXA as a typed `Medication`, with RXR (route) and RXC (component) segments grouped positionally under their parent. D-05: returns `[]` when no RX* parent present. D-06: not memoized. The give *amount* and give *strength* are surfaced separately and never reconciled. Each medication also carries its TQ1 / legacy embedded-TQ (RXE-1) `timings` (repeat pattern verbatim, never resolved to a schedule). Each medication also carries `orderControl`: ORC-1 of the ORC that opened its order group (every RX* up to the next ORC shares it), exactly as sent. It is never interpreted into an active, held or discontinued state, and it is omitted when no ORC precedes the RX* or ORC-1 is empty, never carried over from an earlier group. ###### Returns readonly [`Medication`](#medication)[] ###### Example ```ts for (const med of msg.medications()) { console.log(med.orderControl); // e.g. "NW" (new) or "DC" (discontinue), verbatim console.log(med.context, med.giveCode?.identifier, med.giveCode?.nameOfCodingSystem); console.log(med.amount?.minimum, med.strength?.value); for (const t of med.timings) console.log(t.repeatPattern?.code, t.totalOccurrences); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`medications`](#medications) ##### nextOfKin() > **nextOfKin**(): readonly [`NextOfKin`](#nextofkin-1)[] Every NK1 as a NextOfKin entry in document order. D-05: returns `[]` when no NK1 present. ###### Returns readonly [`NextOfKin`](#nextofkin-1)[] ###### Example ```ts for (const nk of msg.nextOfKin()) { console.log(nk.name?.familyName, nk.relationship?.text); } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`nextOfKin`](#nextofkin) ##### notes() > **notes**(): readonly `string`[] Message-level NTE notes: every NTE segment with no recognized preceding parent (not immediately following a `PID`, `ORC`, `OBR`, or `OBX`), surfaced verbatim in document order so nothing is dropped. Notes that DO attach to a specific patient / order / result are exposed on those helper outputs (`msg.patient?.notes`, `order.notes`, `observation.notes`), not here. D-05: returns `[]` when there are none. D-06: NOT memoized. ###### Returns readonly `string`[] ###### Example ```ts for (const note of msg.notes()) console.log(note); // message-level narrative ``` ###### Inherited from [`Hl7Message`](#hl7message).[`notes`](#notes) ##### observations() > **observations**(): readonly [`Observation`](#observation)[] Every OBX segment as a typed Observation in document order. D-05: returns `[]` when no OBX present. D-06: NOT memoized: each call re-walks `rawSegments`. Value type is discriminated per D-13. ###### Returns readonly [`Observation`](#observation)[] ###### Example ```ts for (const obs of msg.observations()) { if (obs.valueType === "NM") console.log(obs.value); // number | undefined } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`observations`](#observations) ##### orders() > **orders**(): readonly [`Order`](#order-1)[] Every OBR as an Order with its OBX children grouped positionally (D-12) and its TQ1 / legacy embedded-TQ (ORC-7) `timings` (the repeat pattern is surfaced verbatim, never resolved to a schedule). D-05: returns `[]` when no OBR present. D-06: not memoized. ###### Returns readonly [`Order`](#order-1)[] ###### Example ```ts for (const order of msg.orders()) { console.log(order.placerOrderNumber, order.observations.length); for (const t of order.timings) console.log(t.repeatPattern?.code); // e.g. "Q6H": verbatim } ``` ###### Inherited from [`Hl7Message`](#hl7message).[`orders`](#orders) ##### part() > **part**(`name`): [`Segment`](#segment)\<`string`\> \| `undefined` The first segment of `name`, or `undefined` when the message carries none. ###### Parameters ###### name [`OverlayRequiredSegment`](#overlayrequiredsegment)\<`K`\> ###### Returns [`Segment`](#segment)\<`string`\> \| `undefined` ###### Overrides [`Hl7Message`](#hl7message).[`part`](#part) ##### parts() > **parts**(`name`): readonly [`Segment`](#segment)\<`string`\>[] Every segment of `name` in document order; empty when there are none. ###### Parameters ###### name [`OverlayRequiredSegment`](#overlayrequiredsegment)\<`K`\> ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Overrides [`Hl7Message`](#hl7message).[`parts`](#parts) ##### prettyPrint() > **prettyPrint**(): `string` Emit this message as a human-readable multi-line string for logs and debugging (SER-04). Single opinionated format (D-22 no options): header line with type / controlId / timestamp / segment count, then one line per segment with labeled `[N]=value` fields (D-23). Composite values render as their raw HL7 string: depth stops at field level (D-24). Pure: never warns, never throws (D-26). **Field values render as their raw HL7 string representation.** Embedded delimiters in user data appear as escape sequences: e.g. a patient family name containing `|` renders as `Smith\F\Jones` (NOT `Smith|Jones`). This preserves round-trip fidelity: copy-pasting prettyPrint output into `parseHL7` yields a structurally equivalent message. For un-escaped human display, parse the composite first via typed accessors (e.g. `msg.patient?.familyName`): those return already-decoded strings. ###### Returns `string` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); console.log(msg.prettyPrint()); // HL7 ADT^A01 controlId=MSG001 timestamp=2026-04-19T10:15:00Z (5 segments) // MSH [3]=SENDAPP [4]=SENDFAC ... // PID [1]=1 [3]=MRN123 [5]=Doe^John ``` ###### Inherited from [`Hl7Message`](#hl7message).[`prettyPrint`](#prettyprint) ##### removeSegment() > **removeSegment**(`segmentType`, `occurrenceOrOptions?`): `this` Remove segments by type + occurrence or by type + all. Call shapes: - `removeSegment("NTE")`: remove the FIRST NTE (occurrence 0). - `removeSegment("OBX", 1)`: remove the SECOND OBX (0-indexed per D-01). - `removeSegment("OBX", { all: true })`: remove ALL OBX segments. `MSH` is protected: `removeSegment("MSH")` throws `TypeError` (every HL7 message must retain its MSH segment). Unknown segment types are a no-op (idempotent; no throw). Segment name must match the D-19 shape regex: invalid shapes throw `TypeError` for symmetry with `addSegment`. Invalidates caches on return; warnings untouched (D-16). ###### Parameters ###### segmentType `string` ###### occurrenceOrOptions? `number` \| \{ `all?`: `boolean`; \} ###### Returns `this` ###### Example ```ts msg.removeSegment("NTE"); // remove first NTE msg.removeSegment("OBX", 1); // remove second OBX msg.removeSegment("OBX", { all: true }); // remove all remaining OBX ``` ###### Inherited from [`Hl7Message`](#hl7message).[`removeSegment`](#removesegment) ##### segments() > **segments**(`segmentType`): readonly [`Segment`](#segment)\<`string`\>[] Return the cached array of `Segment` wrappers for `segmentType` in document order. The returned array identity and the individual Segment instances are both stable across calls (D-11). Invalidated wholesale by the mutation methods. **Segment names are matched ignoring ASCII case, on both sides.** A sender that ships `obx` is returned by `segments("OBX")`, and `segments("obx")` returns the same array (one cache entry, so the D-11 identity guarantee does not split across spellings). A `SEGMENT_CASE` warning records the sender's deviation; `Segment.raw.name` is the spelling that arrived. ###### Parameters ###### segmentType `string` ###### Returns readonly [`Segment`](#segment)\<`string`\>[] ###### Example ```ts const pid = msg.segments("PID")[0]; if (pid !== undefined) console.log(pid.field(5).value); ``` ###### Inherited from [`Hl7Message`](#hl7message).[`segments`](#segments) ##### setComposite() > **setComposite**\<`K`\>(`path`, `kind`, `value`): `this` Set a **typed composite** at a field (or field-repetition) dot-path: the conservative-emit mirror of the typed read accessors (`asXpn`/`asCx`/…). The caller passes a structured value (an XPN name, a CX identifier, a TS timestamp, …) by its [CompositeKind](#compositekind), and the setter encodes it into a spec-clean field using the encode-safe path: any delimiter embedded in a component value is **escaped, never injected**, so a `familyName` of `"Smith^Jr"` re-parses to exactly that string rather than forging a component boundary. No hand-assembly of `^`/`&`/`~`. The path must resolve to a **field** (`"PID.5"`) or a specific **repetition** of a field (`"PID.11[1]"`). A component/subcomponent-level path (`"PID.5.1"`) is rejected with `TypeError`: a composite occupies a whole field, not a single component. Like [setField](#setfield), the target segment must already exist (`addSegment` first); the repetition defaults to index 0 and other repetitions of the field are preserved. Never fabricates: an omitted optional composite field encodes to an empty/absent component, never a defaulted value; an all-empty composite clears the field. Segment/helper caches are invalidated on success; the frozen `warnings` array is untouched. ###### Type Parameters ###### K `K` *extends* [`CompositeKind`](#compositekind) ###### Parameters ###### path `string` ###### kind `K` ###### value [`CompositeValueByKind`](#compositevaluebykind)\[`K`\] ###### Returns `this` ###### Example ```ts const msg = buildMessage({ type: "ADT^A01" }).addSegment("PID", [""]); msg.setComposite("PID.5", "XPN", { familyName: "Smith", givenName: "Ann" }); msg.setComposite("PID.3", "CX", { idNumber: "MRN001", identifierTypeCode: "MR" }); msg.setComposite("PID.7", "TS", "19880705"); msg.get("PID.5.1"); // "Smith" ``` ###### Inherited from [`Hl7Message`](#hl7message).[`setComposite`](#setcomposite) ##### setField() > **setField**(`path`, `value`): `this` Set the string value at a dot-path. Mutates the underlying tree and returns `this` for chaining (D-15). Auto-creates missing repetitions, components, and subcomponents WITHIN an existing field, but does NOT auto-create segments: callers must `addSegment` first (throws `TypeError` with an actionable message otherwise). The value is accepted verbatim: unescaped delimiter characters are NOT rejected on input (D-18). Re-escaping is the serializer's concern. MSH-1 / MSH-2 follow the user-facing HL7 convention: `setField("MSH.3", ...)` targets MSH-3 (sending application), matching `msg.get("MSH.3")`. Segment/Field wrapper caches are invalidated wholesale on success (D-17). The frozen `warnings` array is never touched (D-16). ###### Parameters ###### path `string` ###### value `string` ###### Returns `this` ###### Example ```ts msg.setField("PID.8", "F"); // patient sex → F msg.setField("PID.5.1", "Jones"); // family name msg.setField("PID.4[2].1", "MRN2"); // create third repetition of PID-4 ``` ###### Inherited from [`Hl7Message`](#hl7message).[`setField`](#setfield) ##### toJSON() > **toJSON**(): [`SerializedMessage`](#serializedmessage) Emit this message as a structured `SerializedMessage` JSON projection (SER-03). Invoked automatically by `JSON.stringify(msg)` (D-18). Re-walks `rawSegments` on every call (D-30 no caching). Mirrors the raw tree one-for-one, preserves `isNull`, always includes `warnings: []`, and includes `profile: { name, lineage }` only when `this.profile` is truthy (D-19/D-20). Pure: never warns, never throws. ###### Returns [`SerializedMessage`](#serializedmessage) ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); const snap = msg.toJSON(); console.log(snap.segments[0]?.name); // "MSH" console.log(JSON.stringify(msg)); // same content, auto-invokes toJSON ``` ###### Inherited from [`Hl7Message`](#hl7message).[`toJSON`](#tojson) ##### toString() > **toString**(): `string` Emit this message as spec-clean HL7 (SER-01). Re-walks `rawSegments` on every call (D-30 no caching). Segments are joined with `\r` per D-05; MSH-1 and MSH-2 are inlined verbatim from `this.encodingCharacters` per D-06; every field string passes through `reescape` per D-04. `RawField.isNull === true` is preserved as the HL7 literal `""` (D-02). Pure: never warns, never throws (D-07). ###### Returns `string` ###### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); console.log(msg.toString()); // spec-clean, CR-separated HL7 ``` ###### Inherited from [`Hl7Message`](#hl7message).[`toString`](#tostring) *** ### UsageOutcomes The two usage outcomes a [DeclaredConditionalUsage](#declaredconditionalusage) token records: the usage that applies when the condition is true, and the one that applies when it is false. Both are simple codes. #### Example ```ts import type { UsageOutcomes } from "@cosyte/hl7"; const outcomes: UsageOutcomes = { whenTrue: "RE", whenFalse: "X" }; ``` #### Properties ##### whenFalse > `readonly` **whenFalse**: [`SimpleUsageCode`](#simpleusagecode) The usage that applies when the condition is resolved false. ##### whenTrue > `readonly` **whenTrue**: [`SimpleUsageCode`](#simpleusagecode) The usage that applies when the condition is resolved true. *** ### UsageResolution A caller-supplied resolution of one declared-conditional rule. The engine evaluates no condition predicate and reads nothing from the message to decide an outcome: **the caller decides**, and passes an ordered list of these alongside the message and the profile. The `segment` name plus the 1-indexed `field` (absent when the target is a segment rule) is the LOCUS the resolution applies to. It applies to the RULE at that locus, hence to every occurrence of the segment and every repetition of the field: there is no per-occurrence resolution. A resolution whose locus matches no declared rule, matches more than one, or matches a rule whose usage is not a declared conditional is a profile defect ([FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed)), never a silent no-op. #### Example ```ts import type { UsageResolution } from "@cosyte/hl7"; // "PID-8's condition is true for this message." const resolution: UsageResolution = { segment: "PID", field: 8, outcome: true }; ``` #### Properties ##### field? > `readonly` `optional` **field?**: `number` 1-indexed field position when the target is a field rule; absent for a segment rule. ##### outcome > `readonly` **outcome**: `boolean` `true` applies the token's true outcome, `false` its false outcome. ##### segment > `readonly` **segment**: `string` Segment name of the target rule (e.g. `"PID"`). *** ### Visit PV1-derived visit view (HELPERS-03). `msg.visit` is `undefined` when no PV1 segment exists; this interface describes the shape when present. D-24a: doctor fields use XCN (not flat strings). Date/time fields are the fidelity `TS` (precision + timezone preserved). #### Example ```ts import type { Visit } from "@cosyte/hl7"; const v: Visit = { patientClass: "I", location: { pointOfCare: "ICU", room: "101" }, visitNumber: "VISIT001", }; console.log(v.attendingDoctor?.familyName); console.log(v.admitDateTime?.raw); ``` #### Properties ##### admitDateTime? > `readonly` `optional` **admitDateTime?**: [`DtmParts`](#dtmparts) PV1-44 admit date/time as the fidelity `TS`. ##### attendingDoctor? > `readonly` `optional` **attendingDoctor?**: [`XCN`](#xcn-1) PV1-7 attending doctor (D-24a XCN). ##### dischargeDateTime? > `readonly` `optional` **dischargeDateTime?**: [`DtmParts`](#dtmparts) PV1-45 discharge date/time as the fidelity `TS`. ##### location? > `readonly` `optional` **location?**: [`PL`](#pl-1) PV1-3 assigned patient location (ward / room / bed) as PL. ##### patientClass? > `readonly` `optional` **patientClass?**: `string` PV1-2 patient class ("I"=inpatient, "O"=outpatient, "E"=ER, ...). ##### referringDoctor? > `readonly` `optional` **referringDoctor?**: [`XCN`](#xcn-1) PV1-8 referring doctor (D-24a XCN). ##### visitNumber? > `readonly` `optional` **visitNumber?**: `string` PV1-19 visit number. *** ### VxuImmunization Typed RXA (administered dose) content for [buildVxu](#buildvxu). #### Example ```ts import type { VxuImmunization } from "@cosyte/hl7"; const dose: VxuImmunization = { orderControl: "RE", administeredDateTime: "20260801", vaccineCode: { identifier: "115", text: "Tdap", nameOfCodingSystem: "CVX" }, doseAmount: "0.5", doseUnits: { identifier: "mL", nameOfCodingSystem: "UCUM" }, completionStatus: "CP", actionCode: "A", }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` RXA-21 Action Code (`A` add, `D` delete, `U` update). Never defaulted. ##### administeredDateTime? > `readonly` `optional` **administeredDateTime?**: `string` \| [`DtmParts`](#dtmparts) RXA-3 Date/Time Start of Administration. ##### administrationSubIdCounter? > `readonly` `optional` **administrationSubIdCounter?**: `string` RXA-2 Administration Sub-ID Counter. ##### completionStatus? > `readonly` `optional` **completionStatus?**: `string` RXA-20 Completion Status (`CP`, `RE`, `NA`, `PA`). ##### doseAmount? > `readonly` `optional` **doseAmount?**: `string` \| `number` \| [`NM`](#nm-1) RXA-6 Administered Amount (emitted verbatim: the caller owns its precision). ##### doseUnits? > `readonly` `optional` **doseUnits?**: [`CWE`](#cwe-1) RXA-7 Administered Units. ##### expirationDate? > `readonly` `optional` **expirationDate?**: `string` \| [`DtmParts`](#dtmparts) RXA-16 Substance Expiration Date. ##### giveSubIdCounter? > `readonly` `optional` **giveSubIdCounter?**: `string` RXA-1 Give Sub-ID Counter. ##### informationSource? > `readonly` `optional` **informationSource?**: [`CWE`](#cwe-1) RXA-9 Administration Notes: the immunization information source (HL7 Table NIP001). ##### lotNumber? > `readonly` `optional` **lotNumber?**: `string` RXA-15 Substance Lot Number. ##### manufacturer? > `readonly` `optional` **manufacturer?**: [`CWE`](#cwe-1) RXA-17 Substance Manufacturer Name (MVX, HL7 Table 0227). ##### observations? > `readonly` `optional` **observations?**: readonly [`OruObservation`](#oruobservation)[] OBX children of this dose (eligibility, funding source, …). ##### orderControl? > `readonly` `optional` **orderControl?**: `string` ORC-1 Order Control of the ORC that opens this dose's order group. An ORC is emitted only when this is supplied; the read side surfaces it as `orderControl`. ##### refusalReason? > `readonly` `optional` **refusalReason?**: [`CWE`](#cwe-1) RXA-18 Substance/Treatment Refusal Reason: a refused dose carries this. ##### routes? > `readonly` `optional` **routes?**: readonly [`VxuRoute`](#vxuroute)[] RXR children of this dose (route / administration site). ##### vaccineCode? > `readonly` `optional` **vaccineCode?**: [`CWE`](#cwe-1) RXA-5 Administered Code (CVX, HL7 Table 0292). *** ### VxuRoute Typed RXR (route/site) content grouped under one [VxuImmunization](#vxuimmunization). #### Example ```ts import type { VxuRoute } from "@cosyte/hl7"; const route: VxuRoute = { route: { identifier: "IM", text: "Intramuscular" }, site: { identifier: "LD", text: "Left deltoid" }, }; ``` #### Properties ##### route? > `readonly` `optional` **route?**: [`CWE`](#cwe-1) RXR-1 Route (HL7 Table 0162). ##### site? > `readonly` `optional` **site?**: [`CWE`](#cwe-1) RXR-2 Administration Site (HL7 Table 0163). *** ### XAD HL7 v2 Extended Address (XAD): structured postal address per HL7 Chapter 2. All 12 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. street: street address (house number + street name). 2. otherDesignation: apartment number, suite, floor, etc. 3. city 4. stateOrProvince 5. zipOrPostalCode 6. country (ISO-3166 3-letter, e.g. "USA", "CAN") 7. addressType (H=Home, B=Business, M=Mailing, O=Office, P=Permanent, ...) 8. otherGeographicDesignation 9. countyParishCode 10. censusTract 11. addressRepresentationCode 12. addressValidityRange #### Example ```ts import type { XAD } from "@cosyte/hl7"; const addr: XAD = { street: "123 Main St", city: "Boston", stateOrProvince: "MA" }; ``` #### Properties ##### addressRepresentationCode? > `readonly` `optional` **addressRepresentationCode?**: `string` ##### addressType? > `readonly` `optional` **addressType?**: `string` ##### addressValidityRange? > `readonly` `optional` **addressValidityRange?**: `string` ##### censusTract? > `readonly` `optional` **censusTract?**: `string` ##### city? > `readonly` `optional` **city?**: `string` ##### country? > `readonly` `optional` **country?**: `string` ##### countyParishCode? > `readonly` `optional` **countyParishCode?**: `string` ##### otherDesignation? > `readonly` `optional` **otherDesignation?**: `string` ##### otherGeographicDesignation? > `readonly` `optional` **otherGeographicDesignation?**: `string` ##### stateOrProvince? > `readonly` `optional` **stateOrProvince?**: `string` ##### street? > `readonly` `optional` **street?**: `string` ##### zipOrPostalCode? > `readonly` `optional` **zipOrPostalCode?**: `string` *** ### XCN HL7 v2 Extended Composite ID Number and Name for Persons (XCN): per HL7 Chapter 2.A.88. All 13 v1 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. idNumber: e.g. employee ID, NPI digits, DEA number (CX-1 analogue) 2. familyName (XPN-1) 3. givenName (XPN-2) 4. secondName: second and further given names (XPN-3) 5. suffix: Jr., III, etc. (XPN-4) 6. prefix: Dr., Mrs., etc. (XPN-5) 7. degree: MD, PhD, etc. (XPN-6) 8. sourceTable 9. assigningAuthority: nested HD (CX-4 analogue) 10. nameTypeCode: L=Legal, M=Maiden, N=Nickname, ... (XPN-7) 11. identifierCheckDigit 12. checkDigitScheme: ISO 7064, M10, M11, NPI 13. identifierTypeCode: "NPI", "DN" (DEA number), ... (CX-5 analogue) #### Example ```ts import type { XCN } from "@cosyte/hl7"; const orderingProvider: XCN = { idNumber: "1234567890", familyName: "Smith", givenName: "Jane", identifierTypeCode: "NPI", }; ``` #### Properties ##### assigningAuthority? > `readonly` `optional` **assigningAuthority?**: [`HD`](#hd-1) ##### checkDigitScheme? > `readonly` `optional` **checkDigitScheme?**: `string` ##### degree? > `readonly` `optional` **degree?**: `string` ##### familyName? > `readonly` `optional` **familyName?**: `string` ##### givenName? > `readonly` `optional` **givenName?**: `string` ##### identifierCheckDigit? > `readonly` `optional` **identifierCheckDigit?**: `string` ##### identifierTypeCode? > `readonly` `optional` **identifierTypeCode?**: `string` ##### idNumber? > `readonly` `optional` **idNumber?**: `string` ##### nameTypeCode? > `readonly` `optional` **nameTypeCode?**: `string` ##### prefix? > `readonly` `optional` **prefix?**: `string` ##### secondName? > `readonly` `optional` **secondName?**: `string` ##### sourceTable? > `readonly` `optional` **sourceTable?**: `string` ##### suffix? > `readonly` `optional` **suffix?**: `string` *** ### XPN HL7 v2 Extended Person Name (XPN): structured name per HL7 Chapter 2. All 14 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. familyName 2. givenName 3. secondName (or "second and further given names") 4. suffix (e.g. Jr., III) 5. prefix (e.g. Dr., Mrs.) 6. degree (e.g. MD, PhD) 7. nameTypeCode (L=Legal, M=Maiden, N=Nickname, S=Coded Pseudo-Name, ...) 8. nameRepresentationCode 9. nameContext (flattened to string in v1: CWE nesting is out of scope) 10. nameValidityRange 11. nameAssemblyOrder (F=family first, G=given first) 12. effectiveDate (raw HL7 TS string: caller may parse via parseDtm) 13. expirationDate 14. professionalSuffix #### Example ```ts import type { XPN } from "@cosyte/hl7"; const name: XPN = { familyName: "Smith", givenName: "Jane", prefix: "Mrs." }; ``` #### Properties ##### degree? > `readonly` `optional` **degree?**: `string` ##### effectiveDate? > `readonly` `optional` **effectiveDate?**: `string` ##### expirationDate? > `readonly` `optional` **expirationDate?**: `string` ##### familyName? > `readonly` `optional` **familyName?**: `string` ##### givenName? > `readonly` `optional` **givenName?**: `string` ##### nameAssemblyOrder? > `readonly` `optional` **nameAssemblyOrder?**: `string` ##### nameContext? > `readonly` `optional` **nameContext?**: `string` ##### nameRepresentationCode? > `readonly` `optional` **nameRepresentationCode?**: `string` ##### nameTypeCode? > `readonly` `optional` **nameTypeCode?**: `string` ##### nameValidityRange? > `readonly` `optional` **nameValidityRange?**: `string` ##### prefix? > `readonly` `optional` **prefix?**: `string` ##### professionalSuffix? > `readonly` `optional` **professionalSuffix?**: `string` ##### secondName? > `readonly` `optional` **secondName?**: `string` ##### suffix? > `readonly` `optional` **suffix?**: `string` *** ### XTN HL7 v2 Extended Telecommunication Number (XTN): structured telecom per HL7 Chapter 2. All 12 v1 components are optional. Fields are OMITTED when the underlying component is absent (exactOptionalPropertyTypes). Component positions (HL7 1-indexed; this interface is 0-indexed by key): 1. telephoneNumber: formatted or unformatted phone number 2. telecommunicationUseCode: PRN=Primary Residence, WPN=Work, NET=Internet, ORN=Other Residence, BPN=Beeper, VHN=Vacation Home, ASN=Answering Service, EMR=Emergency, ... 3. telecommunicationEquipmentType: PH=Phone, FX=Fax, MD=Modem, CP=Cellular Phone, BP=Beeper, Internet, X.400, TDD, TTY 4. emailAddress 5. countryCode (e.g. "+1") 6. areaCityCode 7. localNumber 8. extension 9. anyText: free-text note 10. extensionPrefix (e.g. "x") 11. speedDialCode 12. unformattedTelephoneNumber #### Example ```ts import type { XTN } from "@cosyte/hl7"; const phone: XTN = { telephoneNumber: "(555) 555-1234", telecommunicationUseCode: "WPN", telecommunicationEquipmentType: "PH", }; ``` #### Properties ##### anyText? > `readonly` `optional` **anyText?**: `string` ##### areaCityCode? > `readonly` `optional` **areaCityCode?**: `string` ##### countryCode? > `readonly` `optional` **countryCode?**: `string` ##### emailAddress? > `readonly` `optional` **emailAddress?**: `string` ##### extension? > `readonly` `optional` **extension?**: `string` ##### extensionPrefix? > `readonly` `optional` **extensionPrefix?**: `string` ##### localNumber? > `readonly` `optional` **localNumber?**: `string` ##### speedDialCode? > `readonly` `optional` **speedDialCode?**: `string` ##### telecommunicationEquipmentType? > `readonly` `optional` **telecommunicationEquipmentType?**: `string` ##### telecommunicationUseCode? > `readonly` `optional` **telecommunicationUseCode?**: `string` ##### telephoneNumber? > `readonly` `optional` **telephoneNumber?**: `string` ##### unformattedTelephoneNumber? > `readonly` `optional` **unformattedTelephoneNumber?**: `string` ## Type Aliases ### AckCode > **AckCode** = *typeof* [`ACK_CODES`](#ack_codes)\[keyof *typeof* [`ACK_CODES`](#ack_codes)\] Acknowledgment code union (HL7 Table 0008). Narrow on this to know whether a disposition is accept (`AA`/`CA`), error (`AE`/`CE`), or reject (`AR`/`CR`). *** ### AckCondition > **AckCondition** = *typeof* [`ACK_CONDITIONS`](#ack_conditions)\[keyof *typeof* [`ACK_CONDITIONS`](#ack_conditions)\] Accept/application acknowledgment condition union (HL7 Table 0155). *** ### AckMode > **AckMode** = `"original"` \| `"enhanced"` The two HL7 acknowledgment modes. **original** = both MSH-15 and MSH-16 are absent/null; **enhanced** = either is present (HL7 v2 Chapter 2 §2.9). *** ### AllergySource > **AllergySource** = `"AL1"` \| `"IAM"` The segment an [Allergy](#allergy) entry was read from: `"AL1"` (Patient Allergy Information) or `"IAM"` (Patient Adverse Reaction Information, the segment an ADT^A60 carries in place of AL1). #### Example ```ts import type { AllergySource } from "@cosyte/hl7"; const source: AllergySource = "IAM"; ``` *** ### BatchEnvelopeName > **BatchEnvelopeName** = `"FHS"` \| `"BHS"` \| `"BTS"` \| `"FTS"` The four HL7 batch-protocol envelope segment names (Ch. 2 §2.10.3). #### Example ```ts import type { BatchEnvelopeName } from "@cosyte/hl7"; const trailer: BatchEnvelopeName = "BTS"; ``` *** ### BatchMessageEntry > **BatchMessageEntry**\<`M`\> = \{ `message`: `M`; `ok`: `true`; `position`: [`Hl7Position`](#hl7position); `raw`: `string`; \} \| \{ `error`: [`Hl7ParseError`](#hl7parseerror); `ok`: `false`; `position`: [`Hl7Position`](#hl7position); `raw`: `string`; \} One message extracted from a batch stream. Discriminated on `ok`: a successful parse carries the `Hl7Message` (whose own `.warnings` hold any per-message Tier-2 deviations); a message that hit one of the four Tier-3 fatal conditions carries the `Hl7ParseError` instead: **isolated**, so the rest of the batch still yields. `raw` is the message's verbatim source (re-parseable by [parseHL7](#parsehl7)); `position` is the message's `MSH` (or, for pre-`MSH` stray content, its first) segment index in the stream. `M` is the parsed-message type each `ok` entry carries. It is `Hl7Message` unless the split was given a statically known profile, in which case it is that profile's narrowed view and `entry.message.part("ZDP")?.get(name)` is checked against the names the profile declares for `ZDP`. #### Type Parameters ##### M `M` *extends* [`Hl7Message`](#hl7message) = [`Hl7Message`](#hl7message) the message type an `ok` entry carries. #### Example ```ts import { splitBatch } from "@cosyte/hl7"; for (const entry of splitBatch(raw).messages) { if (entry.ok) handle(entry.message); else quarantine(entry.raw, entry.error.code); } ``` *** ### CharsetTreatment > **CharsetTreatment** = `"decode"` \| `"verbatim"` How the parser treats a resolved MSH-18 character set. - `decode`: decode the byte stream to text with [CharsetResolution.decoder](#decoder). - `verbatim`: read the raw bytes as a 1:1 `latin1` mapping; do not decode (byte-recoverable for single-byte content: see the module header). *** ### CompositeKind > **CompositeKind** = `"XPN"` \| `"XAD"` \| `"CX"` \| `"CWE"` \| `"CE"` \| `"XTN"` \| `"PL"` \| `"TS"` \| `"NM"` \| `"HD"` \| `"XCN"` The 11 typed composite kinds this module can encode. Mirrors the read-side composite set (`XPN`, `XAD`, `CX`, `CWE`, `CE`, `XTN`, `PL`, `TS`, `NM`, `HD`, `XCN`). *** ### ConditionPredicate > **ConditionPredicate** = [`PresencePredicate`](#presencepredicate) \| [`ComparisonPredicate`](#comparisonpredicate) \| [`ConnectedPredicate`](#connectedpredicate) A **condition predicate**: the computable test a conditionally-used element's usage is decided by, expressed in the published condition-predicate language as a presence statement, a comparison statement, or two of those joined by a connector. A rule declares one on its `condition`, and only a rule whose usage is conditional (`C`, `CE`, or a declared conditional such as `C(RE/X)`) may carry one: there is nothing for a predicate to decide anywhere else, so a predicate on any other rule is `PROFILE_MALFORMED` rather than a silent no-op. A rule may not declare a predicate AND take a caller-supplied resolution: that too is `PROFILE_MALFORMED`, so neither source is silently preferred over the other. **The predicate reads the message only.** It never makes a network call and never consults a bundled code set: every value it compares against is one the profile author wrote down. **Declare exactly one of `presence`, `verb` and `connector`, and OMIT the others.** A key set to `undefined` still declares it, so a statement assembled from an options bag has to leave out the keys it does not use rather than pass them through empty. A statement declaring two of them is `PROFILE_MALFORMED`: the engine will not guess which question was meant, because deciding a conditional element's usage from the wrong question turns a required element into a permitted absence. #### Example ```ts import type { ConditionPredicate } from "@cosyte/hl7"; const simple: ConditionPredicate = { location: { segment: "PID", field: 7 }, presence: "is valued", }; const complex: ConditionPredicate = { connector: "OR", left: simple, right: { location: { segment: "PID", field: 8 }, verb: "is", values: ["M", "F"] }, }; ``` *** ### DeclaredConditionalUsage > **DeclaredConditionalUsage** = `` `C(${SimpleUsageCode}/${SimpleUsageCode})` `` A **declared conditional** usage token: `C(t/f)`, where `t` is the usage that applies when the condition is true and `f` the usage that applies when it is false. Both outcomes are [SimpleUsageCode](#simpleusagecode)s; the token is uppercase, carries no whitespace, and does not nest (`C(C(R/X)/O)` is not one). A rule declared `C(R/X)` is Required when the condition holds and Not-permitted when it does not. Two sources can decide that, and a rule uses exactly one of them: the [ConditionPredicate](#conditionpredicate) on its `condition`, which the engine evaluates against the message, or a caller-supplied [UsageResolution](#usageresolution). Declaring both at one locus is [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed), never a silent preference. With neither supplied, the rule is evaluated exactly as a `C` rule with no predicate is: presence not evaluated, every other constraint applied as usual. #### Example ```ts import type { DeclaredConditionalUsage } from "@cosyte/hl7"; const usage: DeclaredConditionalUsage = "C(RE/X)"; ``` *** ### DftPatient > **DftPatient** = [`AdtPatient`](#adtpatient) Typed PID content for [buildDft](#builddft): the same shape every family shares. #### Example ```ts import type { DftPatient } from "@cosyte/hl7"; const patient: DftPatient = { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" }, accountNumber: { idNumber: "ACCT-1", identifierTypeCode: "AN" }, }; ``` *** ### DtmPrecision > **DtmPrecision** = `"year"` \| `"month"` \| `"day"` \| `"hour"` \| `"minute"` \| `"second"` \| `"fraction"` Stated precision of a parsed HL7 DTM value: the number of populated characters (excluding the timezone offset) determines which level applies. A value's precision is preserved verbatim: `|1970|` is `"year"`, never silently promoted to a full timestamp. #### Example ```ts import { parseDtm } from "@cosyte/hl7"; console.log(parseDtm("1970").precision); // "year" console.log(parseDtm("198807050000").precision); // "minute" ``` *** ### ErrSeverity > **ErrSeverity** = *typeof* [`ERR_SEVERITIES`](#err_severities)\[keyof *typeof* [`ERR_SEVERITIES`](#err_severities)\] Error-severity union (HL7 Table 0516, ERR-4). *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] Discriminant type for `Hl7ParseError.code`. Narrowing a caught error by this code lets consumers write exhaustive `switch` blocks (enabled by the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free comparison against the `FATAL_CODES` registry. #### Example ```ts import type { FatalCode } from "@cosyte/hl7"; function describe(code: FatalCode): string { switch (code) { case "EMPTY_INPUT": return "input was empty"; case "NO_MSH_SEGMENT": return "missing MSH"; case "MSH_TOO_SHORT": return "MSH truncated"; case "INVALID_ENCODING_CHARACTERS": return "bad MSH-1/MSH-2"; } } ``` *** ### FindingCode > **FindingCode** = *typeof* [`FINDING_CODES`](#finding_codes)\[keyof *typeof* [`FINDING_CODES`](#finding_codes)\] Discriminant union of every [ConformanceFinding](#conformancefinding) code. Enables exhaustive `switch` narrowing (the `switch-exhaustiveness-check` lint rule). *** ### FindingSeverity > **FindingSeverity** = `"error"` \| `"warning"` \| `"info"` Severity of a [ConformanceFinding](#conformancefinding). `error` is a constraint violation; `warning` / `info` are author-downgraded advisories (via a rule's `severity`). [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed) is always `error`. *** ### Hl7StreamSource > **Hl7StreamSource** = `AsyncIterable`\<`string` \| `Buffer` \| `Uint8Array`\> \| `Iterable`\<`string` \| `Buffer` \| `Uint8Array`\> A chunked source `parseStream` can consume: a Node `Readable`, any async-iterable, or any plain iterable of chunks. Chunks are `string` (text stream) or `Buffer`/`Uint8Array` (binary stream). A real Node stream in binary mode yields `Buffer`s; in text mode it yields `string`s: a source is expected to be **homogeneous** (all-text or all-binary), which every real `Readable` is. A binary chunk is decoded 1:1 via `latin1` (a lossless byte↔codepoint mapping) so each message's own MSH-18 charset resolution runs on its original bytes, exactly as [splitBatch](#splitbatch) does. #### Example ```ts import { createReadStream } from "node:fs"; import { parseStream } from "@cosyte/hl7"; const src: Hl7StreamSource = createReadStream("feed.hl7"); for await (const entry of parseStream(src)) { if (entry.ok) handle(entry.message); else quarantine(entry.raw, entry.error.code); } ``` *** ### IdentityEventKind > **IdentityEventKind** = `"merge"` \| `"move"` \| `"change"` \| `"link"` \| `"unlink"` \| `"add"` \| `"update"` Classification of a recognized identity trigger event. - `merge`: A18 / A34 / A35 / A36 / A39 / A40 / A41 / A42 (MRG expected). Two patient records become one; the prior record is retired. - `move`: A43 / A44 / A45 (MRG expected). Information moves between identifier lists, accounts or visits that both go on existing. - `change`: A47 / A49 / A50 / A51, the change-identifier family (MRG expected). One identifier is replaced by another on the SAME record: the direction is still MRG (prior) to PID (surviving), but no records are being conflated, so a consumer must not treat it as a patient merge. - `link` / `unlink`: A24 / A37 (two PID groups, no MRG) - `add` / `update`: A28 / A31 (person add/update, no MRG) #### Example ```ts import type { IdentityEventKind } from "@cosyte/hl7"; const kind: IdentityEventKind = "merge"; ``` *** ### IdentityRole > **IdentityRole** = `"surviving"` \| `"prior"` \| `"linked"` \| `"subject"` Role of one party in an identity event. `surviving` / `subject` / `linked` parties are ONLY ever sourced from PID (+ PV1); `prior` parties are ONLY ever sourced from MRG. That is the role-labelling invariant. #### Example ```ts import type { IdentityRole } from "@cosyte/hl7"; const role: IdentityRole = "surviving"; ``` *** ### ImmunizationRecordOrigin > **ImmunizationRecordOrigin** = `"administered"` \| `"historical"` Whether an `Immunization` records a dose that was **administered** by the reporting system or is **historical** information sourced from elsewhere, derived conservatively from RXA-9.1 against HL7 Table NIP001 (Immunization Information Source, CDC v2.5.1 Immunization Messaging IG): - `"administered"`: RXA-9.1 is exactly `"00"` (New immunization record). - `"historical"`: RXA-9.1 is `"01".."08"` (any "Historical information …" source). **Fail-safe:** this is OMITTED (never guessed) when RXA-9 is absent or carries a code outside the NIP001 administered/historical set. The raw RXA-9 claim is always preserved verbatim on `Immunization.informationSource`, so a consumer can inspect the original code even when `recordOrigin` is undefined. The distinction matters because an IIS de-duplicates a *historical* report differently from a dose it believes was *administered*: guessing corrupts the registry. *** ### ImmunizationStatusBasis > **ImmunizationStatusBasis** = \{ `field`: `"RXA-20"`; `map`: [`ImmunizationStatusMap`](#immunizationstatusmap); `table`: [`ImmunizationStatusTable`](#immunizationstatustable) & `object`; \} \| \{ `field`: `"RXA-21"`; `table`: [`ImmunizationStatusTable`](#immunizationstatustable) & `object`; \} \| \{ `codeSet`: [`ImmunizationStatusCodeSet`](#immunizationstatuscodeset); `field`: `"RXA-5"`; \} What decided an [ImmunizationAdministrationStatus](#immunizationadministrationstatus-1): the RXA field the deciding rule read, and the table, map or code set it read that field against. Discriminated on `field`. - `"RXA-21"`: the action code decided (`"delete-requested"`, or `"undetermined"` for an RXA-21 outside Table 0323). - `"RXA-5"`: the vaccine code decided (`"no-vaccine-administered"`, or `"undetermined"` for a CVX `998` contradicted by the other triplet). - `"RXA-20"`: the completion status decided, by the Table 0322 map. #### Union Members ##### Type Literal \{ `field`: `"RXA-20"`; `map`: [`ImmunizationStatusMap`](#immunizationstatusmap); `table`: [`ImmunizationStatusTable`](#immunizationstatustable) & `object`; \} ###### field > `readonly` **field**: `"RXA-20"` RXA-20 Completion Status decided. ###### map > `readonly` **map**: [`ImmunizationStatusMap`](#immunizationstatusmap) The Table 0322 to Event Status map the classification followed. ###### table > `readonly` **table**: [`ImmunizationStatusTable`](#immunizationstatustable) & `object` HL7 Table 0322, the table RXA-20 was read against. ###### Type Declaration ###### name > `readonly` **name**: `"HL7 Table 0322"` *** ##### Type Literal \{ `field`: `"RXA-21"`; `table`: [`ImmunizationStatusTable`](#immunizationstatustable) & `object`; \} ###### field > `readonly` **field**: `"RXA-21"` RXA-21 Action Code decided. ###### table > `readonly` **table**: [`ImmunizationStatusTable`](#immunizationstatustable) & `object` HL7 Table 0323, the table RXA-21 was read against. ###### Type Declaration ###### name > `readonly` **name**: `"HL7 Table 0323"` *** ##### Type Literal \{ `codeSet`: [`ImmunizationStatusCodeSet`](#immunizationstatuscodeset); `field`: `"RXA-5"`; \} ###### codeSet > `readonly` **codeSet**: [`ImmunizationStatusCodeSet`](#immunizationstatuscodeset) The CDC CVX code set, for code `998`. ###### field > `readonly` **field**: `"RXA-5"` RXA-5 Administered Code decided. #### Example ```ts import type { ImmunizationStatusBasis } from "@cosyte/hl7"; const basis: ImmunizationStatusBasis = { field: "RXA-21", table: { name: "HL7 Table 0323", version: "3.0.0" }, }; ``` *** ### ImmunizationStatusClass > **ImmunizationStatusClass** = `"completed"` \| `"not-done"` \| `"no-vaccine-administered"` \| `"delete-requested"` \| `"undetermined"` What an immunization record's administration status classifies to. `"completed"` and `"not-done"` are the target codes of HL7's published Table 0322 to Event Status map (RXA-20 `CP` and `PA` are `"completed"`, `RE` and `NA` are `"not-done"`). - `"no-vaccine-administered"`: RXA-5 carries CDC CVX code `998` ("no vaccine administered"), whatever RXA-20 says. - `"delete-requested"`: RXA-21 is exactly `D`. The record asks the receiver to delete an administration sent earlier; it is not a dose. - `"undetermined"`: this library's fail-safe, not a map target. It covers an absent, empty or `""` (null) RXA-20, any RXA-20 or RXA-21 that is not exactly one code of its table (a lowercase code, whitespace around it, a code written as an escape sequence, a VT or FS byte inside the field, more than one repetition, component or subcomponent), and an RXA-5 that codes CVX `998` in one triplet and a different identifier in the other. Only `"completed"` counts as a dose given. An `"undetermined"` record is never a dose by default: read the raw codes and decide. #### Example ```ts import type { ImmunizationStatusClass } from "@cosyte/hl7"; const notADose: readonly ImmunizationStatusClass[] = [ "not-done", "no-vaccine-administered", "delete-requested", "undetermined", ]; ``` *** ### MdmObservation > **MdmObservation** = [`OruObservation`](#oruobservation) Typed OBX content for [buildMdm](#buildmdm): one line of the transcribed body. #### Example ```ts import type { MdmObservation } from "@cosyte/hl7"; const line: MdmObservation = { setId: "1", valueType: "TX", value: "Discharge summary text." }; ``` *** ### MdmPatient > **MdmPatient** = [`AdtPatient`](#adtpatient) Typed PID content for [buildMdm](#buildmdm): the same shape every family shares. #### Example ```ts import type { MdmPatient } from "@cosyte/hl7"; const patient: MdmPatient = { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" }, name: { familyName: "Test", givenName: "Ann" }, }; ``` *** ### MedicationContext > **MedicationContext** = `"order"` \| `"encoded"` \| `"dispense"` \| `"administration"` Which pharmacy/treatment segment a `Medication` was extracted from: the clinical "phase" of the medication. Each value maps 1:1 to one RX* parent segment (Ch. 4A): - `"order"`: **RXO** (Pharmacy/Treatment Order): the originally *requested* give code/amount/dosage-form, before pharmacy encoding. - `"encoded"`: **RXE** (Encoded Order): the pharmacy-encoded give code/amount, plus the give *strength* (RXE-25/26): the only context that carries a separate strength. - `"dispense"`: **RXD** (Dispense): what was actually dispensed. - `"administration"`: **RXA** (Administration): what was actually given to the patient. The context is preserved verbatim and never collapsed: an RDE order that carries both an RXO request and an RXE encoded line surfaces as TWO `Medication` entries with distinct contexts: the helper never reconciles one against the other. *** ### Observation > **Observation** = [`ObservationBase`](#observationbase) & \{ `value`: `number` \| `undefined`; `valueType`: `"NM"`; \} \| \{ `value`: [`SN`](#sn) \| `undefined`; `valueType`: `"SN"`; \} \| \{ `value`: [`TS`](#ts-1) \| `undefined`; `valueType`: `"TS"` \| `"DT"`; \} \| \{ `value`: [`CWE`](#cwe-1) \| [`CE`](#ce) \| `undefined`; `valueType`: `"CWE"` \| `"CE"`; \} \| \{ `value`: `string` \| `undefined`; `valueType`: `string`; \} One OBX segment as a typed Observation. `value` is discriminated by `valueType` (OBX-2) per D-13: - `"NM"` → `number | undefined` - `"SN"` → `SN | undefined` (structured numeric: comparator / range / ratio) - `"TS" | "DT"` → `TS | undefined` (fidelity parts preserved) - `"CWE" | "CE"` → `CWE | CE | undefined` (full composite per D-14) - other (`"ST"`, `"TX"`, `"FT"`, `"ID"`, `"IS"`, `"NA"`, unknown) → `string | undefined` (decoded, D-23) D-22: `value` is `undefined` when OBX-5 is empty OR malformed for its declared type (never throws, never `NaN`). A `TS`/`DT` value is always the `TS` structure; check its `.valid` flag rather than expecting a `Date`. #### Example ```ts import type { Observation } from "@cosyte/hl7"; const glucose: Observation = { setId: "1", identifier: { identifier: "GLU", text: "Glucose" }, valueType: "NM", value: 120, units: { identifier: "mg/dL" }, referenceRange: "80-110", abnormalFlags: "H", status: "F", resultStatus: { classification: "final", code: "F", table: { name: "HL7 Table 0085", version: "3.0.0" }, map: { url: "http://hl7.org/fhir/uv/v2mappings/ConceptMap/table-hl70085-to-observation-status", version: "1.0.0", }, }, }; ``` *** ### OnWarningCallback > **OnWarningCallback** = (`warning`) => `void` Callback invoked inline each time the parser emits a Tier-2 warning. Always fires BEFORE the warning is appended to `Hl7Message.warnings` so consumers observe warnings in the same order the parser discovered them. #### Parameters ##### warning [`Hl7ParseWarning`](#hl7parsewarning) #### Returns `void` #### Example ```ts import { parseHL7, type OnWarningCallback } from "@cosyte/hl7"; const onWarning: OnWarningCallback = (w) => { console.warn(w.code, w.message); }; parseHL7(raw, { onWarning }); ``` *** ### OrmObservation > **OrmObservation** = [`OruObservation`](#oruobservation) Typed OBX content for [buildOrm](#buildorm): an observation carried with the order. #### Example ```ts import type { OrmObservation } from "@cosyte/hl7"; const note: OrmObservation = { setId: "1", valueType: "ST", value: "Fasting" }; ``` *** ### OrmPatient > **OrmPatient** = [`AdtPatient`](#adtpatient) Typed PID content for [buildOrm](#buildorm): the same shape every family shares. #### Example ```ts import type { OrmPatient } from "@cosyte/hl7"; const patient: OrmPatient = { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }; ``` *** ### OruPatient > **OruPatient** = [`AdtPatient`](#adtpatient) Typed PID content for [buildOru](#buildoru). Reuses [AdtPatient](#adtpatient): the patient-identification shape is identical across message families. *** ### OverlayKey > **OverlayKey** = keyof [`OverlayStructures`](#overlaystructures) Every string [Hl7Message.is](#is) narrows on: one key per (message code, trigger event) pair the structure registry recognizes, plus a code-only key for each registry entry that matches on message code alone. Any other string is a documented `false`, never a throw: the three-component MSH-9 form (`"ADT^A01^ADT_A01"`) is not a key, because `is` compares the pair the parser already extracted rather than parsing its own argument, and a caller who wants a raw comparison has `msg.meta.type`. #### Example ```ts import type { OverlayKey } from "@cosyte/hl7"; const key: OverlayKey = "ORU^R01"; // const nope: OverlayKey = "ADT^A01^ADT_A01"; // not a key: does not compile ``` *** ### OverlayMessageCode > **OverlayMessageCode**\<`K`\> = `K` *extends* `` `${infer C}^${string}` `` ? `C` : `K` The MSH-9.1 message code one overlay key names, as a literal type. #### Type Parameters ##### K `K` *extends* [`OverlayKey`](#overlaykey) #### Example ```ts import type { OverlayMessageCode } from "@cosyte/hl7"; type Code = OverlayMessageCode<"ORU^R01">; // "ORU" ``` *** ### OverlayRequiredSegment > **OverlayRequiredSegment**\<`K`\> = [`OverlayStructures`](#overlaystructures)\[`K`\]\[`number`\] The segment names one overlay key's published structure marks required: the names `part` and `parts` accept on a message narrowed to that key. #### Type Parameters ##### K `K` *extends* [`OverlayKey`](#overlaykey) #### Example ```ts import type { OverlayRequiredSegment } from "@cosyte/hl7"; type A01Segments = OverlayRequiredSegment<"ADT^A01">; // "EVN" | "MSH" | "PID" | "PV1" ``` *** ### OverlayTriggerEvent > **OverlayTriggerEvent**\<`K`\> = `K` *extends* `` `${string}^${infer E}` `` ? `E` : `never` The MSH-9.2 trigger event one overlay key names, as a literal type, or `never` for a code-only key. A code-only key (`"ACK"`) constrains MSH-9.2 to nothing at all, since it carries the acknowledged message's trigger event, so a message narrowed to one keeps the base `triggerEvent?: string`. #### Type Parameters ##### K `K` *extends* [`OverlayKey`](#overlaykey) #### Example ```ts import type { OverlayTriggerEvent } from "@cosyte/hl7"; type Event = OverlayTriggerEvent<"ORU^R01">; // "R01" type None = OverlayTriggerEvent<"ACK">; // never: MSH-9.2 is unconstrained ``` *** ### PredicateConnector > **PredicateConnector** = `"AND"` \| `"OR"` \| `"XOR"` The connectors that join two condition-predicate statements, with the published semantics: `AND` is true only when both operands are true, `OR` when at least one is, `XOR` when exactly one is. #### Example ```ts import type { PredicateConnector } from "@cosyte/hl7"; const connector: PredicateConnector = "AND"; ``` *** ### PredicatePresence > **PredicatePresence** = `"is valued"` \| `"is not valued"` The two proposition statements that ask whether an element carries content, from the published condition-predicate language (`IF CWE.1 (Identifier) is valued`). **A presence statement is always determinate**: an element the message does not carry is simply not valued, which is an answer rather than a gap. That is what separates it from a comparison, which needs content to compare and has none when the element is absent. #### Example ```ts import type { PredicatePresence } from "@cosyte/hl7"; const statement: PredicatePresence = "is valued"; ``` *** ### PredicateVerb > **PredicateVerb** = `"is"` \| `"is not"` \| `"contains"` \| `"does not contain"` \| `"matches"` \| `"does not match"` The closed set of verbs a comparison statement may use, from the published condition-predicate language's verb table. Each pairs the element's content against the statement's value list: - **`is` / `is not`**: the value equals (does not equal) a listed value, whole and case-sensitive. - **`contains` / `does not contain`**: the value carries (does not carry) a listed value as a substring, case-sensitive. - **`matches` / `does not match`**: the value matches (does not match) a listed value read as a regular expression, anchored over the whole value. **A negative verb negates the COMPARISON, not the statement.** Each pair is an exact complement value by value, and both halves are quantified the same way: existentially, over every repetition of the field and every occurrence of the segment the location names (see [ComparisonPredicate](#comparisonpredicate)). Three consequences, and the second is the one that surprises: - Where the message carries exactly ONE value at that location, the pair behaves as an exact complement: one of the two decides true and the other false. - Where the location REPEATS, both halves can decide true at once, because both are existential. `PID-3.1 is 'MRN1'` asks whether SOME identifier is `MRN1`, and `PID-3.1 is not 'MRN1'` asks whether SOME identifier is not `MRN1`; for `MRN1~MRN2` the answer to both is yes. So a negative verb is NOT a way to ask whether no repetition is a listed value: the language carries no negation connector, so that question cannot be written at all, and `AL1-3.1 is not 'PENICILLIN'` decides true for a patient who has that allergy and one other. - Where the message carries no value at that location, neither half decides. The comparison is unevaluatable whichever verb it uses. #### Example ```ts import type { PredicateVerb } from "@cosyte/hl7"; const verb: PredicateVerb = "contains"; ``` *** ### ProfileFieldName > **ProfileFieldName**\<`P`, `S`\> = `P` *extends* `object` ? `DeclaredFieldName`\<`C`, `S`\> : `string` The field names profile `P` declares for segment type `S`, as a closed union of string literals, or `string` when the compiler cannot know them. `string` is the answer for every case where a declaration is not statically visible, and there are more of those than there are narrowing ones: a value typed as the general [Profile](#profile-2) interface, a segment type the profile does not declare, a segment declared with an empty field map, and a declaration map typed with an index signature. Narrowing is per segment type, never a flat union of every name the profile declares. #### Type Parameters ##### P `P` ##### S `S` *extends* `string` #### Example ```ts import { defineProfile, type ProfileFieldName } from "@cosyte/hl7"; const epic = defineProfile({ name: "epic", customSegments: { ZDP: { fields: { departmentCode: 3, departmentName: 4 } } }, }); type Zdp = ProfileFieldName; // "departmentCode" | "departmentName" type Zzz = ProfileFieldName; // string: undeclared segment type ``` *** ### ProfileLevel > **ProfileLevel** = `"standard"` \| `"constrainable"` \| `"implementable"` The three **profile levels** the HL7 conformance methodology defines, in the order it defines them: from the least constrained to the most. - **`standard`**: the base standard definitions and constraints as-is. The overall structure is established, but the full declaration of requirements has yet to be specified, so considerable openness still exists. - **`constrainable`**: further constrains a parent profile, but not all element attributes are fully constrained: optionality is still in place somewhere. - **`implementable`**: defines all elements such that all optionality and openness have been removed. Every element is either supported (`R` or `RE`) or it is not (`X`), and a conditional usage's true and false outcomes are drawn from those same three codes. **Why a consumer cares, and it is the only reason this exists.** The methodology draws the completeness line here: a complete assessment of an interface declaring conformance to an implementable profile can be determined, while for standard-level and constrainable-level profiles not all aspects can be. So an empty [ConformanceResult.findings](#findings) means two different things at two different levels, and [ConformanceResult.level](#level-1) is what tells them apart. **The level is the author's CLAIM, checked for internal consistency.** hl7 verifies that a profile claiming `implementable` has actually removed the optionality the level asserts is gone, and refuses the claim as [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed) when it has not. Nothing here is an external or accredited attestation, and zero findings at `implementable` level is still not one: see [ConformanceResult](#conformanceresult). #### Example ```ts import type { ProfileLevel } from "@cosyte/hl7"; const level: ProfileLevel = "implementable"; ``` *** ### RepeatPatternKind > **RepeatPatternKind** = `"parametric"` \| `"named"` \| `"unknown"` How an order/medication `RepeatPattern` code (HL7 Table 0335) is classified, **provenance only, never used to resolve a schedule**. The `code` is always authoritative and surfaced verbatim; this flag only tells a consumer *what kind* of pattern it is looking at so it can decide whether a load-bearing integer is present. - `"parametric"`: a `Q` template (`Q6H`, `Q30M`, `Q2D`, `Q1W`, `Q3J5`) whose **integer is load-bearing**: `Q6H` (every 6 hours) is a different dose count from `Q8H`. The parsed integer + unit ride on [RepeatPattern.interval](#interval). - `"named"`: a recognized fixed Table-0335 mnemonic scheduled at institution-specified times (`BID`, `TID`, `QID`, `QOD`, `QHS`, `QAM`, `QPM`, `QSHIFT`, `PRN`, `AC`, `PC`, `HS`, `C`). No numeric interval. - `"unknown"`: anything else (a local code, free text, an unrecognized mnemonic). Surfaced **verbatim**, never mapped to a frequency. *** ### ResultStatusClass > **ResultStatusClass** = `"final"` \| `"corrected"` \| `"amended"` \| `"preliminary"` \| `"entered-in-error"` \| `"cancelled"` \| `"registered"` \| `"partial"` \| `"undetermined"` What a result status classifies to. The first eight values are the target codes of HL7's published v2-to-FHIR status maps (Table 0085 to Observation Status for OBX-11, Table 0123 to Diagnostic Report Status for OBR-25). `"undetermined"` is this library's fail-safe, not a map target. It covers every code HL7's map leaves unmapped (OBX-11 `B`, `I`, `N`, `O`, `R`, `S`, `V`, `U`; OBR-25 `A`, `Y`, `Z`, `M`, `N`), an absent, empty or `""` (null) field, and any field that is not exactly one code of its table: a lowercase letter, a code with whitespace around it, a code written as an escape sequence, a code outside the table, or a field with more than one repetition, component or subcomponent. An `"undetermined"` value is never a current result by default: read the raw code and decide. #### Example ```ts import type { ResultStatusClass } from "@cosyte/hl7"; const retracted: readonly ResultStatusClass[] = ["entered-in-error", "cancelled"]; ``` *** ### SchemaEmitTarget > **SchemaEmitTarget** = *typeof* [`SCHEMA_EMIT_TARGETS`](#schema_emit_targets)\[`number`\] One of the emission targets this library supports. #### Example ```ts import { emitMessageSchema, type SchemaEmitTarget } from "@cosyte/hl7"; const target: SchemaEmitTarget = "json-schema-2020-12"; emitMessageSchema(target).startsWith("{"); ``` *** ### SimpleUsageCode > **SimpleUsageCode** = `"R"` \| `"RE"` \| `"C"` \| `"CE"` \| `"O"` \| `"X"` \| `"B"` The seven **simple** HL7 v2 conformance usage codes (HL7 Conformance Methodology, Message Profiles; IHE ITI TF Vol.2 Appendix C). They constrain whether an element must, may, or must not appear: - **`R`**: Required. The element SHALL be present (with a value). Absent → [FINDING\_CODES.PROFILE\_REQUIRED\_ABSENT](#profile_required_absent). - **`RE`**: Required but may be Empty. The element is supported and SHALL be sent when the sender has the data; its **absence is never a violation** (this engine cannot know whether the sender had the data). - **`C`**: Conditional. Presence depends on a condition predicate. A rule that declares one on its `condition` is evaluated by it: predicate true is Required, predicate false is Not-permitted, which are the outcomes IHE specifies for `C`. A rule that declares NO predicate has its **presence not evaluated** (treated as optional); its length / value-set / cardinality rules still apply when it IS present. - **`CE`**: Conditional but may be Empty. With a predicate, true is Required-but-may-be-Empty and false is Not-permitted, again IHE's outcomes. With no predicate, the same non-evaluation as `C`. - **`O`**: Optional. No presence constraint. - **`X`**: Not supported / not permitted. The element SHALL NOT be present. Present → [FINDING\_CODES.PROFILE\_NOT\_PERMITTED](#profile_not_permitted). - **`B`**: Backward Compatible. Presence is **not evaluated**, exactly as for `C`; the element's length / value-set / cardinality rules still apply when it IS present. See the note below on where that behaviour comes from. ## Two bounded decisions this library makes rather than inherits **`B`'s presence behaviour is this library's decision, not a sourced requirement.** The HL7 conformance methodology's table of usage indicators allowable in a message profile lists `B` among them and carries no definition text for it, so no source settles what a validator should do with a `B` element. This library chooses the reading that imposes no presence obligation (so `B` behaves as `C` does on presence) while preserving the author's declared intent in the profile. A later, separate change may revisit it against a source that defines the code. **A declared conditional's outcome domain is unrestricted BY THE TYPE, and narrowed by the level a profile claims.** [DeclaredConditionalUsage](#declaredconditionalusage) admits any simple code as either outcome, because the full indicator set is the CONSTRAINABLE level's vocabulary and that is what a profile claims when it declares no level. The methodology separately restricts an IMPLEMENTABLE profile to `R`, `RE` or `X` per element, and its conditional outcomes likewise. A profile that declares [ConformanceProfile.level](#level) `implementable` is held to exactly that: `C`, `CE`, `O`, `B`, a conditional whose outcomes are not all drawn from `R` / `RE` / `X`, and a rule that declares no usage at all are each [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed) there, and none of them is refused at any other level. #### Example ```ts import type { SimpleUsageCode } from "@cosyte/hl7"; const usage: SimpleUsageCode = "R"; ``` *** ### SiuPatient > **SiuPatient** = [`AdtPatient`](#adtpatient) Typed PID content for [buildSiu](#buildsiu): the same shape every family shares. #### Example ```ts import type { SiuPatient } from "@cosyte/hl7"; const patient: SiuPatient = { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }; ``` *** ### SiuResourceKind > **SiuResourceKind** = `"service"` \| `"general"` \| `"location"` \| `"personnel"` Which AI* segment carries one [SiuResource](#siuresource): AIS, AIG, AIL or AIP. #### Example ```ts import type { SiuResourceKind } from "@cosyte/hl7"; const kind: SiuResourceKind = "location"; // emitted as AIL ``` *** ### StreamMessageEntry > **StreamMessageEntry**\<`M`\> = \{ `message`: `M`; `ok`: `true`; `position`: [`Hl7Position`](#hl7position); `raw`: `string`; `streamWarnings`: readonly [`Hl7ParseWarning`](#hl7parsewarning)[]; \} \| \{ `error`: [`Hl7ParseError`](#hl7parseerror); `ok`: `false`; `position`: [`Hl7Position`](#hl7position); `raw`: `string`; `streamWarnings`: readonly [`Hl7ParseWarning`](#hl7parsewarning)[]; \} One message yielded by [parseStream](#parsestream). Discriminated on `ok`, mirroring [splitBatch](#splitbatch)'s per-message entry: a successful parse carries the `Hl7Message` (whose own `.warnings` hold per-message Tier-2 deviations); a message that hit one of the four Tier-3 fatals carries the `Hl7ParseError` instead: **isolated**, so the rest of the stream still yields. `raw` is the message's verbatim source (re-parseable by [parseHL7](#parsehl7)); `position.segmentIndex` is the message's `MSH` (or, for stray pre-`MSH` content, its first) segment index in the overall stream: the streaming analogue of [splitBatch](#splitbatch)'s message position. `streamWarnings` holds **stream-level** diagnostics that are not per-message parse warnings: today only [unterminatedStreamMessage](#unterminatedstreammessage) on a final message that lacked a terminator. It is kept separate from `message.warnings` (which stays exactly what a whole-buffer `parseHL7` of the same bytes would produce) and is **empty** for every message but a possibly-truncated final one. Both the entry and `streamWarnings` are frozen. `M` is the parsed-message type each `ok` entry carries. It is `Hl7Message` unless the stream was given a statically known profile, in which case it is that profile's narrowed view and `entry.message.part("ZDP")?.get(name)` is checked against the names the profile declares for `ZDP`. #### Type Parameters ##### M `M` *extends* [`Hl7Message`](#hl7message) = [`Hl7Message`](#hl7message) the message type an `ok` entry carries. #### Example ```ts import { parseStream, WARNING_CODES } from "@cosyte/hl7"; for await (const entry of parseStream(source)) { for (const w of entry.streamWarnings) { if (w.code === WARNING_CODES.UNTERMINATED_STREAM_MESSAGE) { // the feed may have been cut off mid-message } } } ``` *** ### StructureDerivation > **StructureDerivation** = `"published"` \| `"retained-transcription"` How one registry entry's expectations were obtained. `"published"` means every expectation was read off HL7's own machine-readable structure definitions. `"retained-transcription"` means the publication carries no structure for the pair at all and the previous hand transcription was kept rather than dropped; such an entry always carries the reason. *** ### StructureFindingCode > **StructureFindingCode** = *typeof* [`STRUCTURE_FINDING_CODES`](#structure_finding_codes)\[keyof *typeof* [`STRUCTURE_FINDING_CODES`](#structure_finding_codes)\] Discriminant union of every [StructureFinding](#structurefinding) code. Enables exhaustive `switch` narrowing. #### Example ```ts import type { StructureFindingCode } from "@cosyte/hl7"; const code: StructureFindingCode = "STRUCTURE_SEGMENT_UNEXPECTED"; ``` *** ### StructureNotValidatedReason > **StructureNotValidatedReason** = *typeof* [`STRUCTURE_NOT_VALIDATED_REASONS`](#structure_not_validated_reasons)\[keyof *typeof* [`STRUCTURE_NOT_VALIDATED_REASONS`](#structure_not_validated_reasons)\] Discriminant union of every reason validation did not run. #### Example ```ts import type { StructureNotValidatedReason } from "@cosyte/hl7"; const reason: StructureNotValidatedReason = "RETAINED_TRANSCRIPTION"; ``` *** ### ToDateOptions > **ToDateOptions** = [`DtmToDateOptions`](#dtmtodateoptions) The options bag [toDate](#todate) accepts: `{ assumeOffsetMinutes?: number }` and nothing else. An ALIAS of the pre-existing [DtmToDateOptions](#dtmtodateoptions), which is unchanged and still exported, so the two names denote one type and a caller may use either. It exists because every `@cosyte` parser spells this options type `ToDateOptions`, and a consumer writing across two of them should be able to spell it the same way in both. Adding the alias costs nothing at runtime: it is erased at compile time and no value changes. #### Example ```ts import { parseDtm, toDate } from "@cosyte/hl7"; import type { ToDateOptions } from "@cosyte/hl7"; const assumeUtc: ToDateOptions = { assumeOffsetMinutes: 0 }; toDate(parseDtm("20250102"), assumeUtc)?.toISOString(); // "2025-01-02T00:00:00.000Z" ``` *** ### TS > **TS** = [`DtmParts`](#dtmparts) HL7 v2 Time Stamp (TS) / Date Time (DTM) composite: the raw HL7 string plus its parsed parts, preserving the **stated precision** and **timezone fidelity**. This is a [DtmParts](#dtmparts): `valid` is `false` (with no parts) for unparseable input: NEVER throws (TYPES-04 no-throw guarantee). There is deliberately **no `date` field**: a day-only value coerced to a JS `Date` at UTC midnight silently shifts the calendar day in negative-offset zones. To obtain an absolute instant, call `dtmToDate(ts)` explicitly (and supply `assumeOffsetMinutes` for an offset-less value). #### Example ```ts import type { TS } from "@cosyte/hl7"; import { dtmToDate } from "@cosyte/hl7"; const dob: TS = { raw: "19880705", valid: true, precision: "day", year: 1988, month: 7, day: 5, hasTimezone: false }; console.log(dob.precision); // "day": not a full timestamp console.log(dtmToDate(dob)); // undefined: refuses to guess the zone ``` *** ### TypedMeta > **TypedMeta**\<`K`\> = `Omit`\<[`Meta`](#meta-1), `"messageCode"` \| `"triggerEvent"`\> & `object` & \[[`OverlayTriggerEvent`](#overlaytriggerevent)\<`K`\>\] *extends* \[`never`\] ? `Pick`\<[`Meta`](#meta-1), `"triggerEvent"`\> : `object` `Meta` with the message code (and, for a key that names one, the trigger event) at the literal type the key established. Every other member is the `Meta` member unchanged: narrowing a message type says nothing about whether the sender populated MSH-10 or MSH-7. #### Type Declaration ##### messageCode > `readonly` **messageCode**: [`OverlayMessageCode`](#overlaymessagecode)\<`K`\> #### Type Parameters ##### K `K` *extends* [`OverlayKey`](#overlaykey) #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; const msg = parseHL7(raw); if (msg.is("ADT^A01")) { const code: "ADT" = msg.meta.messageCode; // no cast, no optional chain const event: "A01" = msg.meta.triggerEvent; console.log(code, event, msg.meta.controlId); // controlId is still optional } ``` *** ### UsageCode > **UsageCode** = [`SimpleUsageCode`](#simpleusagecode) \| [`DeclaredConditionalUsage`](#declaredconditionalusage) Everything a rule's `usage` may be declared as: one of the seven [SimpleUsageCode](#simpleusagecode)s, or a [DeclaredConditionalUsage](#declaredconditionalusage) token. **This is not the same type as [UsageCodeRegistryEntry](#usagecoderegistryentry).** The registry publishes `C(a/b)` as the NOTATION for the declared-conditional family, and `a` / `b` are placeholders rather than usage codes, so the literal `"C(a/b)"` is deliberately NOT assignable here. Neither type widens to a bare `string`: a typo such as `"RQ"` is a compile error, and [defineConformanceProfile](#defineconformanceprofile) is the runtime backstop rather than the replacement. #### Example ```ts import type { UsageCode } from "@cosyte/hl7"; const simple: UsageCode = "R"; const conditional: UsageCode = "C(RE/X)"; ``` *** ### UsageCodeRegistryEntry > **UsageCodeRegistryEntry** = [`SimpleUsageCode`](#simpleusagecode) \| `"C(a/b)"` One entry of the exported [USAGE\_CODES](#usage_codes) vocabulary listing: a [SimpleUsageCode](#simpleusagecode), or the literal notation `"C(a/b)"` that stands for the whole declared-conditional family. Distinct from [UsageCode](#usagecode) on purpose, so the placeholder can be listed without ever becoming assignable where a rule's usage is expected. #### Example ```ts import type { UsageCodeRegistryEntry } from "@cosyte/hl7"; const entry: UsageCodeRegistryEntry = "C(a/b)"; ``` *** ### VxuObservation > **VxuObservation** = [`OruObservation`](#oruobservation) Typed OBX content for [buildVxu](#buildvxu) (eligibility, funding source, …). #### Example ```ts import type { VxuObservation } from "@cosyte/hl7"; const eligibility: VxuObservation = { setId: "1", valueType: "CE", identifier: { identifier: "64994-7", text: "Vaccine funding program eligibility" }, value: "V02", }; ``` *** ### VxuPatient > **VxuPatient** = [`AdtPatient`](#adtpatient) Typed PID content for [buildVxu](#buildvxu): the same shape every family shares. #### Example ```ts import type { VxuPatient } from "@cosyte/hl7"; const patient: VxuPatient = { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" }, name: { familyName: "Test", givenName: "Ann" }, }; ``` *** ### WarningCode > **WarningCode** = *typeof* [`WARNING_CODES`](#warning_codes)\[keyof *typeof* [`WARNING_CODES`](#warning_codes)\] Discriminant type for `Hl7ParseWarning.code`. Narrowing a warning by this code lets consumers write exhaustive `switch` blocks (enabled by the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free comparison against the `WARNING_CODES` registry. #### Example ```ts import type { Hl7ParseWarning, WarningCode } from "@cosyte/hl7"; function describe(w: Hl7ParseWarning): string { const code: WarningCode = w.code; switch (code) { case "MLLP_FRAMING_STRIPPED": return "stripped MLLP framing"; default: return `warning: ${code}`; } } ``` ## Variables ### ACK\_CODES > `const` **ACK\_CODES**: `object` HL7 Table 0008: Acknowledgment code (MSA-1). The two acknowledgment **vocabularies**: - **original** mode: `AA` Application Accept · `AE` Application Error · `AR` Application Reject. - **enhanced** mode accept-level: `CA` Commit Accept · `CE` Commit Error · `CR` Commit Reject (the application-level response in enhanced mode reuses `AA`/`AE`/`AR`). #### Type Declaration ##### AA > `readonly` **AA**: `"AA"` = `"AA"` ##### AE > `readonly` **AE**: `"AE"` = `"AE"` ##### AR > `readonly` **AR**: `"AR"` = `"AR"` ##### CA > `readonly` **CA**: `"CA"` = `"CA"` ##### CE > `readonly` **CE**: `"CE"` = `"CE"` ##### CR > `readonly` **CR**: `"CR"` = `"CR"` #### Example ```ts import { ACK_CODES } from "@cosyte/hl7"; ACK_CODES.AA; // "AA" ``` *** ### ACK\_CONDITIONS > `const` **ACK\_CONDITIONS**: `object` HL7 Table 0155: Accept/application acknowledgment conditions (MSH-15 / MSH-16): `AL` Always · `NE` Never · `ER` Error/reject conditions only · `SU` Successful completion only. Exposed read-only for adapters that surface the inbound sender's stated acknowledgment expectations. #### Type Declaration ##### AL > `readonly` **AL**: `"AL"` = `"AL"` ##### ER > `readonly` **ER**: `"ER"` = `"ER"` ##### NE > `readonly` **NE**: `"NE"` = `"NE"` ##### SU > `readonly` **SU**: `"SU"` = `"SU"` #### Example ```ts import { ACK_CONDITIONS } from "@cosyte/hl7"; ACK_CONDITIONS.AL; // "AL" (Always) ``` *** ### AMBIGUOUS\_DATE\_ORDER > `const` **AMBIGUOUS\_DATE\_ORDER**: `"AMBIGUOUS_DATE_ORDER"` = `"AMBIGUOUS_DATE_ORDER"` Stable identifier reported on [DtmParts.ambiguity](#ambiguity) when a slash date's field order cannot be established. Compare against it to tell "ambiguous, refused" apart from "malformed" (no timestamp at all) and from "resolved via a fallback" (`valid: true` with a `matchedFormat`). Distinct from the `TIMESTAMP_FALLBACK_FORMAT` warning code, which reports the opposite outcome: a value that DID resolve. #### Example ```ts import { parseHL7, AMBIGUOUS_DATE_ORDER } from "@cosyte/hl7"; const ts = parseHL7(raw).meta.timestamp; if (ts?.ambiguity?.code === AMBIGUOUS_DATE_ORDER) { console.log(ts.ambiguity.message); } ``` *** ### BUILTIN\_DATE\_FALLBACKS > `const` **BUILTIN\_DATE\_FALLBACKS**: readonly `string`[] Ordered list of built-in timestamp formats parseDtmCascade falls back to when neither the strict HL7 DTM match nor any user-supplied format succeeds. ISO-8601 is tried first (most constrained); `MM/DD/YYYY HH:mm:ss` last (it overlaps the date-only form). **The two `MM/DD/YYYY` entries resolve only what has a single reading.** A slash value whose first two components are both in 1-12 (`05/07/1988`) is also a legal day-first date, and no built-in decides between them: such a value is REFUSED with a [DtmAmbiguity](#dtmambiguity) report instead of being read as May 7. `07/25/1988` has one reading and still resolves; `05/05/1988` has two that agree and still resolves. To read day-first values, declare the order: `parseHL7(raw, { dateFormats: ["DD/MM/YYYY"] })`, or a profile's `dateFormats`. Both are tried ahead of this list. #### Example ```ts import { BUILTIN_DATE_FALLBACKS } from "@cosyte/hl7"; console.log(BUILTIN_DATE_FALLBACKS); // ["ISO-8601", "YYYY-MM-DD", "MM/DD/YYYY", "MM/DD/YYYY HH:mm:ss"] ``` *** ### DEFAULT\_ENCODING\_CHARACTERS > `const` **DEFAULT\_ENCODING\_CHARACTERS**: [`EncodingCharacters`](#encodingcharacters-1) The HL7 default 5-tuple of encoding characters used when a message does not override them via MSH-1 / MSH-2. Re-used by downstream stages (the escape map, `parseHL7`) as a synthetic-message fallback. #### Example ```ts import { DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; DEFAULT_ENCODING_CHARACTERS.field; // "|" ``` *** ### ERR\_CONDITION\_CODE\_SYSTEM > `const` **ERR\_CONDITION\_CODE\_SYSTEM**: `"HL70357"` = `"HL70357"` Code-system name emitted in ERR-3.3 for Table 0357 condition codes. *** ### ERR\_CONDITION\_CODES > `const` **ERR\_CONDITION\_CODES**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 Table 0357: Message error condition codes (ERR-3.1 → ERR-3.2 text). Frozen read-only map of `code → standard display text`. The code system name emitted in ERR-3.3 is [ERR\_CONDITION\_CODE\_SYSTEM](#err_condition_code_system). Codes `104` (value too long) and `105` (table value not found) are v2.7+ additions; the rest are present from v2.5. `buildAck` emits whatever code it is told (it never invents a condition) and looks up the display text here. #### Example ```ts import { ERR_CONDITION_CODES } from "@cosyte/hl7"; ERR_CONDITION_CODES["101"]; // "Required field missing" ``` *** ### ERR\_SEVERITIES > `const` **ERR\_SEVERITIES**: `object` HL7 Table 0516: Error severity (ERR-4). A v2.5+ construct (ERR was structured differently in v2.3.1). #### Type Declaration ##### E > `readonly` **E**: `"E"` = `"E"` Error. ##### I > `readonly` **I**: `"I"` = `"I"` Information. ##### W > `readonly` **W**: `"W"` = `"W"` Warning. #### Example ```ts import { ERR_SEVERITIES } from "@cosyte/hl7"; ERR_SEVERITIES.E; // "E" (Error) ``` *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable string codes for every Tier-3 fatal the parser may throw. Locked at four codes: anything else MUST be a Tier-2 warning. Consumers narrow on `err.code` to react to specific structural failures. #### Type Declaration ##### EMPTY\_INPUT > `readonly` **EMPTY\_INPUT**: `"EMPTY_INPUT"` = `"EMPTY_INPUT"` ##### INVALID\_ENCODING\_CHARACTERS > `readonly` **INVALID\_ENCODING\_CHARACTERS**: `"INVALID_ENCODING_CHARACTERS"` = `"INVALID_ENCODING_CHARACTERS"` ##### MSH\_TOO\_SHORT > `readonly` **MSH\_TOO\_SHORT**: `"MSH_TOO_SHORT"` = `"MSH_TOO_SHORT"` ##### NO\_MSH\_SEGMENT > `readonly` **NO\_MSH\_SEGMENT**: `"NO_MSH_SEGMENT"` = `"NO_MSH_SEGMENT"` #### Example ```ts import { parseHL7, FATAL_CODES, Hl7ParseError } from "@cosyte/hl7"; try { parseHL7(""); } catch (err) { if (err instanceof Hl7ParseError && err.code === FATAL_CODES.EMPTY_INPUT) { // handle empty input } } ``` *** ### FINDING\_CODES > `const` **FINDING\_CODES**: `object` The frozen registry of finding codes. Stable, additive string codes: a consumer compares `finding.code === FINDING_CODES.PROFILE_REQUIRED_ABSENT`. Segment-level vs field-level is disambiguated by whether the finding's [FindingLocus.field](#field-4) is present, not by separate codes. #### Type Declaration ##### PROFILE\_CARDINALITY > `readonly` **PROFILE\_CARDINALITY**: `"PROFILE_CARDINALITY"` = `"PROFILE_CARDINALITY"` A segment-occurrence or field-repetition count is outside its cardinality. ##### PROFILE\_CODING\_SYSTEM\_MISMATCH > `readonly` **PROFILE\_CODING\_SYSTEM\_MISMATCH**: `"PROFILE_CODING_SYSTEM_MISMATCH"` = `"PROFILE_CODING_SYSTEM_MISMATCH"` A present repetition does not carry the coding system the rule binds its value set to: its coding-system component resolves to a different system, to none this library recognizes, or is blank or absent altogether. A distinct code on purpose, because it is a different answer from [FINDING\_CODES.PROFILE\_VALUE\_NOT\_IN\_SET](#profile_value_not_in_set): "a code we do not accept" and "a code that looks right but claims the wrong system" are two problems with two remedies, and one code for both would hide the second inside the first. A rule that declares no [FieldRule.codingSystem](#codingsystem) never emits it. ##### PROFILE\_CONDITION\_UNEVALUATABLE > `readonly` **PROFILE\_CONDITION\_UNEVALUATABLE**: `"PROFILE_CONDITION_UNEVALUATABLE"` = `"PROFILE_CONDITION_UNEVALUATABLE"` A rule's declared [ConditionPredicate](#conditionpredicate) could not be decided against this message, so the rule's usage was never selected and the element's presence was NOT assessed. A distinct code on purpose: an unassessed conditional is filterable by code, without matching a message string. ##### PROFILE\_LENGTH > `readonly` **PROFILE\_LENGTH**: `"PROFILE_LENGTH"` = `"PROFILE_LENGTH"` A checked component value exceeds the declared maximum length. ##### PROFILE\_MALFORMED > `readonly` **PROFILE\_MALFORMED**: `"PROFILE_MALFORMED"` = `"PROFILE_MALFORMED"` The profile ITSELF is structurally malformed (a diagnostic, not a message finding). ##### PROFILE\_NOT\_PERMITTED > `readonly` **PROFILE\_NOT\_PERMITTED**: `"PROFILE_NOT_PERMITTED"` = `"PROFILE_NOT_PERMITTED"` A Not-permitted (`X`) segment or field is present. ##### PROFILE\_REQUIRED\_ABSENT > `readonly` **PROFILE\_REQUIRED\_ABSENT**: `"PROFILE_REQUIRED_ABSENT"` = `"PROFILE_REQUIRED_ABSENT"` A Required (`R`) segment or field is absent (or present-but-empty). ##### PROFILE\_UNDECLARED\_CONTENT > `readonly` **PROFILE\_UNDECLARED\_CONTENT**: `"PROFILE_UNDECLARED_CONTENT"` = `"PROFILE_UNDECLARED_CONTENT"` A present repetition carries a non-empty value at a component index the field rule does not declare. A distinct code on purpose: "a value we do not accept" and "content in an element the profile never specified" are two problems with two remedies, and the second is the one an author cannot see from any other finding. It fires only for a field rule carrying at least one [FieldRule.components](#components) entry, because declaring component rules is what closes the component set; a rule that declares none (the member omitted, or an empty list) can never emit it. ##### PROFILE\_VALUE\_NOT\_IN\_SET > `readonly` **PROFILE\_VALUE\_NOT\_IN\_SET**: `"PROFILE_VALUE_NOT_IN_SET"` = `"PROFILE_VALUE_NOT_IN_SET"` A checked component value is not a member of the consumer-supplied value set. #### Example ```ts import { validateAgainstProfile, FINDING_CODES } from "@cosyte/hl7"; const { findings } = validateAgainstProfile(msg, profile); const missing = findings.filter((f) => f.code === FINDING_CODES.PROFILE_REQUIRED_ABSENT); ``` *** ### JSON\_SCHEMA\_DIALECT > `const` **JSON\_SCHEMA\_DIALECT**: `"https://json-schema.org/draft/2020-12/schema"` = `"https://json-schema.org/draft/2020-12/schema"` The JSON Schema dialect the emitted document declares, and the only one this library emits. #### Example ```ts import { JSON_SCHEMA_DIALECT, messageJsonSchema } from "@cosyte/hl7"; messageJsonSchema().$schema === JSON_SCHEMA_DIALECT; // true ``` *** ### KNOWN\_CODING\_SYSTEMS > `const` **KNOWN\_CODING\_SYSTEMS**: readonly [`KnownCodingSystem`](#knowncodingsystem)[] The safety-relevant subset of HL7 Table 0396 this library recognizes for provenance. Deliberately small and frozen: it is NOT the full Table 0396 registry. Each entry's source is recorded in `docs-content/spec-notes-coding-system.md`. Note on `I10`: Table 0396 registers `I10` as **ICD-10** (the WHO base classification). US v2 feeds frequently send `I10` when they mean ICD-10-**CM**, but that clinical-modification specificity is the sender's convention, not what the acronym registers: so this map reports the registered claim (`"ICD-10"`) and does not silently upgrade it to CM. #### Example ```ts import { KNOWN_CODING_SYSTEMS } from "@cosyte/hl7"; console.log(KNOWN_CODING_SYSTEMS.find((s) => s.id === "LN")?.name); // "LOINC" ``` *** ### KNOWN\_SEGMENTS > `const` **KNOWN\_SEGMENTS**: `ReadonlySet`\<`string`\> Frozen set of every standard HL7 v2 segment name the library recognises. Consumers parsing any segment whose name is neither in this set nor declared by the active profile will see `UNKNOWN_SEGMENT` in `msg.warnings`. #### Example ```ts import { KNOWN_SEGMENTS } from "@cosyte/hl7"; console.log(KNOWN_SEGMENTS.has("PID")); // true console.log(KNOWN_SEGMENTS.has("ZPI")); // false ``` *** ### MESSAGE\_STRUCTURE\_DEFINITIONS > `const` **MESSAGE\_STRUCTURE\_DEFINITIONS**: readonly [`MessageStructureDefinition`](#messagestructuredefinition)[] The derived expected-segment registry. Generated from HL7's published, machine-readable message structures (see `STRUCTURE_REGISTRY_PROVENANCE` for the publication, the commit and the per-file hashes) and frozen: data, not config. Deliberately narrow in COVERAGE (twelve message codes) and not narrow in CONTENT: within those codes it expects exactly what the publication marks required. It is still a safety net, not a conformance validator. #### Example ```ts import { MESSAGE_STRUCTURE_DEFINITIONS } from "@cosyte/hl7"; const adt = MESSAGE_STRUCTURE_DEFINITIONS.find((d) => d.messageCode === "ADT" && d.triggerEvents.includes("A01"), ); console.log(adt?.requiredSegments); // ["EVN", "MSH", "PID", "PV1"] ``` *** ### PREDICATE\_CONNECTORS > `const` **PREDICATE\_CONNECTORS**: readonly [`PredicateConnector`](#predicateconnector)[] The frozen, ordered listing of the connectors a condition predicate may use: `AND`, `OR`, `XOR`. Like [PREDICATE\_VERBS](#predicate_verbs) this listing IS the accept-set. #### Example ```ts import { PREDICATE_CONNECTORS } from "@cosyte/hl7"; PREDICATE_CONNECTORS.length; // 3 PREDICATE_CONNECTORS[2]; // "XOR" ``` *** ### PREDICATE\_VERBS > `const` **PREDICATE\_VERBS**: readonly [`PredicateVerb`](#predicateverb)[] The frozen, ordered listing of the comparison verbs a condition predicate may use: `is`, `is not`, `contains`, `does not contain`, `matches`, `does not match`. Unlike [USAGE\_CODES](#usage_codes), this listing IS the accept-set: a verb outside it is `PROFILE_MALFORMED`, and the [PredicateVerb](#predicateverb) type refuses it at compile time where the author writes TypeScript. #### Example ```ts import { PREDICATE_VERBS } from "@cosyte/hl7"; PREDICATE_VERBS.length; // 6 PREDICATE_VERBS[0]; // "is" ``` *** ### PROFILE\_LEVELS > `const` **PROFILE\_LEVELS**: readonly [`ProfileLevel`](#profilelevel)[] The frozen listing of the three [ProfileLevel](#profilelevel)s, ordered from the least constrained to the most: `standard`, `constrainable`, `implementable`. Unlike [USAGE\_CODES](#usage_codes) this listing IS the accept-set: a [ConformanceProfile.level](#level) outside it is [FINDING\_CODES.PROFILE\_MALFORMED](#profile_malformed), and the [ProfileLevel](#profilelevel) type refuses it at compile time where the author writes TypeScript. #### Example ```ts import { PROFILE_LEVELS } from "@cosyte/hl7"; PROFILE_LEVELS.length; // 3 PROFILE_LEVELS[0]; // "standard" ``` *** ### profiles > `const` **profiles**: `object` Namespace object exposing the 8 shipped built-in vendor profiles (epic, cerner, meditech, athena, genericLab, visage, philips, va). Each is authored via the public `defineProfile()` API (BIP-01..09). #### Type Declaration ##### athena > `readonly` **athena**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZCA`: \{ `fields`: \{ `careTeamRole`: `number`; `providerId`: `number`; `providerName`: `number`; \}; \}; \}; `dateFormats`: `string`[]; `description`: `string`; `name`: `string`; \}\> ##### cerner > `readonly` **cerner**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZCO`: \{ `fields`: \{ `commentText`: `number`; `continuationFlag`: `number`; \}; \}; `ZDS`: \{ `fields`: \{ `summaryText`: `number`; \}; \}; \}; `dateFormats`: `string`[]; `description`: `string`; `name`: `string`; \}\> ##### epic > `readonly` **epic**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZDP`: \{ `fields`: \{ `departmentCode`: `number`; `departmentName`: `number`; \}; \}; `ZRS`: \{ `fields`: \{ `resultStatus`: `number`; `statusDateTime`: `number`; \}; \}; \}; `dateFormats`: `string`[]; `description`: `string`; `name`: `string`; \}\> ##### genericLab > `readonly` **genericLab**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZLB`: \{ `fields`: \{ `methodOverride`: `number`; `specimenOverride`: `number`; \}; \}; `ZNT`: \{ `fields`: \{ `noteText`: `number`; \}; \}; \}; `dateFormats`: `string`[]; `description`: `string`; `name`: `string`; \}\> ##### meditech > `readonly` **meditech**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZF1`: \{ `fields`: \{ `copayMaximum`: `number`; `copayMinimum`: `number`; `misServiceGroup`: `number`; `providerEncounter`: `number`; `serviceGroupCopay`: `number`; `visitCopay`: `number`; \}; \}; `ZF2`: \{ `fields`: \{ `encounterDate`: `number`; `encounterProcedure`: `number`; `encounterProcedureCharge`: `number`; `encounterProcedureQuantity`: `number`; `providerEncounter`: `number`; `prvProcedureAmountDue`: `number`; `prvProcedureAmountPaid`: `number`; `setId`: `number`; \}; \}; \}; `dateFormats`: `string`[]; `description`: `string`; `name`: `string`; \}\> ##### philips > `readonly` **philips**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZAO`: \{ `fields`: \{ `acquisitionStatus`: `number`; `bodyPart`: `number`; `departmentId`: `number`; `device`: `number`; `modality`: `number`; `orderAdditionalDetails`: `number`; `orderCreatedBy`: `number`; `orderCustomDate1`: `number`; `orderCustomDate2`: `number`; `orderCustomDate3`: `number`; `orderCustomNumber1`: `number`; `orderCustomNumber2`: `number`; `orderCustomString1`: `number`; `orderCustomString2`: `number`; `orderCustomString3`: `number`; `orderCustomString4`: `number`; `orderCustomString5`: `number`; `orderCustomString6`: `number`; `orderCustomString7`: `number`; `orderUpdatedBy`: `number`; `orderWithNoImages`: `number`; `radiologistFamilyName`: `number`; `radiologistGivenName`: `number`; `radiologistId`: `number`; `radiologistMiddleName`: `number`; `resultTransferStatus`: `number`; `section`: `number`; `technicianFamilyName`: `number`; `technicianGivenName`: `number`; `technicianId`: `number`; `technicianMiddleName`: `number`; \}; \}; `ZAP`: \{ `fields`: \{ `patientAdditionalDetails`: `number`; `patientCustomDate1`: `number`; `patientCustomDate2`: `number`; `patientCustomDate3`: `number`; `patientCustomNumber1`: `number`; `patientCustomNumber2`: `number`; `patientCustomString1`: `number`; `patientCustomString2`: `number`; `patientCustomString3`: `number`; `patientCustomString4`: `number`; `patientCustomString5`: `number`; `patientCustomString6`: `number`; `patientCustomString7`: `number`; \}; \}; `ZAV`: \{ `fields`: \{ `visitAdditionalDetails`: `number`; \}; \}; `ZDS`: \{ `fields`: \{ `studyInstanceUid`: `number`; \}; \}; `ZEB`: \{ `fields`: \{ `encryptedPatientInfo`: `number`; \}; \}; `ZLK`: \{ `fields`: \{ `externalWorkitemId`: `number`; `orderLinkId`: `number`; \}; \}; \}; `description`: `string`; `name`: `string`; \}\> ##### va > `readonly` **va**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZDS`: \{ `fields`: \{ `studyInstanceUid`: `number`; \}; \}; \}; `description`: `string`; `name`: `string`; \}\> ##### visage > `readonly` **visage**: [`DefinedProfile`](#definedprofile)\<\{ `customSegments`: \{ `ZDS`: \{ `fields`: \{ `studyInstanceUid`: `number`; \}; \}; \}; `description`: `string`; `name`: `string`; \}\> #### Example ```ts import { parseHL7, profiles } from "@cosyte/hl7"; const msg = parseHL7(raw, profiles.epic); console.log(msg.profile?.name); // "epic" ``` *** ### SCHEMA\_EMIT\_TARGETS > `const` **SCHEMA\_EMIT\_TARGETS**: readonly \[`"json-schema-2020-12"`, `"zod"`\] Every emission target this library supports, as a caller names it. #### Example ```ts import { SCHEMA_EMIT_TARGETS } from "@cosyte/hl7"; SCHEMA_EMIT_TARGETS.includes("zod"); // true ``` *** ### STRUCTURE\_FINDING\_CODES > `const` **STRUCTURE\_FINDING\_CODES**: `object` The frozen registry of published-structure finding codes. Stable, additive string codes: a consumer compares `finding.code === STRUCTURE_FINDING_CODES.STRUCTURE_SEGMENT_CARDINALITY`. Deliberately separate from the conformance engine's `FINDING_CODES`. Those report a violation of a profile the CALLER wrote; these report a divergence from the publication itself, and a consumer routing the two to the same place is making a decision rather than inheriting one. #### Type Declaration ##### STRUCTURE\_SEGMENT\_CARDINALITY > `readonly` **STRUCTURE\_SEGMENT\_CARDINALITY**: `"STRUCTURE_SEGMENT_CARDINALITY"` = `"STRUCTURE_SEGMENT_CARDINALITY"` A segment occurs fewer times than the published structure's minimum along the path from the structure root, or more times than its maximum. ##### STRUCTURE\_SEGMENT\_OUT\_OF\_ORDER > `readonly` **STRUCTURE\_SEGMENT\_OUT\_OF\_ORDER**: `"STRUCTURE_SEGMENT_OUT_OF_ORDER"` = `"STRUCTURE_SEGMENT_OUT_OF_ORDER"` A segment appears where the published structure does not allow it: the message's segment sequence stops being a beginning the published order allows. The order is read against the publication's own bounds, at every locus: a node may occur as few and as many times as the publication says there, so a group's required leading segment cannot be skipped on one occurrence and consumed on the next, whether or not that group repeats. The one bound dropped for a segment name is the one the cardinality check already reported for it, so that too few or too many occurrences is one finding rather than two. Where two segments arrive in an order the publication does not allow, the finding names the one that arrived late: the segment the published order puts first and the message delivered second, at the occurrence the message carries it. That is not always where the reading stopped, because the publication is often free to skip past the segment a message delayed, so an adjacent pair whose exchange the publication derives is what identifies the two. Every adjacent pair is asked, not a shortlist near the stop: in a message that repeats a group the pair can sit well in front of it. Where more than one pair would do, the earliest is named. Where no exchange derives, the defect is not two segments in the wrong order: the finding then names the segment that belonged where the reading stopped, and failing that the one that could not be placed. ##### STRUCTURE\_SEGMENT\_UNEXPECTED > `readonly` **STRUCTURE\_SEGMENT\_UNEXPECTED**: `"STRUCTURE_SEGMENT_UNEXPECTED"` = `"STRUCTURE_SEGMENT_UNEXPECTED"` A segment the published structure does not name anywhere, a `Z` segment included. Distinct from the other two on purpose: a segment the publication never mentions has no position to be out of and no count to exceed. A line carrying no segment name at all is not one of these. A message ending with a segment terminator parses as a trailing segment whose type is the empty string, and the publication names segments rather than blank lines; the parse reports that line, under `UNKNOWN_SEGMENT`. #### Example ```ts import { validateMessageStructure, STRUCTURE_FINDING_CODES } from "@cosyte/hl7"; const { findings } = validateMessageStructure(msg); const misordered = findings.filter( (f) => f.code === STRUCTURE_FINDING_CODES.STRUCTURE_SEGMENT_OUT_OF_ORDER, ); ``` *** ### STRUCTURE\_NOT\_VALIDATED\_REASONS > `const` **STRUCTURE\_NOT\_VALIDATED\_REASONS**: `object` The frozen registry of reasons validation did not run. Every one of them is a question the publication cannot answer for this message, never a defect found in it. #### Type Declaration ##### EMPTY\_EXPECTATION > `readonly` **EMPTY\_EXPECTATION**: `"EMPTY_EXPECTATION"` = `"EMPTY_EXPECTATION"` The registry entry resolves to no ordered expectation at all. A published structure whose expectation is empty says nothing about the message, and treating "says nothing" as "allows nothing" would report every segment as unexpected. ##### NO\_MESSAGE\_TYPE > `readonly` **NO\_MESSAGE\_TYPE**: `"NO_MESSAGE_TYPE"` = `"NO_MESSAGE_TYPE"` MSH-9.1 is absent or blank, so there is no message type to resolve. ##### RETAINED\_TRANSCRIPTION > `readonly` **RETAINED\_TRANSCRIPTION**: `"RETAINED_TRANSCRIPTION"` = `"RETAINED_TRANSCRIPTION"` The message type is recognized only through a retained transcription: the publication carries no structure for it, so there is no published order and no published maximum to check against. ##### UNRECOGNIZED\_MESSAGE\_TYPE > `readonly` **UNRECOGNIZED\_MESSAGE\_TYPE**: `"UNRECOGNIZED_MESSAGE_TYPE"` = `"UNRECOGNIZED_MESSAGE_TYPE"` The structure registry models no published structure for this message type. #### Example ```ts import { validateMessageStructure, STRUCTURE_NOT_VALIDATED_REASONS } from "@cosyte/hl7"; const result = validateMessageStructure(msg); const unmodelled = result.reason === STRUCTURE_NOT_VALIDATED_REASONS.UNRECOGNIZED_MESSAGE_TYPE; ``` *** ### STRUCTURE\_REGISTRY\_PROVENANCE > `const` **STRUCTURE\_REGISTRY\_PROVENANCE**: `Readonly`\<\{ `families`: readonly `Readonly`\<\{ `members`: readonly `string`[]; `structureId`: `string`; \}\>[]; `files`: readonly `Readonly`\<\{ `bytes`: `number`; `path`: `string`; `sha256`: `string`; `url`: `string`; \}\>[]; `pairs`: readonly `Readonly`\<\{ `messageCode`: `string`; `structureId`: `string`; `triggerEvent`: `string`; \}\>[]; `publication`: `Readonly`\<\{ `commit`: `string`; `commitDate`: `string`; `name`: `string`; `repository`: `string`; `repositoryUrl`: `string`; `tree`: `string`; \}\>; `snapshotTakenAt`: `string`; \}\> Where every expectation in [MESSAGE\_STRUCTURE\_DEFINITIONS](#message_structure_definitions) came from: the publication and the commit it was read at, the sha256 and upstream URL of every vendored file, the variant family behind each referenced structure, and the structure id behind each recognized (message code, trigger event) pair. A consumer auditing a warning can answer "says who?" without leaving the package and without a network call. #### Example ```ts import { STRUCTURE_REGISTRY_PROVENANCE } from "@cosyte/hl7"; console.log(STRUCTURE_REGISTRY_PROVENANCE.publication.repository); // "HL7/v2ig" const a01 = STRUCTURE_REGISTRY_PROVENANCE.pairs.find( (p) => p.messageCode === "ADT" && p.triggerEvent === "A01", ); console.log(a01?.structureId); // "ADT_A01" ``` *** ### SUPPORTED\_BUILDER\_MESSAGES > `const` **SUPPORTED\_BUILDER\_MESSAGES**: readonly [`SupportedBuilderMessage`](#supportedbuildermessage)[] Every (message code, trigger event) pair the typed builders can author, with the builder that authors it. Frozen; derived from the structure registry, so it can never claim a pair the structure net would warn on. #### Example ```ts import { SUPPORTED_BUILDER_MESSAGES } from "@cosyte/hl7"; for (const m of SUPPORTED_BUILDER_MESSAGES) { console.log(`${m.messageCode}^${m.triggerEvent}`, m.builder); } ``` *** ### SUPPORTED\_DATE\_TOKENS > `const` **SUPPORTED\_DATE\_TOKENS**: readonly `string`[] = `TOKEN_ORDER` Every date-format token the library's format matcher and the `defineProfile()` `dateFormats` validator recognize. Re-exported so profile authors can introspect the valid token set. Every token listed here is honoured by the matcher: none is silently accepted at definition time and then treated as literal characters. The vocabulary carries **no two-digit-year token** (no century window is applied, so a two-digit year cannot be resolved) and **no timezone token** (a value with no offset is flagged, never resolved). #### Example ```ts import { SUPPORTED_DATE_TOKENS } from "@cosyte/hl7"; console.log(SUPPORTED_DATE_TOKENS); // ["YYYY", "MM", "M", "MMM", "MMMM", "DD", "D", "HH", "H", "hh", "h", "mm", "ss", "SSSS", "A"] ``` *** ### SUPPORTED\_OVERLAY\_MESSAGES > `const` **SUPPORTED\_OVERLAY\_MESSAGES**: readonly [`SupportedOverlayMessage`](#supportedoverlaymessage)[] The published support claim for typed message overlays: every key [Hl7Message.is](#is) narrows on, with the segments that pair's published structure requires. Frozen, and derived from the structure registry at module load rather than hand-listed, so it can never claim a pair the structure net does not recognize. #### Example ```ts import { SUPPORTED_OVERLAY_MESSAGES } from "@cosyte/hl7"; console.log(SUPPORTED_OVERLAY_MESSAGES.length); // one entry per published key for (const m of SUPPORTED_OVERLAY_MESSAGES) console.log(m.key, m.requiredSegments); ``` *** ### TYPED\_BUILDER\_COVERAGE > `const` **TYPED\_BUILDER\_COVERAGE**: readonly [`TypedBuilderCoverage`](#typedbuildercoverage)[] What each typed builder can emit. Frozen data, exposed so a consumer can check the support claim's exclusions for themselves: a registry pair whose required segments are not a subset of the matching entry's `segments` is exactly the pair the support set leaves out. #### Example ```ts import { TYPED_BUILDER_COVERAGE, findMessageStructureDefinition } from "@cosyte/hl7"; const cov = TYPED_BUILDER_COVERAGE.find((c) => c.messageCode === "ADT"); const a20 = findMessageStructureDefinition("ADT", "A20"); // A20 requires NPU, which no typed init supplies, so A20 is not published: console.log(a20?.requiredSegments.every((s) => cov?.segments.includes(s))); // false ``` *** ### USAGE\_CODES > `const` **USAGE\_CODES**: readonly [`UsageCodeRegistryEntry`](#usagecoderegistryentry)[] The frozen, ordered registry of HL7 v2 usage indicators a message profile may use: `R`, `RE`, `C`, `CE`, `O`, `X`, `C(a/b)`, `B`. **It is a published vocabulary LISTING, not a membership oracle for what a rule may declare.** The two questions are different and neither answers the other: - `C(a/b)` is listed here and is REFUSED on a rule, because `a` and `b` are placeholders for usage codes rather than usage codes. - `C(RE/X)` is accepted on a rule and is NOT listed here, because the listing carries the family's notation once instead of all of its members. What a rule may declare is answered by [UsageCode](#usagecode) at compile time and by [defineConformanceProfile](#defineconformanceprofile) / [validateAgainstProfile](#validateagainstprofile) at run time. Do not re-derive this array as an accept-set. #### Example ```ts import { USAGE_CODES } from "@cosyte/hl7"; USAGE_CODES.length; // 8 USAGE_CODES[0]; // "R" ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, synced with `package.json#version` at build time by downstream phases. Exported now so consumers (and the type-check pipeline) have at least one symbol to resolve through the `exports` map. #### Example ```ts import { VERSION } from "@cosyte/hl7"; console.log(VERSION); ``` *** ### WARNING\_CODES > `const` **WARNING\_CODES**: `object` Stable string codes for every Tier-2 warning the parser may emit. The registry is frozen via `as const` so TypeScript infers the exact string literal union for `WarningCode`: there is zero runtime cost and no magic-string comparisons for consumers. #### Type Declaration ##### ACK\_NO\_CORRELATION\_ID > `readonly` **ACK\_NO\_CORRELATION\_ID**: `"ACK_NO_CORRELATION_ID"` = `"ACK_NO_CORRELATION_ID"` ##### BATCH\_COUNT\_MISMATCH > `readonly` **BATCH\_COUNT\_MISMATCH**: `"BATCH_COUNT_MISMATCH"` = `"BATCH_COUNT_MISMATCH"` ##### BATCH\_MISSING\_TRAILER > `readonly` **BATCH\_MISSING\_TRAILER**: `"BATCH_MISSING_TRAILER"` = `"BATCH_MISSING_TRAILER"` ##### DUPLICATE\_REQUIRED\_SEGMENT > `readonly` **DUPLICATE\_REQUIRED\_SEGMENT**: `"DUPLICATE_REQUIRED_SEGMENT"` = `"DUPLICATE_REQUIRED_SEGMENT"` ##### ENCODING\_MISMATCH > `readonly` **ENCODING\_MISMATCH**: `"ENCODING_MISMATCH"` = `"ENCODING_MISMATCH"` ##### EXTRA\_FIELDS > `readonly` **EXTRA\_FIELDS**: `"EXTRA_FIELDS"` = `"EXTRA_FIELDS"` ##### FIELD\_WHITESPACE\_TRIMMED > `readonly` **FIELD\_WHITESPACE\_TRIMMED**: `"FIELD_WHITESPACE_TRIMMED"` = `"FIELD_WHITESPACE_TRIMMED"` ##### MERGE\_MISSING\_PRIOR\_OR\_SURVIVOR > `readonly` **MERGE\_MISSING\_PRIOR\_OR\_SURVIVOR**: `"MERGE_MISSING_PRIOR_OR_SURVIVOR"` = `"MERGE_MISSING_PRIOR_OR_SURVIVOR"` ##### MISSING\_EXPECTED\_GROUP > `readonly` **MISSING\_EXPECTED\_GROUP**: `"MISSING_EXPECTED_GROUP"` = `"MISSING_EXPECTED_GROUP"` ##### MISSING\_REQUIRED\_FIELD > `readonly` **MISSING\_REQUIRED\_FIELD**: `"MISSING_REQUIRED_FIELD"` = `"MISSING_REQUIRED_FIELD"` ##### MLLP\_FRAMING\_STRIPPED > `readonly` **MLLP\_FRAMING\_STRIPPED**: `"MLLP_FRAMING_STRIPPED"` = `"MLLP_FRAMING_STRIPPED"` ##### OUT\_OF\_ORDER\_SEGMENT > `readonly` **OUT\_OF\_ORDER\_SEGMENT**: `"OUT_OF_ORDER_SEGMENT"` = `"OUT_OF_ORDER_SEGMENT"` ##### SEGMENT\_CASE > `readonly` **SEGMENT\_CASE**: `"SEGMENT_CASE"` = `"SEGMENT_CASE"` ##### TIMESTAMP\_FALLBACK\_FORMAT > `readonly` **TIMESTAMP\_FALLBACK\_FORMAT**: `"TIMESTAMP_FALLBACK_FORMAT"` = `"TIMESTAMP_FALLBACK_FORMAT"` ##### UNKNOWN\_CHARSET > `readonly` **UNKNOWN\_CHARSET**: `"UNKNOWN_CHARSET"` = `"UNKNOWN_CHARSET"` ##### UNKNOWN\_ESCAPE\_SEQUENCE > `readonly` **UNKNOWN\_ESCAPE\_SEQUENCE**: `"UNKNOWN_ESCAPE_SEQUENCE"` = `"UNKNOWN_ESCAPE_SEQUENCE"` ##### UNKNOWN\_SEGMENT > `readonly` **UNKNOWN\_SEGMENT**: `"UNKNOWN_SEGMENT"` = `"UNKNOWN_SEGMENT"` ##### UNSUPPORTED\_CHARSET > `readonly` **UNSUPPORTED\_CHARSET**: `"UNSUPPORTED_CHARSET"` = `"UNSUPPORTED_CHARSET"` ##### UNTERMINATED\_STREAM\_MESSAGE > `readonly` **UNTERMINATED\_STREAM\_MESSAGE**: `"UNTERMINATED_STREAM_MESSAGE"` = `"UNTERMINATED_STREAM_MESSAGE"` ##### VERSION\_MISMATCH > `readonly` **VERSION\_MISMATCH**: `"VERSION_MISMATCH"` = `"VERSION_MISMATCH"` #### Example ```ts import { parseHL7, WARNING_CODES } from "@cosyte/hl7"; const msg = parseHL7(raw); if (msg.warnings.some((w) => w.code === WARNING_CODES.MLLP_FRAMING_STRIPPED)) { // handle MLLP-wrapped input } ``` ## Functions ### ackNoCorrelationId() > **ackNoCorrelationId**(`position`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `ACK_NO_CORRELATION_ID` warning. Emitted by `buildAck`, not by the parser: the inbound message carried no MSH-10 message control ID, so the generated ACK leaves MSA-2 empty and, when a positive accept was requested, downgrades it to an error code rather than fabricating an unverifiable `AA`/`CA`. The `position` references the inbound MSH segment. The message NEVER echoes a PHI value: only the structural fact. #### Parameters ##### position [`Hl7Position`](#hl7position) #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { buildAck, WARNING_CODES } from "@cosyte/hl7"; const ack = buildAck(inbound, { code: "AA" }); // inbound has no MSH-10 ack.warnings.some((w) => w.code === WARNING_CODES.ACK_NO_CORRELATION_ID); // true ``` *** ### alternateCodingSystemOf() > **alternateCodingSystemOf**(`coded`): [`CodingSystemInfo`](#codingsysteminfo) \| `undefined` Provenance of a coded element's **alternate** coding system (CWE.6 / CE.6). Returns `undefined` when the element claims no alternate system. Useful for dual-coded fields (e.g. a problem carrying both SNOMED CT and ICD-10), where assuming a single coding system would be unsafe. #### Parameters ##### coded [`CodedSystemFields`](#codedsystemfields) #### Returns [`CodingSystemInfo`](#codingsysteminfo) \| `undefined` #### Example ```ts import { alternateCodingSystemOf } from "@cosyte/hl7"; const alt = alternateCodingSystemOf(dg.code); if (alt) console.log("also coded in", alt.name ?? alt.claimed); ``` *** ### analyzeMessageStructure() > **analyzeMessageStructure**(`messageCode`, `triggerEvent`, `presentSegmentNames`): [`MessageStructure`](#messagestructure) Analyze a parsed message's structure against the derived expected-segment registry. Pure: it takes the message code, trigger event, and the set of segment names actually present, and returns a `MessageStructure` summary. It decides nothing about whether to warn: the caller (parser) emits one `MISSING_EXPECTED_GROUP` warning per name in `missingSegments`. #### Parameters ##### messageCode `string` MSH-9.1 (e.g. `"ORU"`); empty string if absent. ##### triggerEvent `string` MSH-9.2 (e.g. `"R01"`); empty string if absent. ##### presentSegmentNames `ReadonlySet`\<`string`\> the set of segment names present in the message. #### Returns [`MessageStructure`](#messagestructure) #### Example ```ts import { analyzeMessageStructure } from "@cosyte/hl7"; const s = analyzeMessageStructure("ORU", "R01", new Set(["MSH", "PID"])); console.log(s.recognized); // true console.log(s.missingSegments); // ["OBR"] (the published minimum of one) ``` *** ### batchCountMismatch() > **batchCountMismatch**(`position`, `unit`, `declared`, `actual`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `BATCH_COUNT_MISMATCH` warning. Emitted by `splitBatch()`: attached to the returned `BatchSplitResult.warnings`, never to `Hl7Message.warnings`: when a declared envelope count does not equal the count actually split out: a **BTS-1** batch message count that differs from the messages found in that batch, or an **FTS-1** file batch count that differs from the batches found in the file (HL7 v2 Ch. 2 §2.10.3; BTS-1 / FTS-1 per the BTS / FTS segment definitions in §2.15). The splitter **never drops the tail** to make the numbers agree: the mismatch is surfaced and every message is still returned; the caller decides whether to reject. The message carries only the declared-vs-actual **integers** and the unit (`message` for BTS-1, `batch` for FTS-1): NEVER a field value, facility identifier, or any other content, so no PHI is exposed. `position` references the trailer segment (`BTS`/`FTS`) that declared the count. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### unit `"message"` \| `"batch"` ##### declared `number` ##### actual `number` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { batchCountMismatch } from "@cosyte/hl7"; const w = batchCountMismatch({ segmentIndex: 4 }, "message", 3, 2); ``` *** ### batchMissingTrailer() > **batchMissingTrailer**(`position`, `header`, `expectedTrailer`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `BATCH_MISSING_TRAILER` warning. Emitted by `splitBatch()` (attached to `BatchSplitResult.warnings`) when an envelope header opens a scope that is never closed: a **BHS** batch header with no matching **BTS** trailer, or an **FHS** file header with no matching **FTS** trailer (HL7 v2 Ch. 2 §2.10.3: each envelope segment is optional, but a profile such as an IIS file-submission spec may mandate the full frame). `splitBatch` **does not enforce** such a rule: it splits, warns, and leaves the accept/reject decision to the caller; the parse never throws for this. The message carries only the header/trailer segment names: NEVER a field value, so no PHI is exposed. `position` references the unmatched header segment (`FHS`/`BHS`). #### Parameters ##### position [`Hl7Position`](#hl7position) ##### header `"FHS"` \| `"BHS"` ##### expectedTrailer `"BTS"` \| `"FTS"` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { batchMissingTrailer } from "@cosyte/hl7"; const w = batchMissingTrailer({ segmentIndex: 0 }, "BHS", "BTS"); ``` *** ### buildAck() > **buildAck**(`inbound`, `options`): [`Hl7Message`](#hl7message) Build a spec-clean ACK (`MSH` + `MSA` [+ `ERR`…]) responding to `inbound`. Behavior: - **MSH**: sender/receiver are swapped (inbound MSH-5/6 → ACK MSH-3/4; inbound MSH-3/4 → ACK MSH-5/6); MSH-7 is the current UTC time; MSH-9 is `ACK` (with the inbound trigger event echoed as `ACK^^ACK` when present); MSH-10 is a freshly generated control id; MSH-11 (processing id) and MSH-12 (version) echo the inbound values. - **MSA**: MSA-1 = `code`; **MSA-2 echoes the full inbound MSH-10 field** (the raw field structure is carried over whole: a vendor-quirk id like `ID^X` is never truncated to its first component). The echo carries the inbound field's escape-fidelity overlay, so an id bearing a hex escape (`ID\X41\Q`) or a preserved escape (`\H\`) echoes **byte-verbatim**, not canonicalized: exactly the bytes the sender put on the wire, which is what MSA-2 correlation compares. The only structural transform is trailing-empty canonicalization (D-02). (A sender that used *custom* encoding characters is **re-delimited spec-cleanly**: the overlay carries the sender's raw bytes in the sender's alphabet, so echoing them verbatim under the ACK's default alphabet would corrupt the field's structure and break correlation: the overlay is therefore bypassed and the decoded id is re-escaped under default, which re-parses back to the same control id. Default-delimiter senders, the norm, keep the byte-exact overlay echo.) - **ERR**: one segment per supplied `AckErrorDetail`: ERR-2 location (when given), ERR-3 the Table 0357 condition code as a CWE (`code^text^HL70357`), ERR-4 the Table 0516 severity. **Fail-safe.** If the inbound message has no MSH-10, the ACK cannot be correlated. `buildAck` then leaves MSA-2 empty and, if a positive accept (`AA`/`CA`) was requested, **downgrades it** to the matching error code (`AE`/`CE`): it never fabricates an unverifiable positive ACK. The returned message carries an `ACK_NO_CORRELATION_ID` warning. (This is the inbound-side complement to `@cosyte/mllp`'s "no commit ⇒ never AA".) Pure aside from the generated control id + timestamp; never throws except on a programming error (`inbound` not an `Hl7Message`, or an unknown `code`). #### Parameters ##### inbound [`Hl7Message`](#hl7message) ##### options [`BuildAckOptions`](#buildackoptions) #### Returns [`Hl7Message`](#hl7message) #### Example ```ts import { buildAck, parseHL7 } from "@cosyte/hl7"; const inbound = parseHL7(raw); const ack = buildAck(inbound, { code: "AA" }); console.log(ack.toString()); // MSH|...\rMSA|AA| parseHL7(ack.toString()).meta.type; // "ACK" (round-trips clean) ``` *** ### buildAdt() > **buildAdt**(`event`, `init`): [`Hl7Message`](#hl7message) Build a spec-clean ADT message for `event` (the MSH-9.2 trigger, e.g. `"A01"`, `"A04"`, `"A08"`) from typed inputs. The result is a real [Hl7Message](#hl7message): `msg.toString()` serialises it, `parseHL7(msg.toString())` round-trips with zero warnings, and `msg.patient` / `msg.visit` read back the values supplied. #### Parameters ##### event `string` the ADT trigger event (MSH-9.2). Required, non-empty. ##### init [`BuildAdtInit`](#buildadtinit) the MSH envelope + typed PID/PV1/EVN content (`patient` required), plus `priorIdentity` for a merge, move or identifier-change event. #### Returns [`Hl7Message`](#hl7message) #### Throws TypeError when `event` is empty, when `init.patient` is absent, or when the requested trigger event's published structure requires MRG and no prior identity carrying a merge key was supplied. #### Examples ```ts import { buildAdt, parseHL7 } from "@cosyte/hl7"; // A merge: the prior identifiers are retired in favour of the surviving ones. const merge = buildAdt("A40", { patient: { identifiers: { idNumber: "MRN-NEW", identifierTypeCode: "MR" } }, priorIdentity: { identifiers: { idNumber: "MRN-OLD", identifierTypeCode: "MR" } }, }); const ev = parseHL7(merge.toString()).identityEvents()[0]; ev?.prior?.identifiers[0]?.idNumber; // "MRN-OLD" ev?.surviving?.identifiers[0]?.idNumber; // "MRN-NEW" ``` ```ts import { buildAdt, parseHL7 } from "@cosyte/hl7"; const msg = buildAdt("A01", { sendingApp: "CLINIC", receivingApp: "LAB", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" }, name: { familyName: "Test", givenName: "Ann" }, birthDateTime: "19880705", administrativeSex: "F", }, visit: { patientClass: "I", assignedLocation: { pointOfCare: "ICU", room: "1", bed: "A" } }, }); const round = parseHL7(msg.toString()); round.warnings.length; // 0 round.patient?.familyName; // "Test" round.structure.missingGroups; // [] ``` *** ### buildDft() > **buildDft**(`event`, `init`): [`Hl7Message`](#hl7message) Build a spec-clean DFT message for `event` (the MSH-9.2 trigger, e.g. `"P03"` post detail financial transaction, `"P11"` post detail financial transactions with a new appointment) from typed inputs. The result is a real [Hl7Message](#hl7message): `msg.toString()` serialises it, `parseHL7(msg.toString())` round-trips with zero warnings, and `msg.charges()` reads back the transactions supplied, in the order supplied. #### Parameters ##### event `string` the DFT trigger event (MSH-9.2). Required, non-empty. ##### init [`BuildDftInit`](#builddftinit) the MSH envelope + typed PID/FT1 content. `patient` and a non-empty `charges` list are required. #### Returns [`Hl7Message`](#hl7message) #### Throws TypeError when `event` is empty, when `init` is not an object, when `patient` is absent, or when `charges` is absent or empty. #### Example ```ts import { buildDft, parseHL7 } from "@cosyte/hl7"; const msg = buildDft("P03", { sendingApp: "CLINIC", receivingApp: "BILLING", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }, charges: [ { setId: "1", transactionDate: "20260801", transactionType: "CG", transactionCode: { identifier: "80053", text: "Metabolic panel" }, quantity: "1", amountExtended: { price: "150.00", denomination: "USD" }, diagnoses: [{ identifier: "E11.9" }], }, ], }); const round = parseHL7(msg.toString()); round.warnings.length; // 0 round.charges()[0]?.transactionCode?.identifier; // "80053" round.charges()[0]?.amountExtended; // "150.00&USD": wire text, never a number ``` *** ### buildMdm() > **buildMdm**(`event`, `init`): [`Hl7Message`](#hl7message) Build a spec-clean MDM message for `event` (the MSH-9.2 trigger, e.g. `"T02"` original document notification and content, `"T06"` document addendum notification and content) from typed inputs. The result is a real [Hl7Message](#hl7message): `msg.toString()` serialises it, `parseHL7(msg.toString())` round-trips with zero warnings, and `msg.documents()` reads back the document header and body supplied. #### Parameters ##### event `string` the MDM trigger event (MSH-9.2). Required, non-empty. ##### init [`BuildMdmInit`](#buildmdminit) the MSH envelope + typed PID/PV1/TXA content. `patient` and `document` are required; `document.body` is required for the content events. #### Returns [`Hl7Message`](#hl7message) #### Throws TypeError when `event` is empty, when `init` is not an object, when `patient` or `document` is absent, or when a content event is asked for with no body. #### Example ```ts import { buildMdm, parseHL7 } from "@cosyte/hl7"; const msg = buildMdm("T02", { sendingApp: "TRANSCRIPTION", receivingApp: "EHR", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }, document: { documentType: "DS", uniqueDocumentNumber: "DOC-1001", completionStatus: "AU", availabilityStatus: "AV", body: [{ setId: "1", valueType: "TX", value: "Discharge summary text." }], }, }); const round = parseHL7(msg.toString()); round.warnings.length; // 0 round.documents()[0]?.completionStatus; // "AU" round.documents()[0]?.observations[0]?.value; // "Discharge summary text." ``` *** ### buildMessage() > **buildMessage**(`init`): [`Hl7Message`](#hl7message) Construct an outbound `Hl7Message` from semantic MSH fields (SER-06). Synthesises a complete MSH `RawSegment` per D-10/D-11 and hands to `new Hl7Message({...})`. Callers chain `.addSegment(name, fields)` (mutation method) to append PID, OBX, etc. Defaults applied when fields are omitted (D-10): - `controlId` → `generateControlId()` (D-12) - `timestamp` → `formatHl7Timestamp(new Date())` (D-13) - `version` → `"2.5"` - `processingId` → `"P"` - `sendingApp`/`sendingFacility`/`receivingApp`/`receivingFacility` → empty Encoding characters are always `DEFAULT_ENCODING_CHARACTERS` (D-14); no option to customise in v1. **Empty string vs. omitted field (W1):** at the HL7 wire level, passing `sendingApp: ""` and omitting `sendingApp` produce IDENTICAL output (both emit as absent: `||` at the MSH-3 position). If you need to emit an HL7 explicit null (`""`, the two-char literal) at a specific position, build the message first, then use `setField`: ```ts const msg = buildMessage({ type: "ADT^A01" }); msg.setField("MSH.3", '""'); // sets RawField.isNull = true // msg.toString() now emits MSH-3 as `""` (2 chars), not as absent. ``` `BuildMessageInit` carries the same note on the input shape. #### Parameters ##### init [`BuildMessageInit`](#buildmessageinit) #### Returns [`Hl7Message`](#hl7message) #### Example ```ts import { buildMessage, parseHL7 } from "@cosyte/hl7"; const msg = buildMessage({ type: "ADT^A01", sendingApp: "CLINIC", sendingFacility: "MAIN", receivingApp: "LAB", receivingFacility: "REF", }).addSegment("PID", ["", "", "MRN123", "", "Doe^John"]); // Spec-clean HL7 string round-trips through parseHL7: const round = parseHL7(msg.toString()); console.log(round.meta.type); // "ADT^A01" ``` *** ### buildOrm() > **buildOrm**(`init`): [`Hl7Message`](#hl7message) Build a spec-clean `ORM^O01` general-order message from typed inputs. The result is a real [Hl7Message](#hl7message): `msg.toString()` serialises it, `parseHL7(msg.toString())` round-trips with zero warnings, and `msg.orders()` reads back the orders supplied, in the order supplied. #### Parameters ##### init [`BuildOrmInit`](#buildorminit) the MSH envelope + typed PID/ORC/OBR content. `patient` and a non-empty `orders` list are required. #### Returns [`Hl7Message`](#hl7message) #### Throws TypeError when `init` is not an object, when `patient` is absent, or when `orders` is absent or empty. #### Example ```ts import { buildOrm, parseHL7 } from "@cosyte/hl7"; const msg = buildOrm({ sendingApp: "EHR", receivingApp: "LAB", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" } }, orders: [ { orderControl: "NW", setId: "1", placerOrderNumber: "PL-1001", universalServiceId: { identifier: "CBC", text: "Complete Blood Count" }, orderingProvider: { idNumber: "9990", familyName: "Welby" }, }, ], }); const round = parseHL7(msg.toString()); round.warnings.length; // 0 round.orders()[0]?.orderControl; // "NW" round.orders()[0]?.universalServiceId?.identifier; // "CBC" ``` *** ### buildOru() > **buildOru**(`init`): [`Hl7Message`](#hl7message) Build a spec-clean ORU^R01 observation-result message from typed inputs. The result is a real [Hl7Message](#hl7message): `msg.toString()` serialises it, `parseHL7(msg.toString())` round-trips with zero warnings, and `msg.observations()` reads back the results supplied. #### Parameters ##### init [`BuildOruInit`](#buildoruinit) the MSH envelope + typed PID/OBR/OBX content. `patient` and a non-empty `observations` list are required. #### Returns [`Hl7Message`](#hl7message) #### Throws TypeError when `patient` is absent or `observations` is empty. #### Example ```ts import { buildOru, parseHL7 } from "@cosyte/hl7"; const msg = buildOru({ sendingApp: "LAB", receivingApp: "EHR", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" }, name: { familyName: "Test", givenName: "Ann" } }, order: { universalServiceId: { identifier: "CBC", text: "Complete Blood Count", nameOfCodingSystem: "L" }, resultStatus: "F" }, observations: [ { setId: "1", valueType: "NM", identifier: { identifier: "WBC", text: "White Blood Cells", nameOfCodingSystem: "LN" }, value: "7.2", units: { identifier: "10*3/uL" }, observationResultStatus: "F" }, ], }); const round = parseHL7(msg.toString()); round.warnings.length; // 0 round.observations()[0]?.value; // "7.2" ``` *** ### buildSiu() > **buildSiu**(`event`, `init`): [`Hl7Message`](#hl7message) Build a spec-clean SIU message for `event` (the MSH-9.2 trigger, e.g. `"S12"` new appointment, `"S14"` appointment modification, `"S15"` appointment cancellation) from typed inputs. The result is a real [Hl7Message](#hl7message): `msg.toString()` serialises it, `parseHL7(msg.toString())` round-trips with zero warnings, and `msg.appointments()` reads back the identifiers, timing and resources supplied. #### Parameters ##### event `string` the SIU trigger event (MSH-9.2). Required, non-empty. ##### init [`BuildSiuInit`](#buildsiuinit) the MSH envelope + typed SCH/RGS/AI* content. `appointment` and a non-empty `resourceGroups` list are required. #### Returns [`Hl7Message`](#hl7message) #### Throws TypeError when `event` is empty, when `init` is not an object, when `appointment` is absent, or when `resourceGroups` is absent or empty. #### Example ```ts import { buildSiu, parseHL7 } from "@cosyte/hl7"; const msg = buildSiu("S12", { sendingApp: "SCHEDULING", receivingApp: "EHR", appointment: { placerAppointmentId: "PL-1001", fillerAppointmentId: "FL-2002", startDateTime: "20260801090000", endDateTime: "20260801093000", fillerStatusCode: { identifier: "Booked" }, }, resourceGroups: [ { setId: "1", resources: [ { kind: "location", code: { identifier: "OR-1" } }, { kind: "personnel", person: { idNumber: "9990", familyName: "Welby" } }, ], }, ], }); const round = parseHL7(msg.toString()); round.warnings.length; // 0 round.appointments()[0]?.fillerAppointmentId; // "FL-2002" round.appointments()[0]?.resources.map((r) => r.kind); // ["location", "personnel"] ``` *** ### buildVxu() > **buildVxu**(`init`): [`Hl7Message`](#hl7message) Build a spec-clean `VXU^V04` vaccination-record-update message from typed inputs. The result is a real [Hl7Message](#hl7message): `msg.toString()` serialises it, `parseHL7(msg.toString())` round-trips with zero warnings, and `msg.immunizations()` reads back the doses supplied, in the order supplied. #### Parameters ##### init [`BuildVxuInit`](#buildvxuinit) the MSH envelope + typed PID/RXA content. `patient` and a non-empty `immunizations` list are required. #### Returns [`Hl7Message`](#hl7message) #### Throws TypeError when `init` is not an object, when `patient` is absent, or when `immunizations` is absent or empty. #### Example ```ts import { buildVxu, parseHL7 } from "@cosyte/hl7"; const msg = buildVxu({ sendingApp: "CLINIC", receivingApp: "IIS", patient: { identifiers: { idNumber: "MRN001", identifierTypeCode: "MR" }, name: { familyName: "Test", givenName: "Ann" }, }, immunizations: [ { orderControl: "RE", administeredDateTime: "20260801", vaccineCode: { identifier: "115", text: "Tdap", nameOfCodingSystem: "CVX" }, doseAmount: "0.5", doseUnits: { identifier: "mL", nameOfCodingSystem: "UCUM" }, completionStatus: "CP", actionCode: "A", routes: [{ route: { identifier: "IM", text: "Intramuscular" } }], }, ], }); const round = parseHL7(msg.toString()); round.warnings.length; // 0 round.immunizations()[0]?.vaccineCode?.identifier; // "115" ``` *** ### canonicalCharset() > **canonicalCharset**(`raw`): `string` The canonical Table-0211 code for a label, for equality comparison (e.g. the MSH-18-vs-`options.charset` `ENCODING_MISMATCH` check). Synonyms collapse to one canonical string (`"UNICODE UTF-8"` and `"UTF-8"` → `"UTF-8"`), so a synonym pair does not raise a false mismatch; an unrecognized label returns its normalized form so two genuinely different labels still differ. #### Parameters ##### raw `string` #### Returns `string` #### Example ```ts import { canonicalCharset } from "@cosyte/hl7"; canonicalCharset("unicode utf-8") === canonicalCharset("UTF-8"); // true ``` *** ### codingSystem() > **codingSystem**(`id`): [`CodingSystemInfo`](#codingsysteminfo) \| `undefined` Resolve a raw coding-system id (a CWE.3 / CE.3 "Name of Coding System" value) to its provenance. Returns `undefined` when there is no claim to resolve: `id` is `undefined`, empty, or whitespace-only. Matching is case-insensitive and tolerant of surrounding whitespace, and normalizes the well-known aliases in [KNOWN\_CODING\_SYSTEMS](#known_coding_systems) (e.g. `"LOINC"` → `LN`, `"SNOMED"` → `SCT`, `"RxNorm"` → `RXN`). An unrecognized id is returned verbatim with `known: false`: never guessed. #### Parameters ##### id `string` \| `undefined` #### Returns [`CodingSystemInfo`](#codingsysteminfo) \| `undefined` #### Example ```ts import { codingSystem } from "@cosyte/hl7"; codingSystem("LN"); // { claimed: "LN", known: true, id: "LN", name: "LOINC" } codingSystem("loinc"); // { claimed: "loinc", known: true, id: "LN", name: "LOINC" } codingSystem("99zL"); // { claimed: "99zL", known: false } codingSystem(undefined); // undefined ``` *** ### codingSystemOf() > **codingSystemOf**(`coded`): [`CodingSystemInfo`](#codingsysteminfo) \| `undefined` Provenance of a coded element's **primary** coding system (CWE.3 / CE.3). Returns `undefined` when the element claims no primary system. #### Parameters ##### coded [`CodedSystemFields`](#codedsystemfields) #### Returns [`CodingSystemInfo`](#codingsysteminfo) \| `undefined` #### Example ```ts import { parseHL7, codingSystemOf } from "@cosyte/hl7"; const msg = parseHL7(raw); for (const dg of msg.diagnoses()) { const sys = dg.code && codingSystemOf(dg.code); console.log(dg.code?.identifier, sys?.name ?? sys?.claimed ?? "(no system)"); } ``` *** ### decodeText() > **decodeText**(`input`, `enc?`): `string` Decode a field's escape-bearing HL7 text to its human value: the five delimiter escapes (`\F\ \S\ \T\ \R\ \E\`), the truncation escape (`\P`), `\.br\` (→ newline), and hex (`\Xdddd…`) are resolved; presentational escapes (`\H\`/`\N`, formatting, charset, vendor `\Z..`) are **preserved verbatim** so nothing is lost. Never throws. This is the one-call inverse of [encodeText](#encodetext) for delimiter-bearing content. It does **not** interpret formatting/highlight: for a normalized display string use [renderText](#rendertext). #### Parameters ##### input `string` the field's escape-bearing text. ##### enc? [`EncodingCharacters`](#encodingcharacters-1) = `DEFAULT_ENCODING_CHARACTERS` encoding characters; defaults to the HL7 standard `|^~\&`. #### Returns `string` the decoded value string. #### Example ```ts import { decodeText } from "@cosyte/hl7"; decodeText("Doe\\S\\John"); // "Doe^John": \S\ → component separator decodeText("line1\\.br\\line2"); // "line1\nline2" ``` *** ### defineConformanceProfile() > **defineConformanceProfile**(`profile`): [`ConformanceProfile`](#conformanceprofile) The **fail-fast authoring** gate for a conformance profile: a malformed profile raises a typed `ProfileDefinitionError` at build time, before any validation runs. Runs [collectProfileDefects](https://docs.cosyte.com/hl7/api/@cosyte/namespaces/conformance.md#collectprofiledefects); on any defect, throws a single [ProfileDefinitionError](#profiledefinitionerror) listing every defect. On success, returns the profile typed as [ConformanceProfile](#conformanceprofile). This is **optional**: [validateAgainstProfile](#validateagainstprofile) tolerates a raw profile object and never throws: but it lets an author catch a typo when the profile is written rather than when it is run. #### Parameters ##### profile `unknown` #### Returns [`ConformanceProfile`](#conformanceprofile) #### Throws when the profile is structurally malformed. #### Example ```ts import { defineConformanceProfile } from "@cosyte/hl7"; const profile = defineConformanceProfile({ name: "example-adt-min", segments: [{ segment: "PID", usage: "R" }], }); // A typo throws at authoring time: // defineConformanceProfile({ name: "x", segments: [{ segment: "pid" }] }); // → ProfileDefinitionError: segments[0].segment must be a valid segment name… ``` *** ### defineProfile() > **defineProfile**\<`O`\>(`opts`): [`DefinedProfile`](#definedprofile)\<`O`\> Build a readonly `Profile` from a validated options object. Invalid input throws `ProfileDefinitionError` with an actionable message (PROF-02): bad Z-segment names (D-05), malformed date formats (D-08), unknown top-level keys with typo hints (D-07), and missing/empty name. `opts.extends` accepts a single parent `Profile` or an array of them. Parents are merged into the result: `lineage` is the parents' lineages followed by this profile's own name, `dateFormats`, `customSegments` and `segmentOverrides` are merged, `description` inherits when not supplied, and `onWarning` handlers are composed. With no parent, `lineage === [opts.name]`. `segmentOverrides` names fields on STANDARD segments and is ADDITIVE ONLY: a declaration there gives `seg.get(name)` a new name to resolve and changes no existing read. Keys must be standard segment names, so a Z-segment key is refused and pointed back at `customSegments`. The returned profile CARRIES ITS DECLARED FIELD NAMES IN ITS TYPE: parse with it and `seg.get(name)` on a declared segment type is checked against the names declared for that type, a typo failing to compile instead of silently reading `undefined`. That is additive, and the value stays assignable to the general `Profile` interface with no cast; a caller who annotates the variable `Profile` gives the narrowing up rather than hitting an error, and one who passes an options object typed as `DefineProfileOptions` never had it in the first place. #### Type Parameters ##### O `O` *extends* [`DefineProfileOptions`](#defineprofileoptions) #### Parameters ##### opts `O` #### Returns [`DefinedProfile`](#definedprofile)\<`O`\> #### Example ```ts import { defineProfile, parseHL7 } from "@cosyte/hl7"; const epic = defineProfile({ name: "epic", description: "Epic-specific quirks and ADT date formats", dateFormats: ["MM/DD/YYYY HH:mm:ss", "MM/DD/YYYY"], customSegments: { ZDP: { fields: { departmentCode: 3, departmentName: 4 } }, ZRS: { fields: { resultStatus: 1, statusDateTime: 2 } }, }, }); console.log(epic.name); // "epic" console.log(epic.lineage); // ["epic"] console.log(epic.describe?.()); const msg = parseHL7(raw, epic); console.log(msg.part("ZDP")?.get("departmentCode")?.value); // narrowed // msg.part("ZDP")?.get("resultStatus"); // does not compile: that is a ZRS name ``` *** ### detectAckMode() > **detectAckMode**(`inbound`): [`AckMode`](#ackmode) Detect the HL7 acknowledgment mode of an inbound message from MSH-15 (accept acknowledgment type) and MSH-16 (application acknowledgment type), per HL7 v2 Chapter 2 §2.9: **original** when *both* are absent/empty; **enhanced** when *either* is present. This split is spec-exact (unlike the disposition mapping, which has no single correct model: see the package README). #### Parameters ##### inbound [`Hl7Message`](#hl7message) #### Returns [`AckMode`](#ackmode) #### Example ```ts import { detectAckMode, parseHL7 } from "@cosyte/hl7"; detectAckMode(parseHL7(raw)); // "original" | "enhanced" ``` *** ### downgradePositiveAck() > **downgradePositiveAck**(`code`): [`AckCode`](#ackcode) Downgrade a positive acknowledgment code to its matching error code, `AA` → `AE` (original mode), `CA` → `CE` (enhanced mode). Every other code passes through unchanged. This is the **single upstream source of truth** for the fail-safe downgrade pair: `buildAck` applies it when the inbound carries no MSH-10 correlation id, and `@cosyte/mllp`'s `ack-from-hl7` adapter applies it when the inbound cannot be parsed at all: neither ever fabricates an unverifiable positive acknowledgment, and neither carries its own copy of the mapping. #### Parameters ##### code [`AckCode`](#ackcode) #### Returns [`AckCode`](#ackcode) #### Example ```ts import { downgradePositiveAck } from "@cosyte/hl7"; downgradePositiveAck("AA"); // "AE" downgradePositiveAck("CA"); // "CE" downgradePositiveAck("AR"); // "AR" (unchanged) ``` *** ### dtmToDate() > **dtmToDate**(`parts`, `options?`): `Date` \| `undefined` Materialize an absolute-instant JS `Date` from [DtmParts](#dtmparts): **only on explicit caller request**. Truncated fields fill to their lowest legal value (month → January, day → 1, time → 0) *for instant construction only*; the value's stated `precision` still tells the truth. Timezone resolution is honest: - has an offset → the exact instant, using that offset; - no offset + `assumeOffsetMinutes` supplied → that offset is applied; - no offset + nothing supplied → `undefined` (never a silent UTC guess). Returns `undefined` for an invalid value or an unresolvable zone; never throws. #### Parameters ##### parts [`DtmParts`](#dtmparts) ##### options? [`DtmToDateOptions`](#dtmtodateoptions) = `{}` #### Returns `Date` \| `undefined` #### Example ```ts import { parseDtm, dtmToDate } from "@cosyte/hl7"; dtmToDate(parseDtm("20250102153045-0500"))?.toISOString(); // "2025-01-02T20:30:45.000Z": exact, offset-derived dtmToDate(parseDtm("20250102")); // undefined: refuses to guess the zone dtmToDate(parseDtm("20250102"), { assumeOffsetMinutes: 0 })?.toISOString(); // "2025-01-02T00:00:00.000Z": caller explicitly assumed UTC ``` *** ### duplicateRequiredSegment() > **duplicateRequiredSegment**(`position`, `segmentName`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `DUPLICATE_REQUIRED_SEGMENT` warning. Emitted when a segment the profile marks as singleton appears more than once (e.g. two `MSH` segments). The parser keeps both; the warning alerts the consumer to potential sender bugs. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### segmentName `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { duplicateRequiredSegment } from "@cosyte/hl7"; const w = duplicateRequiredSegment({ segmentIndex: 1 }, "MSH"); ``` *** ### emitMessageSchema() > **emitMessageSchema**(`target`): `string` Emit the named artifact for the serialized message projection, as text. `json-schema-2020-12` returns the JSON Schema document serialized with two-space indentation and a trailing newline; `zod` returns the TypeScript source text. Any other name throws [SchemaTargetError](#schematargeterror) and returns nothing at all. Pure and repeatable: no message is read, no network is touched, and two calls for the same target in one process return the identical string. #### Parameters ##### target `string` #### Returns `string` #### Example ```ts import { emitMessageSchema } from "@cosyte/hl7"; const schema = emitMessageSchema("json-schema-2020-12"); const zod = emitMessageSchema("zod"); schema === emitMessageSchema("json-schema-2020-12"); // true zod.includes("import { z } from \"zod\";"); // true ``` *** ### encodeCe() > **encodeCe**(`v`): [`RawField`](#rawfield) Encode a CE (coded element, 6 modelled + preserved `extraComponents`). #### Parameters ##### v [`CE`](#ce) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeCe } from "@cosyte/hl7"; encodeCe({ identifier: "GLU", text: "Glucose", nameOfCodingSystem: "L" }); ``` *** ### encodeComposite() > **encodeComposite**\<`K`\>(`kind`, `value`): [`RawField`](#rawfield) Encode any typed composite into a spec-clean `RawField` by its [CompositeKind](#compositekind). The single dispatcher `setComposite` and the typed builders (`buildAdt`/`buildOru`) route through. Encoding takes **no** encoding-characters argument on purpose: the field it produces carries the **decoded** component values, and the actual delimiter-escaping happens later in the serializer against the message's own encoding characters: so a composite is delimiter-independent to encode. #### Type Parameters ##### K `K` *extends* [`CompositeKind`](#compositekind) #### Parameters ##### kind `K` ##### value [`CompositeValueByKind`](#compositevaluebykind)\[`K`\] #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeComposite } from "@cosyte/hl7"; // A hostile family name cannot break framing on emit: const f = encodeComposite("XPN", { familyName: "Smith^Jr", givenName: "Ann" }); ``` *** ### encodeCompositeReps() > **encodeCompositeReps**\<`K`\>(`kind`, `values`): [`RawField`](#rawfield) Encode an array of typed composites into a single repeating `RawField`: one HL7 repetition (`~`-joined on emit) per array element. Used for repeating fields such as PID-3 (patient identifier list) and PID-11 (addresses). An empty array yields an absent field. #### Type Parameters ##### K `K` *extends* [`CompositeKind`](#compositekind) #### Parameters ##### kind `K` ##### values readonly [`CompositeValueByKind`](#compositevaluebykind)\[`K`\][] #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeCompositeReps } from "@cosyte/hl7"; const ids = encodeCompositeReps("CX", [ { idNumber: "MRN001", identifierTypeCode: "MR" }, { idNumber: "9990", identifierTypeCode: "SS" }, ]); ``` *** ### encodeCwe() > **encodeCwe**(`v`): [`RawField`](#rawfield) Encode a CWE (coded element, 9 modelled + preserved `extraComponents`). #### Parameters ##### v [`CWE`](#cwe-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeCwe } from "@cosyte/hl7"; encodeCwe({ identifier: "GLU", text: "Glucose", nameOfCodingSystem: "LN" }); ``` *** ### encodeCx() > **encodeCx**(`v`): [`RawField`](#rawfield) Encode a CX (identifier): component 4 is a nested HD (assigningAuthority). #### Parameters ##### v [`CX`](#cx-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeCx } from "@cosyte/hl7"; encodeCx({ idNumber: "MRN001", assigningAuthority: { namespaceId: "HOSP" }, identifierTypeCode: "MR" }); ``` *** ### encodeHd() > **encodeHd**(`v`): [`RawField`](#rawfield) Encode an HD (hierarchic designator, 3 components) to a spec-clean field. #### Parameters ##### v [`HD`](#hd-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeHd } from "@cosyte/hl7"; encodeHd({ namespaceId: "EPIC", universalId: "1.2.840", universalIdType: "ISO" }); ``` *** ### encodeNm() > **encodeNm**(`v`): [`RawField`](#rawfield) Encode an NM numeric. Accepts the typed [NM](#nm-1) (its `raw` string emitted verbatim, preserving the sender's precision/formatting), a `number`, or a raw string. A `number` is stringified with `String(n)`; the value is never reconciled or rounded. #### Parameters ##### v `string` \| `number` \| [`NM`](#nm-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeNm } from "@cosyte/hl7"; encodeNm("120.50"); // precision preserved verbatim ``` *** ### encodePl() > **encodePl**(`v`): [`RawField`](#rawfield) Encode a PL (person location): component 4 is a nested HD (facility). #### Parameters ##### v [`PL`](#pl-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodePl } from "@cosyte/hl7"; encodePl({ pointOfCare: "ICU", room: "101", bed: "A" }); ``` *** ### encodeText() > **encodeText**(`input`, `enc?`): `string` **Encode-safe** direction: escape an arbitrary string so it can be placed in an HL7 field as data without ever breaking framing. Every reserved character : the escape char (escaped **first**, so decoding is unambiguous), the field, component, subcomponent, and repetition separators, the declared truncation char, and the framing-critical `\n`/`\r`: is replaced by its escape sequence, so the value **cannot inject a delimiter or forge a component / subcomponent / repetition boundary**. The hard invariant, property-tested over arbitrary strings: `decodeText(encodeText(s, enc), enc) === s`, and a message field carrying `encodeText(s)` **cannot forge a component / subcomponent / repetition boundary or break framing**: the value never escapes its field. Two caveats are inherent to HL7 field encoding, not to this codec, and apply to *whole-field* re-parse (they do not weaken the no-injection guarantee): the two-character string `""` is HL7's explicit-null token, so a field whose entire value is `""` re-parses as null; and the default parser trims field whitespace, so a value with leading/trailing spaces re-parses trimmed. Encode such values into a component/subcomponent position, or parse with trimming off, to preserve them exactly. #### Parameters ##### input `string` the arbitrary string to encode. ##### enc? [`EncodingCharacters`](#encodingcharacters-1) = `DEFAULT_ENCODING_CHARACTERS` encoding characters; defaults to the HL7 standard `|^~\&`. #### Returns `string` the spec-clean, delimiter-safe field body. #### Example ```ts import { encodeText, parseHL7 } from "@cosyte/hl7"; // A value full of delimiters cannot break out of its field: const hostile = "a|b^c~d\\e&f"; const body = encodeText(hostile); // "a\\F\\b\\S\\c\\R\\d\\E\\e\\T\\f" const msg = parseHL7(`MSH|^~\\&|A|B|C|D|20260101||ADT^A01|1|P|2.5\rNTE|1||${body}`); msg.segments("NTE")[0]?.field(3).value === hostile; // true: round-trips exactly ``` *** ### encodeTs() > **encodeTs**(`v`): [`RawField`](#rawfield) Encode a TS/DTM timestamp. Accepts either the typed [TS](#ts-1) (its `raw` string is emitted verbatim: the serializer never re-derives a timestamp from parts) or a pre-formatted HL7 timestamp string. #### Parameters ##### v `string` \| [`DtmParts`](#dtmparts) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeTs } from "@cosyte/hl7"; encodeTs("20260721101500"); ``` *** ### encodeXad() > **encodeXad**(`v`): [`RawField`](#rawfield) Encode an XAD (12 components) to a spec-clean field. #### Parameters ##### v [`XAD`](#xad-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeXad } from "@cosyte/hl7"; encodeXad({ street: "123 Main St", city: "Boston", stateOrProvince: "MA" }); ``` *** ### encodeXcn() > **encodeXcn**(`v`): [`RawField`](#rawfield) Encode an XCN: component 9 is a nested HD (assigningAuthority). #### Parameters ##### v [`XCN`](#xcn-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeXcn } from "@cosyte/hl7"; encodeXcn({ idNumber: "1234567890", familyName: "Welby", identifierTypeCode: "NPI" }); ``` *** ### encodeXpn() > **encodeXpn**(`v`): [`RawField`](#rawfield) Encode an XPN (14 components) to a spec-clean field. #### Parameters ##### v [`XPN`](#xpn-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeXpn } from "@cosyte/hl7"; encodeXpn({ familyName: "Doe", givenName: "Jane", prefix: "Dr" }); ``` *** ### encodeXtn() > **encodeXtn**(`v`): [`RawField`](#rawfield) Encode an XTN (telecom, 12 components) to a spec-clean field. #### Parameters ##### v [`XTN`](#xtn-1) #### Returns [`RawField`](#rawfield) #### Example ```ts import { encodeXtn } from "@cosyte/hl7"; encodeXtn({ telephoneNumber: "555-1234", telecommunicationUseCode: "PRN" }); ``` *** ### encodingMismatch() > **encodingMismatch**(`position`, `detail`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `ENCODING_MISMATCH` warning. Emitted when the MSH-2 encoding characters declared by the sender do not match what the parser observed downstream (e.g. the sender declares `^~\&` but uses `!@#$` as actual separators in later segments). #### Parameters ##### position [`Hl7Position`](#hl7position) ##### detail `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { encodingMismatch } from "@cosyte/hl7"; const w = encodingMismatch({ segmentIndex: 0 }, "MSH-2 declares ^~\\& but segment used !@#$"); ``` *** ### extraFields() > **extraFields**(`position`, `segmentName`, `extraCount`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `EXTRA_FIELDS` warning. Emitted when a segment contains more fields than the profile definition (or HL7 spec) declares: the extras are preserved on `RawSegment.fields` but flagged for consumers. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### segmentName `string` ##### extraCount `number` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { extraFields } from "@cosyte/hl7"; const w = extraFields({ segmentIndex: 4 }, "PID", 3); ``` *** ### fieldWhitespaceTrimmed() > **fieldWhitespaceTrimmed**(`position`, `leadingCount`, `trailingCount`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `FIELD_WHITESPACE_TRIMMED` warning. Emitted when the parser trims leading or trailing whitespace from a field value (the `trimFields` option, on by default). The message carries only the leading/trailing character **counts**: NEVER the field value itself (before or after trimming): so no PHI is exposed; the trimmed value is still preserved verbatim in the parsed output. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### leadingCount `number` ##### trailingCount `number` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { fieldWhitespaceTrimmed } from "@cosyte/hl7"; const w = fieldWhitespaceTrimmed({ segmentIndex: 1, fieldIndex: 5 }, 2, 1); ``` *** ### findMessageStructureDefinition() > **findMessageStructureDefinition**(`messageCode`, `triggerEvent`): [`MessageStructureDefinition`](#messagestructuredefinition) \| `undefined` The registry entry behind a recognized (message code, trigger event) pair, or `undefined` when the pair is not modelled. Exposed so a consumer reading a structural warning can reach the published structure it came from. #### Parameters ##### messageCode `string` ##### triggerEvent `string` #### Returns [`MessageStructureDefinition`](#messagestructuredefinition) \| `undefined` #### Example ```ts import { findMessageStructureDefinition } from "@cosyte/hl7"; const def = findMessageStructureDefinition("ORU", "R01"); console.log(def?.structureId); // "ORU_R01" ``` *** ### formatDtm() > **formatDtm**(`parts`): `string` Reconstruct the HL7 DTM string from [DtmParts](#dtmparts). The inverse of [parseDtm](#parsedtm) for a strict HL7 parse: `formatDtm(parseDtm(s)) === s` for any well-formed `s`, including the byte-preserving `-0000` (which HL7, unlike RFC 3339, treats as UTC but whose sign we retain for exact round-trip). Returns the raw string unchanged when `parts.valid` is `false`. Emits exactly the populated precision, no zero-fill, so a year-only value re-serializes to four characters, never `YYYY0101`. #### Parameters ##### parts [`DtmParts`](#dtmparts) #### Returns `string` #### Example ```ts import { parseDtm, formatDtm } from "@cosyte/hl7"; formatDtm(parseDtm("198807050000")); // "198807050000" formatDtm(parseDtm("1970")); // "1970" ``` *** ### getDefaultProfile() > **getDefaultProfile**(): [`Profile`](#profile-2) \| `undefined` Return the current default profile, or `undefined` if none is registered. Consistent with `msg.profile` convention: `undefined` rather than `null`. #### Returns [`Profile`](#profile-2) \| `undefined` #### Example ```ts import { getDefaultProfile } from "@cosyte/hl7"; const p = getDefaultProfile(); if (p !== undefined) console.log("default profile:", p.name); ``` *** ### interpretAck() > **interpretAck**(`msg`): [`Acknowledgment`](#acknowledgment) Interpret an `Hl7Message` as an acknowledgment. Never throws; a message with no MSA segment yields an all-`false`, empty-`errors` view. The result is deeply frozen. #### Parameters ##### msg [`Hl7Message`](#hl7message) #### Returns [`Acknowledgment`](#acknowledgment) #### Example ```ts import { interpretAck, parseHL7 } from "@cosyte/hl7"; const view = interpretAck(parseHL7("MSH|^~\\&|...\rMSA|AA|MSG001")); view.accepted; // true view.controlId; // "MSG001" ``` *** ### isPositiveAck() > **isPositiveAck**(`code`): `boolean` True iff `code` is a positive accept (`AA`/`CA`). Unknown/absent → false. #### Parameters ##### code `string` \| `undefined` #### Returns `boolean` #### Example ```ts isPositiveAck("AA"); // true isPositiveAck("AE"); // false ``` *** ### isSchemaEmitTarget() > **isSchemaEmitTarget**(`name`): name is "json-schema-2020-12" \| "zod" Whether a name is one of the supported emission targets. #### Parameters ##### name `string` #### Returns name is "json-schema-2020-12" \| "zod" #### Example ```ts import { isSchemaEmitTarget } from "@cosyte/hl7"; isSchemaEmitTarget("zod"); // true isSchemaEmitTarget("json-schema"); // false ``` *** ### mergeMissingPriorOrSurvivor() > **mergeMissingPriorOrSurvivor**(`position`, `eventType`, `missing`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `MERGE_MISSING_PRIOR_OR_SURVIVOR` warning. Emitted by `identityEvents()` (a read-side helper: it attaches to the returned `IdentityEvent.warnings`, never to `Hl7Message.warnings`) when a merge/move trigger event (A18/A34/A35/A36/A39/A40/A41/A42/A43/A44) is missing one side of the spec-mandated MRG (prior) → PID (surviving) pair: or when that side carries no usable identity field: no MRG segment in the patient group, no PID for an orphaned MRG, or a PID/MRG whose identifier, account, and visit fields are all empty or version-gated (a v2.7+ MRG whose only content was the withdrawn MRG-4 must not read as "nothing to retire"). The helper surfaces whatever IS present and never guesses the merge direction: this warning is the signal that the pair is incomplete. The message carries only the structural facts (trigger event code + which role is missing): NEVER an identifier, name, or any other field value, so no PHI is exposed (HL7 v2 Ch. 3: the PID carries surviving and the MRG carries non-surviving identity information). #### Parameters ##### position [`Hl7Position`](#hl7position) ##### eventType `string` ##### missing `"prior"` \| `"survivor"` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { mergeMissingPriorOrSurvivor } from "@cosyte/hl7"; const w = mergeMissingPriorOrSurvivor({ segmentIndex: 1 }, "A40", "prior"); ``` *** ### messageJsonSchema() > **messageJsonSchema**(): [`JsonSchemaDocument`](#jsonschemadocument) Build the JSON Schema document for the serialized message projection. Pure and repeatable: it reads the measured shape and the warning-code registry, nothing else. It parses no message, reads no network and returns a fresh document on every call, so two calls in one process produce equal documents whatever happened between them. #### Returns [`JsonSchemaDocument`](#jsonschemadocument) #### Example ```ts import { messageJsonSchema } from "@cosyte/hl7"; const schema = messageJsonSchema(); schema.$schema; // "https://json-schema.org/draft/2020-12/schema" schema.required; // ["encodingCharacters", "segments", "warnings"] schema.$defs["SerializedProfile"]?.additionalProperties; // false ``` *** ### messageZodSource() > **messageZodSource**(): `string` Render the whole Zod module for the serialized message projection. Pure and repeatable: it reads the measured shape and the warning-code registry, nothing else. It parses no message, reads no network, and two calls in one process return the identical string whatever happened between them. The output declares exactly one import, whose module specifier is `zod`, one `const` per named object in an order where a definition always precedes the definition that references it, and one inferred type alias for the projection. #### Returns `string` #### Example ```ts import { messageZodSource } from "@cosyte/hl7"; const source = messageZodSource(); source.startsWith("//"); // a header comment source.includes('import { z } from "zod";'); // the single import source.includes("export const SerializedMessageSchema"); ``` *** ### missingExpectedGroup() > **missingExpectedGroup**(`position`, `messageType`, `groupName`, `anchorSegments`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `MISSING_EXPECTED_GROUP` warning. Emitted once per absent Required segment group when the message's (MSH-9.1, MSH-9.2) type is one the structure safety net recognizes and an expected group is entirely missing: e.g. an `ORU^R01` carrying no `OBR`/`OBX` result group, the signature of a truncated or misrouted feed. Tier-2 and additive: lenient parse never throws on it, `strict` mode may promote it. The message carries only the structural fact (message type, group name, anchor segment names), never a field value: the message type is shape-checked before it is interpolated, and withheld if it does not match. `position` references MSH-9. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### messageType `string` ##### groupName `string` ##### anchorSegments readonly `string`[] #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { missingExpectedGroup } from "@cosyte/hl7"; const w = missingExpectedGroup( { segmentIndex: 0, fieldIndex: 9 }, "ORU^R01", "result", ["OBR", "OBX"], ); ``` *** ### missingRequiredField() > **missingRequiredField**(`position`, `segmentName`, `fieldIndex`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `MISSING_REQUIRED_FIELD` warning. Emitted when a field the active profile marks as required is empty or missing. Distinct from the `NO_MSH_SEGMENT` fatal, which escalates a missing MSH altogether. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### segmentName `string` ##### fieldIndex `number` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { missingRequiredField } from "@cosyte/hl7"; const w = missingRequiredField({ segmentIndex: 0, fieldIndex: 3 }, "MSH", 3); ``` *** ### missingRequiredSegment() > **missingRequiredSegment**(`position`, `messageType`, `segmentName`, `structureId`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `MISSING_EXPECTED_GROUP` warning naming one absent required SEGMENT. This is what the parser emits: once per segment the published structure definition gives a minimum of one and the message does not carry, e.g. an `ADT^A01` with no `EVN`. It shares the code with [missingExpectedGroup](#missingexpectedgroup) on purpose. The code is public contract that consumers narrow on, and the change here is which structural fact the message names, not which class of finding it is. Tier-2 and additive: lenient parse never throws on it, `strict` mode may promote it. The message carries only structural identifiers (the message type, the segment name, the published structure id), never a field value: the message type is shape-checked before it is interpolated and withheld if it does not match, and the other two are registry data rather than anything read off the message. `position` references MSH-9. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### messageType `string` ##### segmentName `string` ##### structureId `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { missingRequiredSegment } from "@cosyte/hl7"; const w = missingRequiredSegment( { segmentIndex: 0, fieldIndex: 9 }, "ADT^A01", "EVN", "ADT_A01", ); ``` *** ### mllpFramingStripped() > **mllpFramingStripped**(`position`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `MLLP_FRAMING_STRIPPED` warning. Emitted once per parse when the preprocessor detects and removes MLLP framing bytes (`0x0B` / `0x1C` / trailing `0x0D`) from the input. #### Parameters ##### position [`Hl7Position`](#hl7position) #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { mllpFramingStripped } from "@cosyte/hl7"; const w = mllpFramingStripped({ segmentIndex: 0 }); ``` *** ### outOfOrderSegment() > **outOfOrderSegment**(`position`, `segmentName`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `OUT_OF_ORDER_SEGMENT` warning. Emitted when a segment appears outside the order the active profile declares (e.g. `EVN` appearing before `MSH` in a typical ADT message). #### Parameters ##### position [`Hl7Position`](#hl7position) ##### segmentName `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { outOfOrderSegment } from "@cosyte/hl7"; const w = outOfOrderSegment({ segmentIndex: 2 }, "EVN"); ``` *** ### parseCe() > **parseCe**(`rep`, `enc`): [`CE`](#ce) Parse an HL7 v2 CE repetition into a structured `CE` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`CE`](#ce) #### Example ```ts import { parseCe, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["GLU"] }, { subcomponents: ["Glucose"] }, { subcomponents: ["LN"] }, ] }; const ce = parseCe(rep, DEFAULT_ENCODING_CHARACTERS); console.log(ce.identifier); // "GLU" ``` *** ### parseCwe() > **parseCwe**(`rep`, `enc`): [`CWE`](#cwe-1) Parse an HL7 v2 CWE repetition into a structured `CWE` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`CWE`](#cwe-1) #### Example ```ts import { parseCwe, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["GLU"] }, { subcomponents: ["Glucose"] }, { subcomponents: ["LN"] }, ] }; const cwe = parseCwe(rep, DEFAULT_ENCODING_CHARACTERS); console.log(cwe.identifier); // "GLU" ``` *** ### parseCx() > **parseCx**(`rep`, `enc`): [`CX`](#cx-1) Parse an HL7 v2 CX repetition into a structured `CX` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). Component 4 (`assigningAuthority`) is parsed as a nested `HD`; see component table in the `CX` interface JSDoc for the v1 simplifications on components 6/9/10. #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`CX`](#cx-1) #### Example ```ts import { parseCx, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["123"] }, { subcomponents: [""] }, { subcomponents: [""] }, { subcomponents: ["EPIC", "1.2.840.114350", "ISO"] }, { subcomponents: ["MR"] }, ] }; const cx = parseCx(rep, DEFAULT_ENCODING_CHARACTERS); console.log(cx.idNumber); // "123" console.log(cx.assigningAuthority?.namespaceId); // "EPIC" ``` *** ### parseDtm() > **parseDtm**(`raw`): [`DtmParts`](#dtmparts) Parse an HL7 v2 TS/DTM string into typed [DtmParts](#dtmparts), **preserving the stated precision and timezone fidelity**. Never zero-fills a truncation, never coerces to a `Date`, and never assumes UTC for a missing offset. Returns `{ raw, valid: false, hasTimezone: false }` (no parts) for empty, malformed, or calendar-out-of-range input: never throws. A fractional component is only accepted at full second precision. #### Parameters ##### raw `string` #### Returns [`DtmParts`](#dtmparts) #### Example ```ts import { parseDtm } from "@cosyte/hl7"; parseDtm("1970"); // { raw: "1970", valid: true, precision: "year", year: 1970, hasTimezone: false } parseDtm("20250102153045.5-0500"); // precision "fraction", fractionalSeconds "5", hasTimezone true, offsetMinutes -300 parseDtm("not-a-date"); // { raw: "not-a-date", valid: false, hasTimezone: false } ``` *** ### parseHd() > **parseHd**(`rep`, `enc`): [`HD`](#hd-1) Parse an HL7 v2 HD repetition into a structured `HD` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`HD`](#hd-1) #### Example ```ts import { parseHd, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["EPIC"] }, { subcomponents: ["1.2.840.114350"] }, { subcomponents: ["ISO"] }, ] }; const hd = parseHd(rep, DEFAULT_ENCODING_CHARACTERS); console.log(hd.namespaceId); // "EPIC" console.log(hd.universalIdType); // "ISO" ``` *** ### parseHL7() **`Internal`** implementation signature; overload signatures above carry the public JSDoc + @example. #### Call Signature > **parseHL7**(`raw`): [`Hl7Message`](#hl7message) Parse a raw HL7 v2 message (string or `Buffer`) into an `Hl7Message`. The parser is lenient by default: recoverable deviations from the HL7 spec are reported via `msg.warnings` and (optionally) `options.onWarning` but do not throw. Four unrecoverable structural errors throw `Hl7ParseError`: `NO_MSH_SEGMENT`, `MSH_TOO_SHORT`, `INVALID_ENCODING_CHARACTERS`, `EMPTY_INPUT`. Opt into strict mode with `{ strict: true }` to escalate every Tier-2 warning into an `Hl7ParseError`. ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ##### Returns [`Hl7Message`](#hl7message) ##### Examples ```ts import { parseHL7, WARNING_CODES } from "@cosyte/hl7"; const msg = parseHL7( "MSH|^~\\&|APP|FAC|APP|FAC|20250101||ADT^A01|1|P|2.5\rPID|||123", ); console.log(msg.version); // "2.5" console.log(msg.warnings.length); // 0 for (const w of msg.warnings) { if (w.code === WARNING_CODES.MLLP_FRAMING_STRIPPED) { // handle MLLP-framed sender } } ``` ```ts import { defineProfile, parseHL7 } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // A statically known profile narrows the field-name reader per segment type: const msg = parseHL7(raw, vendor); console.log(msg.part("ZDP")?.get("departmentCode")?.value); // msg.part("ZDP")?.get("departmentCod"); // does not compile: not declared ``` #### Call Signature > **parseHL7**\<`P`\>(`raw`, `profile`): [`ProfiledMessage`](#profiledmessage)\<`P`\> Parse a raw HL7 v2 message (string or `Buffer`) into an `Hl7Message`. The parser is lenient by default: recoverable deviations from the HL7 spec are reported via `msg.warnings` and (optionally) `options.onWarning` but do not throw. Four unrecoverable structural errors throw `Hl7ParseError`: `NO_MSH_SEGMENT`, `MSH_TOO_SHORT`, `INVALID_ENCODING_CHARACTERS`, `EMPTY_INPUT`. Opt into strict mode with `{ strict: true }` to escalate every Tier-2 warning into an `Hl7ParseError`. ##### Type Parameters ###### P `P` *extends* [`Profile`](#profile-2) ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ###### profile `P` ##### Returns [`ProfiledMessage`](#profiledmessage)\<`P`\> ##### Examples ```ts import { parseHL7, WARNING_CODES } from "@cosyte/hl7"; const msg = parseHL7( "MSH|^~\\&|APP|FAC|APP|FAC|20250101||ADT^A01|1|P|2.5\rPID|||123", ); console.log(msg.version); // "2.5" console.log(msg.warnings.length); // 0 for (const w of msg.warnings) { if (w.code === WARNING_CODES.MLLP_FRAMING_STRIPPED) { // handle MLLP-framed sender } } ``` ```ts import { defineProfile, parseHL7 } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // A statically known profile narrows the field-name reader per segment type: const msg = parseHL7(raw, vendor); console.log(msg.part("ZDP")?.get("departmentCode")?.value); // msg.part("ZDP")?.get("departmentCod"); // does not compile: not declared ``` #### Call Signature > **parseHL7**\<`P`\>(`raw`, `options`): [`ProfiledMessage`](#profiledmessage)\<`P`\> Parse a raw HL7 v2 message (string or `Buffer`) into an `Hl7Message`. The parser is lenient by default: recoverable deviations from the HL7 spec are reported via `msg.warnings` and (optionally) `options.onWarning` but do not throw. Four unrecoverable structural errors throw `Hl7ParseError`: `NO_MSH_SEGMENT`, `MSH_TOO_SHORT`, `INVALID_ENCODING_CHARACTERS`, `EMPTY_INPUT`. Opt into strict mode with `{ strict: true }` to escalate every Tier-2 warning into an `Hl7ParseError`. ##### Type Parameters ###### P `P` *extends* [`Profile`](#profile-2) ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ###### options [`ParseOptions`](#parseoptions) & `object` ##### Returns [`ProfiledMessage`](#profiledmessage)\<`P`\> ##### Examples ```ts import { parseHL7, WARNING_CODES } from "@cosyte/hl7"; const msg = parseHL7( "MSH|^~\\&|APP|FAC|APP|FAC|20250101||ADT^A01|1|P|2.5\rPID|||123", ); console.log(msg.version); // "2.5" console.log(msg.warnings.length); // 0 for (const w of msg.warnings) { if (w.code === WARNING_CODES.MLLP_FRAMING_STRIPPED) { // handle MLLP-framed sender } } ``` ```ts import { defineProfile, parseHL7 } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // A statically known profile narrows the field-name reader per segment type: const msg = parseHL7(raw, vendor); console.log(msg.part("ZDP")?.get("departmentCode")?.value); // msg.part("ZDP")?.get("departmentCod"); // does not compile: not declared ``` #### Call Signature > **parseHL7**(`raw`, `options`): [`Hl7Message`](#hl7message) Parse a raw HL7 v2 message (string or `Buffer`) into an `Hl7Message`. The parser is lenient by default: recoverable deviations from the HL7 spec are reported via `msg.warnings` and (optionally) `options.onWarning` but do not throw. Four unrecoverable structural errors throw `Hl7ParseError`: `NO_MSH_SEGMENT`, `MSH_TOO_SHORT`, `INVALID_ENCODING_CHARACTERS`, `EMPTY_INPUT`. Opt into strict mode with `{ strict: true }` to escalate every Tier-2 warning into an `Hl7ParseError`. ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ###### options [`ParseOptions`](#parseoptions) ##### Returns [`Hl7Message`](#hl7message) ##### Examples ```ts import { parseHL7, WARNING_CODES } from "@cosyte/hl7"; const msg = parseHL7( "MSH|^~\\&|APP|FAC|APP|FAC|20250101||ADT^A01|1|P|2.5\rPID|||123", ); console.log(msg.version); // "2.5" console.log(msg.warnings.length); // 0 for (const w of msg.warnings) { if (w.code === WARNING_CODES.MLLP_FRAMING_STRIPPED) { // handle MLLP-framed sender } } ``` ```ts import { defineProfile, parseHL7 } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // A statically known profile narrows the field-name reader per segment type: const msg = parseHL7(raw, vendor); console.log(msg.part("ZDP")?.get("departmentCode")?.value); // msg.part("ZDP")?.get("departmentCod"); // does not compile: not declared ``` *** ### parseNm() > **parseNm**(`rep`, `_enc`): [`NM`](#nm-1) Parse an HL7 v2 NM repetition into `{ raw, value }`. Uses `Number(raw)` for strict numeric parsing: trailing non-numeric characters produce `NaN`, normalized to `undefined`. Empty raw also produces `undefined`. `Number("")` is `0` in JS: which is the wrong answer for an empty HL7 numeric field. The explicit empty-string check below returns `{ raw: "", value: undefined }` so empty inputs match missing inputs. #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### \_enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`NM`](#nm-1) #### Example ```ts import { parseNm, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [{ subcomponents: ["120.5"] }] }; const nm = parseNm(rep, DEFAULT_ENCODING_CHARACTERS); console.log(nm.value); // 120.5 ``` *** ### parsePath() > **parsePath**(`path`): [`DotPath`](#dotpath) Parse an HL7 dot-path string into a `DotPath` descriptor. Accepts shapes like `SEG`, `SEG[n]`, `SEG.N`, `SEG.N[r]`, `SEG.N.C`, `SEG.N.C.S`, and all combinations up to `SEG[n].N[r].C.S`. Throws `TypeError` with the offending path string on any malformed input. #### Parameters ##### path `string` #### Returns [`DotPath`](#dotpath) #### Example ```ts import { parsePath } from "@cosyte/hl7"; parsePath("PID.5.1"); // { segmentType: "PID", segmentIndex: 0, fieldIndex: 5, componentIndex: 1 } parsePath("OBX[2].5"); // { segmentType: "OBX", segmentIndex: 2, fieldIndex: 5 } parsePath("PID.3[1].1"); // { segmentType: "PID", segmentIndex: 0, fieldIndex: 3, repetitionIndex: 1, componentIndex: 1 } ``` *** ### parsePl() > **parsePl**(`rep`, `enc`): [`PL`](#pl-1) Parse an HL7 v2 PL repetition into a structured `PL` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). Component 4 (`facility`) is parsed as a nested `HD`; see component table in the `PL` interface JSDoc for the v1 simplifications. #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`PL`](#pl-1) #### Example ```ts import { parsePl, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["ICU"] }, { subcomponents: ["101"] }, { subcomponents: ["A"] }, { subcomponents: ["HOSP", "1.2.3", "UUID"] }, ] }; const pl = parsePl(rep, DEFAULT_ENCODING_CHARACTERS); console.log(pl.pointOfCare); // "ICU" console.log(pl.facility?.namespaceId); // "HOSP" ``` *** ### parseSn() > **parseSn**(`rep`, `enc`): [`SN`](#sn) \| `undefined` Parse an HL7 v2 SN repetition into a structured `SN`, or `undefined` when the field carries no usable structured-numeric content (empty, or so malformed that no comparator, number, or separator can be recovered). Components are returned verbatim (already decoded by the tokenizer); `num1`/`num2` use strict `Number()` parsing. Fail-safe: a non-operator value in the comparator slot (SN.1) is dropped rather than surfaced as a relation, and a non-numeric SN.2/SN.4 becomes `undefined`: the parser never emits a confident wrong comparator or number. #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`SN`](#sn) \| `undefined` #### Example ```ts import { parseSn, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: [">"] }, { subcomponents: ["90"] }, ] }; const sn = parseSn(rep, DEFAULT_ENCODING_CHARACTERS); console.log(sn?.comparator, sn?.num1); // ">" 90 ``` *** ### parseStream() **`Internal`** implementation signature; overloads above carry the public JSDoc. #### Call Signature > **parseStream**(`source`): `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`Hl7Message`](#hl7message)\>, `void`, `void`\> Incrementally parse a chunked HL7 v2 byte / text stream, yielding one [StreamMessageEntry](#streammessageentry) per `MSH`-delimited message as its boundary completes, with **O(one-message)** memory: the whole stream is never retained. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` batch frame (HL7 v2 Ch. 2 §2.10.3): - a message split across chunk boundaries (mid-segment, mid-field, even mid-`MSH|^~\&`) is reassembled correctly: feeding the same bytes in 1-byte chunks vs. one chunk yields **identical** messages; - `\r`, `\r\n`, and `\n` segment terminators are all tolerated (a `\r\n` split across a chunk boundary is not mistaken for a bare `\r`); - each message is parsed by the shipped [parseHL7](#parsehl7) (no second grammar), `ok` entries carry the `Hl7Message`, a Tier-3 fatal is an isolated failure entry; a **malformed message never suppresses later messages**; - batch-envelope segments (`FHS`/`BHS`/`BTS`/`FTS`) are treated as boundaries and never yielded as messages, so `yielded count == MSH count` (envelope count reconciliation is [splitBatch](#splitbatch)'s job); - a final message with no trailing terminator is still yielded, flagged with a stream-level [unterminatedStreamMessage](#unterminatedstreammessage) warning: **never a throw, the tail is never dropped**. The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …), exactly as [splitBatch](#splitbatch) forwards it. ##### Parameters ###### source [`Hl7StreamSource`](#hl7streamsource) ##### Returns `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`Hl7Message`](#hl7message)\>, `void`, `void`\> ##### Examples ```ts import { parseStream } from "@cosyte/hl7"; async function* chunks() { yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r"; yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r"; } let count = 0; for await (const entry of parseStream(chunks())) { if (entry.ok) count += 1; // 2: one per MSH, streamed, released each time } ``` ```ts import { defineProfile, parseStream } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for await (const entry of parseStream(source, vendor)) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` #### Call Signature > **parseStream**\<`P`\>(`source`, `profile`): `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\>, `void`, `void`\> Incrementally parse a chunked HL7 v2 byte / text stream, yielding one [StreamMessageEntry](#streammessageentry) per `MSH`-delimited message as its boundary completes, with **O(one-message)** memory: the whole stream is never retained. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` batch frame (HL7 v2 Ch. 2 §2.10.3): - a message split across chunk boundaries (mid-segment, mid-field, even mid-`MSH|^~\&`) is reassembled correctly: feeding the same bytes in 1-byte chunks vs. one chunk yields **identical** messages; - `\r`, `\r\n`, and `\n` segment terminators are all tolerated (a `\r\n` split across a chunk boundary is not mistaken for a bare `\r`); - each message is parsed by the shipped [parseHL7](#parsehl7) (no second grammar), `ok` entries carry the `Hl7Message`, a Tier-3 fatal is an isolated failure entry; a **malformed message never suppresses later messages**; - batch-envelope segments (`FHS`/`BHS`/`BTS`/`FTS`) are treated as boundaries and never yielded as messages, so `yielded count == MSH count` (envelope count reconciliation is [splitBatch](#splitbatch)'s job); - a final message with no trailing terminator is still yielded, flagged with a stream-level [unterminatedStreamMessage](#unterminatedstreammessage) warning: **never a throw, the tail is never dropped**. The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …), exactly as [splitBatch](#splitbatch) forwards it. ##### Type Parameters ###### P `P` *extends* [`Profile`](#profile-2) ##### Parameters ###### source [`Hl7StreamSource`](#hl7streamsource) ###### profile `P` ##### Returns `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\>, `void`, `void`\> ##### Examples ```ts import { parseStream } from "@cosyte/hl7"; async function* chunks() { yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r"; yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r"; } let count = 0; for await (const entry of parseStream(chunks())) { if (entry.ok) count += 1; // 2: one per MSH, streamed, released each time } ``` ```ts import { defineProfile, parseStream } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for await (const entry of parseStream(source, vendor)) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` #### Call Signature > **parseStream**\<`P`\>(`source`, `options`): `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\>, `void`, `void`\> Incrementally parse a chunked HL7 v2 byte / text stream, yielding one [StreamMessageEntry](#streammessageentry) per `MSH`-delimited message as its boundary completes, with **O(one-message)** memory: the whole stream is never retained. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` batch frame (HL7 v2 Ch. 2 §2.10.3): - a message split across chunk boundaries (mid-segment, mid-field, even mid-`MSH|^~\&`) is reassembled correctly: feeding the same bytes in 1-byte chunks vs. one chunk yields **identical** messages; - `\r`, `\r\n`, and `\n` segment terminators are all tolerated (a `\r\n` split across a chunk boundary is not mistaken for a bare `\r`); - each message is parsed by the shipped [parseHL7](#parsehl7) (no second grammar), `ok` entries carry the `Hl7Message`, a Tier-3 fatal is an isolated failure entry; a **malformed message never suppresses later messages**; - batch-envelope segments (`FHS`/`BHS`/`BTS`/`FTS`) are treated as boundaries and never yielded as messages, so `yielded count == MSH count` (envelope count reconciliation is [splitBatch](#splitbatch)'s job); - a final message with no trailing terminator is still yielded, flagged with a stream-level [unterminatedStreamMessage](#unterminatedstreammessage) warning: **never a throw, the tail is never dropped**. The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …), exactly as [splitBatch](#splitbatch) forwards it. ##### Type Parameters ###### P `P` *extends* [`Profile`](#profile-2) ##### Parameters ###### source [`Hl7StreamSource`](#hl7streamsource) ###### options [`ParseOptions`](#parseoptions) & `object` ##### Returns `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\>, `void`, `void`\> ##### Examples ```ts import { parseStream } from "@cosyte/hl7"; async function* chunks() { yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r"; yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r"; } let count = 0; for await (const entry of parseStream(chunks())) { if (entry.ok) count += 1; // 2: one per MSH, streamed, released each time } ``` ```ts import { defineProfile, parseStream } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for await (const entry of parseStream(source, vendor)) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` #### Call Signature > **parseStream**(`source`, `options`): `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`Hl7Message`](#hl7message)\>, `void`, `void`\> Incrementally parse a chunked HL7 v2 byte / text stream, yielding one [StreamMessageEntry](#streammessageentry) per `MSH`-delimited message as its boundary completes, with **O(one-message)** memory: the whole stream is never retained. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` batch frame (HL7 v2 Ch. 2 §2.10.3): - a message split across chunk boundaries (mid-segment, mid-field, even mid-`MSH|^~\&`) is reassembled correctly: feeding the same bytes in 1-byte chunks vs. one chunk yields **identical** messages; - `\r`, `\r\n`, and `\n` segment terminators are all tolerated (a `\r\n` split across a chunk boundary is not mistaken for a bare `\r`); - each message is parsed by the shipped [parseHL7](#parsehl7) (no second grammar), `ok` entries carry the `Hl7Message`, a Tier-3 fatal is an isolated failure entry; a **malformed message never suppresses later messages**; - batch-envelope segments (`FHS`/`BHS`/`BTS`/`FTS`) are treated as boundaries and never yielded as messages, so `yielded count == MSH count` (envelope count reconciliation is [splitBatch](#splitbatch)'s job); - a final message with no trailing terminator is still yielded, flagged with a stream-level [unterminatedStreamMessage](#unterminatedstreammessage) warning: **never a throw, the tail is never dropped**. The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …), exactly as [splitBatch](#splitbatch) forwards it. ##### Parameters ###### source [`Hl7StreamSource`](#hl7streamsource) ###### options [`ParseOptions`](#parseoptions) ##### Returns `AsyncGenerator`\<[`StreamMessageEntry`](#streammessageentry)\<[`Hl7Message`](#hl7message)\>, `void`, `void`\> ##### Examples ```ts import { parseStream } from "@cosyte/hl7"; async function* chunks() { yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r"; yield "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r"; } let count = 0; for await (const entry of parseStream(chunks())) { if (entry.ok) count += 1; // 2: one per MSH, streamed, released each time } ``` ```ts import { defineProfile, parseStream } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for await (const entry of parseStream(source, vendor)) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` *** ### parseTs() > **parseTs**(`rep`, `_enc`, `dateFormats?`): [`DtmParts`](#dtmparts) Parse an HL7 v2 TS/DTM repetition into fidelity [TS](#ts-1) parts. The canonical HL7 shape is tried first; `dateFormats`, when the caller declared any, is then tried in order and a match reports itself on `matchedFormat`. Nothing else is tried: the library's built-in fallback list is reserved for the non-composite `msg.meta.timestamp` path, because guessing `MM/DD/YYYY` on a date of birth a vendor wrote day-first produces a plausible wrong date rather than a visible failure. `dateFormats` reaches this parser from `Hl7Message.dateFormats`, which is `ParseOptions.dateFormats` followed by the applied profile's, deduplicated first-occurrence-wins. Omit it (or pass an empty list) and the parse is exactly the strict HL7 one. The result is frozen so the immutability guarantee holds for callers that destructure or retain it. #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### \_enc [`EncodingCharacters`](#encodingcharacters-1) ##### dateFormats? readonly `string`[] #### Returns [`DtmParts`](#dtmparts) #### Example ```ts import { parseTs, DEFAULT_ENCODING_CHARACTERS, dtmToDate } from "@cosyte/hl7"; const rep = { components: [{ subcomponents: ["20250102153045-0500"] }] }; const ts = parseTs(rep, DEFAULT_ENCODING_CHARACTERS); console.log(ts.raw); // "20250102153045-0500" console.log(ts.precision, ts.hasTimezone); // "second" true console.log(dtmToDate(ts)?.toISOString()); // "2025-01-02T20:30:45.000Z" const vendor = { components: [{ subcomponents: ["07/05/1988"] }] }; const dob = parseTs(vendor, DEFAULT_ENCODING_CHARACTERS, ["MM/DD/YYYY"]); console.log(dob.month, dob.matchedFormat); // 7 "MM/DD/YYYY" ``` *** ### parseXad() > **parseXad**(`rep`, `enc`): [`XAD`](#xad-1) Parse an HL7 v2 XAD repetition into a structured `XAD` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`XAD`](#xad-1) #### Example ```ts import { parseXad, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["123 Main St"] }, { subcomponents: ["Apt 4"] }, { subcomponents: ["Boston"] }, { subcomponents: ["MA"] }, { subcomponents: ["02101"] }, ] }; const addr = parseXad(rep, DEFAULT_ENCODING_CHARACTERS); console.log(addr.city); // "Boston" ``` *** ### parseXcn() > **parseXcn**(`rep`, `enc`): [`XCN`](#xcn-1) Parse an HL7 v2 XCN repetition into a structured `XCN` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). Component 9 (`assigningAuthority`) is parsed as a nested `HD`; see component table in the `XCN` interface JSDoc for the v1 trimming. #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`XCN`](#xcn-1) #### Example ```ts import { parseXcn, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["1234567890"] }, { subcomponents: ["Smith"] }, { subcomponents: ["Jane"] }, { subcomponents: [""] }, { subcomponents: [""] }, { subcomponents: [""] }, { subcomponents: [""] }, { subcomponents: [""] }, { subcomponents: ["HOSP", "1.2.3", "ISO"] }, { subcomponents: ["L"] }, { subcomponents: [""] }, { subcomponents: [""] }, { subcomponents: ["NPI"] }, ] }; const xcn = parseXcn(rep, DEFAULT_ENCODING_CHARACTERS); console.log(xcn.idNumber); // "1234567890" console.log(xcn.familyName); // "Smith" console.log(xcn.assigningAuthority?.namespaceId); // "HOSP" console.log(xcn.identifierTypeCode); // "NPI" ``` *** ### parseXpn() > **parseXpn**(`rep`, `enc`): [`XPN`](#xpn-1) Parse an HL7 v2 XPN repetition into a structured `XPN` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`XPN`](#xpn-1) #### Example ```ts import { parseXpn, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["Smith"] }, { subcomponents: ["Jane"] }, ] }; const xpn = parseXpn(rep, DEFAULT_ENCODING_CHARACTERS); console.log(xpn.familyName); // "Smith" console.log(xpn.givenName); // "Jane" ``` *** ### parseXtn() > **parseXtn**(`rep`, `enc`): [`XTN`](#xtn-1) Parse an HL7 v2 XTN repetition into a structured `XTN` object. Components are returned verbatim (already decoded once by the tokenizer: never re-unescaped). Absent / empty components are OMITTED from the result (exactOptionalPropertyTypes semantics). Components past position 12 are silently ignored in v1. #### Parameters ##### rep [`RawRepetition`](#rawrepetition) ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns [`XTN`](#xtn-1) #### Example ```ts import { parseXtn, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const rep = { components: [ { subcomponents: ["555-1234"] }, { subcomponents: ["PRN"] }, { subcomponents: ["PH"] }, ] }; const xtn = parseXtn(rep, DEFAULT_ENCODING_CHARACTERS); console.log(xtn.telephoneNumber); // "555-1234" ``` *** ### pickMrn() > **pickMrn**(`identifiers`): `string` \| `undefined` Pick the Medical Record Number string from a list of PID-3 CX identifiers. D-07: prefer the first CX whose `identifierTypeCode === "MR"` (HL7 v2.5+ canonical MRN marker). D-10: the match is case-SENSITIVE: lowercase `"mr"` does NOT match; spec mandates uppercase. D-08: when no MR-typed identifier is found, fall back to the first CX's `idNumber`. Returns `undefined` when the first CX has no `idNumber` (even if later CXs do: the fallback is strictly "first CX", not "first CX with idNumber", because we need a deterministic answer). D-21: no warning emitted. Callers who need strict MR resolution can walk `patient.identifiers` themselves. #### Parameters ##### identifiers readonly [`CX`](#cx-1)[] #### Returns `string` \| `undefined` #### Example ```ts import { pickMrn } from "@cosyte/hl7"; pickMrn([ { idNumber: "X1" }, { idNumber: "MRN001", identifierTypeCode: "MR" }, ]); // → "MRN001" pickMrn([{ idNumber: "X1" }]); // → "X1" (fallback: no MR entry) pickMrn([]); // → undefined ``` *** ### reescape() > **reescape**(`input`, `enc`): `string` Re-escape reserved characters back into their `\X\` forms so the serializer can emit spec-clean HL7. This is the inverse of `unescape` for every delimiter-bearing character; round-trip cleanliness (`unescape(reescape(x, enc), enc, emit, pos) === x`) is a documented property covered by tests. The characters re-escaped: enc.escape → \E\ enc.field → \F\ enc.component → \S\ enc.subcomponent → \T\ enc.repetition → \R\ enc.truncation → \P\ (only when MSH-2 declared one, v2.7+) "\n" (LF) → \.br\ "\r" (CR) → \X0D\ (a decoded CR is the HL7 segment separator; emitting it raw would corrupt wire framing, so it re-encodes to its hex escape: see below) **Lossy by construction for the non-delimiter escape families.** `reescape` only knows about the reserved characters above: it cannot reconstruct a recognize-and-preserve escape (`\H\`, formatting, charset, `\Z..`) or the original bytes of a hex escape (`\X41\` decoded to `A`; casing of `\X0d`), because those decode to ordinary characters that carry no "I was an escape" marker. Byte-verbatim emit for those families is the serializer's job via the `RawComponent.rawSubcomponents` overlay (see escapeFidelityRaw); `reescape` is the fallback for content that has no overlay (constructed values, `Field`-level re-escapes). Iteration uses `for..of`, which walks Unicode code points (not UTF-16 code units), so user-supplied content containing non-BMP characters round-trips correctly without special surrogate-pair handling. #### Parameters ##### input `string` ##### enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns `string` #### Example ```ts import { reescape, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; reescape("Smith|John", DEFAULT_ENCODING_CHARACTERS); // "Smith\\F\\John" reescape("line1\nline2", DEFAULT_ENCODING_CHARACTERS); // "line1\\.br\\line2" ``` *** ### renderText() > **renderText**(`input`, `enc?`, `opts?`): [`RenderedText`](#renderedtext) Render an HL7 v2 escape/formatting-bearing string into a normalized display model (plain text + highlight-aware runs). A **read projection**: it never mutates the underlying field, and it never fabricates: an escape it cannot render is preserved as literal characters and flagged in [RenderedText.unrenderedSequences](#unrenderedsequences). `input` is the field's escape-bearing text. Pass the **wire form** (e.g. `Field.text`, byte-verbatim for parsed content) for the most faithful result: a wire `\E\H\E\` (an escaped literal backslash-H-backslash) then renders as the three literal characters `\H`, never as a highlight. Passing an already-decoded `Field.value` also works: its `\.br\` is already a newline (rendered as one line break) and its `\H`/formatting sentinels are still recognized. Never throws for any input. #### Parameters ##### input `string` the escape-bearing field text to render. ##### enc? [`EncodingCharacters`](#encodingcharacters-1) = `DEFAULT_ENCODING_CHARACTERS` the message's encoding characters (for `\F\`/`\S`/… targets); defaults to the HL7 standard `|^~\&`. ##### opts? [`RenderTextOptions`](#rendertextoptions) see [RenderTextOptions](#rendertextoptions). #### Returns [`RenderedText`](#renderedtext) the normalized [RenderedText](#renderedtext). #### Example ```ts import { renderText } from "@cosyte/hl7"; const r = renderText("Specimen received.\\.br\\Gross exam \\H\\normal\\N\\."); r.text; // "Specimen received.\nGross exam normal." r.runs; // [ { text: "Specimen received.\nGross exam ", highlighted: false }, // { text: "normal", highlighted: true }, // { text: ".", highlighted: false } ] r.unrenderedSequences; // [] ``` *** ### resolveCharset() > **resolveCharset**(`raw`): [`CharsetResolution`](#charsetresolution) Resolve an MSH-18 label (or an `options.charset` override) to a [CharsetResolution](#charsetresolution). A blank / `undefined` label resolves to the ASCII default. A recognized Table-0211 code returns its registry entry. An unrecognized label resolves to a `verbatim`, `recognized: false` outcome so the caller preserves bytes and emits `UNKNOWN_CHARSET`. #### Parameters ##### raw `string` \| `undefined` #### Returns [`CharsetResolution`](#charsetresolution) #### Example ```ts import { resolveCharset } from "@cosyte/hl7"; resolveCharset("UNICODE UTF-8"); // { canonical: "UTF-8", treatment: "decode", ... } resolveCharset("ISO IR87"); // { canonical: "ISO IR87", treatment: "verbatim", recognized: true } resolveCharset("WINDOWS-1252"); // { canonical: "WINDOWS-1252", treatment: "verbatim", recognized: false } ``` *** ### resolvePath() > **resolvePath**(`path`, `segments`, `_enc`): `string` \| `undefined` Resolve a dot-path string against a raw segment tree to its decoded leaf value. Returns `undefined` whenever the path does not resolve (missing segment, out-of-range field/component/subcomponent/repetition), and never throws on a missing value. Throws `TypeError` only when `path` itself is malformed: callers relying on "never throws" should pre-validate or wrap in try/catch. #### Parameters ##### path `string` ##### segments readonly [`RawSegment`](#rawsegment)[] ##### \_enc [`EncodingCharacters`](#encodingcharacters-1) #### Returns `string` \| `undefined` #### Example ```ts import { parseHL7, resolvePath } from "@cosyte/hl7"; const msg = parseHL7(raw); resolvePath("PID.5.1", msg.rawSegments, msg.encodingCharacters); // "Smith" resolvePath("NOT.9", msg.rawSegments, msg.encodingCharacters); // undefined ``` *** ### segmentCase() > **segmentCase**(`position`, `observed`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `SEGMENT_CASE` warning. Emitted when a segment identifier carries an ASCII lowercase letter (e.g. `pid` instead of `PID`, or `Obx` instead of `OBX`), which no segment identifier in the HL7 v2 standard does. The parser accepts the segment and resolves it as the segment it names, so `msg.segments("PID")`, `msg.patient` and `observations()` all see it; the warning is how a consumer learns the sender is non-conforming. "Resolved as", not "rewritten to": the original spelling stays on `RawSegment.name` and is what serialization re-emits, so the byte-verbatim round-trip is unaffected. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### observed `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { segmentCase } from "@cosyte/hl7"; const w = segmentCase({ segmentIndex: 3 }, "pid"); ``` *** ### setDefaultProfile() > **setDefaultProfile**(`profile`): `void` Register a process-scoped default profile. `parseHL7(raw)` (with no explicit profile arg) consults `getDefaultProfile()` and applies the returned profile if any. Pass `null` (or `undefined`) to clear. Effects are IDENTICAL to passing the profile explicitly as the second arg of `parseHL7` (D-20): customSegments, dateFormats, onWarning chain, and profile attribution all apply the same way. Explicit args ALWAYS win: `parseHL7(raw, myProfile)` uses `myProfile` regardless of the default; `parseHL7(raw, { profile: null })` opts out of the default for a single call without changing the registered default. **Test hygiene:** This is the ONLY mutable module-scoped state in the library. Tests that call `setDefaultProfile` MUST clean up in `afterEach` (`setDefaultProfile(null)`) or default-profile bleed will infect subsequent tests. #### Parameters ##### profile [`Profile`](#profile-2) \| `null` #### Returns `void` #### Example ```ts import { setDefaultProfile, profiles, parseHL7 } from "@cosyte/hl7"; // Set once at app startup setDefaultProfile(profiles.epic); // Every parseHL7 call in the app now uses profiles.epic unless // another profile is passed explicitly. const msg = parseHL7(raw); console.log(msg.profile?.name); // "epic" // Clear when done (or in test teardown): setDefaultProfile(null); ``` *** ### splitBatch() **`Internal`** implementation signature; overloads above carry the public JSDoc. #### Call Signature > **splitBatch**(`raw`): [`BatchSplitResult`](#batchsplitresult) Split a raw HL7 v2 **batch / file** stream into its individual messages plus the envelope metadata. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` frame (HL7 v2 Ch. 2 §2.10.3): - handles a file with **multiple batches** and a batch with multiple messages; - a **bare single message** (no envelope) passes straight through as one entry; - a **malformed message mid-batch is isolated** (returned as a typed failure entry): its siblings are still returned, the tail is never dropped; - reconciles BTS-1 (batch message count) and FTS-1 (file batch count) and emits [batchCountMismatch](#batchcountmismatch) on a mismatch: counts only, never PHI; - emits [batchMissingTrailer](#batchmissingtrailer) when a `BHS`/`FHS` header opens a scope no `BTS`/`FTS` closes: a warning, never a throw (the caller decides to reject). The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …). Under `strict`, a message that would warn surfaces as a failure entry (still isolated). `splitBatch` itself never throws: an empty stream yields an empty result. ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ##### Returns [`BatchSplitResult`](#batchsplitresult) ##### Examples ```ts import { splitBatch } from "@cosyte/hl7"; const { messages, warnings } = splitBatch( "FHS|^~\\&|SENDER\r" + "BHS|^~\\&|SENDER\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r" + "BTS|2\rFTS|1\r", ); console.log(messages.length); // 2 for (const entry of messages) { if (entry.ok) console.log(entry.message.version); // "2.5" } console.log(warnings.length); // 0: declared counts match ``` ```ts import { defineProfile, splitBatch } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for (const entry of splitBatch(rawBatchFile, vendor).messages) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` #### Call Signature > **splitBatch**\<`P`\>(`raw`, `profile`): [`BatchSplitResult`](#batchsplitresult)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\> Split a raw HL7 v2 **batch / file** stream into its individual messages plus the envelope metadata. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` frame (HL7 v2 Ch. 2 §2.10.3): - handles a file with **multiple batches** and a batch with multiple messages; - a **bare single message** (no envelope) passes straight through as one entry; - a **malformed message mid-batch is isolated** (returned as a typed failure entry): its siblings are still returned, the tail is never dropped; - reconciles BTS-1 (batch message count) and FTS-1 (file batch count) and emits [batchCountMismatch](#batchcountmismatch) on a mismatch: counts only, never PHI; - emits [batchMissingTrailer](#batchmissingtrailer) when a `BHS`/`FHS` header opens a scope no `BTS`/`FTS` closes: a warning, never a throw (the caller decides to reject). The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …). Under `strict`, a message that would warn surfaces as a failure entry (still isolated). `splitBatch` itself never throws: an empty stream yields an empty result. ##### Type Parameters ###### P `P` *extends* [`Profile`](#profile-2) ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ###### profile `P` ##### Returns [`BatchSplitResult`](#batchsplitresult)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\> ##### Examples ```ts import { splitBatch } from "@cosyte/hl7"; const { messages, warnings } = splitBatch( "FHS|^~\\&|SENDER\r" + "BHS|^~\\&|SENDER\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r" + "BTS|2\rFTS|1\r", ); console.log(messages.length); // 2 for (const entry of messages) { if (entry.ok) console.log(entry.message.version); // "2.5" } console.log(warnings.length); // 0: declared counts match ``` ```ts import { defineProfile, splitBatch } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for (const entry of splitBatch(rawBatchFile, vendor).messages) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` #### Call Signature > **splitBatch**\<`P`\>(`raw`, `options`): [`BatchSplitResult`](#batchsplitresult)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\> Split a raw HL7 v2 **batch / file** stream into its individual messages plus the envelope metadata. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` frame (HL7 v2 Ch. 2 §2.10.3): - handles a file with **multiple batches** and a batch with multiple messages; - a **bare single message** (no envelope) passes straight through as one entry; - a **malformed message mid-batch is isolated** (returned as a typed failure entry): its siblings are still returned, the tail is never dropped; - reconciles BTS-1 (batch message count) and FTS-1 (file batch count) and emits [batchCountMismatch](#batchcountmismatch) on a mismatch: counts only, never PHI; - emits [batchMissingTrailer](#batchmissingtrailer) when a `BHS`/`FHS` header opens a scope no `BTS`/`FTS` closes: a warning, never a throw (the caller decides to reject). The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …). Under `strict`, a message that would warn surfaces as a failure entry (still isolated). `splitBatch` itself never throws: an empty stream yields an empty result. ##### Type Parameters ###### P `P` *extends* [`Profile`](#profile-2) ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ###### options [`ParseOptions`](#parseoptions) & `object` ##### Returns [`BatchSplitResult`](#batchsplitresult)\<[`ProfiledMessage`](#profiledmessage)\<`P`\>\> ##### Examples ```ts import { splitBatch } from "@cosyte/hl7"; const { messages, warnings } = splitBatch( "FHS|^~\\&|SENDER\r" + "BHS|^~\\&|SENDER\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r" + "BTS|2\rFTS|1\r", ); console.log(messages.length); // 2 for (const entry of messages) { if (entry.ok) console.log(entry.message.version); // "2.5" } console.log(warnings.length); // 0: declared counts match ``` ```ts import { defineProfile, splitBatch } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for (const entry of splitBatch(rawBatchFile, vendor).messages) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` #### Call Signature > **splitBatch**(`raw`, `options`): [`BatchSplitResult`](#batchsplitresult) Split a raw HL7 v2 **batch / file** stream into its individual messages plus the envelope metadata. Demarcates by `MSH` boundaries inside the optional `[FHS] { [BHS] { MSH… } [BTS] } [FTS]` frame (HL7 v2 Ch. 2 §2.10.3): - handles a file with **multiple batches** and a batch with multiple messages; - a **bare single message** (no envelope) passes straight through as one entry; - a **malformed message mid-batch is isolated** (returned as a typed failure entry): its siblings are still returned, the tail is never dropped; - reconciles BTS-1 (batch message count) and FTS-1 (file batch count) and emits [batchCountMismatch](#batchcountmismatch) on a mismatch: counts only, never PHI; - emits [batchMissingTrailer](#batchmissingtrailer) when a `BHS`/`FHS` header opens a scope no `BTS`/`FTS` closes: a warning, never a throw (the caller decides to reject). The second argument, when given, is forwarded verbatim to [parseHL7](#parsehl7) for **each** message (profile, `strict`, `charset`, `dateFormats`, …). Under `strict`, a message that would warn surfaces as a failure entry (still isolated). `splitBatch` itself never throws: an empty stream yields an empty result. ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ###### options [`ParseOptions`](#parseoptions) ##### Returns [`BatchSplitResult`](#batchsplitresult) ##### Examples ```ts import { splitBatch } from "@cosyte/hl7"; const { messages, warnings } = splitBatch( "FHS|^~\\&|SENDER\r" + "BHS|^~\\&|SENDER\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|1|P|2.5\rPID|||1\r" + "MSH|^~\\&|A|F|B|G|20260101||ADT^A01|2|P|2.5\rPID|||2\r" + "BTS|2\rFTS|1\r", ); console.log(messages.length); // 2 for (const entry of messages) { if (entry.ok) console.log(entry.message.version); // "2.5" } console.log(warnings.length); // 0: declared counts match ``` ```ts import { defineProfile, splitBatch } from "@cosyte/hl7"; const vendor = defineProfile({ name: "vendor", customSegments: { ZDP: { fields: { departmentCode: 3 } } }, }); // The profile is forwarded to every message, narrowing each one's reader: for (const entry of splitBatch(rawBatchFile, vendor).messages) { if (entry.ok) console.log(entry.message.part("ZDP")?.get("departmentCode")?.value); } ``` *** ### supportsBuilderMessage() > **supportsBuilderMessage**(`messageCode`, `triggerEvent`): `boolean` Can a typed builder author this (message code, trigger event) pair as a spec-clean, zero-warning message? A published entry whose trigger event is the empty string matches any trigger event for that message code. A `false` answer does not mean the builder refuses the event: a builder that takes a trigger event still emits the content supplied. It means the library does not claim the result is structurally complete. #### Parameters ##### messageCode `string` MSH-9.1, e.g. `"SIU"`. ##### triggerEvent `string` MSH-9.2, e.g. `"S12"`. #### Returns `boolean` #### Example ```ts import { supportsBuilderMessage } from "@cosyte/hl7"; supportsBuilderMessage("SIU", "S12"); // true supportsBuilderMessage("ADT", "A20"); // false: the structure requires NPU ``` *** ### timestampFallbackFormat() > **timestampFallbackFormat**(`position`, `matchedFormat`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `TIMESTAMP_FALLBACK_FORMAT` warning: a date/time value that did not parse as strict HL7 but did match a fallback format. This is the warning's constructor, not its emit site. The only code path that can raise it is the lenient cascade behind `msg.meta.timestamp`, and only when a caller hands that cascade an emit callback, so a parse does not put this warning on `msg.warnings` today. Read `matchedFormat` on the `TS` to see which format answered a value; a typed datetime field never consults the built-in fallbacks at all. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### matchedFormat `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { timestampFallbackFormat } from "@cosyte/hl7"; const w = timestampFallbackFormat( { segmentIndex: 1, fieldIndex: 7 }, "YYYY-MM-DD", ); ``` *** ### toDate() > **toDate**(`value`, `options?`): `Date` \| `undefined` Materialize an absolute-instant JS `Date` from a parsed HL7 datetime, ONLY when the zone is determinate. Delegates to [dtmToDate](#dtmtodate), so the rule is the one this parser has always applied: - the value carries an explicit offset: the exact instant from THAT offset, and `options.assumeOffsetMinutes` is ignored; - no offset and `assumeOffsetMinutes` supplied as a finite number: that offset is applied, including an explicit `0` meaning "treat this naive value as UTC"; - no offset and no usable option: `undefined`. The host timezone is NEVER read and UTC is NEVER assumed. "Usable" is checked rather than assumed, because a published package is called from JavaScript as well as from TypeScript. An options bag that is `null`, or an `assumeOffsetMinutes` that is not a finite number, names no zone: it answers `undefined` rather than coercing to one. A string `"0"`, a `true` and an `[]` all multiply to a number in JavaScript, so a converter that simply arithmetics them hands back an instant the caller never asked for, and `0` in particular is silent UTC. The offset the VALUE states is held to the same account, one step earlier: it wins outright over any assumption, so it is checked to be a zone before it is allowed to win. A `hasTimezone` value whose `offsetMinutes` is a string, a boolean, an array, `NaN` or a count of minutes a whole day or more from UTC states no zone either, and coercing it would fabricate exactly the confident instant this function exists to refuse. Components below the stated precision fill to their lowest legal value (month to 1, day to 1, time to 0) FOR INSTANT CONSTRUCTION ONLY; the value's stated precision is unchanged, and a later `toObject` or `toISO` on the same value returns exactly what it returned before. A four-digit year below 100 stays that year: `0050` is year 50, never 1950. Returns `undefined` for an invalid value, an unresolvable zone, a value whose stated components do not name a real calendar date or a real zone, and for `undefined` / `null`. Never throws, for any input, on either parameter. An impossible day is refused rather than rolled into the following month, so no instant this returns is a day away from the value the sender wrote. #### Parameters ##### value [`DtmParts`](#dtmparts) \| `null` \| `undefined` ##### options? [`DtmToDateOptions`](#dtmtodateoptions) \| `null` #### Returns `Date` \| `undefined` #### Example ```ts import { parseDtm, toDate } from "@cosyte/hl7"; toDate(parseDtm("20250102")); // undefined: refuses to guess toDate(parseDtm("20250102"), { assumeOffsetMinutes: 0 })?.toISOString(); // "2025-01-02T00:00:00.000Z": the caller chose UTC toDate(parseDtm("20250102153045-0500"))?.toISOString(); // "2025-01-02T20:30:45.000Z": exact, offset-derived toDate(parseDtm("20240230"), { assumeOffsetMinutes: 0 }); // undefined: February has no 30th, and 1 March is not what was written ``` *** ### toISO() > **toISO**(`value`): `string` \| `undefined` Render a parsed HL7 datetime as ISO-8601 TRUNCATED TO ITS STATED PRECISION, never padded out: a year-precision value renders four characters, a day-precision value renders ten. Fractional digits are rendered VERBATIM as stated, neither padded to three nor rounded. An explicit offset is appended as `Z` when it is exactly zero (including HL7's `-0000`), otherwise as `+HH:MM` / `-HH:MM`. When the value carried NO offset, NOTHING is appended: the string is deliberately zone-less and no `Z` is fabricated. Because a zero offset renders `Z`, this is NOT a byte round trip of the wire value and is not meant to be; `formatDtm` is the round-trip route and is unchanged. Returns `undefined` for a value the parser marked invalid, for a value with no stated year (the HL7 DTM datatype mandates a leading four-digit year, so this parser produces no time-only value), for a value whose stated components do not name a real calendar date or a real zone, and for `undefined` / `null`. Never throws. A string this returns is always one an ISO-8601 reader reads back: `"2024-02-30"` is never rendered, because every reader silently moves it to 1 March, and neither is an offset past `+23:59`, because every reader answers an Invalid Date for it. #### Parameters ##### value [`DtmParts`](#dtmparts) \| `null` \| `undefined` #### Returns `string` \| `undefined` #### Example ```ts import { parseDtm, toISO } from "@cosyte/hl7"; toISO(parseDtm("1970")); // "1970" toISO(parseDtm("19700705")); // "1970-07-05": no fabricated Z toISO(parseDtm("20250102153045.5-0500")); // "2025-01-02T15:30:45.5-05:00" toISO(parseDtm("20250102153045-0000")); // "2025-01-02T15:30:45Z" toISO(parseDtm("20230229")); // undefined: 2023 is not a leap year toISO(parseDtm("20250102153045+2400")); // undefined: no zone is 24 hours east ``` *** ### toObject() > **toObject**(`value`): [`DateParts`](#dateparts) \| `undefined` Project a parsed HL7 datetime onto the shared [DateParts](#dateparts) shape: a frozen plain object carrying ONLY the calendar components the value stated. Returns `undefined` for a value the parser marked invalid, for a value stating no components at all, for a value whose stated components do not name a real calendar date (`20240230` names no day: February has no 30th) or a real zone (an offset has to be a whole number of minutes within a day of UTC), and for `undefined` / `null`. Never throws. Every value in the object it returns is therefore a finite number, which is what makes `offsetMinutes` safe to read as minutes east of UTC. The value's own stated precision is untouched by the call: this is a projection, not a conversion of the parsed value. #### Parameters ##### value [`DtmParts`](#dtmparts) \| `null` \| `undefined` #### Returns [`DateParts`](#dateparts) \| `undefined` #### Example ```ts import { parseDtm, toObject } from "@cosyte/hl7"; toObject(parseDtm("19880705")); // { year: 1988, month: 7, day: 5 } toObject(parseDtm("20250102153045.0500-0430")); // { year: 2025, month: 1, day: 2, hour: 15, minute: 30, second: 45, // millisecond: 50, offsetMinutes: -270 } toObject(parseDtm("not-a-date")); // undefined toObject(parseDtm("20240230")); // undefined: February has no 30th toObject(parseDtm("20240229")); // { year: 2024, month: 2, day: 29 }: 2024 is a leap year ``` *** ### unescape() > **unescape**(`input`, `enc`, `emit`, `position`): `string` Expand HL7 escape sequences (`\F\`, `\S`, `\T\`, `\R`, `\E\`, `\.br`, `\X..\`, vendor-specific `\Z..`) inside a field, component, or subcomponent string. The escape delimiter comes from `enc.escape` (default ``), so senders using a non-default escape character are handled transparently. Unknown or malformed sequences are preserved VERBATIM in the output and emit an `UNKNOWN_ESCAPE_SEQUENCE` warning via `emit`. Unterminated escapes (an escape character with no closing partner before end-of-input) are also preserved in full and warn once: the scan is strictly O(n) and cannot infinite-loop on malformed input. #### Parameters ##### input `string` ##### enc [`EncodingCharacters`](#encodingcharacters-1) ##### emit (`w`) => `void` ##### position [`Hl7Position`](#hl7position) #### Returns `string` #### Example ```ts import { unescape, DEFAULT_ENCODING_CHARACTERS } from "@cosyte/hl7"; const warnings: Array = []; const out = unescape( "patient\\F\\name\\.br\\DOB", DEFAULT_ENCODING_CHARACTERS, (w) => warnings.push(w), { segmentIndex: 1, fieldIndex: 5 }, ); // out === "patient|name\nDOB" ``` *** ### unknownCharset() > **unknownCharset**(`position`, `requested`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `UNKNOWN_CHARSET` warning. Emitted when MSH-18 (or an `options.charset` override) declares a value that is **not a recognized HL7 Table-0211 character set**. The parser never guesses an encoding: it reads the raw bytes as `latin1` (a 1:1 byte mapping) so single-byte content stays recoverable, rather than corrupting it with replacement characters. The message carries the charset code only, never a decoded field value: the code is shape-checked before it is interpolated, and withheld if it does not match. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### requested `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { unknownCharset } from "@cosyte/hl7"; const w = unknownCharset({ segmentIndex: 0, fieldIndex: 18 }, "ISO IR 999"); ``` *** ### unknownEscapeSequence() > **unknownEscapeSequence**(`position`, `body`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `UNKNOWN_ESCAPE_SEQUENCE` warning for a **terminated** escape sequence (`\..\`) whose body is not a recognized HL7 escape. The message NEVER embeds the escape body: only its length and, when the body's first character is a recognized HL7 escape-identifier letter (structural HL7 grammar, not PHI: e.g. `Z` for a vendor escape, `.` for a formatting escape), that single letter. A body that doesn't start with a known escape letter is untrusted field text and names no character at all. The sequence itself is still preserved verbatim in the parsed output: only the WARNING message is PHI-safe. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### body `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { unknownEscapeSequence } from "@cosyte/hl7"; const w = unknownEscapeSequence({ segmentIndex: 2, fieldIndex: 3 }, "Z99"); // message: `Unknown HL7 escape sequence of type "Z" (3 chars) preserved verbatim.` ``` *** ### unknownSegment() > **unknownSegment**(`position`, `segmentName`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `UNKNOWN_SEGMENT` warning. Emitted when a segment identifier is not in the HL7 spec's standard set and not registered in the active profile's `customSegments`. The message states that the comparison ignored case, because the parser folds a segment name to its canonical ASCII-uppercase spelling before looking it up. An earlier wording read "not in HL7 spec and no profile claim" while the lookup was case-SENSITIVE, so a sender shipping `obx` was told its segment was absent from the standard. `OBX` is in the standard; the real fault was the case, and the message sent the integrator looking for a missing spec entry that was never missing. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### segmentName `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { unknownSegment } from "@cosyte/hl7"; const w = unknownSegment({ segmentIndex: 7 }, "ZZZ"); ``` *** ### unsupportedCharset() > **unsupportedCharset**(`position`, `code`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `UNSUPPORTED_CHARSET` warning. Emitted when a **recognized** Table-0211 character set is not decoded into text: either because this parser does not decode it (the multibyte / ISO-2022-switched East-Asian sets: JIS, GB 18030, KS X 1001, CNS 11643, BIG-5: and the wide Unicode transforms UTF-16 / UTF-32), or because a strict decode of a decodable set **failed** (a byte invalid / undefined for the declared set, or an ICU build lacking the label). In every case the raw bytes are read as `latin1`, never guessed at, so the parser does not emit replacement-char-corrupted text. Single-byte content stays byte-recoverable; multibyte content is best-effort (a content byte can coincide with a structural delimiter: see the parser's known-limitations). The switch escapes (`\Cxxyy\` / `\Mxxyyzz`) are recognized and preserved by the escape layer; full stateful decoding is a documented non-goal. The message carries the charset code only, never a decoded field value: the code is shape-checked before it is interpolated, and withheld if it does not match. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### code `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { unsupportedCharset } from "@cosyte/hl7"; const w = unsupportedCharset({ segmentIndex: 0, fieldIndex: 18 }, "ISO IR87"); ``` *** ### unterminatedEscapeSequence() > **unterminatedEscapeSequence**(`position`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `UNKNOWN_ESCAPE_SEQUENCE` warning for an **unterminated** escape (an escape character with no closing partner before end-of-input). The "body" in this case is the entire remainder of the field, so the message carries NEITHER the body NOR its length (the length of a truncated tail is itself derivable field-shape information): just the fact that an unterminated escape was found, and the position. The remainder is still preserved verbatim in the parsed output. #### Parameters ##### position [`Hl7Position`](#hl7position) #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { unterminatedEscapeSequence } from "@cosyte/hl7"; const w = unterminatedEscapeSequence({ segmentIndex: 2, fieldIndex: 3 }); // message: `Unterminated HL7 escape sequence preserved verbatim.` ``` *** ### unterminatedStreamMessage() > **unterminatedStreamMessage**(`position`): [`Hl7ParseWarning`](#hl7parsewarning) Build an `UNTERMINATED_STREAM_MESSAGE` warning. Emitted by `parseStream()`: attached to a [StreamMessageEntry](#streammessageentry)'s `streamWarnings`, NOT to `Hl7Message.warnings`: when the **final** message in a stream ends without a segment terminator (its last segment ran to end-of-stream with no trailing `\r`/`\r\n`/`\n`). The message is still parsed and yielded **in full**; this is the fail-safe signal that the tail may have been truncated mid-message (a cut-off feed), never a reason to drop it and never a throw. Only the last message can be unterminated: every earlier message is closed by the terminator that precedes the next `MSH`/envelope boundary. The message carries only the structural fact, NEVER a field value, so no PHI is exposed. `position` references the message's `MSH` segment index. #### Parameters ##### position [`Hl7Position`](#hl7position) #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { unterminatedStreamMessage } from "@cosyte/hl7"; const w = unterminatedStreamMessage({ segmentIndex: 6 }); ``` *** ### usageOutcomes() > **usageOutcomes**(`rule`): [`UsageOutcomes`](#usageoutcomes) \| `undefined` The true and false usage outcomes a rule's declared conditional records, as two separate simple codes: for `C(RE/X)`, `{ whenTrue: "RE", whenFalse: "X" }`. Returns `undefined` when the rule's usage is a simple code or is absent, which is how it reports "this rule has no outcomes". **It never throws**, including for a value forced through `any`. #### Parameters ##### rule [`FieldRule`](#fieldrule) \| [`SegmentRule`](#segmentrule) a segment rule or a field rule from a conformance profile. #### Returns [`UsageOutcomes`](#usageoutcomes) \| `undefined` the two outcomes, or `undefined` when the rule declares none. #### Example ```ts import { usageOutcomes, type FieldRule } from "@cosyte/hl7"; const conditional: FieldRule = { field: 8, usage: "C(RE/X)" }; usageOutcomes(conditional); // { whenTrue: "RE", whenFalse: "X" } const simple: FieldRule = { field: 8, usage: "R" }; usageOutcomes(simple); // undefined (a simple usage records no outcomes) ``` *** ### validateAgainstProfile() > **validateAgainstProfile**(`message`, `profile`, `resolutions?`): [`ConformanceResult`](#conformanceresult) Validate a parsed HL7 v2 message against a **user-authored** declarative conformance profile and return typed findings. **Never throws.** A well-formed profile is evaluated rule by rule; a malformed profile is reported as `PROFILE_MALFORMED` findings (the engine does not validate against a profile it cannot trust: never a silent pass). Either way you get a [ConformanceResult](#conformanceresult). For fail-fast authoring, run [defineConformanceProfile](#defineconformanceprofile) first: it throws on a malformed profile. **`findings.length === 0` is NOT a conformance attestation.** It means every rule the profile declared was satisfied: nothing about the parts of the message the profile did not cover, and nothing about clinical correctness. **No PHI in findings**: each finding names the structural locus (segment / field / component / repetition) and the rule, never the offending value. **Read-only**: the message is never mutated. **A conditional usage takes its outcome from one source, never two.** A rule that declares a [ConditionPredicate](#conditionpredicate) on its `condition` has it evaluated against the message, and the true or false outcome selects the usage the rest of the rule is checked under: the outcomes a declared conditional such as `C(R/X)` writes down, or the ones IHE states for the bare `C` and `CE` codes. A rule that declares none takes the outcome of a matching [UsageResolution](#usageresolution) in `resolutions` instead. Declaring both at one locus is `PROFILE_MALFORMED`. With neither, the rule is evaluated exactly as a `C` rule is: presence not evaluated, every other constraint applied as usual. **A predicate the message cannot decide is reported, never guessed.** It yields a `PROFILE_CONDITION_UNEVALUATABLE` finding and the element's presence is not assessed, so an unassessed conditional can never hide inside an empty findings list. **A value set may be bound to the coding system its codes come from.** A rule that declares [FieldRule.codingSystem](#codingsystem) has the message's own coding-system component compared against it by resolved identity, per present repetition, and a mismatch is `PROFILE_CODING_SYSTEM_MISMATCH`: a distinct code, so "a code we do not accept" and "a code that looks right but claims the wrong system" stop being one answer. The check is additive and opt-in: a rule that declares no binding behaves exactly as it always has. Still no code set and still no network call, because this compares the sender's CLAIM against the profile and never the code against a terminology. **A field rule may declare what its COMPONENTS are, and doing so closes the set.** Each entry of `components` carries a 1-indexed index and an optional usage code whose cardinality is IMPLIED rather than declared (`R` is `[1..1]`, `RE` and `O` are `[0..1]`, `X` is `[0..0]`; a component has no repetition construct, so each reduces to presence). Per present repetition, `R` with nothing at that component is `PROFILE_REQUIRED_ABSENT` and `X` with something there is `PROFILE_NOT_PERMITTED`, both at a locus naming the component. Content at an index the rule does NOT declare is `PROFILE_UNDECLARED_CONTENT`, one finding per undeclared index per repetition: the methodology counts content in an element the profile never specified as a conformance violation rather than harmless extra content. A field rule that declares no `components` declares no depth and is checked exactly as it always was, so this is additive and opt-in per rule. **Every result carries the [ProfileLevel](#profilelevel) in force**, beside the profile name, because an empty findings list means two different things at two different levels. A profile may declare [ConformanceProfile.level](#level); one that declares none is assessed and echoed as `constrainable`, the weaker claim, so silence never reads as `implementable`. A profile that DOES claim `implementable` is held to the level's own terminus at profile-shape time: every segment, field and component rule must declare a usage, and that usage must be `R`, `RE` or `X` or a declared conditional whose outcomes are drawn from those three. A claim that fails is `PROFILE_MALFORMED`, and a refused claim is never the level echoed on the result. The level changes no message check at all: it decides which findings the PROFILE is refused for, never which findings a MESSAGE produces. Omitting `resolutions` (or passing `undefined`) is the same as passing an empty list. Any OTHER value that is not a list of well-formed resolutions is a `PROFILE_MALFORMED` finding, never read as "no resolutions": a mis-typed argument must not return a clean result for elements nothing checked. #### Parameters ##### message [`Hl7Message`](#hl7message) a parsed message from [parseHL7](#parsehl7). ##### profile [`ConformanceProfile`](#conformanceprofile) the consumer's declarative [ConformanceProfile](#conformanceprofile). ##### resolutions? readonly [`UsageResolution`](#usageresolution)[] optional caller-supplied outcomes for declared-conditional rules that declare no predicate. #### Returns [`ConformanceResult`](#conformanceresult) the profile name, the level in force, and the ordered findings (empty ⇒ no declared rule violated). #### Example ```ts import { parseHL7, validateAgainstProfile, type ConformanceProfile } from "@cosyte/hl7"; const profile: ConformanceProfile = { name: "example-adt-min", segments: [ { segment: "PID", usage: "R", fields: [ { field: 3, name: "Patient Identifiers", usage: "R" }, { field: 8, name: "Administrative Sex", usage: "C(RE/X)", // "Required but may be empty when a birth date was sent, else not permitted." condition: { location: { segment: "PID", field: 7 }, presence: "is valued" }, valueSet: ["M", "F", "U"], }, ] }, ], }; const { findings } = validateAgainstProfile(parseHL7(raw), profile); for (const f of findings) console.log(f.severity, f.code, f.message); // findings.length === 0 ⇒ no declared rule violated (NOT an attestation) ``` *** ### validateMessageStructure() > **validateMessageStructure**(`message`): [`StructureValidationResult`](#structurevalidationresult) Validate a parsed message against HL7's own published structure for its trigger event: segment order, segment occurrence counts, and segments the published structure does not name. **Opt-in and read-only.** Nothing calls it for you, it never throws, and it leaves the message exactly as it found it: same segments, same warnings, same serialization. **Check `validated` before you read `findings`.** A message type the registry does not model, one recognized only through a retained transcription, and one carrying no readable message type at all each come back with `validated: false`, a `reason`, and no findings: that is "the publication cannot answer", not "the message is fine". **Zero findings is not a conformance attestation.** It means this message did not break the published structure in the three ways checked here. Field content, datatypes, value sets and HL7 tables are all unchecked, the covered message codes are twelve rather than the whole standard, and the publication behind it is vendored at a fixed commit. #### Parameters ##### message [`Hl7Message`](#hl7message) a parsed message. Input the parser rejects never reaches here: the parse fails first, with the fatal error it always raised. #### Returns [`StructureValidationResult`](#structurevalidationresult) #### Example ```ts import { parseHL7, validateMessageStructure } from "@cosyte/hl7"; const result = validateMessageStructure(parseHL7(raw)); result.validated; // false for a message type the registry does not model result.structureId; // e.g. "ADT_A01-A": the variant the findings are against result.findings.map((f) => f.code); ``` *** ### versionMismatch() > **versionMismatch**(`position`, `declared`, `expected`): [`Hl7ParseWarning`](#hl7parsewarning) Build a `VERSION_MISMATCH` warning. Emitted when MSH-12 declares an HL7 version that does not match what the active profile or `ParseOptions` expected. #### Parameters ##### position [`Hl7Position`](#hl7position) ##### declared `string` ##### expected `string` #### Returns [`Hl7ParseWarning`](#hl7parsewarning) #### Example ```ts import { versionMismatch } from "@cosyte/hl7"; const w = versionMismatch({ segmentIndex: 0, fieldIndex: 12 }, "2.9", "2.5"); ``` --- URL: https://docs.cosyte.com/hl7/troubleshooting Markdown: https://docs.cosyte.com/hl7/troubleshooting.md # Troubleshooting `@cosyte/hl7` follows Postel's Law: the parser is **liberal** (tolerate and warn), the serializer is **conservative** (always spec-clean). Most surprises come from that split. This page covers the common ones, then the [Known Limitations](#known-limitations) you should design around. ## "I got warnings, not an error" That's the default and it's intentional. A vendor quirk becomes a **tolerance warning** on `msg.warnings`. The parse still succeeds. Each warning carries a stable code you can branch on: ```ts runnable import { parseHL7 } from "@cosyte/hl7"; // A ZZZ segment isn't a known HL7 segment: tolerated, surfaced as a warning. const raw = "MSH|^~\\&|A|B|C|D|20260101||ADT^A01|1|P|2.5\rZZZ|1"; const msg = parseHL7(raw); msg.warnings.some((w) => w.code === "UNKNOWN_SEGMENT"); // => true ``` The 20 warning codes are a **public, versioned contract**. They won't be renamed under you without a breaking change. See [Warning and fatal codes](https://docs.cosyte.com/hl7/warning-and-fatal-codes.md) for what each one means. ## "I want failures, not warnings" Pass `{ strict: true }`. Strict mode promotes every Tier-2 tolerance warning to a thrown `Hl7ParseError`, so nothing quirky slips through silently: ```ts runnable throws import { parseHL7 } from "@cosyte/hl7"; const raw = "MSH|^~\\&|A|B|C|D|20260101||ADT^A01|1|P|2.5\rZZZ|1"; // The same input that warned above now throws under strict. parseHL7(raw, { strict: true }); ``` ## "It threw even though I didn't ask for strict" Four **fatal** structural codes throw regardless of mode. They mean the input isn't recoverable as HL7 at all: `NO_MSH_SEGMENT`, `MSH_TOO_SHORT`, `INVALID_ENCODING_CHARACTERS`, and `EMPTY_INPUT`. ```ts runnable throws import { parseHL7 } from "@cosyte/hl7"; // No MSH, no message: fatal even in the default lenient mode. parseHL7(""); ``` Catch `Hl7ParseError` and read its `code` to distinguish them. Everything _else_ is a warning. ## "Non-ASCII text comes out garbled" Character set is declared in **MSH-18** (HL7 Table 0211). The parser resolves it and decodes accordingly; `resolveCharset` / `canonicalCharset` let you inspect how a given code is treated (decoded vs. preserved) before you rely on it. ## "A batch/file won't parse" `parseHL7` parses **one** message. For an `FHS`/`BHS`-led batch or file envelope, use `splitBatch`, which demarcates the individual MSH-led messages, parses each independently (a malformed message is isolated, never dropping its siblings), and reconciles the declared `BTS-1`/`FTS-1` counts: ```ts import { splitBatch } from "@cosyte/hl7"; const result = splitBatch(rawBatch); for (const entry of result.messages) { // each entry is an ok/failure record: a bad message never suppresses the rest } ``` ## Known limitations Design around these. They're deliberate scope choices, not bugs: - **Vendor profiles are structural and evidence-grounded.** Built-ins ship for Epic, Cerner, Meditech, Athena, a generic lab, and the Visage 7 and Philips Vue PACS imaging systems. A vendor "quirk" is encoded **only** when a real document grounds it: a publicly published vendor interface spec (as with Visage 7 and Philips Vue PACS) or a real de-identified feed, never invented, so broader per-vendor coverage expands as grounded sources arrive rather than shipping speculative rules. - **A miscased segment name resolves, but is not rewritten.** A sender shipping `pid` or `obx`, which no segment identifier in the HL7 v2 standard does, is matched by `msg.segments("PID")`, `msg.patient`, `observations()` and `msg.get("PID.5")`, and reports `SEGMENT_CASE`. The comparison folds **ASCII** case only, so a non-ASCII lookalike (a dotless Turkish `ı`) is deliberately _not_ folded into `PID` and surfaces as `UNKNOWN_SEGMENT` instead: guessing there would invent a patient identity the sender never sent. Three consequences worth designing around: the spelling that arrived is preserved on `RawSegment.name` and is what `toString()` re-emits, so the round-trip stays byte-exact rather than being silently corrected; a lowercase **`msh`** is still the `NO_MSH_SEGMENT` fatal, because MSH is what delimiter discovery reads before any segment name exists to fold; and folding necessarily widens the set of malformed lines that can be read as a segment at all, so an unescaped line break whose next three characters happen to spell a segment name in **any** case (`dg1` as well as `DG1`) yields an empty entry in the matching helper. That is the same three-character residue the bounded-messages note below describes, and the reason to treat a message carrying unescaped breaks as suspect rather than as data. - **No terminology validation, no network, no bundled codesets.** `codingSystem()` reports what a code _claims_ (HL7 Table 0396). It does not validate a value against LOINC, SNOMED CT, RxNorm, or any external system, and nothing here makes a network call. - **One ACK acknowledges one message.** Batch-level ACK reconciliation is out of scope; for MLLP transport framing and ACK correlation over the wire, use the sibling package [`@cosyte/mllp`](https://github.com/cosyte/mllp). - **Datetimes are fidelity values, not eager `Date`s.** `TS` fields preserve the raw string and a `precision` so timezone and precision are never silently lost; convert to a `Date` explicitly when you need one. - **`Hl7ParseError.snippet` is the field to redact first.** It carries up to 40 characters of raw input verbatim and the library never redacts it, so scrub it at your logging edge. Under `{ strict: true }` the escalated error carries a `snippet` of the first 40 characters of the input, so it remains the field to redact on that path too. - **Warning and error _messages_ are bounded, but not absolutely content-free.** A token lifted from the input is echoed **only when it matches the form the spec defines for it**: a three-character segment identifier, an MSH-9 type, an MSH-12 version, or a charset label the closed Table 0211 actually contains. Anything else becomes ``. That is what stops an unescaped line break inside a narrative field, which HL7 v2 requires to be sent as `\.br\` and which real senders send raw anyway, from forging a "segment identifier" out of a line of clinical text. A message cannot carry a field's value, but it can carry a residue of up to three characters when a malformed line happens to look like a segment identifier, so prefer logging `w.code` and `w.position` over `w.message` if your posture is strict. - **Below 1.0.** The 20 warning codes are a stable contract, and so are the exported functions and the message and helper surfaces; below 1.0 a breaking change moves the minor version, and the changelog says how to migrate. For the published version, ask the registry (`npm view @cosyte/hl7 version`) rather than a doc page. ## Still stuck? Open an issue at [github.com/cosyte/hl7/issues](https://github.com/cosyte/hl7/issues) with a **synthetic** (never real-PHI) message that reproduces it. --- # @cosyte/mllp > Minimal Lower Layer Protocol (MLLP) client and server for HL7 message transport. These are the 11 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/mllp/intro Markdown: https://docs.cosyte.com/mllp/intro.md # @cosyte/mllp A production-grade MLLP client and server for Node.js: the transport-only sibling to `@cosyte/hl7`. It moves HL7 v2 messages over TCP and gives you the parts the spec leaves to you: canonical framing (`VT + payload + FS + CR`), ACK correlation, auto-reconnect with backoff, backpressure, TLS, and an in-memory transport so your tests never open a socket. Zero runtime dependencies; Node stdlib only. This package is transport, not parsing. It carries bytes and never inspects the payload. Pair it with `@cosyte/hl7` (or any parser) when you need to read fields. The `ack-from-hl7` subpath is the one optional bridge between the two. ## Install ```bash npm install @cosyte/mllp ``` `@cosyte/hl7` is an **optional** peer dependency. Install it only if you use the `ack-from-hl7` subpath: ```bash npm install @cosyte/hl7 ``` ## Send a message ```ts import { MllpClient } from "@cosyte/mllp"; const client = new MllpClient({ host: "127.0.0.1", port: 2575 }); await client.connect(); const ack = await client.send(Buffer.from(rawHl7)); // resolves on the correlated ACK frame await client[Symbol.asyncDispose](); ``` `send` resolves with the ACK frame the remote returns, matched back to your message, not just the next bytes off the wire. Reconnects, backoff, and backpressure are handled for you; pass an `AbortSignal` to bound any await. ## Receive messages ```ts import { createServer } from "@cosyte/mllp"; const server = createServer({ onMessage: (payload, meta) => { // Each inbound message, de-framed for you. `payload` is a raw Buffer. // Charset decoding stays with the caller. How you acknowledge is next. route(payload); }, }); await server.listen(2575, "127.0.0.1"); ``` The server frames and de-frames for you; you decide what to send back. The payload is a raw `Buffer`. Charset decoding stays with the caller. The `'message'` event and the `onMessage` callback always fire **before** any ACK is sent. ## Fail-safe ACKs: the commit contract A positive acknowledgement (`AA`) tells the sender "you may forget this message. I have it." So a server must **never** emit `AA` before the message is durably handled. `@cosyte/mllp` makes that structural: pair `autoAck: 'AA'` with an `onMessage` handler, and the server **awaits your handler (the durable-commit step) and only then ACKs**: `AA` on success, a **negative** code on failure, never `AA` before commit. ```ts const server = createServer({ autoAck: "AA", onMessage: async (payload) => { await db.commit(payload); // throw here ⇒ AE (resend may succeed), never AA }, }); ``` - **Handler resolves ⇒ `AA`** (HL7 Table 0008), echoing the inbound `MSH-10` into `MSA-2`. - **Handler throws / rejects ⇒ `AE`** (application error, the sender may resend). - **Handler throws `MllpAckError({ ackCode: 'AR' })` ⇒ `AR`** (application reject, do not resend unchanged). On failure the server emits a PHI-safe `'nack'` event carrying only `{ connectionId, ackCode }`. The payload and the thrown error's message (which may carry PHI) never reach the wire or the event. `autoAck: 'AA'` **without** an `onMessage` handler degrades to a **transport-accept**: `AA` means "bytes received and framed", not "application-processed": only safe when a downstream component owns durability. For full control, pass `autoAck: fn` to build the ACK bytes yourself, or omit `autoAck` for manual mode (`respond()` / `conn.send()`). ## Framing and tolerance The encoder is strict: it always emits canonical `VT + payload + FS + CR`. The decoder is liberal where you let it be: real-world senders drop the leading `VT`, append a stray `LF`, or pad with whitespace, and each tolerated deviation surfaces as a warning with a **stable code** and byte offset rather than failing the frame (Postel's Law). Codes such as `MLLP_MISSING_LEADING_VT` and `MLLP_TRAILING_BYTES` are part of the public API. Note that **tolerance is opt-in**: a bare `FrameReader` is _strict_ by default, while `MllpServer` ships tolerant defaults (`allowFsOnly`, `allowLfAfterFs`, `allowLeadingWhitespace`) because it is the side that must accept what real senders emit. Accumulators are bounded: frames past `maxFrameSizeBytes` (16 MB default) throw `MLLP_FRAME_TOO_LARGE` instead of growing unbounded. See [Framing & tolerance](https://docs.cosyte.com/mllp/framing.md) for the flag-by-flag table and the full warning-code registry. ## Testing without sockets ```ts import { Connection } from "@cosyte/mllp"; import { InMemoryTransport } from "@cosyte/mllp/testing"; // Two connected, in-process ends: a write to one delivers synchronously to the other. const [clientSide, serverSide] = InMemoryTransport.pair(); const conn = new Connection({ transport: clientSide }); // Drive framing + ACK correlation deterministically against `serverSide`. ``` The in-memory transport is a first-class deliverable from the `@cosyte/mllp/testing` subpath. Pair two ends with `InMemoryTransport.pair()` and hand one to a `Connection` (deterministic, no ports, no certs) and reserve real sockets for integration smoke tests. Chunked reads, backpressure and an abrupt disconnect are all one call each, and `runDifferential` ships alongside it to check a real engine before go-live: see [Testing & verification](https://docs.cosyte.com/mllp/testing.md). ## ACK from an HL7 message The optional `ack-from-hl7` subpath builds a spec-correct, MLLP-framed ACK from an inbound HL7 v2 message: a thin adapter over `@cosyte/hl7`'s `buildAck` (hl7 owns the ACK content and control tables; this package frames and correlates). It is the only place this package touches `@cosyte/hl7`, and the peer is loaded lazily on first call: ```ts import { buildAckAA, buildAckAE } from "@cosyte/mllp/ack-from-hl7"; // After your handler durably commits the message (the commit contract): const { frame, code, correlationId } = buildAckAA(payload); // requires the @cosyte/hl7 peer dep socket.write(frame); // VT + ACK + FS + CR, MSA-2 echoes the inbound MSH-10 verbatim // On a processing failure, never acknowledge what you did not commit: socket.write(buildAckAE(payload, { error: { conditionCode: "207" } }).frame); ``` `buildMllpAck` is the core (explicit `code`); `buildAckAA/AE/AR/CA/CE/CR` are the six Table-0008 conveniences; `detectMode` reports original-vs-enhanced from the inbound MSH-15/16. Fail-safe by construction: an inbound without a findable MSH-10 **never** yields a positive ACK. The disposition downgrades (`AA`→`AE`, `CA`→`CE`) and the result carries a warning (`ACK_NO_CORRELATION_ID` from the peer, or `MLLP_ACK_INBOUND_UNPARSEABLE` when the inbound could not be parsed at all). Warnings carry codes and structural context only, never message content. The builder's own limits (it trusts your disposition, MSA-2 is a canonical re-serialization rather than the inbound's original bytes, and there is no enhanced-mode sequencing) are covered in [ACKs & the commit contract](https://docs.cosyte.com/mllp/acks.md). ## Next - **[Framing & tolerance](https://docs.cosyte.com/mllp/framing.md)**: the wire format, the opt-in tolerance flags, the stable warning codes, and the PHI contract on diagnostics. - **[ACKs & the commit contract](https://docs.cosyte.com/mllp/acks.md)**: the page to read before you put this in front of a clinical system. - **[Connection, reconnect & backpressure](https://docs.cosyte.com/mllp/reliability.md)**: the 6-state machine, backoff, dead-peer detection, and load shedding. - **[MLLPS / TLS](https://docs.cosyte.com/mllp/tls.md)**: mutual TLS, the ATNA TLS 1.2 floor, bind safety. - **[Testing & verification](https://docs.cosyte.com/mllp/testing.md)**: the socket-free in-memory transport, and the differential harness for checking a real engine before go-live. - **[Known limitations & non-goals](https://docs.cosyte.com/mllp/limitations.md)**: what _not_ to trust this package to do. Read it before you depend on it. - **[Conformance statement](https://docs.cosyte.com/mllp/conformance.md)**: the single document to hand a conformance reviewer, declaring the framing tolerances, the per-role acknowledgement verdicts and the IHE options in actor-and-option terms. - The **API reference** for every export, generated from source. - For parsing the payloads this transport carries, see **`@cosyte/hl7`**. --- URL: https://docs.cosyte.com/mllp/installation Markdown: https://docs.cosyte.com/mllp/installation.md # Installation `@cosyte/mllp` is a **zero-dependency** TypeScript MLLP client + server for Node.js. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration. It is **transport, not parsing**. It moves HL7 v2 bytes over TCP and never inspects the payload. > **Status:** published on npm and public, so the `npm install @cosyte/mllp` command below is live, > not aspirational. Where the package sits on the release ladder, and what that means for the > stability of its public surface, is stated in one place: > [API stability](https://docs.cosyte.com/mllp/limitations.md#api-stability). Read it before you pin. ## Prerequisites - **Node.js >= 22.** The whole `@cosyte/*` suite targets ES2023 / Node 22+. This package leans on `Symbol.asyncDispose` and `AbortSignal` on its public surface, both 2026 Node baseline. - A package manager: `pnpm`, `npm`, or `yarn`. - **No runtime dependencies.** The client, server, and framing are Node stdlib only (`net`, `tls`, `stream`, `events`, `buffer`, `timers`). ## Install ```bash npm install @cosyte/mllp ``` ### The optional `@cosyte/hl7` peer `@cosyte/hl7` is an **optional** peer dependency, needed **only** if you use the [`ack-from-hl7`](https://docs.cosyte.com/mllp/acks.md) subpath (which builds spec-correct ACKs by delegating the parsing to `@cosyte/hl7`). Everything else (framing, the client, the server, TLS, the in-memory transport) works without it. Install it only if you reach for that subpath: ```bash npm install @cosyte/hl7 ``` Calling `ack-from-hl7` without the peer installed throws a typed `MllpPeerMissingError`, not a bare module-not-found. ## Smoke test Confirm the package resolves and its version symbol is present: ```ts runnable import { VERSION } from "@cosyte/mllp"; typeof VERSION; // => "string" ``` If that resolves, the install is good. Head to the [Quickstart](https://docs.cosyte.com/mllp/quickstart.md). ## Module systems `@cosyte/mllp` is `"type": "module"` and exposes both conditions, so both of these resolve to the right build without extra configuration: ```ts // ESM / TypeScript import { MllpClient, MllpServer } from "@cosyte/mllp"; ``` ```js // CommonJS const { MllpClient, MllpServer } = require("@cosyte/mllp"); ``` The types are published per-condition (`.d.ts` for `import`, `.d.cts` for `require`) and gated by `attw` on every release, across all three subpaths (root, `/testing`, `/ack-from-hl7`), so editor IntelliSense matches the build you actually load. A build that would publish a tarball without those declarations fails the release rather than shipping. --- URL: https://docs.cosyte.com/mllp/quickstart Markdown: https://docs.cosyte.com/mllp/quickstart.md # Quickstart `@cosyte/mllp` is **transport, not parsing**: it frames HL7 v2 bytes onto the wire, correlates the ACK that comes back, and never inspects the payload. This page starts with the program the README opens with, a server and a client on loopback, then goes down to the frame (`VT + payload + FS + CR`) that everything else (the client, the server, reconnect, TLS) is built on. ## Send a message and read the acknowledgement Start a server, send it a message, and read the acknowledgement it built. Auto-ACK is on by default, and the server awaits your handler, which is the durable commit step, before it answers. The payload API is **Buffer-first** everywhere: HL7 v2 messages are raw bytes with caller-managed charset decoding. ```ts runnable import { createStarterClient, createStarterServer } from "@cosyte/mllp"; const committed: Buffer[] = []; // Port 0: the OS picks a free port. The host defaults to loopback, 127.0.0.1. const server = await createStarterServer({ port: 0, onMessage: async (payload) => { committed.push(payload); // your durable commit: a throw here answers AE, never AA }, }); const client = await createStarterClient({ host: "127.0.0.1", port: server.getStats().port ?? 0 }); const ack = await client.send( Buffer.from( "MSH|^~\\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20260101120000||ADT^A01|CTRL0001|P|2.5.1\r", ), ); // Log the shape of the acknowledgement, never its field values. const msa = ack .toString("utf8") .split("\r") .find((segment) => segment.startsWith("MSA|")) ?.split("|"); console.log("acknowledgement code:", msa?.[1]); console.log("MSA-2 echoes the control id sent:", msa?.[2] === "CTRL0001"); console.log("messages committed:", committed.length); await client.close(); await server.close(); ``` ```text acknowledgement code: AA MSA-2 echoes the control id sent: true messages committed: 1 ``` `MSA-2` echoes the control id you sent, byte for byte, which is what lets the client match the answer to the message. The acknowledgement's own `MSH-7` and `MSH-10` change on every run. A handler that throws is answered `AE`, never `AA`: see [ACKs & the commit contract](https://docs.cosyte.com/mllp/acks.md). ## Frame a message, read it back `encodeFrame` wraps a payload in the canonical MLLP envelope; `FrameReader` de-frames a byte stream back into payloads. The encoder is **strict** (it always emits `VT + payload + FS + CR`) and the reader hands each complete payload to `onFrame`, regardless of how the bytes were chunked on the wire: ```ts runnable import { encodeFrame, FrameReader } from "@cosyte/mllp"; // A synthetic HL7 v2 message: bytes the transport carries and never reads. const payload = Buffer.from("MSH|^~\\&|SEND|FAC|RECV|FAC|20260717||ADT^A01|MSG00001|P|2.5"); const frame = encodeFrame(payload); // Canonical framing: … . frame[0]; // => 0x0b frame[frame.length - 2]; // => 0x1c frame[frame.length - 1]; // => 0x0d // Feed the framed bytes back through a reader; the payload comes out byte-for-byte. const received: Buffer[] = []; new FrameReader({ onFrame: (p) => received.push(p) }).push(frame); received.length; // => 1 received[0]?.equals(payload); // => true ``` The payload round-trips **exactly**. The transport adds and strips three delimiter bytes and changes nothing in between. What those bytes _mean_ is the HL7 parser's job, not this package's. ## Tolerate a real sender's quirks, loudly Real senders drop the leading ``, append a stray ``, or pad with whitespace. A bare `FrameReader` is **strict by default**; you opt in to each tolerance flag by flag, and every tolerated deviation surfaces as a warning with a **stable code** rather than passing silently (Postel's Law): ```ts runnable import { FrameReader } from "@cosyte/mllp"; // A sender padded the frame with a leading space before . const padded = Buffer.from([0x20, 0x0b, 0x41, 0x1c, 0x0d]); // SP, VT, "A", FS, CR const codes = []; const reader = new FrameReader({ onFrame: () => {}, onWarning: (w) => codes.push(w.code), allowLeadingWhitespace: true, // opt in to tolerate the padding }); reader.push(padded); codes.includes("MLLP_LEADING_WHITESPACE"); // => true ``` The warning codes are a **public, versioned contract**. Log pipelines key on them, so renaming one is a breaking change. `MllpServer` ships tolerant defaults because it is the side that must accept what real senders emit; see [Framing & tolerance](https://docs.cosyte.com/mllp/framing.md) for the flag-by-flag table. ## The encoder refuses an unframable payload MLLP is **not byte-transparent**: the delimiters are literal byte values, so a payload must not itself contain `0x0B` (``) or `0x1C` (``). Rather than emit a frame a peer would mis-split, the strict encoder throws: ```ts runnable throws import { encodeFrame } from "@cosyte/mllp"; // The payload contains a raw (0x1C). It cannot be framed unambiguously. encodeFrame(Buffer.from([0x41, 0x1c, 0x42])); // throws MllpFramingError (MLLP_PAYLOAD_CONTAINS_FS) ``` That is why a payload's charset matters to the transport: UTF-16/UTF-32 put those bytes inside ordinary characters. Use a single-byte encoding, UTF-8, or Shift_JIS. See [Known limitations](https://docs.cosyte.com/mllp/limitations.md). ## Send a message over a connection On a real link, `createStarterClient` is the batteries-included path (auto-reconnect on, sensible backoff and backpressure). `send()` resolves with **the ACK correlated to your message**, not with whatever bytes arrive next: ```ts import { createStarterClient } from "@cosyte/mllp"; await using client = await createStarterClient({ host: "127.0.0.1", port: 2575 }); const ack = await client.send(Buffer.from(rawHl7)); // resolves on the correlated ACK frame // Pass an AbortSignal to bound any await; the client is disposed on scope exit. ``` `send` never resolves without its ACK, so a message can never silently "deliver". Reconnects, backoff, and backpressure are handled for you. See [Connection, reconnect & backpressure](https://docs.cosyte.com/mllp/reliability.md). ## Receive messages: with the commit contract Server-side, pair `autoAck: 'AA'` with an `onMessage` handler and the server treats your handler **as** the durable-commit step: it awaits it, and only then acknowledges: `AA` on success, a **negative** code if your handler throws. A positive ACK can never precede a successful commit: ```ts import { createServer } from "@cosyte/mllp"; const server = createServer({ autoAck: "AA", onMessage: async (payload, meta) => { // `payload` is a raw Buffer. Charset decoding stays with you. await db.commit(payload); // throw here ⇒ AE (a resend may succeed), never AA }, }); await server.listen(2575, "127.0.0.1"); ``` This is the page to internalize before you put the package in front of a clinical system. Read [ACKs & the commit contract](https://docs.cosyte.com/mllp/acks.md) next. > **About runnable examples.** The blocks tagged ` ```ts runnable ` above are extracted by the docs > build, executed against the package, and their `// =>` results asserted, so a documented example > can never silently drift from the code. The first one opens a loopback socket on a port the > operating system picks and closes both ends, so it runs as it stands. The two client and server > blocks after the framing examples name a fixed port and a database of yours, so they are shown as > plain ` ```ts ` illustrations. For socket-free _integration_ tests, the `@cosyte/mllp/testing` > subpath's in-memory transport wires a `Connection` end-to-end with no ports and no certs: > [Testing & verification](https://docs.cosyte.com/mllp/testing.md). ## Next - [Framing & tolerance](https://docs.cosyte.com/mllp/framing.md): the wire format, the opt-in tolerance flags, and the stable warning-code registry. - [ACKs & the commit contract](https://docs.cosyte.com/mllp/acks.md): the page to read before a clinical deployment. - [Connection, reconnect & backpressure](https://docs.cosyte.com/mllp/reliability.md): the 6-state machine, backoff, and load shedding. - [Testing & verification](https://docs.cosyte.com/mllp/testing.md): the socket-free in-memory transport, and the differential harness for checking a real engine before go-live. - [Known limitations & non-goals](https://docs.cosyte.com/mllp/limitations.md): what _not_ to trust this transport to do. --- URL: https://docs.cosyte.com/mllp/framing Markdown: https://docs.cosyte.com/mllp/framing.md # Framing & tolerance MLLP is a thin envelope around a payload. A frame is three delimiters and the bytes between them: ``` payload 0x0B ... 0x1C 0x0D ``` That is the whole protocol. Everything else (what the payload _means_, whether it was accepted, whether it should be resent) belongs to the HL7 v2 message inside, not to MLLP. `@cosyte/mllp` carries the bytes and never inspects them. MLLP is **not byte-transparent**: the delimiters are literal byte values, so a payload must not contain `0x0B` or `0x1C`. See [Limitations](https://docs.cosyte.com/mllp/limitations.md) for what that means for charsets. ## Postel's Law: strict encoder, liberal decoder The encoder is **strict by default**. `encodeFrame()` emits canonical `VT + payload + FS + CR`, and if the payload itself contains a framing byte it throws (`MLLP_PAYLOAD_CONTAINS_VT` / `MLLP_PAYLOAD_CONTAINS_FS`) rather than emitting a frame a peer would mis-split. The one opt-in that changes that is `allowDelimiterBytesInPayload`, which passes those bytes through verbatim with a warning each; the frame it produces _is_ one a conformant peer will mis-split, so turning it on is a deliberate decision to emit a non-conformant block. It is off by default on every path, including the `ack-from-hl7` builder that exposes it as a passthrough. It is declared, with the rest, on the [Conformance statement](https://docs.cosyte.com/mllp/conformance.md#encoder-deviations). The decoder is **liberal, but only where you opt in.** This is the part most easily misread: > **`FrameReader` is strict by default.** Tolerance is opt-in, flag by flag. The **server** is what > ships a tolerant default (it enables three of the four flags below), because it is the side that > has to accept whatever a real-world sender emits. ```ts import { FrameReader } from "@cosyte/mllp"; const reader = new FrameReader({ onFrame: (payload, byteOffset, warnings) => handle(payload), onWarning: (w) => logger.warn({ code: w.code, byteOffset: w.byteOffset }), allowFsOnly: true, // accept a frame ending with no allowLfAfterFs: true, // tolerate a stray after allowLeadingWhitespace: true, // tolerate padding before allowMissingLeadingVt: false, // still reject a frame with no }); reader.push(chunk); // chunk boundaries are irrelevant, frames may split across any number of chunks ``` `MllpServer` applies these defaults to every accepted connection, and merges anything you pass in `ServerOptions.framing` over the top: | Flag | `FrameReader` default | `MllpServer` default | | ------------------------ | --------------------- | -------------------- | | `allowFsOnly` | `false` | **`true`** | | `allowLfAfterFs` | `false` | **`true`** | | `allowLeadingWhitespace` | `false` | **`true`** | | `allowMissingLeadingVt` | `false` | `false` | The same four flags are declared as deviations from the strict block, each with its warning code and its per-role default, on the [Conformance statement](https://docs.cosyte.com/mllp/conformance.md#framing-tolerances). `allowMissingLeadingVt` stays **off** even on the server. A stream with no `` is not a tolerable quirk. It is an unframed stream, and guessing where a message starts is how you mis-split a clinical message. Turn it on only for a specific peer you have identified. Setting `strict: true` overrides every opt-in above and rejects all four deviations. ## Warning codes are a public API A tolerated deviation is never silent. It surfaces as a frozen `MllpWarning` carrying a **stable code** and the **absolute stream byte offset** where the anomaly was detected. Renaming or removing a code is a breaking change. Log pipelines and dashboards key on them. | Code | Meaning | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MLLP_MISSING_LEADING_VT` | Frame began without `` (requires `allowMissingLeadingVt`). | | `MLLP_FS_WITHOUT_CR` | Frame ended `` with no trailing ``. | | `MLLP_LF_AFTER_FS` | A stray `` followed ``, common from line-oriented senders. | | `MLLP_LEADING_WHITESPACE` | Padding bytes before ``. | | `MLLP_TRAILING_BYTES` | **Not benign junk: read this one.** **Reserved** for a `` appearing _mid-payload_: the partial payload accumulated so far is **discarded** and a new frame started, i.e. the delivered payload is only the **remnant** of a truncated message. It is frame-scoped (attached to the delivered remnant, never a neighbour) and is what the server's auto-ACK path keys on to refuse a positive `AA` for a destroyed message. (A stray byte after `` under `allowFsOnly` is reported by `MLLP_FS_WITHOUT_CR`, not this code.) | | `MLLP_PAYLOAD_CONTAINS_VT` | **Encoder, strict:** payload contains `0x0B`. Throws. | | `MLLP_PAYLOAD_CONTAINS_FS` | **Encoder, strict:** payload contains `0x1C`. Throws. | | `MLLP_EMPTY_PAYLOAD` | Nothing between `` and ``. | | `MLLP_FRAME_TOO_LARGE` | Accumulator exceeded `maxFrameSizeBytes`. **Throws**. See below. | | `MLLP_ACK_UNMATCHED_CONTROL_ID` | An inbound ACK's MSA-2 matched no pending send. | | `MLLP_ACK_AFTER_TIMEOUT` | A late ACK arrived after its send had already timed out. | `MLLP_EMPTY_PAYLOAD` and `MLLP_TRAILING_BYTES` remain _warnings_ (never throws) even under `strict: true`. Do not read `MLLP_TRAILING_BYTES` as cosmetic, though: its mid-payload `` case means a message was **truncated**, and it is worth alerting on. Three further codes (`MLLP_ACK_INBOUND_UNPARSEABLE`, `MLLP_ACK_CONTROL_ID_NOT_VERBATIM`, and `MLLP_ACK_CONTROL_ID_UNVERIFIABLE`) are scoped to the [`ack-from-hl7`](https://docs.cosyte.com/mllp/acks.md) subpath and appear in `MllpAck.warnings`, not in the framing registry. ## What throws, and what happens when it does `FrameReader.push()` **throws** `MllpFramingError` in two situations: 1. **`MLLP_FRAME_TOO_LARGE`**: the accumulator crossed `maxFrameSizeBytes`. A decoder that buffers until it sees `` is otherwise a memory-exhaustion vector: a peer that opens a socket, sends ``, and then streams forever would grow your process until it died. The default cap is **16 MB**. ```ts new FrameReader({ onFrame, maxFrameSizeBytes: 4 * 1024 * 1024 }); // 4 MiB ``` 2. **A structural violation whose tolerance opt-in is off**: `MLLP_MISSING_LEADING_VT`, `MLLP_FS_WITHOUT_CR`, or `MLLP_LF_AFTER_FS`. Since a bare `FrameReader` is strict by default, _all three_ throw unless you enable them. On an `MllpServer`, two are enabled by default, but `allowMissingLeadingVt` is **not**, so a single non-whitespace byte where a `` was expected throws on a default server. **A throw kills the connection, never the process.** `Connection` catches it, surfaces it as a frozen `'error'` event (`phase: 'receive'`, `connectionCause: 'framing-fatal'`, with the `MllpFramingError` preserved as `cause` so the stable `code` and `byteOffset` survive), and destroys **that connection only**. A server drops the one bad peer and keeps serving everyone else. This holds even when _your_ code is the thing that throws. Node calls event listeners **synchronously**, so a throwing subscriber unwinds the stack it was called from, and on this package's hot paths that stack bottoms out in a socket callback. Every event emitted by `Connection`, `MllpServer`, and `MllpClient` is therefore dispatched with containment: a throwing subscriber is reported on `'error'` and cannot take the process down with it, nor skip the work queued behind it. That last part matters as much as the crash: a throwing `'nack'` subscriber must not be able to suppress the negative ACK, and a throwing `'message'` subscriber must not be able to break ACK correlation. (A throwing `'error'` subscriber is the one case simply swallowed: reporting is what just failed, so there is nowhere left to report it to.) The one deliberate exception: if your **server** hits an accept-loop error (`EMFILE`/`ENFILE`) and you have **no** `'error'` listener attached, it still crashes loudly, on purpose. A silent accept outage on a healthcare listener is worse than a loud one. The connection is dropped rather than resynchronized on purpose: once `push` has thrown, the reader's position within the byte stream is no longer trustworthy, and guessing where the next frame begins is how a clinical message gets silently mis-split. `framing-fatal` is classified **permanent**, so a client does **not** auto-reconnect into it. A peer speaking something that is not MLLP (an HTTP probe on the wrong port, a health check) would otherwise be retried forever. See [Connection, reconnect & backpressure](https://docs.cosyte.com/mllp/reliability.md). If a peer's quirk is _expected_ (it pads with junk, omits the leading ``, sends bare ``) the tolerance opt-ins above are the supported answer. They turn the throw into a warning and recover the payload. ## Diagnostics never echo a run of payload bytes `MllpFramingError.snippet` carries **at most a single byte** (the one at the structural violation) and **never a run of payload content**. The payload of an HL7 v2 message is PHI, and an error message is the easiest way for PHI to escape into a log aggregator. - `MLLP_FRAME_TOO_LARGE` carries an **empty** snippet. The anomaly is the frame's _size_, not any particular byte, so there is nothing to show. - `MLLP_PAYLOAD_CONTAINS_VT` / `_FS` carry **only the offending delimiter byte**, itself a control byte the `code` already names. - `MLLP_MISSING_LEADING_VT` / `MLLP_FS_WITHOUT_CR` carry the **one byte found where a framing byte was expected**. Being fully precise: that byte is not itself a framing byte, so on a missing-`` stream it is the first byte of the unframed content (typically the `M` of `MSH`). One byte, never a run, but if even that is more than your threat model allows, do not log `snippet` directly. Warning `message` fields are stable, human-readable descriptions. They carry structural facts (a byte offset, an accumulated size) and **never a run of payload content**. The same one-byte exception the list above spells out applies here too: `MLLP_MISSING_LEADING_VT` and `MLLP_FS_WITHOUT_CR` render the hex of the single byte found where a framing byte was expected, which on a missing-`` stream is the first byte of the unframed content. The ACK-correlation warnings go further, because the value they report on is a **field**, not one byte. `MLLP_ACK_UNMATCHED_CONTROL_ID` and `MLLP_ACK_AFTER_TIMEOUT` take their `message` from a frozen registry: the text is identical for a given code no matter what arrived on the wire, and the control ID is reported only as `controlIdBytes`, its byte length. The ID itself is withheld because a warning goes to a log and MSH-10 is payload content. Nothing is lost by that: the outbound bytes are the payload you handed to `send()`, and the inbound frame reaches you on the `'message'` event. Correlate on `code` + `byteOffset`, and if you need the field values themselves, log them deliberately through your own PHI-aware channel. --- URL: https://docs.cosyte.com/mllp/acks Markdown: https://docs.cosyte.com/mllp/acks.md # ACKs & the commit contract This is the page that matters most. MLLP framing is trivial; **acknowledgement is where a transport can hurt a patient.** A positive acknowledgement (`AA`) means one thing to the sender: _"you may forget this message: I have it."_ A well-behaved sending system deletes its copy, or marks the message delivered and never retries. So if a receiver says `AA` for a message it then drops (because the database was down, because the handler threw, because the process was killed between "bytes received" and "row committed") the message is **silently gone**. No error, no retry, no alarm. A lab result, an allergy update, a discharge order: acknowledged and lost. `@cosyte/mllp` is built so that this is hard to do by accident. ## The commit contract **A positive ACK can never precede a successful durable commit.** Pair `autoAck: 'AA'` with an `onMessage` handler and the server treats your handler _as_ the commit step: it **awaits** it, and only then acknowledges. ```ts import { createServer } from "@cosyte/mllp"; const server = createServer({ autoAck: "AA", onMessage: async (payload, meta, conn) => { await db.commit(payload); // the durable-commit step // throw from here and the sender gets a NEGATIVE ack, never AA }, }); await server.listen(2575, "127.0.0.1"); ``` | Handler outcome | ACK the sender receives | | -------------------------------------------- | ------------------------------------------------------------------------------ | | Resolves | **`AA`**: application accept. MSA-2 echoes the inbound MSH-10. | | Throws / rejects | **`AE`**: application error. The sender **may resend**; a retry could succeed. | | Throws `new MllpAckError({ ackCode: "AR" })` | **`AR`**: application reject. Do not resend unchanged. | `AE` is the default on failure precisely because it is the _safe_ failure: it invites a retry. Use `AR` only when a retry of the identical bytes is guaranteed to fail again (a malformed message, an unsupported type). Telling a sender "don't bother resending" is a decision to lose the message if you are wrong. Do **not** call `conn.send()` inside `onMessage` when `autoAck` is set. The server is already acknowledging, and you would emit two ACKs for one message. ## `autoAck: 'AA'` with no handler is a _transport_-accept ```ts const server = createServer({ autoAck: "AA" }); // ⚠️ read this before using it ``` With no `onMessage` handler there is nothing to await, so the `AA` is sent on frame receipt. That `AA` truthfully means **"bytes received and framed"**, and nothing more. It does **not** mean "application-processed". This is safe only when something downstream owns durability _before_ the ACK goes out. On its own, for clinical messages, it is exactly the failure mode described at the top of this page. If you are reaching for it, reach for the commit-gated form instead. Whichever form you use, the auto-ACK path **never answers `AA` for a message it could not correlate**. A positive `AA`/`CA` is downgraded to a non-positive `AE`/`CE` when the inbound has no readable `MSH`, an empty MSH-10, a batch or concatenated-message shape a single MSA-2 cannot acknowledge, or bytes the decoder discarded mid-frame (`MLLP_TRAILING_BYTES`). An uncorrelatable positive ACK is worse than a negative one: the sender believes a message you never received was delivered, or resends it as a duplicate. The same downgrade guards `buildRawAck` directly. ### The auto-ACK answers in the half of Table 0008 the sender asked for HL7 Table 0008 has two halves: the application-mode `AA`/`AE`/`AR`, and the accept-mode `CA`/`CE`/`CR` of the enhanced acknowledgement protocol (v2.5.1 §2.9). Which half a responder answers in is not its own choice. **MSH-15** (accept acknowledgment type, HL7 Table 0155) states the conditions under which the sender wants an accept acknowledgement, and it is a _condition_, not a switch: `AL` always, `NE` never, `ER` on an error or reject only, `SU` on a successful completion only. So the request can only be evaluated once the disposition is known. The auto-ACK path evaluates exactly that. Rows are MSH-15; columns are the disposition the commit contract above reached; the cell is what goes on the wire: | MSH-15 | handler resolved | handler threw | handler threw `MllpAckError({ ackCode: 'AR' })` | | ------------------ | ---------------- | -------------- | ----------------------------------------------- | | absent / empty | `AA` | `AE` | `AR` | | `NE` | `AA` | `AE` | `AR` | | `AL` | **`CA`** | **`CE`** | **`CR`** | | `ER` | `AA` | **`CE`** | **`CR`** | | `SU` | **`CA`** | `AE` | `AR` | | outside Table 0155 | `AA` + warning | `AE` + warning | `AR` + warning | `CR` for a rejected message is what a commit acknowledgement is required to carry for an unrecognised message type or trigger event. Two inbound shapes leave that table and keep the answer they always had: a **batch or concatenated frame** (still the warned, non-positive `AE`, whatever its MSH-15 says: a single MSA-2 can echo one control ID, and commit-accepting a batch nobody read is the same lie in the other half of the table), and a **header the server cannot scan** for MSH-15 at all, which behaves exactly as it did before, adding no new failure. A value in MSH-15 that is not one of the four Table 0155 codes is **read as if the field were absent, reported, and never fatal**. The server emits an `'ackModeWarning'` event carrying `MLLP_ACK_ACCEPT_TYPE_UNRECOGNISED`, and the acknowledgement itself is not downgraded, negated or otherwise changed for that reason. Reading a field is not validating it, and answering a message your handler committed with anything but a positive code would make a discharged sender resend a message that is already in storage. ```ts server.on("ackModeWarning", ({ code }) => metrics.increment("mllp.ack_mode", { code })); ``` The server still sends **exactly one** acknowledgement per inbound message, in every mode. The later application acknowledgement of the two-part protocol is the consumer's to send; this package selects the code, it does not orchestrate the second exchange. Every acknowledgement mode is recorded with a separate client verdict and server verdict on the [Conformance statement](https://docs.cosyte.com/mllp/conformance.md#acknowledgement-modes). ## Full control - **`autoAck: fn`**: `fn(payload, meta, conn)` returns the ACK bytes. You own MSA-1 entirely, including enhanced-mode `CA`/`CE`/`CR`. Your `onMessage` handler, if present, runs first as an observer and its return value is ignored. - **`autoAck` unset (manual mode)**: your `onMessage` handler owns the response and sends it itself via `conn.send(encodeFrame(ackPayload))`. The `'message'` event always fires **before** the ACK is sent, whichever mode you are in. ## Failures are reported PHI-safely Whenever the server returns a negative ACK instead of `AA` on the auto-ACK path, it emits a `'nack'` event carrying only `{ connectionId, ackCode, reason }`. The payload never appears in it, and neither does the thrown error's message, which in real systems tends to contain exactly the record that failed to write. Nothing from the failure reaches the wire beyond the ACK code itself. The `reason` is a PHI-free enum: `'handler-rejected'` (a commit-gated handler threw/rejected), `'uncorrelatable-inbound'` (no readable `MSH`, empty MSH-10, or a batch/concatenated frame), or `'discarded-bytes'` (a mid-frame `VT` made the decoder discard bytes and deliver only a fragment). ```ts server.on("nack", ({ connectionId, ackCode, reason }) => metrics.increment("mllp.nack", { ackCode, reason }), ); ``` ## ACK correlation on the client `client.send()` resolves with **the ACK for your message**, not with whatever bytes arrive next. - **FIFO (default).** ACKs are matched to sends in order. - **`correlateByControlId: true`.** ACKs are matched by **MSH-10 → MSA-2**, which supports a peer that acknowledges out of order. An ACK whose MSA-2 matches nothing pending raises `MLLP_ACK_UNMATCHED_CONTROL_ID`; a late ACK for an already-timed-out send raises `MLLP_ACK_AFTER_TIMEOUT` and is dropped. `ackTimeoutMs` (default 30 s) bounds the wait. Its clock starts at the socket **write-flush**, not at the `send()` call. Time a message spent queued behind backpressure is not charged against the peer's response budget. **A timeout is not a failure to deliver.** It means you do not know. The message may have been committed by the receiver and the ACK lost on the way back. This is the at-least-once boundary; see [Limitations](https://docs.cosyte.com/mllp/limitations.md). ### Enhanced mode: one send, two acknowledgements HL7 v2.5.1 §2.9 separates the two things an acknowledgement can mean. The **accept** acknowledgement is the one that discharges the sender: the receiver has the bytes in safe storage and you may stop resending. The **application** acknowledgement is a later, separate exchange saying what the receiving application actually did with the message. A message asks for that protocol by carrying a non-null **MSH-15** or **MSH-16**, and §2.9 states the equivalence that ties the two protocols together: the original protocol _is_ the enhanced protocol with MSH-15 = `NE` and MSH-16 = `AL`. The single ACK this package has always correlated is, in that framing, the application acknowledgement. So on an interface that runs enhanced mode, `client.send()` gives you both: ```ts const client = createClient({ host, port, correlateByControlId: true }); const applicationAck = await client.send(payload, { // Fires when the peer commits the message. The send has NOT settled yet. onCommitAck: ({ code }) => logger.info({ commit: code }), // 'CA' }); // Resolves on the LATER application acknowledgement: read MSA-1 off it. ``` - A **`CA`** reports the commit disposition through `onCommitAck` and leaves the send pending. The report is scoped to your own send, which is how you know which message it belongs to. Nothing is logged to tell you. - The later **`AA`, `AE` or `AR`** settles the send, and all three settle it _successfully_, with the acknowledgement handed to you. `AE` and `AR` are the receiving application's clinical verdict on a message it did take custody of, and [judging that verdict is not this package's job](https://docs.cosyte.com/mllp/limitations.md#it-does-not-decide-clinical-acceptance). Read MSA-1 off the buffer you are given. - A **`CE` or `CR`** rejects the send at once with `MllpCommitRejectedError` (`err.commitCode`). The peer refused custody of the bytes, so no application acknowledgement is coming, and waiting out the second window would report the same failure later and less precisely. - MSH-15 `AL` with MSH-16 `NE` is a legal pair: it asks for the commit and nothing after it, so a single `CA` settles such a send. - An acknowledgement whose MSA-1 is **empty or outside Table 0008 is not guessed**. The send stays pending until its wait expires and the acknowledgement is surfaced with a stable code (`MLLP_ACK_MSA1_ABSENT`, `MLLP_ACK_MSA1_UNCLASSIFIABLE`), so it is reported rather than resolved into a success. A repeat `CA` for a send already reported is surfaced the same way (`MLLP_ACK_COMMIT_ALREADY_REPORTED`) and does not restart the wait. - A further acknowledgement for a send that is already settled or failed draws `MLLP_ACK_SEND_ALREADY_DISPOSED`, for as long as that send is remembered, however many arrive. **Two-phase correlation requires `correlateByControlId: true`.** MSA-2 is the only thing that can attribute a second acknowledgement to a send, and the default is FIFO. An enhanced-mode send on a FIFO client keeps the ordinary single-acknowledgement behaviour and emits `MLLP_ACK_TWO_PHASE_UNAVAILABLE`; it is not refused, and it is never left pending on an acknowledgement that would otherwise have settled it. **A value in MSH-15 or MSH-16 that is not one of the four Table 0155 codes is warned and defaulted, never refused.** The field reads as its default (MSH-15 as `NE`, MSH-16 as `AL`), a `MLLP_ACK_ACCEPT_TYPE_UNRECOGNISED` or `MLLP_ACK_APPLICATION_TYPE_UNRECOGNISED` warning names which field it was, and your bytes go to the wire unaltered. This is a transport: no send is refused, delayed or altered over the _value_ of a field. Both fields empty is an original-mode send, and nothing above applies to it. It behaves exactly as it always has: one acknowledgement, settled by the first match, on the same timeout with the same error. #### The two waits | wait | starts at | bounded by | ends with | | --------------------------- | -------------------------- | ------------------------- | ------------------------- | | first acknowledgement | the socket write-flush | `ackTimeoutMs` | `MllpTimeoutError` | | application acknowledgement | the accept acknowledgement | `applicationAckTimeoutMs` | `MllpApplicationAckError` | The second window defaults to whatever `ackTimeoutMs` is in force for that send, and takes a global or a per-send override. It is measured from the accept acknowledgement, not from the send: with both windows at 10 s, a `CA` at 9 s and an `AA` at 12 s settles the send successfully at 12 s, and the same send with no application acknowledgement fails at 19 s. Receiving the accept acknowledgement **stops** the first timer, so it can no longer expire against a send the peer has already answered. `MllpApplicationAckError` carries `commitCode` (the disposition you did receive) and a `reason` of `'timeout'` or `'connection-lost'`. It is a distinct type from `MllpTimeoutError` on purpose: that one means no acknowledgement arrived at all, and this one means the peer said in writing that it holds your message and then never reported what it did with it. A send waiting in that state when the link drops is **failed, not re-sent**: the peer already has the message, so putting it back on the wire is how one clinical message becomes two. A conditional application condition that is never met ends at that expiry. MSH-16 `SU` whose peer applies the message unsuccessfully, or `ER` whose peer applies it successfully, never draws a second acknowledgement, and the sender cannot know in advance which way it will go. That is the shape of the protocol, not a defect in this client. ### Correlation diagnostics report lengths, not control IDs Everything correlation reports is a number. The warning payload carries `controlIdBytes` (the control ID's byte length, or `null` when there was none to read) and `elapsedSinceSendMs`, and its `message` is a frozen registry entry that does not vary with the wire. `MllpTimeoutError` carries `messageControlIdBytes` for the same reason: an `Error` is logged, and its `stack` is what an error reporter ships off the box. The `'warning'` event carries three kinds, so narrow before reading any of their fields: a framing warning is a plain `MllpWarning` and has none of them; a correlation warning is an `AckCorrelationWarning`; an acknowledgement-mode warning is an `AckModeWarning`, whose `code` is an `AckModeCode` rather than a `WarningCode` and whose text comes from `ackModeDiagnosticMessage(code)`. An `AckModeWarning` is deliberately **not** counted in `getStats().warningsByCode`, whose keys are the decoder's own codes. ```ts import { ackDiagnosticMessage } from "@cosyte/mllp"; client.on("warning", (w) => { if (w.code === "MLLP_ACK_UNMATCHED_CONTROL_ID" || w.code === "MLLP_ACK_AFTER_TIMEOUT") { logger.warn({ code: w.code, idBytes: w.controlIdBytes, elapsedMs: w.elapsedSinceSendMs }); } else { logger.warn({ code: w.code, byteOffset: w.byteOffset }); } }); ``` `ackDiagnosticMessage(code)` is exported so you can compare a warning's `message` against the registry entry, or render your own text from the code and never log ours. MSH-10 is payload content, so it is not on any of those surfaces. You are not missing anything you do not already have: the outbound bytes are the payload you passed to `send()`, and the inbound ACK frame reaches you on the `'message'` event, both under your own PHI handling rather than your logger's. The same rule binds every acknowledgement-mode surface, in **both** correlation modes. An `AckModeWarning` carries stable codes, byte counts, byte offsets, elapsed times, and an MSA-1 value drawn from the closed six-code Table 0008 set, and nothing else: an MSA-1 the reader could not classify is reported by its **byte length** only (`msa1Bytes`), never by its bytes, because the whole reason a value is unclassifiable is that nobody knows what it is. `MllpApplicationAckError` and `MllpCommitRejectedError` report `messageControlIdBytes` for the same reason `MllpTimeoutError` does. The per-send `onCommitAck` report is not a diagnostic surface and is not restricted here: it hands you the accept acknowledgement for your own send, the same way `send()` already resolves with the whole ACK. ## Building spec-correct ACKs: `ack-from-hl7` Framing an ACK means building a real HL7 v2 `ACK^` message. That is _parsing_ work, and this package does not parse, so the optional `@cosyte/mllp/ack-from-hl7` subpath is a thin adapter over [`@cosyte/hl7`](https://github.com/cosyte/hl7)'s `buildAck`. It is the only place the two packages touch, and the peer is loaded lazily on first call. ```ts import { buildAckAA, buildAckAE } from "@cosyte/mllp/ack-from-hl7"; // Only after your handler durably committed (the commit contract): const { frame, code, correlationId } = buildAckAA(payload); conn.send(frame); // VT + ACK + FS + CR, MSA-2 echoing the inbound MSH-10 // On a processing failure, never acknowledge what you did not commit: conn.send(buildAckAE(payload, { error: { conditionCode: "207" } }).frame); ``` `buildMllpAck` is the core (explicit `code`); `buildAckAA/AE/AR/CA/CE/CR` are the six Table-0008 conveniences; `detectMode` reports original-vs-enhanced from the inbound MSH-15/16. **Fail-safe by construction:** an inbound message with no findable MSH-10 **never** yields a positive ACK. The disposition downgrades (`AA`→`AE`, `CA`→`CE`) and the result carries a warning: `ACK_NO_CORRELATION_ID` from the peer, or `MLLP_ACK_INBOUND_UNPARSEABLE` when the inbound could not be parsed at all. An unparseable message is one you cannot have understood, so it is one you must not accept. `@cosyte/hl7` is an **optional peer dependency**. Install it only if you use this subpath. Calling in without it throws a typed `MllpPeerMissingError` rather than a bare module-not-found. ### MSA-2 echoes MSH-10 _verbatim_, and says so when it can't HL7 v2.5.1 §2.9.2.2 requires MSA-2 to carry the inbound MSH-10 **verbatim**. That is not a formality: the sender keys its in-flight store on the control-ID bytes it put on the wire, so an ACK whose MSA-2 differs by a single byte is an ACK it cannot match. The send never settles, it times out, it resends, and the receiver commits the clinical message **twice**. So `buildMllpAck` decodes raw `Buffer` input as **`latin1`** and encodes the ACK back with the same codec. `latin1` is a 1:1 map between the 256 byte values and `U+0000`-`U+00FF`, which makes the round-trip the exact identity for _any_ inbound bytes, including a high-bit control ID under an `MSH-18` of `8859/1`. (It is the only codec for which that holds. `ascii` masks the high bit; `utf8` folds invalid sequences onto `U+FFFD`; and a `TextDecoder`'s `iso-8859-1` is aliased by the WHATWG Encoding Standard to **windows-1252**, which does not round-trip `0x80`-`0x9F` at all.) Every build is then **checked** against the same byte-level scanners the `@cosyte/mllp` client uses to correlate. If MSA-2 does not match the inbound MSH-10 byte-for-byte, the ACK still goes out (a mismatched ACK tells the peer _something_, which beats silence) but it carries a `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` warning. The warning reports the two byte _lengths_ and withholds the field values: MSH-10 is inbound payload content, a warning goes to a log, and a log is not a place PHI may reach. You already hold both byte strings: your inbound `payload`, and the returned `MllpAck.payload`. **Check your warnings.** ```ts import { buildAckAA, MLLP_ACK_CONTROL_ID_NOT_VERBATIM } from "@cosyte/mllp/ack-from-hl7"; const ack = buildAckAA(payload); if (ack.warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_NOT_VERBATIM)) { // This ACK will NOT correlate at the sender. Investigate before you ship it. } conn.send(ack.frame); ``` **Five** things provoke it. The first is yours; the other four are `@cosyte/hl7`'s serializer, which **re-emits MSH-10 in canonical form rather than copying its bytes**, so anything that form does not preserve, it cannot echo verbatim: 1. **A lossy `encoding` override**: a codec that cannot round-trip the inbound bytes. The default never does; set it only when the receiving system genuinely demands a specific codec. 2. **Non-default delimiters** (`MSH-1`/`MSH-2`). `@cosyte/hl7` always emits the HL7 default `|^~\&`, so an MSH-10 of `ID#X` under a `#` component separator is re-delimited to `ID^X`. 3. **Escape sequences.** Unescaped on read, re-escaped on write: `ID\X` comes back as `ID\E\X`. 4. **Whitespace.** Fields are trimmed: `MSG42 ` comes back as `MSG42`. 5. **Trailing empty components or subcomponents.** Canonicalized away: `ID^` and `ID&` both come back as `ID`. Each yields a _different_ MSH-10 on the wire, and so an ACK the sender cannot match. All five warn. And all five have the same answer: use **`buildRawAck`** (the root export, and what the server's `autoAck` path uses). It is parser-free (it copies the MSH-10 bytes rather than re-serializing them) so it holds the verbatim guarantee across escapes, padding, empty components, and **any** delimiter set. That last claim is exact, and it turns on one invariant worth stating: `buildRawAck` always emits the ACK under the **inbound's own** field separator, never a substituted one. MSH-10 is a product of splitting the inbound MSH _on_ that separator, so it provably cannot contain it, and MSA-2 is read back by splitting on that same separator, so the echo round-trips byte-for-byte regardless of what the control ID contains (a `|`, a `^`, an escape, anything). When an inbound declares a field separator that collides with the HL7 default encoding characters (`MSH-1` = `^`/`~`/`\`/`&`) and offers no usable `MSH-2` of its own, `buildRawAck` substitutes only the one colliding **encoding** character. It does **not** touch the field separator, precisely because the field separator is the only byte that could truncate MSA-2. ### Pass a `Buffer`. The guarantee is a byte guarantee. The verbatim _proof_ (the byte-for-byte comparison, and `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` when it fails) holds for a **`Buffer`** inbound, and _only_ for a `Buffer`. A `Buffer` is the wire bytes, so `buildMllpAck` can compare what it emitted against what actually arrived. Hand it a `string` (or an already-parsed `Hl7Message`) and the wire bytes are **already gone**. It re-encodes your text with the same codec it decodes it with, so the codec cancels on both sides and the verbatim proof cannot run: ```ts const wire = /* MSH-10 = A <0x8B> B, legal under MSH-18 = 8859/1 */; buildAckAA(wire); // MSA-2 = A <0x8B> B ✅ verbatim, verified buildAckAA(wire.toString("latin1")); // MSA-2 = A <0xC2 0x8B> B ⚠️ different id, and it SAYS so ``` The second is the natural call for anyone already holding a decoded payload, and it still emits a _different_ control ID the sender cannot correlate. The encoding is unchanged, because from decoded text there is no way to know the original bytes to encode back to. What changed is the **silence**: because a text inbound's echo cannot be _verified_, `buildMllpAck` no longer passes it off as clean. Whenever the ACK's MSA-2 control ID holds a non-ASCII **code unit** on a `string`/`Hl7Message` inbound (the range where the codec is load-bearing) it emits **`MLLP_ACK_CONTROL_ID_UNVERIFIABLE`**: an explicit "this echo cannot be verified; pass the raw `Buffer` for the byte-level guarantee". An all-ASCII control ID round-trips identically under every codec, so the common case stays quiet. The check reads the control ID's **pre-encoding code units**, not the emitted bytes, on purpose. A lossy `{ encoding: "ascii" }` override truncates a code unit to its low 8 bits, so a value above `0xFF` (say `U+0153`, what a windows-1252 decode yields for a `0x9C` wire byte) is masked _into_ the ASCII byte range (`0x53`, `'S'`). An emitted-byte proxy would fall silent on exactly that corruption; the code units carry the high bit whatever the codec did to the byte, so the strongly-discouraged text-plus-override path is flagged for the same reason the default is. ```ts import { buildAckAA, MLLP_ACK_CONTROL_ID_UNVERIFIABLE } from "@cosyte/mllp/ack-from-hl7"; const ack = buildAckAA(decodedText); if (ack.warnings.some((w) => w.code === MLLP_ACK_CONTROL_ID_UNVERIFIABLE)) { // Pass the raw payload Buffer instead. The echo cannot be verified from decoded text. } ``` It is a _cannot-verify_ signal, not a _known-broken_ one: from a decoded string the two are genuinely indistinguishable (a caller who decoded with `latin1` and re-encodes with `latin1` is byte-safe; one who decoded with `latin1` and lets the `utf8` default re-encode is not, and the string looks identical either way). This is why the package is `Buffer`-first on every public surface. **Pass the raw payload.** ### Only a _text_ codec is accepted, on every input shape The `encoding` override serializes the ACK back to bytes, so it must be a codec that writes characters as a byte stream a peer can read as HL7: `"utf8"`, `"ascii"`, `"latin1"`, or `"binary"`. A **non-text** codec is not: - `"base64"` / `"base64url"` / `"hex"` reinterpret the ACK **string** as encoded data and decode it to unrelated bytes; - `"utf16le"` / `"ucs2"` interleave a NUL after every byte. Either way the emitted frame is wholesale garbage the receiver cannot parse, so `buildMllpAck` **throws a `TypeError` at the boundary** rather than hand back an unusable ACK: ```ts buildAckAA(decodedText, { encoding: "base64" }); // ❌ throws TypeError, not a serializable ACK codec buildAckAA(wireBuffer, { encoding: "base64" }); // ❌ throws too, same reason, on a Buffer buildAckAA(wireBuffer, { encoding: "latin1" }); // ✅ charset codec: the byte-level escape hatch ``` This is a caller mistake, caught loudly and immediately. It applies to a **`Buffer` inbound too**: a non-text codec there garbles the _inbound_ decode so it never parses as `MSH` (routing to the unparseable fallback whose MSA-2 is empty, so the `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` check never runs) and then serializes that fallback ACK to garbage bytes that intermittently contain a framing delimiter and trip the strict frame encoder (`MllpFramingError`, ~3-4 % of calls, identically on Node 22 and 24). It was never the "loud AE" it was once documented to be. The legitimate byte-level escape hatch is unchanged: a **charset** codec on a `Buffer` (`"latin1"` byte-verbatim, or a lossy `"ascii"` that is still caught loudly by `MLLP_ACK_CONTROL_ID_NOT_VERBATIM`) is exactly what serves a receiving system that demands a specific byte-level codec. ### `ack-from-hl7` refuses an HL7 batch, loudly An HL7 batch (§2.10.3) is `[FHS] { [BHS] { MSH … } [BTS] } [FTS]`: a **sequence** of messages. `@cosyte/mllp` does not implement batch ACK, so `buildMllpAck` will not pretend to: an `FHS`/`BHS` envelope yields the warned, non-positive fallback (`AE` + `MLLP_ACK_INBOUND_UNPARSEABLE`, no correlation id). That is deliberate and it is the safe answer. Acknowledging the batch's _first_ message with a positive `AA` would tell the sender the whole batch was accepted, while messages 2..N were never looked at. They would be lost outright, or time out and resend as duplicates. A positive ACK for a message nobody read is precisely what the [commit contract](#the-commit-contract) exists to make impossible. Split the batch and ACK each message yourself, or handle it with `autoAck: fn`. ### Limits of the builder - **It trusts your disposition, with one fail-safe exception.** It never decides clinical accept/reject: you choose `AA`/`AE`/`AR` from your own commit outcome. The exception is a message it cannot correlate (no readable `MSH`, an empty MSH-10, or a batch/concatenated frame) where a requested positive `AA`/`CA` is **downgraded** to `AE`/`CE` rather than fabricate a positive disposition the sender cannot match. Both builders do this (`buildRawAck` on the raw path; `buildMllpAck` on an unparseable inbound). - **MSA-2 is byte-verbatim for a plain control ID under the HL7 default delimiters** (including a high-bit one) and, **on a `Buffer` inbound**, _loud_ rather than silently wrong in the five cases where it cannot be (above). It is the parser's canonical re-serialization, not a byte copy; `buildRawAck` is the byte copy. - **It does not ACK a batch.** An `FHS`/`BHS` envelope is refused with a warned, non-positive `AE`. A frame carrying two concatenated messages is a different shape and this builder does not detect it; which multi-message shape each acknowledgement route refuses is recorded route by route on the [Conformance statement](https://docs.cosyte.com/mllp/conformance.md#batch-and-concatenated-frames-route-by-route). - **It builds one ACK, it does not orchestrate an exchange.** The helpers build any of the six codes, and the server picks the right half of Table 0008 for the message it is answering ([above](#the-auto-ack-answers-in-the-half-of-table-0008-the-sender-asked-for)). _Sending_ a later application acknowledgement of your own, as the responding system in an enhanced-mode exchange, is still your orchestration: this package's server emits exactly one ACK per inbound message. The client side of that exchange is built, and correlates both halves; see [Enhanced mode](#enhanced-mode-one-send-two-acknowledgements). - **No MLLP Release 2 commit-ack bytes.** See [Limitations](https://docs.cosyte.com/mllp/limitations.md). --- URL: https://docs.cosyte.com/mllp/reliability Markdown: https://docs.cosyte.com/mllp/reliability.md # Connection, reconnect & backpressure An MLLP link lives for months and fails in ways a request/response client never sees: the peer half-closes and never tells you, a firewall silently evicts the NAT entry, an interface engine gets restarted nightly. This page covers what `@cosyte/mllp` does about that, and (just as importantly) what it hands back to you. ## The connection is an explicit state machine Never socket flags. `connection.state` is exactly one of six values, and the legal transitions are exactly these: ``` CONNECTING → CONNECTED | RECONNECTING | CLOSED CONNECTED → DRAINING | RECONNECTING | DISCONNECTED | CLOSED DRAINING → DISCONNECTED | CLOSED RECONNECTING → CONNECTING | CLOSED DISCONNECTED → CLOSED ← note: DISCONNECTED does NOT go back to RECONNECTING CLOSED → (terminal) ``` | State | Meaning | | -------------- | -------------------------------------------------------------- | | `CONNECTING` | Socket (and TLS handshake, if any) in progress. | | `CONNECTED` | Framing both ways; sends permitted. | | `DRAINING` | Graceful shutdown: no new sends, in-flight ACKs still awaited. | | `RECONNECTING` | Waiting out backoff before the next attempt. | | `DISCONNECTED` | Down, not (yet) retrying. | | `CLOSED` | **Terminal.** Never re-enters any other state. | Every transition emits a frozen `'stateChange'` event with `{ from, to, reason }`, which is what you put on a dashboard. Like every public event payload in this package, it is `Object.freeze`'d; subscribers cannot mutate shared state. ```ts client.on("stateChange", ({ from, to, reason }) => logger.info({ from, to, reason })); ``` **What you will actually observe on a client reconnect.** `MllpClient` builds a **fresh `Connection`** for each attempt rather than cycling one connection through `RECONNECTING`. So the `stateChange` stream you see across a reconnect is `CONNECTED → DISCONNECTED` on the old connection, then `CONNECTING → CONNECTED` on the new one. The client's own **`'reconnecting'` event** is what tells you a retry is in progress. Dashboard on that, not on a `RECONNECTING` state you will not see. ## Auto-reconnect `createClient` leaves it **off**. Pass `autoReconnect: true` to opt in. **`createStarterClient` turns it on for you** (it is the batteries-included path), so if you started from the quickstart you already have it. ```ts const client = createClient({ host: "mllp.example.com", port: 2575, autoReconnect: true, initialDelayMs: 100, // default maxDelayMs: 30_000, // default cap multiplier: 2, // default jitter: 0.2, // default ±20% }); ``` Backoff is exponential with jitter, capped at 30 s. Jitter is not decoration: without it, a hospital network blip reconnects every client on the floor in the same millisecond and you have built a thundering herd against an interface engine that is already unwell. **Only transient errors are retried.** The classifier (exported as `isTransientConnectionError`) distinguishes a network blip (retry it) from a permanent, _configuration-shaped_ failure. Auto-reconnecting into a misconfiguration is not resilience; it is an infinite loop against an endpoint that cannot possibly answer, and the backoff turns it into a storm. Classified **permanent** (reconnection halts): | Cause | Why retrying is pointless | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tls-verify` | Certificate verification failed. The next attempt meets the same certificate. | | `tls-handshake` | TLS protocol failure (`ERR_SSL_*`, `EPROTO`, OpenSSL alerts). Same. | | `framing-fatal` | The peer is not speaking MLLP (an HTTP probe, a health check, a wrong-port misconfiguration) or is emitting frames past the size cap. Every reconnect meets the same bytes. | | `ENOTFOUND`, `EACCES` | The name does not resolve; the port is not permitted. | Everything else (`ECONNRESET`, `ETIMEDOUT`, `ECONNREFUSED`, `EPIPE`, …) is treated as transient and retried: Postel's Law, applied to peer behavior. Supply `retryStrategy` for full control. It receives a frozen `RetryContext` and returns the next delay, or `null` to stop retrying. ## Detecting a peer that is gone but not closed Two independent, complementary mechanisms, both off by default, and they are not substitutes: - **`keepaliveIntervalMs`**: OS-level TCP keepalive (`socket.setKeepAlive`). Catches the cases where no bytes flow at all: half-open sockets, network partitions, NAT eviction. Cheap; costs nothing when the link is healthy. - **`deadPeerTimeoutMs`**: application-level idle timeout, keyed on last inbound activity. Catches the case TCP cannot see: the socket is _fine_, the peer process is wedged and simply not answering. On trip, the connection is destroyed and (if `autoReconnect` is on) reconnects. A quiet MLLP link is normal at 3 a.m. Set `deadPeerTimeoutMs` above your genuine idle period, or a healthy connection will be torn down all night. ## Backpressure If a peer stops ACKing but the socket still accepts writes, an unbounded client buffers every message your application hands it until the process dies. `@cosyte/mllp` bounds the in-flight + queued set. ```ts const client = createClient({ host, port, highWaterMark: 64, // default: max 64 in-flight + queued onBackpressure: "reject", // default }); ``` `highWaterMark` takes a count (`64`), a byte cap (`{ bytes: 8 * 1024 * 1024 }`), or both (`{ count, bytes }`, the stricter of the two wins). At the cap, `onBackpressure` decides: - **`'reject'` (default)**: `send()` rejects with `MllpBackpressureError`. Your application learns immediately that it is producing faster than the peer can accept, and can shed load, spill to a queue, or page someone. This is the honest default. - **`'wait'`**: `send()` awaits the `'drain'` event, the per-message `ackTimeoutMs`, or your `AbortSignal`, whichever fires first. Convenient, and the right choice when your producer _can_ slow down, but it converts backpressure into latency, so bound it. Set `pipeline: false` to collapse the in-flight set to one: strict send → await-ACK → send. Slower, and required by peers that cannot handle concurrent messages on one connection. ## Shutdown, and what it does _and does not_ do for in-flight messages ⚠️ **Read this before you rely on `close()` during a deploy.** `close()` **waits for the ACKs of sends already written to the transport**, bounded by `drainTimeoutMs` (default 30 s). The wait ends as soon as the last one arrives, so a peer that answers promptly costs you milliseconds, not the timeout; a link that fails mid-drain ends it too. `destroy()` is the opposite and is unchanged: everything settles at once, no ACK is awaited. What is still unresolved when the wait ends is reported **by population**, because whether a resend is safe depends on which one a message is in: | Report | What it means | Resend? | | -------------------------------------------------------- | ------------------------------------------------------------------- | ------- | | `MllpNeverDeliveredError` | still held inside the client; no bytes were written | safe | | `MllpUnknownFateError` | written, no ACK back. Carries `flushedAt`, `elapsedMs`, `byteCount` | unknown | | `MllpApplicationAckError({ reason: 'connection-lost' })` | the peer committed it; its application disposition never arrived | no | Narrow with `instanceof`, never on the text of a message. **A message reported with an unknown fate is exactly that: an _unknown_, not a failure.** It does **not** tell you the receiver did not commit it. The bytes may well have arrived, with the ACK coming back after you stopped listening. It is the same at-least-once boundary as an ACK timeout, and it is resolved the same way: by the receiver's idempotency on `MSH-10` + `MSH-7`, not by this library. Nothing is retried for you. If you would rather see none of these reports at all, **await your sends yourself** before calling `close()`: ```ts const inFlight = messages.map((m) => client.send(m)); await Promise.allSettled(inFlight); // settle first, await client.close(); // then close ``` Every closeable implements `Symbol.asyncDispose`, and every awaitable takes an `AbortSignal`: ```ts await using client = await createStarterClient({ host, port }); await client.send(payload, { signal: AbortSignal.timeout(5_000) }); // disposed on scope exit (including on throw), which closes, and so drains ``` Server-side, `close()` stops accepting new connections and closes existing ones. A commit-gated `onMessage` handler that is mid-`await` is **not** waited on, so the same rule applies: durability is your handler's job, and it must not depend on the process staying alive to finish. ## Observability `getStats()` returns **plain JSON-serializable objects** (no Buffers, no class instances) so they go straight into a log pipeline or a metrics exporter. ```ts logger.info(client.getStats()); ``` Nothing in the stats, the warnings, or the frozen event payloads carries message content. See [Framing](https://docs.cosyte.com/mllp/framing.md) for the PHI contract on diagnostics. --- URL: https://docs.cosyte.com/mllp/tls Markdown: https://docs.cosyte.com/mllp/tls.md # MLLPS / TLS `@cosyte/mllp` supports TLS-wrapped MLLP ("MLLPS") on both the client and the server, built on Node's `node:tls`: no bundled TLS implementation, no extra dependency. This page covers enabling TLS, mutual TLS (mTLS), the verification-on-by-default posture, bind safety, the TLS 1.2 floor, and the typed failure modes. Spec anchor: IHE ATNA, **ITI-19 Authenticate Node** (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html), the "STX: TLS 1.2 floor using BCP195 Option" (ITI TF-2 §3.19.6.2.3), spelled here as the published text spells it. What this package supplies against that option, and what stays yours, is declared on the [Conformance statement](https://docs.cosyte.com/mllp/conformance.md). ## Enabling TLS **Client**: pass `tls: true` for all-defaults (verification **on**), or a `TlsOptions` object to customize: ```ts import { createClient } from "@cosyte/mllp"; const client = createClient({ host: "mllp.example.com", port: 2575, tls: { ca: caPem }, // trust this CA; verification is on by default }); await client.connect(); ``` **Server**: `ServerOptions.tls` requires `cert` + `key`: ```ts import { createServer } from "@cosyte/mllp"; const server = createServer({ tls: { cert: certPem, key: keyPem }, }); await server.listen(2575, "127.0.0.1"); ``` When TLS is configured, the server consumes `tls.Server`'s `'secureConnection'` event (post-handshake sockets) instead of `net.Server`'s `'connection'`, and the client's `onConnect` transport hook maps to `'secureConnect'` (handshake complete) rather than the raw TCP `'connect'`. ## Mutual TLS (mTLS) `ServerTlsOptions.clientAuth` selects the ATNA ITI-19 mutual-authentication mode: | `clientAuth` | Client certificate | Behavior | | ------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `'NONE'` (default) | Not requested | Standard server-authenticated TLS only. | | `'WANT'` | Requested, not required | An absent or untrusted client cert does **not** reject the connection; the peer certificate (if any) is surfaced on the `'connection'` event as `peerCertificate`. | | `'MUST'` | Requested and required | ATNA mutual node authentication. A missing or untrusted client certificate rejects the handshake; the server never accepts the connection. | ```ts const server = createServer({ tls: { cert: certPem, key: keyPem, ca: clientCaPem, clientAuth: "MUST" }, }); server.on("connection", ({ peerCertificate }) => { // { subjectCN, issuerCN, validTo, authorized } | null, content-free, never the full cert object if (peerCertificate !== null) logger.info({ clientCN: peerCertificate.subjectCN }); }); ``` ```ts const client = createClient({ host: "mllp.example.com", port: 2575, tls: { ca: serverCaPem, cert: clientCertPem, key: clientKeyPem }, }); ``` The `'connection'` event's `peerCertificate` includes an `authorized` flag: whether the chain was **verified** against `ServerTlsOptions.ca`. ⚠️ Under `'WANT'`, a peer certificate can be present yet **unverified** (the connection is accepted regardless): never make authorization decisions on `subjectCN` unless `authorized` is `true`. Under `'MUST'` an unverified certificate never reaches the `'connection'` event, so `authorized` is always `true` there. **A note on TLS 1.3 and client-certificate rejection (RFC 8446 §4.4.2).** Under TLS 1.3, a client's own handshake (and therefore `connect()` resolving) can complete before a `clientAuth: 'MUST'` server finishes validating the client's certificate. **`connect()` resolving does not guarantee the server accepted your client certificate.** There is no synchronous signal at `'secureConnect'` time that reveals this, and the server's rejection alert arrives one network round-trip later. No fixed wait can close that gap on a real network. `@cosyte/mllp` handles it by **classification, not timing**: the rejection surfaces moments later as a typed post-connect error (an `'error'` event whose `MllpConnectionError.cause` carries the `ERR_SSL_*`/alert detail), and TLS-protocol-shaped errors are classified **permanent** (see `isTlsProtocolError`). An `autoReconnect` client will not loop against a server that will always reject it. **ACK correlation remains the delivery guarantee:** `send()` never resolves without its ACK, so a rejected session can never silently "deliver" a message. ## Verification is on by default Certificate verification defaults to **on** for every client connection. `tls: true` does not relax it. The only opt-out is the explicit, loud `allowUnverified` flag: ```ts const client = createClient({ host: "127.0.0.1", port: 2575, tls: { allowUnverified: true }, // NEVER do this against an untrusted network }); ``` There is no raw `rejectUnauthorized` surface on `TlsOptions`. `allowUnverified` is the only door, and it is loud by design. Every successful `secureConnect` on a connection configured this way (the initial connect **and every reconnect**) both: - emits a frozen `'securityWarning'` event: `{ code: 'MLLP_TLS_VERIFY_DISABLED', message, host, port, timestamp }` - calls `process.emitWarning(message, { code: 'MLLP_TLS_VERIFY_DISABLED' })` so an insecure connection cannot go unnoticed in logs, monitoring, or `--trace-warnings` output. ## Bind safety Two independent hardening changes apply to **every** `MllpServer`, TLS or plaintext: - **The default bind host is `'127.0.0.1'`**, not `'0.0.0.0'`. `listen(port)` with no host binds loopback only. - **Binding a wildcard host requires an explicit opt-in, enforced against the OS-normalized bound address.** Literal wildcard spellings (`'0.0.0.0'`, `'::'`, `''`, `'::0'`, `'0:0:0:0:0:0:0:0'`, `'::ffff:0.0.0.0'`) are rejected **before** binding. Spellings only the resolver can see (`'0'`, `'0.0'`, `'0x0.0.0.0'`, a hostname resolving to the unspecified address, …) are caught by a **post-bind check** on the address the OS actually bound (`server.address()`, always canonical): the just-bound server is closed immediately and `listen()` rejects with the same typed `MllpConnectionError`. No listening state is left behind, and no `'listening'` event is emitted. Whatever the spelling, a wildcard bind never survives without `ServerOptions.allowWildcardBind: true`: ```ts await server.listen(2575, "0.0.0.0"); // rejects: MllpConnectionError, "refusing to bind wildcard host '0.0.0.0'. // Set ServerOptions.allowWildcardBind: true to bind all interfaces" ``` ```ts const server = createServer({ allowWildcardBind: true }); await server.listen(2575, "0.0.0.0"); // binds; also emits a securityWarning ``` When a wildcard host is actually bound, the server emits the same loud pair as the TLS verification opt-out: a frozen `'securityWarning'` (`code: 'MLLP_BIND_ALL_INTERFACES'`) and `process.emitWarning`, once, at `listen()` time. `listen()` is also **single-flight**: a call while the server is already listening (or while another `listen()` is still in flight) rejects with a typed `MllpConnectionError` rather than racing the first call's post-bind safety checks. Call `close()` before re-listening; sequential `listen()` → `close()` → `listen()` works. This is what makes the no-state/no-event invariant above unconditional: no concurrent bind can ever record listening state for a socket the safety check just closed. ## TLS 1.2 floor and cipher suites `minVersion` defaults to `'TLSv1.2'` on both the client and the server: the IHE ATNA ITI-19 "STX: TLS 1.2 floor using BCP195 Option" (ITI TF-2 §3.19.6.2.3). `TlsOptions`/`ServerTlsOptions` only accept `'TLSv1.2' | 'TLSv1.3'` for `minVersion`/`maxVersion`. TLS 1.0/1.1 are not expressible through this API; the floor cannot be lowered by configuration. ITI TF-2 §3.19.6.2.3 names four TLS 1.2 cipher suites an actor claiming the transport-security option supports. They are listed here in both spellings that matter: the IANA spelling the standard prints (and the one the `'tlsNegotiated'` event below reports), and the OpenSSL spelling a cipher-list string is written in. | IANA name (ITI TF-2 §3.19.6.2.3) | OpenSSL name | | --------------------------------------- | ----------------------------- | | `TLS_DHE_RSA_WITH_AES_128_GCM_SHA256` | `DHE-RSA-AES128-GCM-SHA256` | | `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` | `ECDHE-RSA-AES128-GCM-SHA256` | | `TLS_DHE_RSA_WITH_AES_256_GCM_SHA384` | `DHE-RSA-AES256-GCM-SHA384` | | `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` | `ECDHE-RSA-AES256-GCM-SHA384` | ### Selecting the option: `atnaTransportSecurity` **Off by default.** With it off, `@cosyte/mllp` imposes no cipher list at all and the offered list is the runtime's, which a Node distribution may configure at build time and an operator may replace wholesale from outside the process (`--tls-cipher-list`, `NODE_OPTIONS`). What a link offers is then a property of the deployment, not of this package. Set `atnaTransportSecurity: true` on either side and the offered list becomes those four suites and nothing else, from this package rather than from the runtime: ```ts const server = createServer({ tls: { cert: certPem, key: keyPem, atnaTransportSecurity: true }, }); const client = createClient({ host: "mllp.example.com", port: 2575, tls: { ca: caPem, atnaTransportSecurity: true }, }); ``` ```ts runnable import { ATNA_CIPHER_SUITES, TLS13_DEFAULT_CIPHER_SUITES } from "@cosyte/mllp"; ATNA_CIPHER_SUITES.length; // => 4 TLS13_DEFAULT_CIPHER_SUITES.length; // => 3 ``` Four things to know before you turn it on: - **It only ever narrows what is offered, never widens it.** A peer that supports none of the four fails the handshake instead of negotiating something else. That is the point, and it is why the option is off by default: switching it on can stop a previously working link. - **TLS 1.3 is unaffected.** A TLS 1.3 suite is enabled only by its full name in the cipher list, so a list holding the four TLS 1.2 suites alone would silently turn TLS 1.3 **off**. The three TLS 1.3 suites the runtime enables by default (`TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256`, `TLS_AES_128_GCM_SHA256`) are therefore offered alongside the four. Two peers that reach TLS 1.3 without the option still reach TLS 1.3 with it. - **The server also provides ephemeral Diffie-Hellman parameters.** Two of the four suites are DHE, and a server with no DH parameters cannot actually offer a DHE suite: it would advertise the list and then fail every DHE handshake in it. The group is the one the TLS library selects for the certificate in use, unless you name your own with `dhParameters` (next section), which wins. - **It is mutually exclusive with `ciphers`.** Both declare the offered list, so setting both rejects `connect()` / `listen()` with a typed `MllpTlsConfigurationError` rather than silently discarding one of them. `ciphers` (an OpenSSL cipher-list string) remains available on either side for a list of your own choosing. A list the runtime rejects is a loud, typed failure at connect or listen time; it never falls back to the runtime default list. See "Typed failure modes" below. ### Supplying your own group: `dhParameters` Two of the four suites are DHE, and **a server with no ephemeral Diffie-Hellman parameters cannot offer a DHE suite at all**: it advertises the suite and then fails every handshake in it. `ServerTlsOptions.dhParameters` is how you hand it a group. ```ts import { readFileSync } from "node:fs"; const server = createServer({ tls: { cert: certPem, key: keyPem, ciphers: "DHE-RSA-AES128-GCM-SHA256", dhParameters: readFileSync("dhparam.pem", "utf8"), // openssl dhparam -out dhparam.pem 3072 }, }); ``` Two reasons to reach for it: - **Your site policy names a group.** Turning on `atnaTransportSecurity` gets you a group, but it is the one the TLS library selects for the certificate in use, not one you chose. - **You restricted `ciphers` yourself to a list containing a DHE suite.** Without parameters that list is unofferable in its DHE half, and this is the only way to make it work. What to know: - **The value is PEM content, never a filesystem path.** Every credential on these types is content and this package performs no disk IO for any of it: read the file yourself, as above. A path string is not a Diffie-Hellman parameter block and is refused as such. - **It takes precedence over the group `atnaTransportSecurity` selects.** Setting both is not a conflict and is the intended combination for a deployment with its own policy: the option decides which suites are offered, and this decides which group answers the DHE half of them. Unlike `atnaTransportSecurity` and `ciphers`, which both declare the offered list and therefore refuse each other, these two declare different things. - **There is no client-side counterpart, deliberately.** The side that answers the key exchange supplies the parameters, so this exists on `ServerTlsOptions` and not on `TlsOptions`. - **`"auto"` is not accepted here.** That is the TLS library's own spelling for its automatic selection, and `atnaTransportSecurity` is this package's way of asking for it. - **Parameters that cannot be used reject `listen()`,** with `MLLP_TLS_DH_PARAMETERS_REJECTED` (see the table below). Nothing binds, and nothing falls back to a server running without them. That check exists because the TLS library's own behaviour for parameters it cannot read is to **discard them in silence**, which leaves a listener that answers no DHE handshake and says nothing about why. - **The armour has to survive the trip.** The TLS library reads PEM a line at a time and wants each `-----BEGIN DH PARAMETERS-----` / `-----END DH PARAMETERS-----` boundary alone on its own line, with exactly five dashes on each side. A value that crosses an environment variable, a single-line JSON field or any other whitespace-collapsing layer loses its line breaks and stops being a parameter block, so it is refused here rather than discarded quietly later. Everything the library itself tolerates is tolerated: CRLF endings, a missing final newline, text before or after the block, and a body wrapped at any width or not at all. - **The body has to be the whole parameter structure, and only it.** Behind armour the library reads, what it then parses is a prime, a generator, and at most one optional private-value length behind them. A block carrying anything else, an extra field of any kind or a private-value length wider than the 32 bits the library reads it at, is discarded just as silently as damaged armour is, so it is refused here too. A parameter file from a related standard has exactly that extra field, and armouring one under this label is the wrong file rather than a typo: take `openssl dhparam` output and the case never arises. - **The two values have to be inside the bounds the library uses.** A perfect structure can still carry values the library will not answer a key exchange with, and that failure is invisible from outside: it loads the parameters, refuses them when the handshake asks for a key, and the listener advertises its DHE suite and answers nothing, exactly as if it had discarded them. So the same bounds are applied before anything binds: the prime must be odd, and the generator must be at least 2 and no greater than the prime minus 2. - **What is checked, and what is not.** The block is read before anything binds: it must be PEM, must be a `DH PARAMETERS` block, must decode to the parameter structure the library reads and nothing besides, must carry values inside the bounds above, and the library itself must accept the group (it refuses one below 1024 bits, and one below its configured security level). The one thing **not** checked is that the prime is prime, because a primality test costs seconds on a 3072-bit group and minutes on a large one, and `listen()` is not the place to spend it. A well-formed block whose modulus is composite is therefore accepted here, as it is by the library itself, and the link it carries is weaker than its size suggests. Generate parameters with a tool that produces valid ones (`openssl dhparam`) rather than relying on this check to find out. With neither `atnaTransportSecurity` nor `dhParameters` set, the server supplies no Diffie-Hellman parameters at all, exactly as before either option existed. **What this does not do.** It is the **cipher-suite half** of the transport-security option. Mutual node authentication is `clientAuth` plus a client `cert`/`key`, and the TLS 1.2 floor is `minVersion`, which already defaults to it. Claiming an IHE option is your declaration to make about your actor: this package can support it and evidence it, and cannot make it on anyone's behalf. ## Typed failure modes On the **initial `connect()` path**, TLS failures reject with `MllpConnectionError` carrying an additive `connectionCause`. (Failures on the auto-reconnect path surface as raw socket errors: the permanence classification below still applies to them, but they do not carry a `connectionCause`; see Known limitations.) - **`'tls-verify'`**: a certificate-verification failure: untrusted chain, expired/not-yet-valid certificate, hostname/SAN mismatch, revocation, and related codes (see `isTlsVerificationErrorCode`, also exported for callers who want the same classification). **Classified permanent** by `isTransientConnectionError`. An `autoReconnect` client will **not** loop into a misconfigured or MITM'd endpoint; the state machine goes straight to `CLOSED`. - **`'tls-handshake'`**: a **TLS-protocol-shaped** handshake failure observed before `'secureConnect'`: `ERR_SSL_*` codes, `EPROTO`, or an OpenSSL alert-bearing error (protocol version mismatch, no shared cipher, a required mutual-TLS client certificate rejected by the server, …). The exact boundary is `isTlsProtocolError`, exported for callers who want the same classification. Like `'tls-verify'`, TLS-protocol-shaped errors are classified **permanent** for the reconnect classifier. - **No `connectionCause`**: pure TCP-level failures (`ECONNREFUSED`, `ETIMEDOUT`, `EHOSTUNREACH`, a plain `ECONNRESET`, …) even on a TLS-configured connection. These carry the same shape as plaintext connect failures and stay **transient**. A network blip during a handshake still auto-heals. ```ts try { await client.connect(); } catch (err) { if (err instanceof MllpConnectionError && err.connectionCause === "tls-verify") { // Do not retry blindly. This is a configuration or MITM problem, not a network blip. } } ``` Server-side, a failed handshake (including a rejected client certificate under `clientAuth: 'MUST'`) never crashes the server and never stops it from serving other connections. It emits a frozen `'tlsClientError'` event: `{ remoteAddress, remotePort, message, code, timestamp }`. Only the error's message and code are surfaced, never payload bytes, never a certificate dump. ### Cipher-suite configuration errors A cipher-suite list this package cannot honour is refused **before** a socket exists, so nothing is left connected and nothing is left bound. `connect()` and `listen()` reject with `MllpTlsConfigurationError`, identified by `instanceof` plus a stable `code`, never by matching on the message text: | `code` | Meaning | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MLLP_TLS_CIPHER_LIST_REJECTED` | The TLS library refused the list: no suite in it is available in this build. There is **no fallback to the runtime default list**. | | `MLLP_TLS_CIPHER_OPTION_CONFLICT` | `atnaTransportSecurity` and `ciphers` both declare the offered list. Set exactly one. | | `MLLP_TLS_DH_PARAMETERS_REJECTED` | `ServerTlsOptions.dhParameters` is not usable: not PEM, not a `DH PARAMETERS` block, or a group the TLS library refuses. Nothing is bound and there is **no fallback to a server running without them**. Server side only. | ```ts try { await server.listen(2575); } catch (err) { if (err instanceof MllpTlsConfigurationError && err.code === MLLP_TLS_CIPHER_LIST_REJECTED) { // A configuration problem, not a network one. Nothing is listening. } } ``` The list is validated on its own, with no certificate, key or passphrase in scope, so the error cannot carry credential material. Its `message` is a fixed registry entry per code. A server constructed with a refused configuration stays refused: every `listen()` on it rejects the same way, and it never serves on some other list. ## Observability `client.getStats().tls` and `server.getStats().tls` report whether TLS is configured. `server.getStats().tlsClientErrorsTotal` counts `'tlsClientError'` events since `listen()`. ### What each link actually negotiated Both the client and the server emit a frozen `'tlsNegotiated'` event once per completed TLS handshake, carrying the protocol version and cipher suite that link agreed on. That is what turns a conformance claim into something a deployment can evidence from its own logs rather than assert. ```ts client.on("tlsNegotiated", ({ protocolVersion, cipherSuite, host, port }) => { logger.info({ tls: protocolVersion, suite: cipherSuite, host, port }); // e.g. { tls: 'TLSv1.2', suite: 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', ... } }); server.on("tlsNegotiated", ({ protocolVersion, cipherSuite }) => { metrics.increment(`mllp.tls.${protocolVersion}.${cipherSuite}`); }); ``` - `protocolVersion`, as the TLS library reports it: `'TLSv1.2'` or `'TLSv1.3'`. - `cipherSuite`, in its **IANA** spelling, the one ITI TF-2 §3.19.6.2.3 prints, so it compares against a conformance claim directly. The OpenSSL spelling is a different rendering of the same suite; see the table above. - `host` / `port`, the same routing metadata a `'securityWarning'` carries: the target address for a client, the bound address for a server. - `timestamp`, wall-clock time at emission. It fires **once per connection**, on the initial connect and on every reconnect, so a log records every session rather than only the first. It is **never** emitted for a plaintext connection: there is nothing negotiated to report. It carries no HL7 payload content and no certificate or key material, and it is emitted at handshake-completion time, before any HL7 byte has crossed the link. ## Known limitations - **No PKI or CA management.** `@cosyte/mllp` consumes PEM material you provide; it does not issue, rotate, or manage certificates. - **No certificate rotation/reload.** Certificates are read once at `tls.connect`/ `tls.createServer` construction time. Restart the process to rotate. - **No CRL/OCSP beyond Node's defaults.** Revocation checking is whatever `node:tls` does by default; there is no additional revocation-checking layer. - **Claiming an IHE option stays your declaration to make.** `atnaTransportSecurity` fixes what a link offers and `'tlsNegotiated'` evidences what it agreed on, but the option is claimed by an actor, not by a transport library, and this package cannot make that claim on anyone's behalf. - **With the option off, the cipher list is the runtime's**, and the runtime's is configurable at build time by a Node distribution and replaceable from outside the process. See "TLS 1.2 floor and cipher suites" above. - **Reconnect-path errors do not yet carry `connectionCause`.** The `tls-verify`/`tls-handshake` labels are attached on the initial `connect()` path only; auto-reconnect failures surface as raw socket errors (their transient/permanent classification still applies). --- URL: https://docs.cosyte.com/mllp/testing Markdown: https://docs.cosyte.com/mllp/testing.md # Testing & verification Two shipped tools, aimed at two different questions. `@cosyte/mllp/testing` answers **"does my code drive this library correctly?"** It gives you a socket-free transport, so a test that exercises framing, acknowledgement correlation, backpressure or a dropped connection runs in-process, deterministically, with no ports and no certificates. `runDifferential` answers **"does the engine on the other end behave the way I assume?"** It sends a small corpus of synthetic messages at a peer you name and reports what came back. It is an observation, never a verdict. Both ship in the published package. Neither is a devDependency you have to add. ## Test without a socket: the in-memory transport `InMemoryTransport.pair()` returns two connected, in-process ends. A write to one end delivers to the other end's `onData` handler **synchronously**, before `write()` returns, so a test needs no `await`, no fake timers and no cleanup of a listening port. ```ts runnable import { encodeFrame, FrameReader } from "@cosyte/mllp"; import { InMemoryTransport } from "@cosyte/mllp/testing"; // Two connected, in-process ends. const [clientSide, serverSide] = InMemoryTransport.pair(); // Stand a de-framer up on the receiving end, the way a server would. const received = []; const reader = new FrameReader({ onFrame: (payload) => received.push(payload) }); serverSide.onData((chunk) => reader.push(chunk)); // Synthetic bytes. The transport carries them and never reads them. const message = Buffer.from("MSH|^~\\&|SEND|FAC|RECV|FAC|20260717||ADT^A01|MSG00001|P|2.5"); clientSide.write(encodeFrame(message)); // Already through, with nothing awaited. received.length; // => 1 received[0].equals(message); // => true ``` ### Simulating the conditions a real link produces The pair models the four things a socket does to you, each on demand: | Call | What it simulates | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `split(n)` | Chunked reads. Every subsequent delivery to that end arrives `n` bytes at a time, so a frame is split across as many reads as you like. `split(0)` turns it off. | | `pause()` / `resume()` | Backpressure. While paused, writes aimed at that end are queued and `onData` is not called; `resume()` flushes the queue synchronously, in write order. A queued write returns `false`, the same signal a congested socket gives. | | `destroy(reason)` | A one-sided abrupt disconnect. Fires `onError(reason)` then `onClose` on that end only, leaving the peer untouched, which is what a reset on one socket looks like. | | `close()` | A graceful close. Fires `onClose` on both ends, the way a FIN exchange does. | `simulateConnect()` fires the end's `onConnect` handler, standing in for the moment a real socket finishes connecting. Chunk boundaries are the ones worth exercising deliberately, because a frame split across reads is the case a hand-rolled de-framer gets wrong: ```ts runnable import { encodeFrame, FrameReader } from "@cosyte/mllp"; import { InMemoryTransport } from "@cosyte/mllp/testing"; const [clientSide, serverSide] = InMemoryTransport.pair(); serverSide.split(1); // one byte per read: the worst a real socket can do const frames = []; const reader = new FrameReader({ onFrame: (payload) => frames.push(payload) }); serverSide.onData((chunk) => reader.push(chunk)); clientSide.write(encodeFrame(Buffer.from("A"))); // Four separate reads, one reassembled frame. frames.length; // => 1 frames[0].toString(); // => "A" ``` ### Driving a whole `Connection` over it A pair is a `Transport`, so it goes straight into a `Connection` and you get the state machine, acknowledgement correlation and the warning stream with no network underneath: ```ts import { Connection, encodeFrame } from "@cosyte/mllp"; import { InMemoryTransport } from "@cosyte/mllp/testing"; const [clientSide, serverSide] = InMemoryTransport.pair(); const conn = new Connection({ transport: clientSide, onMessage: (payload) => handle(payload), // framing already stripped onWarning: (w) => logger.warn({ code: w.code, byteOffset: w.byteOffset }), }); // The other end is your test double: answer, stay silent, or drop, whichever the case needs. serverSide.onData(() => serverSide.write(encodeFrame(ackBytes))); ``` Answering nothing is how you exercise an acknowledgement timeout without waiting on a real peer; `serverSide.destroy()` is how you exercise a reset; `serverSide.pause()` is how you exercise backpressure. This package holds itself to the same rule: every test here that can run over the pair does, and real sockets are reserved for integration smoke tests. See [Connection, reconnect & backpressure](https://docs.cosyte.com/mllp/reliability.md) for the states and events you will observe. One thing the pair deliberately refuses: writing to a peer from inside that peer's own `onData` handler throws. Re-entrant delivery would either recurse without end or reorder frames, and a transport that silently reordered frames would make every test above prove the wrong thing. ## Verify your own engine: `runDifferential` Interoperability is proven in this repository against freely available engines only. Whatever you are integrating with is very probably not one of them, and its behaviour is not something this package will claim to know. See [It is not differentially verified against Epic or Cerner](https://docs.cosyte.com/mllp/limitations.md#it-is-not-differentially-verified-against-epic-or-cerner-so-verify-your-own-engine). What ships instead is the harness, so you can observe your own engine before go-live. `runDifferential` opens one connection per message, sends a small corpus of synthetic messages, and reports for each exchange whether the frame that came back was byte-identical to the canonical Release 1 block (`VT` + payload + `FS` + `CR`) and whether its `MSA-2` echoed the `MSH-10` of the message it answered. ```ts // differential.ts import { runDifferential } from "@cosyte/mllp"; const report = await runDifferential({ peer: process.env["MLLP_DIFF_PEER"] }); console.log(JSON.stringify(report, null, 2)); ``` ```bash MLLP_DIFF_PEER=engine.staging.example:2575 node differential.js ``` > **The run sends messages INTO whatever engine you aim it at.** They are synthetic patients, and an > engine that accepts them stores synthetic patients: an admit and an observation result will land in > whatever that endpoint feeds. Aim it at a test or staging endpoint. Never at a production interface > carrying live traffic, and never at an endpoint you do not own. Four things about the report, so you know what you are holding: - **It is an observation, not a verdict.** `result` is one of `parity-observed`, `deviations-observed`, `no-observation` or `skipped`. None of them says a peer is conformant, and a run in which nothing was answered is never presented as a success. Whether an interface is fit to carry clinical traffic is your call, with this evidence in front of you. - **A deviation is named, not guessed at.** Each one carries a stable warning code and a byte offset: `MLLP_MISSING_LEADING_VT`, `MLLP_FS_WITHOUT_CR`, `MLLP_LF_AFTER_FS`, `MLLP_LEADING_WHITESPACE` and `MLLP_FRAME_TOO_LARGE` for the block, and `MLLP_ACK_UNMATCHED_CONTROL_ID` when an acknowledgement does not echo the control ID it answered. See [Framing & tolerance](https://docs.cosyte.com/mllp/framing.md). - **It carries no payload content, by construction.** A code, a byte offset and structural counts. Not a run of the peer's bytes, not the acknowledged control ID, not a truncation of either, so a report is safe to attach to a ticket. That matters, because the engine you aim this at may hold real patients. - **It is JSON.** Plain objects, numbers and strings throughout, so a pipeline reads it without scraping text. Per exchange you get `outcome` (`answered`, `unanswered`, `undecodable-response`, `connection-refused`, `connection-failed` or `connection-dropped`), `byteParity`, `correlation`, the warning codes with their byte offsets, byte counts in both directions, and the elapsed time. The deadline is per exchange and defaults to 10 seconds, so one cold or unreachable endpoint costs one exchange rather than the whole run. With no peer configured (`MLLP_DIFF_PEER` unset or empty) the run **skips** and returns `result: 'skipped'`, so it is safe to leave in a test suite that also runs where no engine exists. An address that is _present_ and is not a `host:port` is a different case: it throws `MllpDifferentialConfigurationError` naming the value, because a silent skip there would read as proof the harness ran. Byte parity means the MLLP **envelope**, compared against the canonical block this package emits. It is not equality of message content, which can never hold: an acknowledgement carries the peer's own control ID, its own timestamp and its own sending application, and it is supposed to. Point it at something you can reach over TLS by supplying your own `connect`, which is handed the resolved peer and returns a transport that is not connected yet: ```ts import tls from "node:tls"; import { runDifferential, TlsTransport } from "@cosyte/mllp"; const report = await runDifferential({ peer: process.env["MLLP_DIFF_PEER"], connect: (peer) => new TlsTransport(tls.connect({ host: peer.host, port: peer.port, ca })), }); ``` ## Next - [Framing & tolerance](https://docs.cosyte.com/mllp/framing.md): the wire format and every warning code a report can name. - [Connection, reconnect & backpressure](https://docs.cosyte.com/mllp/reliability.md): the states and events a pair-driven `Connection` walks through. - [Known limitations & non-goals](https://docs.cosyte.com/mllp/limitations.md): what an observation is evidence of, and what it is not. --- URL: https://docs.cosyte.com/mllp/conformance Markdown: https://docs.cosyte.com/mllp/conformance.md # Conformance statement This page is the one document to hand a conformance reviewer. It declares, in the terms a review is recorded in, which framing behaviours, acknowledgement modes and transport-security options `@cosyte/mllp` implements, and which it does not. **Version declared:** `0.1.1` Everything below describes that release. A conformance behaviour cannot move in this package without this page moving in the same change: the declared tolerance sets on both halves of the codec, the declared warning codes, the per-role acknowledgement verdicts, the declared option names and the version above are all checked against the shipped code by the package's own test suite, which fails and names what drifted. ## What this is, and who did the checking This is a **self-declaration**. It is the package's own statement about its own behaviour, written by the people who wrote the code and verified by the code's own test suite. It is **not** a third-party attestation, and nothing on this page has been assessed by anyone outside this project. That distinction is the whole reason IHE publishes two different kinds of record, and they differ in **who did the checking**: - **An IHE Integration Statement, entered in the Product Registry.** A vendor's own declaration about its own product. Nobody else checks it before it is published. This page is written to feed that route: it gives you the actor-and-option wording such an entry is recorded in. - **Connectathon results, published by IHE.** IHE's own record of what was actually tested, vendor-to-vendor, under supervision, at a Connectathon. That is a third-party record, and it is the one this page cannot substitute for. `@cosyte/mllp` is a **library, not an IHE actor**. An actor is a system a deployer builds and operates; a transport library is one component inside it. So no option below is recorded as claimed, by us or on anyone's behalf. Each one names what this package supplies and what remains yours to do before you enter it in a Product Registry statement or take it to a Connectathon. There is also no implementable profile of MLLP itself to conform to: HL7 v2's conformance profiles profile **messages**, not the lower layer. The external bar for a transport is assembled from the framing block, the HL7 v2 acknowledgement model, IHE's profile of that model, and the IHE ATNA transport-security options, which is exactly what the sections below cover. ### How to read the verdicts Every verdict on this page is one of exactly three words, and they are mutually exclusive: | Verdict | Meaning | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Supported** | The behaviour is implemented in this release and is exercised by this package's own tests. | | **Not supported** | The behaviour is **absent**. It is not implemented, and nothing here approximates it. | | **Unverified** | The behaviour is neither claimed present nor claimed absent. Something is delegated or untested, so we have not established it, and an inference from a specification is not an observation. Every one is listed with its reason under [Behaviours recorded as unverified](#behaviours-recorded-as-unverified). | ## Framing tolerances The strict block is `` payload `` ``. Both halves of the codec default to it, and every departure from it, in either direction, is an explicit opt-in that emits a stable warning code. Each row in the two tables below is therefore a **declared deviation from the strict block**, never a silent one. The decoder is where nearly all of it lives: it is the side that has to survive what a real sender emits. | Option | Strict-block behaviour it deviates from | Warning code | Standalone reader | Server | | ------------------------ | ---------------------------------------------------------- | ------------------------- | ----------------- | ------ | | `allowFsOnly` | A frame ends `` ``; the `` is required. | `MLLP_FS_WITHOUT_CR` | off | on | | `allowLfAfterFs` | The byte after `` is ``, never ``. | `MLLP_LF_AFTER_FS` | off | on | | `allowLeadingWhitespace` | A frame opens at `` with no padding before it. | `MLLP_LEADING_WHITESPACE` | off | on | | `allowMissingLeadingVt` | A frame opens at ``; a stream without one is unframed. | `MLLP_MISSING_LEADING_VT` | off | off | "Standalone reader" is a `FrameReader` you construct yourself. "Server" is every connection an `MllpServer` accepts, which applies the defaults above and merges anything you pass in `ServerOptions.framing` over the top. So three of the four deviations are **on by default on the server** and none is on by default for a standalone reader: a reviewer can tell a deviation the package ships on from one a deployment opted into by reading that pair of columns. With a tolerance **off**, the deviation is not tolerated at all: `FrameReader.push()` throws `MllpFramingError`, and `Connection` destroys that connection rather than resynchronizing it. With it **on**, the deviation becomes the warning code in the table and the payload is recovered. Two more rules bound the set: - **`strict: true` overrides every opt-in above.** Every deviation in the table throws again even where its individual option is enabled. There is no configuration in which these are tolerated more widely than this table says, and every row is exercised in both directions to keep that true. - **`allowMissingLeadingVt` stays off even on the server.** A stream with no `` is not a tolerable quirk; it is an unframed stream, and guessing where a message starts is how a clinical message gets mis-split. The warning codes are a **public API** of this package: renaming or removing one is a breaking change. Two further framing warnings (`MLLP_EMPTY_PAYLOAD` and `MLLP_TRAILING_BYTES`) are **not** tolerance opt-ins, are always warnings and never throw, and are therefore not rows above; see [Framing and tolerance](https://docs.cosyte.com/mllp/framing.md) for the full code table. ### Encoder deviations The encoder defaults to strict and, on the default, has no deviation to declare: `encodeFrame()` emits the canonical block every time, and refuses a payload it could not frame unambiguously rather than emitting one a peer would mis-split. There is nonetheless **one** encoder-side opt-in, and a reviewer should see it here rather than discover it in the type signature: | Option | Strict-block behaviour it deviates from | Warning code | Default | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ------- | | `allowDelimiterBytesInPayload` | The payload contains no `0x0B` and no `0x1C`, because MLLP is not byte-transparent and those bytes are structural. | `MLLP_PAYLOAD_CONTAINS_VT`, `MLLP_PAYLOAD_CONTAINS_FS` | off | Off, a payload carrying either byte throws `MllpFramingError` with the matching code and no frame is produced. On, the offending bytes are passed through verbatim, one warning is emitted per byte, and the frame that goes out is one **a conformant peer will mis-split**: its decoder will read the embedded `0x0B` as the start of a new frame or the embedded `0x1C` as the end of this one. Turning it on is therefore a deliberate decision to emit a non-conformant block, and it is off on every path this package takes by itself, including the acknowledgement builder that exposes it as a passthrough. The honest reading for a conformance review: on defaults, this package cannot emit a non-canonical block. That property is a **default**, not a structural guarantee, and this page will not record it as the latter. ## Acknowledgement modes HL7 v2.5.1 §2.9 splits acknowledgement into two protocols. In the **original** protocol one message draws one acknowledgement. In the **enhanced** protocol it draws two: an _accept_ acknowledgement (`CA`/`CE`/`CR`, "I have your bytes in safe storage; stop resending") and a later, separate _application_ acknowledgement (`AA`/`AE`/`AR`, "here is what my application did with it"). The original protocol is the enhanced protocol with MSH-15 = `NE` and MSH-16 = `AL`. Each mode is recorded separately for the two roles this package ships, because the two do not agree: | Acknowledgement mode | Client role | Server role | | ------------------------------------------ | ------------- | ------------- | | Original mode | Supported | Supported | | Enhanced mode, accept acknowledgement | Supported | Supported | | Enhanced mode, application acknowledgement | Supported | Not supported | | MLLP Release 2 commit acknowledgement | Not supported | Not supported | | Batch acknowledgement | Not supported | Not supported | Row by row: - **Original mode.** One acknowledgement per message, correlated to the send. The client correlates FIFO by default, or by MSH-10 to MSA-2 with `correlateByControlId: true`. On the server the automatic acknowledgement is **opt-in**: with `autoAck` set, every inbound message is answered, and with `autoAck` left unset the server sends nothing of its own, because the message handler owns the response through `conn.send()`. The starter server sets `autoAck` to `AA` for you; the plain server does not. Where the automatic acknowledgement is paired with a message handler, the handler is awaited and a positive `AA` is structurally unable to precede it; with no handler there is nothing to await, and the `AA` truthfully means "bytes received and framed" and nothing more. - **Enhanced mode, accept acknowledgement.** The client accepts a `CA` without settling the send, reports it through the per-send `onCommitAck` callback, and rejects the send at once on a `CE` or `CR`. The server answers in the accept half of Table 0008 when the inbound message's MSH-15 asks for it, evaluated against the disposition its handler reached. - **Enhanced mode, application acknowledgement.** **Supported in the client role, not supported in the server role**, and that asymmetry is deliberate rather than an oversight. The client waits for the later application acknowledgement on its own bound and settles the send on it. **This package's server emits exactly one acknowledgement per inbound message, in every mode**, so the second exchange is the consumer's to orchestrate: a system that must answer twice sends the later acknowledgement itself. Pointing this client at this server with both MSH-15 and MSH-16 asking for acknowledgement therefore ends at the application-acknowledgement timeout. - **MLLP Release 2 commit acknowledgement.** Absent on both roles. The Release 2 commit blocks and Release 2's synchronous "no new content until acknowledged" discipline are not implemented at all, so there is no Release 2 code path a Release 1 link could downgrade into. - **Batch acknowledgement.** Absent on both roles. Nothing here parses a batch, re-bases on the batch's first `MSH`, or answers a `BTS-1` count of messages with one acknowledgement. A positive acknowledgement correlated to the batch's first message would tell a sender the whole batch was accepted while messages 2..N went unread, so refusing is the safe answer, not a gap waiting to be filled with a guess. **What is refused, and by which of this package's two acknowledgement-building routes, is not uniform**, and a responder-builder has to know the difference: it is stated route by route in [Batch and concatenated frames, route by route](#batch-and-concatenated-frames-route-by-route) below rather than summarized here. Enhanced-mode correlation of both halves requires `correlateByControlId: true`, because MSA-2 is the only thing that can attribute a second acknowledgement to a send. An enhanced-mode send on a FIFO client keeps the ordinary single-acknowledgement behaviour and warns rather than being refused. Full detail: [ACKs and the commit contract](https://docs.cosyte.com/mllp/acks.md). ### Batch and concatenated frames, route by route Two frame shapes carry more than one message: an `FHS`/`BHS` batch envelope (§2.10.3), and two or more complete messages concatenated into a single frame, one `MSH` after another. Neither is acknowledged as a batch, per the row above. But this package ships **two** acknowledgement-building routes, and they do not detect those two shapes equally, so the answer is recorded per route rather than as one sentence covering both: | Acknowledgement route | An `FHS`/`BHS` batch envelope | A second `MSH` in the same frame | | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | The server's automatic acknowledgement, `autoAck`, and the raw builder behind it | Refused: a requested positive `AA`/`CA` is downgraded to the non-positive `AE`/`CE` | Refused: a requested positive `AA`/`CA` is downgraded to the non-positive `AE`/`CE` | | The parser-backed builder on the `@cosyte/mllp/ack-from-hl7` subpath | Refused: the warned, non-positive `AE`, carrying `MLLP_ACK_INBOUND_UNPARSEABLE` and no correlation id | Not detected: a positive `AA` correlated to the **first** message, with no warning | **The one cell that is not a refusal is a limitation of this release, named here rather than left for a reviewer to discover.** The parser-backed builder strips leading segment terminators and hands what follows to the parser, which reads the first message and stops; a batch envelope has no parseable message at its head and so falls out into the warned refusal, but a second message sitting behind a perfectly good first one is neither read nor reported. Concatenated frames are rarer than batch envelopes and are not what that route is documented for, which is why the gap has stood; it is a gap all the same. What that means for a deploying actor: if you answer inbound messages through that subpath, **detect the shape yourself before you choose a disposition**. `rawAckUncorrelatable(payload)` is exported for exactly this and returns `true` for both shapes in the table. The server's automatic acknowledgement already applies it, which is why its row reads the way it does, and a system on that route needs to do nothing further. All four cells are exercised against the shipped package on every test run of this repository, including the one that is not a refusal. If any of the four outcomes moves, this table fails the build until it moves with it. ## IHE options Each row names the **actor** the option is recorded against, the **transaction** it belongs to, and the option **spelled as ITI TF-2 spells it**, which is the wording a Product Registry entry or a Connectathon test is written in. "Package option" is the `@cosyte/mllp` option that turns the mechanism on. The role columns record **this package's support for the mechanism**. They are not a claim on the option: see the split below, and note that no row is claimed here. | IHE actor | Transaction | Option, as ITI TF-2 spells it | Package option | Client role | Server role | | ----------------------------- | ------------------------------------ | -------------------------------------------- | ----------------------- | ----------- | ------------- | | Secure Node | Authenticate Node [ITI-19] | STX: TLS 1.2 floor using BCP195 Option | `atnaTransportSecurity` | Supported | Supported | | Secure Node | Authenticate Node [ITI-19] | STX: No Secure Transport | none | Supported | Supported | | Secure Node | Authenticate Node [ITI-19] | FQDN Validation of Server Certificate Option | `servername` | Unverified | Unverified | | Patient Demographics Supplier | Patient Identity Management [ITI-30] | Acknowledgement Support Option | `correlateByControlId` | Supported | Not supported | The IHE actor named in each row is the actor a **deployer** would be claiming the option for. This package is a component inside such an actor, never the actor itself. ### What this package supplies, and what stays yours | Option | What this package supplies | What the deploying actor must still do | Claimed here | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | | STX: TLS 1.2 floor using BCP195 Option | A TLS 1.2 floor that cannot be lowered through this API (`minVersion` defaults to `TLSv1.2` and only `TLSv1.2`/`TLSv1.3` are expressible); with `atnaTransportSecurity: true`, the four TLS 1.2 cipher suites ITI TF-2 §3.19.6.2.3 names and no other TLS 1.2 suite, offered alongside the three TLS 1.3 suites the runtime enables by default so that selecting the option never removes a protocol version that was reachable without it; the ephemeral Diffie-Hellman parameters the two DHE suites among those four need in order to be offerable at all, either the group the TLS library selects for the certificate in use or, through the server's `dhParameters`, a caller-supplied group that takes precedence over it and is refused before anything binds if it is unusable; mutual node authentication through `clientAuth` plus a client certificate and key; and a `'tlsNegotiated'` event per completed handshake reporting, in the IANA spelling the standard prints, what that link actually agreed on. | Supply and manage all certificate material and trust anchors, because this package ships no PKI and issues, rotates and revokes nothing; turn the option on, since it is off by default and with it off the offered suites are the runtime's rather than this package's; supply a Diffie-Hellman group through `dhParameters` wherever local policy names one, because the automatic selection is a default this package did not choose rather than a policy decision; configure mutual authentication for the actor's own trust model; retain the negotiated-parameter evidence; **ship with certificate verification on**, because the client's `tls.allowUnverified` switch turns node authentication off wholesale and only announces it, emitting `MLLP_TLS_VERIFY_DISABLED` on every successful connection rather than refusing to connect; and write and enter the Integration Statement. | No | | STX: No Secure Transport | Plaintext MLLP on both roles, which is the transport this option describes and for which ITI-19 requires no actions. Nothing about the unsecured case is quiet: a server binds `127.0.0.1` by default, a wildcard bind has to be asked for by name and emits `MLLP_BIND_ALL_INTERFACES` when it is, and the strict framing and commit-contract behaviour is identical to the TLS case. | Establish that an unsecured transport is permitted by the local security policy for the information crossing the link, and declare the option deliberately rather than arriving at it by leaving TLS unconfigured. | No | | FQDN Validation of Server Certificate Option | Nothing of its own. The client's identity check is whatever `node:tls` performs by default against `servername` (which defaults to the configured host), and this package neither implements RFC6125 section 6 matching nor overrides the runtime's check. A failed identity check surfaces as a typed `'tls-verify'` failure classified permanent, so a misconfigured endpoint is not retried. | Establish that the runtime's identity check satisfies the option for the deployment; ensure every server certificate carries a `subjectAltName` entry of type DNS-ID; set `servername` to the reference identifier the certificate is meant to match rather than relying on a host that is an address literal; and leave `tls.allowUnverified` off, which is the one switch that removes the identity check along with the rest of certificate verification. | No | | Acknowledgement Support Option | Enhanced acknowledgement mode on the client role: MSH-15 and MSH-16 are read byte-level and never validated, both halves of the exchange are correlated by MSA-2, and each disposition is surfaced with a typed result. On the server role, the accept half only: the server selects the correct half of Table 0008 for the message it answers. | Own the later application acknowledgement in any system built on this package's server, because that server emits exactly one acknowledgement per inbound message; and set `correlateByControlId: true`, without which an enhanced-mode send falls back to single-acknowledgement behaviour with a warning. | No | **Claimed here is `No` on every row, and it always will be.** An IHE option is claimed by an actor, in a statement that actor makes about itself. A library can supply a mechanism and produce evidence, and that is exactly as far as this page goes. ## Behaviours recorded as unverified An unverified behaviour is not a missing one. These are the places where something is delegated, untested, or inferred rather than observed, and recording them as either supported or not supported would hand a reviewer a guess dressed as a finding. | Behaviour | Verdict | Why it is unverified rather than absent | | ------------------------------------------------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FQDN Validation of Server Certificate Option (ITI-19 section 3.19.6.1.4) | Unverified | The client half is delegated wholesale to the runtime's default identity check, which this package neither implements nor overrides, and this package's own harness exercises only the rejection direction, with an address literal as the reference identifier rather than a DNS-ID. The server half is a property of certificates the deployer supplies and this package examines none of them. The mechanism is very probably adequate; it has not been established here, and probably is not a verdict. | | Interoperability with Epic and Cerner interfaces | Unverified | Byte-level interoperability is proven here against freely available engines only. Neither of those two is part of this package's own verification, so their behaviour is inferred from the specification rather than observed, and no claim about either is made anywhere on this page. What the package supplies instead is the differential harness itself: `runDifferential` ships in the published artifact, so a deploying actor can point it at the engine they are integrating with and obtain a per-exchange frame-parity and `MSA-2` correlation report for that engine, named by the stable warning codes this page already declares. The run sends synthetic messages into whatever engine it is aimed at, so aim it at a test or staging endpoint. The resulting report is evidence the actor holds, not a conformance verdict this package issues. See [Known limitations](https://docs.cosyte.com/mllp/limitations.md). | ## Evidence you can run The blocks below execute against the built package on every test run of this repository, so they cannot drift from it. Both carry synthetic bytes only: this page contains no patient data, real or realistic, and the payloads are single ASCII letters chosen because they mean nothing. A declared framing tolerance, exercised, emitting the code this page declares for it: ```ts runnable import { FrameReader } from "@cosyte/mllp"; // Synthetic: "A" then the next frame's , with no between the frames. const twoFrames = Buffer.from([0x0b, 0x41, 0x1c, 0x0b, 0x42, 0x1c, 0x0d]); const codes = []; const reader = new FrameReader({ onFrame: () => {}, onWarning: (w) => codes.push(w.code), allowFsOnly: true, // the declared opt-in }); reader.push(twoFrames); codes; // => ["MLLP_FS_WITHOUT_CR"] ``` The cipher suites the transport-security option offers, and the TLS 1.3 suites carried alongside them so that selecting the option never turns a reachable protocol version off: ```ts runnable import { ATNA_CIPHER_SUITES, TLS13_DEFAULT_CIPHER_SUITES, ATNA_CIPHER_LIST } from "@cosyte/mllp"; ATNA_CIPHER_SUITES.length; // => 4 TLS13_DEFAULT_CIPHER_SUITES.length; // => 3 ATNA_CIPHER_LIST.split(":").length; // => 7 TLS13_DEFAULT_CIPHER_SUITES.every((suite) => ATNA_CIPHER_LIST.includes(suite)); // => true ``` ## Where the detail lives This page is the declaration. The behaviour behind each line is documented at length elsewhere: - [Framing and tolerance](https://docs.cosyte.com/mllp/framing.md): the flag-by-flag table, every warning code, and what throws. - [ACKs and the commit contract](https://docs.cosyte.com/mllp/acks.md): the commit contract, the Table 0008 selection, and both halves of the enhanced-mode exchange. - [MLLPS / TLS](https://docs.cosyte.com/mllp/tls.md): enabling TLS, mutual TLS, the cipher-suite option and the negotiated-parameter event. - [Known limitations and non-goals](https://docs.cosyte.com/mllp/limitations.md): every bound on what this transport promises. --- URL: https://docs.cosyte.com/mllp/api Markdown: https://docs.cosyte.com/mllp/api.md # @cosyte/mllp `@cosyte/mllp`, Production-grade MLLP client and server for Node.js. Transport-only sibling to `@cosyte/hl7`. Handles framing, ACKs, reconnects, backpressure, and TLS without requiring knowledge of the MLLP spec. ## Example ```typescript import { createStarterServer } from '@cosyte/mllp'; const server = await createStarterServer({ port: 2575, onMessage: (buf) => buf }); ``` ## Classes ### Connection A single MLLP connection wrapping a [Transport](#transport-1) with a 6-state FSM, lifecycle events, per-connection warning streams, and `getStats()` observability. Emits events: `stateChange`, `connect`, `disconnect`, `reconnecting`, `close`, `message`, `warning`, `error`. #### Example ```typescript import { Connection, NetTransport } from '@cosyte/mllp'; import { createConnection } from 'node:net'; const socket = createConnection({ host: 'localhost', port: 2575 }); const conn = new Connection({ transport: new NetTransport(socket) }); conn.on('stateChange', ({ from, to }) => console.log(from, '->', to)); conn.on('message', ({ payload }) => console.log('received', payload.length, 'bytes')); socket.on('connect', () => conn.notifyConnect(socket.remoteAddress ?? null, socket.remotePort ?? null) ); ``` #### Extends - `EventEmitter` #### Constructors ##### Constructor > **new Connection**(`opts`): [`Connection`](#connection) Construct a connection over the given transport. Starts in `CONNECTING`; call [Connection.notifyConnect](#notifyconnect) once the transport handshake completes. ###### Parameters ###### opts [`ConnectionOptions`](#connectionoptions) Connection options (the transport, message/warning handlers, drain timeout, framing). ###### Returns [`Connection`](#connection) ###### Overrides `EventEmitter.constructor` #### Properties ##### beforeClose > **beforeClose**: (`drainTimeoutMs`) => `Promise`\<`void`\> beforeClose hook, no-op default that resolves immediately. An owner may replace this instance property to run work before the socket closes. Connection calls it during `close()`, racing against `drainTimeoutMs`. ###### Parameters ###### drainTimeoutMs `number` ###### Returns `Promise`\<`void`\> ##### connectionId > `readonly` **connectionId**: `string` Stable UUIDv4 identifier for this connection. #### Accessors ##### state ###### Get Signature > **get** **state**(): [`ConnectionState`](#connectionstate-1) Current FSM state. One of the 6 `ConnectionState` values. Subscribe to `'stateChange'` events for reactive state monitoring. ###### Returns [`ConnectionState`](#connectionstate-1) #### Methods ##### close() > **close**(`opts?`): `Promise`\<`void`\> Initiate graceful close of the connection. - If `CONNECTING` or `RECONNECTING`: cancels the pending attempt, transitions directly to `CLOSED`, and calls `transport.destroy()`. - If `CONNECTED`: transitions to `DRAINING`, calls `beforeClose(drainTimeoutMs)`, then `DRAINING → DISCONNECTED` once the hook resolves, or `DRAINING → CLOSED` if the drain timeout elapses first. - If `DRAINING`: waits for the existing drain to complete (idempotent). - If `CLOSED` or `DISCONNECTED`: no-op. ###### Parameters ###### opts? ###### drainTimeoutMs? `number` Override drain timeout (default: `opts.drainTimeoutMs ?? 30_000`). ###### Returns `Promise`\<`void`\> ###### Example ```typescript await conn.close({ drainTimeoutMs: 5_000 }); ``` ##### destroy() > **destroy**(`reason?`): `void` Abruptly destroy the connection, discarding any pending writes. Transitions from any non-terminal state directly to `CLOSED` and calls `transport.destroy(reason)`. Emits `'close'` event. Idempotent, safe to call multiple times. ###### Parameters ###### reason? `Error` Optional error to propagate to the transport. ###### Returns `void` ###### Example ```typescript conn.destroy(new Error('Timeout exceeded')); ``` ##### getStats() > **getStats**(): [`ConnectionStats`](#connectionstats) Return a JSON-serializable observability snapshot. All timestamp fields are `Date | null` (not epoch milliseconds). `JSON.stringify()` serialises them to ISO 8601 strings with no information loss. `warningsByCode` reflects every warning ever received regardless of ring buffer truncation. `warningsTruncated` is `true` if the 100-entry ring buffer has overflowed. ###### Returns [`ConnectionStats`](#connectionstats) ###### Example ```typescript const stats = conn.getStats(); console.log(JSON.stringify(stats)); // log-pipeline friendly ``` ##### notifyConnect() > **notifyConnect**(`remoteAddress`, `remotePort`): `void` Notify the connection that the transport handshake has completed. Transitions `CONNECTING → CONNECTED` and emits the `'connect'` event. Called externally by Server/Client after the socket connects. ###### Parameters ###### remoteAddress `string` \| `null` Remote peer IP address, or `null` if unavailable. ###### remotePort `number` \| `null` Remote peer port, or `null` if unavailable. ###### Returns `void` ###### Example ```typescript socket.on('connect', () => conn.notifyConnect(socket.remoteAddress ?? null, socket.remotePort ?? null) ); ``` ##### onWarning() > **onWarning**(`fn`): `void` Register or replace the per-connection warning subscriber. Subsequent calls replace the previous handler (set-once semantics prevent leaks). The handler is called synchronously and any exception it throws is swallowed. ###### Parameters ###### fn (`w`) => `void` ###### Returns `void` ###### Example ```typescript conn.onWarning((w) => logger.warn({ code: w.code, connectionId: w.connectionId })); ``` ##### send() > **send**(`data`): `boolean` Write raw bytes to the transport (no framing applied). The server and the client wrap this with `encodeFrame()` to add MLLP framing. Returns `false` if the connection is CLOSED or DISCONNECTED (no bytes written). ###### Parameters ###### data `Buffer` ###### Returns `boolean` `true` if bytes flushed immediately; `false` if buffered (backpressure) or not writable. ###### Example ```typescript const flushed = conn.send(encodeFrame(payload)); if (!flushed) logger.warn('backpressure detected'); ``` *** ### FrameReader Stateful MLLP frame decoder. Feed raw TCP byte chunks via `push(chunk)`. Complete frames fire synchronously via `onFrame` callback during `push()`. The decoder operates as a 3-state FSM: - `SCANNING_FOR_VT`, waiting for the 0x0B frame-start byte - `READING_PAYLOAD`, accumulating payload bytes until FS (0x1C) - `EXPECTING_CR`, received FS, waiting for CR (0x0D) to complete the frame #### Example ```typescript const reader = new FrameReader({ onFrame: (payload, byteOffset, warnings) => process(payload, byteOffset, warnings), onWarning: (w) => logger.warn(w), }); socket.on('data', (chunk) => reader.push(chunk)); ``` #### Constructors ##### Constructor > **new FrameReader**(`opts`): [`FrameReader`](#framereader) Construct a chunked MLLP frame reader. ###### Parameters ###### opts [`FrameReaderOptions`](#framereaderoptions) Reader options (`onFrame`/`onWarning` callbacks, tolerance, `maxFrameSizeBytes`). ###### Returns [`FrameReader`](#framereader) #### Methods ##### push() > **push**(`chunk`): `void` Feed a chunk of raw TCP bytes into the FSM. Frames fire synchronously via `onFrame` during this call. May throw `MllpFramingError` for unrecoverable framing violations when the matching tolerance is not enabled. ###### Parameters ###### chunk `Buffer` ###### Returns `void` ###### Example ```typescript socket.on('data', (chunk) => reader.push(chunk)); ``` ##### reset() > **reset**(): `void` Clear internal accumulator state and reset byte offset to 0. Call on reconnect / connection reuse to start fresh without allocating a new reader. Pending partial frame state is discarded silently. ###### Returns `void` ###### Example ```typescript socket.on('close', () => reader.reset()); ``` *** ### MllpAckError Error a message handler throws to control the negative acknowledgement the server returns. Throwing **any** error from a commit-gated handler yields `AE` by default; throw an `MllpAckError` to choose `AR` (application reject) instead. The `message` is **never** copied into the ACK bytes or any emitted event, it may carry PHI. Only the static, non-PHI `ackCode` influences the wire response. #### Example ```typescript import { createServer, MllpAckError } from '@cosyte/mllp'; createServer({ autoAck: 'AA', onMessage: async (payload) => { if (!isAcceptable(payload)) { // sender should NOT resend unchanged -> AR throw new MllpAckError('unsupported message type', { ackCode: 'AR' }); } await db.commit(payload); // throw here -> AE (resend may succeed) }, }); ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpAckError**(`message`, `opts?`): [`MllpAckError`](#mllpackerror) ###### Parameters ###### message `string` Diagnostic text for the thrower. Never placed on the wire (may carry PHI). ###### opts? ###### ackCode? [`NegativeAckCode`](#negativeackcode) Negative code to return. Default `'AE'`. ###### cause? `unknown` Underlying error, preserved on `.cause`. ###### Returns [`MllpAckError`](#mllpackerror) ###### Overrides `Error.constructor` #### Properties ##### ackCode > `readonly` **ackCode**: [`NegativeAckCode`](#negativeackcode) The negative acknowledgement code to return (`AE` or `AR`). *** ### MllpApplicationAckError Rejects a `send()` whose peer **committed** the message and then never reported what its application did with it. This is the enhanced-mode outcome that has no original-mode counterpart, and it is deliberately a distinct type from [MllpTimeoutError](#mllptimeouterror): that one means "no acknowledgement at all arrived", and this one means "the accept acknowledgement arrived, the peer took custody, and the application acknowledgement did not follow". Confusing the two would tell an operator a message may never have been received when the peer has said in writing that it was. `commitCode` is the Table 0008 code already received, so a caller can act on the custody transfer even though the application disposition is unknown. It is a member of a closed six-code set, never wire content. #### Example ```typescript try { await client.send(payload); } catch (err) { if (err instanceof MllpApplicationAckError) { logger.warn({ commit: err.commitCode, reason: err.reason, elapsedMs: err.elapsedMs }); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpApplicationAckError**(`message`, `opts`): [`MllpApplicationAckError`](#mllpapplicationackerror) Construct an application-acknowledgement failure. ###### Parameters ###### message `string` Human-readable error message. Structural facts only, never field content. ###### opts Failure context (why, the commit code, control-id byte length, timings). ###### commitCode `"CA"` ###### commitReceivedAt `number` ###### elapsedMs `number` ###### messageControlIdBytes `number` \| `undefined` ###### reason [`ApplicationAckFailure`](#applicationackfailure) ###### Returns [`MllpApplicationAckError`](#mllpapplicationackerror) ###### Overrides `Error.constructor` #### Properties ##### commitCode > `readonly` **commitCode**: `"CA"` The commit disposition already received. Always the positive accept-mode code. ##### commitReceivedAt > `readonly` **commitReceivedAt**: `number` Epoch ms at which the accept acknowledgement was received. ##### elapsedMs > `readonly` **elapsedMs**: `number` Milliseconds between the accept acknowledgement and this failure. ##### messageControlIdBytes > `readonly` **messageControlIdBytes**: `number` \| `undefined` Byte length of the send's MSH-10 control ID, or `undefined` when there was none to read. The control ID itself is deliberately not here, for the reason given on [MllpTimeoutError.messageControlIdBytes](#messagecontrolidbytes-3). ##### name > `readonly` **name**: `"MllpApplicationAckError"` ###### Overrides `Error.name` ##### reason > `readonly` **reason**: [`ApplicationAckFailure`](#applicationackfailure) Why the wait ended. See [ApplicationAckFailure](#applicationackfailure). *** ### MllpBackpressureError Thrown (or rejects the `send()` promise) when the in-flight queue exceeds the configured high-water mark and `onBackpressure: 'reject'` is set. `highWaterMark` accepts a count cap, a byte cap, or both, when both are present, the stricter-of-two trigger wins. #### Example ```typescript try { await client.send(payload); } catch (err) { if (err instanceof MllpBackpressureError) { logger.warn({ queueDepth: err.queueDepth, queueBytes: err.queueBytes, cap: err.highWaterMark, }); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpBackpressureError**(`message`, `opts`): [`MllpBackpressureError`](#mllpbackpressureerror) Construct an MLLP backpressure error. ###### Parameters ###### message `string` Human-readable error message. ###### opts Backpressure context (queue depth, queued bytes, the high-water-mark hit). ###### highWaterMark \{ `bytes?`: `number`; `count?`: `number`; \} ###### highWaterMark.bytes? `number` ###### highWaterMark.count? `number` ###### queueBytes `number` ###### queueDepth `number` ###### Returns [`MllpBackpressureError`](#mllpbackpressureerror) ###### Overrides `Error.constructor` #### Properties ##### highWaterMark > `readonly` **highWaterMark**: `object` The high-water-mark configuration that was triggered. ###### bytes? > `readonly` `optional` **bytes?**: `number` ###### count? > `readonly` `optional` **count?**: `number` ##### name > `readonly` **name**: `"MllpBackpressureError"` ###### Overrides `Error.name` ##### queueBytes > `readonly` **queueBytes**: `number` Total bytes of in-flight + queued frames at the moment of rejection. ##### queueDepth > `readonly` **queueDepth**: `number` Number of in-flight + queued sends at the moment of rejection. *** ### MllpClient MLLP client, composes a single [Connection](#connection) over a [NetTransport](#nettransport) (production) or any other `Transport` (testing via InMemoryTransport). Public events, every payload `Object.freeze`'d before emission: - `'stateChange'`, `{ from, to, reason? }` from the underlying Connection FSM - `'connect'`, `{ connectionId }` once the FSM enters `CONNECTED` - `'disconnect'`, `{ connectionId }` once the FSM enters `DISCONNECTED` - `'reconnecting'`, `{ connectionId, attempt?, delayMs? }` - `'close'`, `{ connectionId }` once the FSM enters terminal `CLOSED` - `'message'`, `{ payload, connectionId, byteOffset, warnings }` for every inbound frame - `'warning'`, `MllpWarning | AckCorrelationWarning`. A framing deviation arrives as a `MllpWarning` enriched with `connectionId` from the Connection layer; the two ACK-correlation codes arrive as [AckCorrelationWarning](#ackcorrelationwarning), which adds `controlIdBytes` and `elapsedSinceSendMs`. Narrow on `code` before reading either. - `'securityWarning'`, `SecurityWarning`. Emitted on every successful `secureConnect` (initial + every reconnect) when `tls.allowUnverified` is `true` with code `MLLP_TLS_VERIFY_DISABLED`. Also mirrored to `process.emitWarning`. - `'tlsNegotiated'`, [NegotiatedTlsParameters](#negotiatedtlsparameters). Emitted once per completed TLS handshake (initial + every reconnect) with the negotiated protocol version and cipher suite. Never emitted on a plaintext connection. - `'error'`, re-emitted from Connection. Guarded by `listenerCount('error') > 0` so absence of a listener does NOT crash the process (server precedent). #### Example ```typescript const client = createClient({ host: 'localhost', port: 2575 }); client.on('stateChange', ({ from, to }) => logger.info({ from, to })); client.on('message', ({ payload }) => logger.info({ bytes: payload.length })); await client.connect(); const ack = await client.send(payloadBuffer); await client.close(); ``` #### Extends - `EventEmitter` #### Constructors ##### Constructor > **new MllpClient**(`opts`): [`MllpClient`](#mllpclient) Construct an MLLP client. Created idle; call `connect()` (or use [createClient](#createclient)/[createStarterClient](#createstarterclient)) to open the connection. ###### Parameters ###### opts [`ClientOptions`](#clientoptions) Client options (host/port, ACK timeout, reconnect/backpressure policy, …). ###### Returns [`MllpClient`](#mllpclient) ###### Overrides `EventEmitter.constructor` #### Accessors ##### state ###### Get Signature > **get** **state**(): [`ConnectionState`](#connectionstate-1) Current FSM state. Mirrors the underlying Connection's state once attached; before `connect()` (or after a `CLOSED` Connection is dropped) reports the client-level baseline (`'DISCONNECTED'`). ###### Returns [`ConnectionState`](#connectionstate-1) #### Methods ##### \_attachExistingConnection() > **\_attachExistingConnection**(`conn`): `void` **`Internal`** **Test seam**, attach an externally-built [Connection](#connection) directly, bypassing the `net.createConnection` + `NetTransport` path. Used by lifecycle tests driving `InMemoryTransport.pair()` for determinism. ###### Parameters ###### conn [`Connection`](#connection) ###### Returns `void` ##### \_captureConnectSignal() > **\_captureConnectSignal**(`signal`): `void` **`Internal`** Test seam, capture or rebind the connect-signal mid-flight. ###### Parameters ###### signal `AbortSignal` ###### Returns `void` ##### \_setReconnectFactory() > **\_setReconnectFactory**(`factory`): `void` **`Internal`** Test seam, install a factory that produces the next reconnect Connection. ###### Parameters ###### factory () => `object` ###### Returns `void` ##### \[asyncDispose\]() > **\[asyncDispose\]**(): `Promise`\<`void`\> Async disposal, delegates to [MllpClient.close](#close-1) for `await using` support. ###### Returns `Promise`\<`void`\> ###### Example ```typescript await using client = createClient({ host: 'localhost', port: 2575 }); await client.connect(); // client.close() is called automatically at end of block ``` ##### close() > **close**(`opts?`): `Promise`\<`void`\> Gracefully close the client, awaiting the acknowledgements of sends already written to the transport. Delegates to [Connection.close](#close), which transitions `CONNECTED → DRAINING → DISCONNECTED` (or `CLOSED` on drain timeout). No-op if no Connection is attached, or if the close has already completed. The drain ends as soon as the last outstanding acknowledgement arrives, so a caller whose peer answers promptly does not wait out `drainTimeoutMs`. Whatever is still unresolved when it does end is reported to its own `send()` caller under one rule, and the whole point of this method is that the two halves of that rule are told apart: * a send already written to the transport rejects with [MllpUnknownFateError](#mllpunknownfateerror), carrying the timestamp at which its bytes went out. It may have been committed; resending it may duplicate a clinical message. Nothing is retried here. * a send still held inside the client rejects with [MllpNeverDeliveredError](#mllpneverdeliverederror). The peer never saw those bytes, so resending them is safe. * a send whose commit disposition the peer had already reported keeps its [MllpApplicationAckError](#mllpapplicationackerror), which names that disposition. The peer's custody of those bytes is known, and a shutdown never downgrades a known fact to an unknown one. Rejects with `DOMException('Aborted', 'AbortError')` if `signal` aborts mid-drain; on abort, the underlying Connection is force-destroyed and every still-pending send is settled under the same rule rather than left pending. A drain cannot make delivery certain, and nothing here claims otherwise: an acknowledgement lost in flight is indistinguishable from a message never received. That is what the unknown-fate report says, and it is why the application still owns idempotency, keyed on MSH-10 plus MSH-7. ###### Parameters ###### opts? ###### drainTimeoutMs? `number` ###### signal? `AbortSignal` ###### Returns `Promise`\<`void`\> ###### Example ```typescript await client.close({ drainTimeoutMs: 5_000 }); ``` ##### connect() > **connect**(`opts?`): `Promise`\<`void`\> Open a TCP (or TLS) connection to the configured `host:port` and attach a [Connection](#connection) to it. Resolves once the FSM enters `CONNECTED`, for TLS, on `'secureConnect'` (handshake complete, including certificate verification when it is on). Rejects with: - `DOMException('Aborted', 'AbortError')` if `signal` is provided and aborts before the connect resolves. - `MllpConnectionError({ phase: 'connect' })` if the underlying socket emits `error` before connecting, OR if the client is already connecting/connected. TLS failures carry a `connectionCause`: `'tls-verify'` for certificate verification failures, `'tls-handshake'` for TLS-protocol-shaped failures ([isTlsProtocolError](#istlsprotocolerror)); pure TCP failures carry none. - `MllpTlsConfigurationError` if the configured cipher-suite list cannot be honoured (the runtime rejects it, or `atnaTransportSecurity` and `ciphers` both declare one). Raised **before** a socket is opened, so nothing is left connected; identify it by `instanceof` plus its stable `code`. **Dual failure signal on initial connect:** the Connection's transport error handler is attached before this promise's own error listener, so a pre-connect socket error produces BOTH the promise rejection AND a client `'error'` event (when an `'error'` listener is attached). Handle whichever fits your flow; they describe the same underlying failure. **TLS 1.3 + mutual TLS caveat (RFC 8446 §4.4.2):** `connect()` resolving does NOT guarantee that a `clientAuth: 'MUST'` server accepted your client certificate. Under TLS 1.3 the client's handshake, and its `'secureConnect'`, can complete before the server finishes validating the certificate; a rejection then surfaces moments later as a typed post-connect error (`'error'` event with an `ERR_SSL_*`/alert cause, classified **permanent**, no auto-reconnect loop). ACK correlation via [MllpClient.send](#send-1) remains the delivery guarantee: no send resolves without its ACK, so a rejected session can never silently "deliver". ###### Parameters ###### opts? ###### signal? `AbortSignal` ###### Returns `Promise`\<`void`\> ###### Example ```typescript const ac = new AbortController(); setTimeout(() => ac.abort(), 5_000); await client.connect({ signal: ac.signal }); ``` ##### destroy() > **destroy**(`reason?`): `void` Abruptly destroy the client, force-transitions the underlying Connection to `CLOSED` immediately. No-op if no Connection is attached. Idempotent. Every pending send is settled at once: no acknowledgement is awaited and no drain timeout is honoured, which is exactly what separates this from [MllpClient.close](#close-1). A `close()` already waiting on acknowledgements stops waiting here and returns. A send still held inside the client is failed with [MllpNeverDeliveredError](#mllpneverdeliverederror), because nothing was ever written for it; that is a statement about the bytes, not about how the client was shut down, so it holds on this path too. ###### Parameters ###### reason? `Error` ###### Returns `void` ###### Example ```typescript client.destroy(new Error('shutting down')); ``` ##### getStats() > **getStats**(): [`ClientStats`](#clientstats) Returns a JSON-serializable observability snapshot. All fields are plain values, no Buffers, no class instances, no Maps, no circular refs. Safe to `JSON.stringify` directly. `inFlight` is the count of correlator entries with `sentAt !== null` (entries actually written to the wire and awaiting ACK), distinct from `queueDepth` which counts ALL live correlator entries (including pre-flush and serialization-queued sends). ###### Returns [`ClientStats`](#clientstats) ###### Example ```typescript setInterval(() => logger.info(JSON.stringify(client.getStats())), 60_000); ``` ##### send() > **send**(`payload`, `opts?`): `Promise`\<`Buffer`\<`ArrayBufferLike`\>\> Send an MLLP-framed payload and await the inbound ACK. Resolves with the ACK Buffer (framing stripped). Rejects with: - `DOMException('Aborted', 'AbortError')` if `signal` aborts before the ACK. - `MllpTimeoutError` if no ACK arrives within `ackTimeoutMs`. The clock starts at the underlying `write()` flush callback, NOT at the `send()` call. - `MllpConnectionError({ phase: 'send' })` if the client is not connected. Emits a frozen `'ack'` event on every successful match. ## Enhanced acknowledgement mode A message whose MSH-15 or MSH-16 is not null asks for the two-part protocol of HL7 v2.5.1 §2.9, and on a client correlating by control ID this method delivers it: the accept acknowledgement (`CA`) is reported through `onCommitAck` **without** settling the send, and the later application acknowledgement (`AA`/`AE`/`AR`) is what resolves it. All three application-mode codes resolve, because which of them the receiving application chose is its clinical verdict and not this transport's to judge; read MSA-1 off the acknowledgement you are handed. A negative commit (`CE`/`CR`) rejects with [MllpCommitRejectedError](#mllpcommitrejectederror) at once, since no application acknowledgement follows a refusal to take custody. A message with both fields empty is an original-mode send and behaves exactly as it always has, one acknowledgement, settled by the first match. ###### Parameters ###### payload `Buffer` Raw bytes; MLLP framing is added internally via `encodeFrame`. ###### opts? ###### ackTimeoutMs? `number` Per-send override of the backpressure wait budget. ###### applicationAckTimeoutMs? `number` Per-send override of the second wait's bound. ###### onCommitAck? (`report`) => `void` Called with the commit disposition when the peer commits the message ahead of its application acknowledgement. See [CommitAckReport](#commitackreport). ###### signal? `AbortSignal` AbortSignal, aborting cancels the ACK wait. ###### Returns `Promise`\<`Buffer`\<`ArrayBufferLike`\>\> ###### Example ```typescript const ack = await client.send(payloadBuffer); logger.info({ ack: ack.toString('utf8') }); ``` *** ### MllpCommitRejectedError Rejects a `send()` whose peer answered with a **negative commit**: HL7 Table 0008 `CE` (commit error) or `CR` (commit reject). Either says the peer did not take custody of the bytes, so no application acknowledgement is coming and there is nothing to wait for. The send fails immediately rather than sitting out a window that would report the same failure later and less precisely. `commitCode` is a member of a closed six-code set, never wire content. #### Example ```typescript try { await client.send(payload); } catch (err) { if (err instanceof MllpCommitRejectedError && err.commitCode === 'CR') { // the peer will not take this message; do not resend it unchanged } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpCommitRejectedError**(`message`, `opts`): [`MllpCommitRejectedError`](#mllpcommitrejectederror) Construct a negative-commit error. ###### Parameters ###### message `string` Human-readable error message. Structural facts only, never field content. ###### opts Failure context (the commit code, control-id byte length, elapsed time). ###### commitCode `"CE"` \| `"CR"` ###### elapsedMs `number` ###### messageControlIdBytes `number` \| `undefined` ###### Returns [`MllpCommitRejectedError`](#mllpcommitrejectederror) ###### Overrides `Error.constructor` #### Properties ##### commitCode > `readonly` **commitCode**: `"CE"` \| `"CR"` The negative accept-mode code the peer sent. ##### elapsedMs > `readonly` **elapsedMs**: `number` Milliseconds between the send's write-flush and this acknowledgement. ##### messageControlIdBytes > `readonly` **messageControlIdBytes**: `number` \| `undefined` Byte length of the send's MSH-10 control ID, or `undefined` when there was none to read. The control ID itself is deliberately not here. ##### name > `readonly` **name**: `"MllpCommitRejectedError"` ###### Overrides `Error.name` *** ### MllpConnectionError Thrown (or emitted via `onError`) for socket-layer problems such as connection refused, ECONNRESET, ETIMEDOUT, or DNS failure. - `cause`, the original OS or TLS error - `phase`, which connection lifecycle phase the failure occurred in - `connectionCause`, optional stable cause-code (e.g. for FIFO reconnect rejections) #### Example ```typescript throw new MllpConnectionError('Connection refused', { cause: osError, phase: 'connect', }); ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpConnectionError**(`message`, `opts`): [`MllpConnectionError`](#mllpconnectionerror) Construct an MLLP connection error. ###### Parameters ###### message `string` Human-readable error message. ###### opts Error context (underlying `cause`, the lifecycle `phase`, optional stable cause code). ###### cause `Error` ###### connectionCause? [`ConnectionErrorCause`](#connectionerrorcause) ###### phase [`ConnectionErrorPhase`](#connectionerrorphase) ###### Returns [`MllpConnectionError`](#mllpconnectionerror) ###### Overrides `Error.constructor` #### Properties ##### cause > `readonly` **cause**: `Error` The original OS or TLS error that caused this connection failure. ###### Overrides `Error.cause` ##### connectionCause? > `readonly` `optional` **connectionCause?**: [`ConnectionErrorCause`](#connectionerrorcause) Optional stable cause code. Present on FIFO reconnect rejections (`'fifo-unsafe'` for queued sends, `'in-flight-orphan'` for in-flight sends). The set of values is a **public API**; see [ConnectionErrorCause](#connectionerrorcause). ##### name > `readonly` **name**: `"MllpConnectionError"` ###### Overrides `Error.name` ##### phase > `readonly` **phase**: [`ConnectionErrorPhase`](#connectionerrorphase) Which connection lifecycle phase the failure occurred in. *** ### MllpDifferentialConfigurationError Thrown when the differential harness is configured with a peer address it cannot use. Rejects the originating `runDifferential()` call. No socket is opened, no canonical message is sent, and no report is produced: there is nothing to report about. Identify it by `instanceof` **and** by the stable [MllpDifferentialConfigurationError.code](#code), never by matching on the message text. #### Example ```typescript import { MllpDifferentialConfigurationError } from '@cosyte/mllp'; try { await runDifferential({ peer: 'not-an-address' }); } catch (err) { if (err instanceof MllpDifferentialConfigurationError) logger.error({ value: err.value }); } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpDifferentialConfigurationError**(`code`, `value`): [`MllpDifferentialConfigurationError`](#mllpdifferentialconfigurationerror) Construct a differential configuration error. ###### Parameters ###### code `"MLLP_DIFF_PEER_UNPARSEABLE"` The stable configuration-error code; also selects the message. ###### value `string` The offending configured value, reported verbatim. ###### Returns [`MllpDifferentialConfigurationError`](#mllpdifferentialconfigurationerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: `"MLLP_DIFF_PEER_UNPARSEABLE"` The stable configuration-error code. Public API; branch on this rather than on the message. ##### name > `readonly` **name**: `"MllpDifferentialConfigurationError"` ###### Overrides `Error.name` ##### value > `readonly` **value**: `string` The offending value exactly as it was configured, so the operator can see which of their settings is wrong without guessing. It is caller configuration, never anything read off a peer. *** ### MllpFramingError Thrown for unrecoverable MLLP wire-format violations. - `code`, stable `WarningCode` identifying the violation - `byteOffset`, absolute stream position where the violation was detected - `snippet`, the single framing-boundary byte that broke the structure, or empty when the anomaly is not a specific byte. **Never a run of payload content** (see the `snippet` PHI contract below); the constructor caps whatever it is given to 64 bytes as a backstop. #### Example ```typescript // Pass only the offending framing-boundary byte, never a slice of payload content. throw new MllpFramingError('MLLP_MISSING_LEADING_VT', byteOffset, Buffer.from([byte])); ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpFramingError**(`code`, `byteOffset`, `snippet`, `message?`): [`MllpFramingError`](#mllpframingerror) Construct an MLLP framing error. ###### Parameters ###### code [`WarningCode`](#warningcode) The stable warning code classifying the violation. ###### byteOffset `number` Absolute stream byte offset where the violation was detected. ###### snippet `Buffer` Raw bytes copied from around the anomaly (capped to 64 bytes). ###### message? `string` Optional human-readable override message. ###### Returns [`MllpFramingError`](#mllpframingerror) ###### Overrides `Error.constructor` #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Absolute byte offset in the stream where the error was detected. ##### code > `readonly` **code**: [`WarningCode`](#warningcode) Stable warning code identifying the violation type. ##### name > `readonly` **name**: `"MllpFramingError"` ###### Overrides `Error.name` ##### snippet > `readonly` **snippet**: `Buffer` Up to 64 bytes copied from around the anomaly. This is a **copied** Buffer, isolated from the source buffer so it remains valid after the underlying buffer is reused or overwritten. **PHI contract:** the decoder only ever populates this with the single framing-boundary byte that violated the frame structure (whose hex value the `message` already discloses), never a run of payload content bytes. For anomalies whose fault is not a specific byte (`MLLP_FRAME_TOO_LARGE`, the accumulated size), the snippet is **empty**: a payload slice on a public error field would leak a field-body slice of clinical content. Callers constructing this directly are responsible for the same discipline. *** ### MllpNeverDeliveredError Rejects a `send()` whose bytes **never reached the transport**: the message was still held inside the client when the client shut down, waiting for room in the send queue or for the single in-flight slot. This is the safe half of a shutdown report. Nothing was written, so the peer cannot have seen this message, cannot have committed it, and cannot produce a duplicate if the application sends it again. It is deliberately a distinct type from [MllpUnknownFateError](#mllpunknownfateerror), so a caller decides between resending and escalating on `instanceof` rather than on the wording of a message. Every field is a count. There is nothing else to carry: the caller passed the payload to `send()` and still holds it. #### Example ```typescript try { await client.send(payload); } catch (err) { if (err instanceof MllpNeverDeliveredError) { // safe to resend: these bytes were never written await backupClient.send(payload); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpNeverDeliveredError**(`opts`): [`MllpNeverDeliveredError`](#mllpneverdeliverederror) Construct a never-delivered report. There is no `message` parameter, deliberately: see NEVER\_DELIVERED\_MESSAGE. ###### Parameters ###### opts Report context (framed byte count, control-id byte length). ###### byteCount `number` ###### messageControlIdBytes `number` \| `undefined` ###### Returns [`MllpNeverDeliveredError`](#mllpneverdeliverederror) ###### Overrides `Error.constructor` #### Properties ##### byteCount > `readonly` **byteCount**: `number` Byte count of the framed message this send would have written. ##### messageControlIdBytes > `readonly` **messageControlIdBytes**: `number` \| `undefined` Byte length of the send's MSH-10 control ID, or `undefined` when there was none to read. The control ID itself is deliberately not here, for the reason given on [MllpTimeoutError.messageControlIdBytes](#messagecontrolidbytes-3). ##### name > `readonly` **name**: `"MllpNeverDeliveredError"` ###### Overrides `Error.name` *** ### MllpServer MLLP TCP server, wraps `net.Server` without extending it. Each accepted socket is wrapped in a `NetTransport`, connected to a `Connection` with server-level framing options, and added to `_connections`. Messages are surfaced via the `'message'` event and the `onMessage` callback. Public events: `'listening'`, `'connection'`, `'message'`, `'nack'`, `'error'`, `'close'`. The `'nack'` event ([NackEvent](#nackevent)) fires whenever the server returns a negative ACK instead of `AA` on the auto-ACK path, a commit-gated `autoAck: 'AA'` handler failing, or a positive auto-ACK **downgraded** because the inbound could not be correlated (an unreadable/uncorrelatable message, or one whose bytes the decoder discarded). The [NackReason](#nackreason) distinguishes the causes. **`'error'` contract:** underlying `net.Server`/`tls.Server` errors are forwarded to the `'error'` event whenever a listener is attached. With **no** listener, the outcome depends on server state: during a `listen()` (the bind window) the error **rejects the `listen()` promise** the primary error surface, and the process never crashes on a bind error; with no `listen()` in flight and the server not serving (e.g. a stale async error after `close()`) the error is dropped; but **while serving**, an unlistened runtime error (e.g. accept-loop `EMFILE`) keeps Node's fail-loud crash-on-unlistened-`'error'` convention, a silent accept outage is impossible. Caveat for `'error'` listeners: the forwarder runs **before** the internal `listen()` rejection handler, so an `'error'` listener that synchronously calls `close()` during the bind window changes the `listen()` rejection from the bind error to the typed close-during-listen `MllpConnectionError`, match on the `'error'` event payload, not the rejection, if you do that. TLS (MLLPS) adds three more: `'tlsClientError'` (a failed TLS handshake, the server logs it and keeps serving other connections), `'securityWarning'` (loud, one-time notice when a wildcard host is bound via `allowWildcardBind: true`), and `'tlsNegotiated'` ([NegotiatedTlsParameters](#negotiatedtlsparameters), the protocol version and cipher suite each accepted link actually negotiated, once per completed handshake and never on a plaintext listener). All event payloads are `Object.freeze()`'d before emission. #### Example ```typescript import { createServer } from '@cosyte/mllp'; const server = createServer({ onMessage: (payload, meta, conn) => { console.log('received from', meta.connectionId); }, }); await server.listen(2575); // await using server = createServer({ ... }); // Symbol.asyncDispose ``` #### Extends - `EventEmitter` #### Constructors ##### Constructor > **new MllpServer**(`opts`): [`MllpServer`](#mllpserver) Construct an MLLP server. Created idle; call `listen()` (or use [createServer](#createserver)/[createStarterServer](#createstarterserver)) to begin accepting connections. ###### Parameters ###### opts [`ServerOptions`](#serveroptions) Server options (bind host/port, auto-ACK policy, message handler, framing, …). ###### Returns [`MllpServer`](#mllpserver) ###### Overrides `EventEmitter.constructor` #### Methods ##### \[asyncDispose\]() > **\[asyncDispose\]**(): `Promise`\<`void`\> Async disposal, delegates to `close()` for `await using` support. ###### Returns `Promise`\<`void`\> ###### Example ```typescript await using server = createServer({ onMessage: handler }); await server.listen(2575); // server.close() is called automatically at end of block ``` ##### close() > **close**(`opts?`): `Promise`\<`void`\> Stop accepting new connections and gracefully close all active connections. Sequence: 1. `net.Server.close()`, stops accepting new connections immediately 2. If `_connections` is empty: resolves immediately (no drain needed) 3. Calls `_drainAll(drainTimeoutMs)`, Promise.all + side-effect setTimeout that force-destroys stragglers after the drain window Calling `close()` while a `listen()` is still in flight proactively settles that `listen()` with a typed `MllpConnectionError` rejection and clears the single-flight guard, the server is immediately re-listenable. `Symbol.asyncDispose` (which delegates here) is therefore safe before `listen()` settles. **Qualification:** a `close({ signal })` whose signal is **already aborted** rejects immediately with `AbortError` and performs no work, it does NOT close the server and does NOT settle the in-flight `listen()`, which simply continues and settles on its own bind outcome. ###### Parameters ###### opts? ###### drainTimeoutMs? `number` Override drain timeout (default: `opts.drainTimeoutMs ?? 30_000`). ###### signal? `AbortSignal` AbortSignal to cancel the close operation. On abort, all active connections are destroyed and the promise rejects with AbortError. ###### Returns `Promise`\<`void`\> ###### Example ```typescript await server.close({ drainTimeoutMs: 5_000 }); ``` ##### getStats() > **getStats**(): [`ServerStats`](#serverstats) Return a JSON-serializable observability snapshot. `totalBytesIn` and `totalBytesOut` aggregate from live connections at call time. ###### Returns [`ServerStats`](#serverstats) ###### Example ```typescript const stats = server.getStats(); logger.info(JSON.stringify(stats)); ``` ##### listen() > **listen**(`port`, `hostOrOpts?`): `Promise`\<`void`\> Start listening on the given port. Resolves once the TCP socket is bound and emits `'listening'` with `Object.freeze({ port: actualPort, host: actualHost })`. **Single-flight:** one `listen()` per server lifecycle at a time. A call while the server is already listening, or while another `listen()` is still in flight, rejects with a typed `MllpConnectionError` (concurrent binds raced each other's post-bind safety checks). Call `close()` before re-listening; sequential `listen()` → `close()` → `listen()` is fine. **A refused TLS configuration** (the runtime rejects the offered suite list, `tls.atnaTransportSecurity` and `tls.ciphers` both declare one, or `tls.dhParameters` are not usable) rejects every `listen()` on this server with a typed `MllpTlsConfigurationError`, checked before the bind. Such a server never listens, never negotiates on any other list and never runs without the Diffie-Hellman parameters it was given; construct a new one with a configuration that resolves. **`close()` during an in-flight `listen()`** rejects that `listen()` with a typed `MllpConnectionError` (never a hang) and clears the single-flight guard, a subsequent `listen()` on the same server works. This makes `Symbol.asyncDispose` (which delegates to `close()`) safe even before a `listen()` has settled. (Exception: `close({ signal })` with an already-aborted signal is a no-op AbortError rejection, the in-flight `listen()` is left to settle on its own bind outcome; see `close()`.) ###### Parameters ###### port `number` TCP port to bind. Use `0` to let the OS assign an ephemeral port. ###### hostOrOpts? `string` \| \{ `host?`: `string`; `signal?`: `AbortSignal`; \} Host string or object with `host` and optional `signal`. Default host: `'127.0.0.1'` (bind-safety hardening). Wildcard hosts are rejected unless `ServerOptions.allowWildcardBind: true`, literal spellings pre-bind, resolver-only shorthands post-bind via the OS-normalized bound address. ###### Returns `Promise`\<`void`\> ###### Example ```typescript await server.listen(2575); // binds 127.0.0.1 await server.listen(0, '127.0.0.1'); await server.listen(0, { host: '127.0.0.1', signal: ac.signal }); ``` *** ### MllpTimeoutError Thrown (or rejects the `send()` promise) when an ACK does not arrive within the configured `ackTimeoutMs`. The timeout clock starts at the underlying `write()` flush callback, NOT at the `send()` call, pre-flush queue time is not charged to the peer. #### Example ```typescript try { await client.send(payload); } catch (err) { if (err instanceof MllpTimeoutError) { logger.warn({ elapsedMs: err.elapsedMs, idBytes: err.messageControlIdBytes }); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpTimeoutError**(`message`, `opts`): [`MllpTimeoutError`](#mllptimeouterror) Construct an MLLP timeout error. ###### Parameters ###### message `string` Human-readable error message. Structural facts only, never field content. ###### opts Timeout context (control-id byte length, elapsed time, flush timestamp). ###### elapsedMs `number` ###### messageControlIdBytes `number` \| `undefined` ###### sentAt `number` ###### Returns [`MllpTimeoutError`](#mllptimeouterror) ###### Overrides `Error.constructor` #### Properties ##### elapsedMs > `readonly` **elapsedMs**: `number` Milliseconds elapsed between write-flush and timeout fire. ##### messageControlIdBytes > `readonly` **messageControlIdBytes**: `number` \| `undefined` Byte length of the timed-out send's MSH-10 control ID, or `undefined` when there was none to read (FIFO mode, or a payload with no MSH-10). **The control ID itself is deliberately not here.** An `Error` is a diagnostic surface: it is logged, and its `stack` is what an error reporter ships off the box. MSH-10 is payload content, and a scanner that returns the wrong field returns payload content of some other kind, which is how a patient identifier reaches a log line. Nothing is lost by withholding it, because this error rejects the very `send()` whose payload the caller passed in, so the caller already holds the bytes. Control IDs are decoded `latin1`, a 1:1 byte to code-unit map, so this count is a byte count. ##### name > `readonly` **name**: `"MllpTimeoutError"` ###### Overrides `Error.name` ##### sentAt > `readonly` **sentAt**: `number` Epoch ms timestamp recorded at write-flush callback. *** ### MllpTlsConfigurationError Thrown for a TLS configuration this package will not open a connection on. Rejects the originating `MllpClient.connect()` or `MllpServer.listen()`; no socket is opened and no listener is left bound. Identify it by `instanceof` **and** by the stable [MllpTlsConfigurationError.code](#code-2), never by matching on the message text. `message` is a frozen registry entry and carries no key material and no passphrase; `cause`, when present, is the TLS library's own error for one input on its own (the suite list and the Diffie-Hellman parameters are each validated with no credential in scope). #### Example ```typescript import { MllpTlsConfigurationError } from '@cosyte/mllp'; try { await server.listen(2575); } catch (err) { if (err instanceof MllpTlsConfigurationError) logger.error({ code: err.code }); } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpTlsConfigurationError**(`code`, `opts?`): [`MllpTlsConfigurationError`](#mllptlsconfigurationerror) Construct a TLS configuration error. ###### Parameters ###### code [`TlsConfigurationErrorCode`](#tlsconfigurationerrorcode) The stable configuration-error code; also selects the message. ###### opts? Optional underlying `cause` from the TLS library. ###### cause? `Error` ###### Returns [`MllpTlsConfigurationError`](#mllptlsconfigurationerror) ###### Overrides `Error.constructor` #### Properties ##### cause > `readonly` **cause**: `Error` \| `undefined` The TLS library's own error, when the runtime is what refused the list. `undefined` when this package refused the configuration itself. ###### Overrides `Error.cause` ##### code > `readonly` **code**: [`TlsConfigurationErrorCode`](#tlsconfigurationerrorcode) The stable configuration-error code. Public API; branch on this rather than on the message. ##### name > `readonly` **name**: `"MllpTlsConfigurationError"` ###### Overrides `Error.name` *** ### MllpUnknownFateError Rejects a `send()` that **was** written to the transport and whose acknowledgement had still not arrived when the client finished closing. The ambiguous half of a shutdown report, and it is reported as ambiguous because it is: an acknowledgement lost on the way back is indistinguishable from a message the peer never received. Nothing is retried automatically. HL7 makes the accept acknowledgement the thing that releases a sender from resending, and puts the retransmission decision on the application and on its peer's duplicate detection, keyed on MSH-10 plus MSH-7. Distinct from [MllpNeverDeliveredError](#mllpneverdeliverederror) (nothing was written, so a resend is safe) and from [MllpApplicationAckError](#mllpapplicationackerror) (the peer said in writing that it took custody, so a resend would commit the message twice). Confusing the three is how a consumer's replay logic either duplicates a clinical message or drops one. `flushedAt` is what a replay decision reasons about: it says when these bytes went out, so a caller can compare it against the peer's own record. **Every field is a byte count or a timestamp, and the message is a constant.** No part of the payload reaches this error, not the control ID, not a truncation of it, and not a hex rendering. An `Error` is a diagnostic surface: it is logged, and its `stack` plus its own properties are what an error reporter ships off the box. #### Example ```typescript try { await client.send(payload); } catch (err) { if (err instanceof MllpUnknownFateError) { // do NOT blindly resend: the peer may already hold this message logger.warn({ flushedAt: err.flushedAt, bytes: err.byteCount }); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new MllpUnknownFateError**(`opts`): [`MllpUnknownFateError`](#mllpunknownfateerror) Construct an unknown-fate report. There is no `message` parameter, deliberately: see UNKNOWN\_FATE\_MESSAGE. ###### Parameters ###### opts Report context (flush timestamp, elapsed time, byte counts). ###### byteCount `number` ###### elapsedMs `number` ###### flushedAt `number` ###### messageControlIdBytes `number` \| `undefined` ###### Returns [`MllpUnknownFateError`](#mllpunknownfateerror) ###### Overrides `Error.constructor` #### Properties ##### byteCount > `readonly` **byteCount**: `number` Byte count of the framed message that was written. ##### elapsedMs > `readonly` **elapsedMs**: `number` Milliseconds between that write and this report. ##### flushedAt > `readonly` **flushedAt**: `number` Epoch ms at which this send's bytes were written to the transport. ##### messageControlIdBytes > `readonly` **messageControlIdBytes**: `number` \| `undefined` Byte length of the send's MSH-10 control ID, or `undefined` when there was none to read. The control ID itself is deliberately not here, for the reason given on [MllpTimeoutError.messageControlIdBytes](#messagecontrolidbytes-3). ##### name > `readonly` **name**: `"MllpUnknownFateError"` ###### Overrides `Error.name` *** ### NetTransport Wraps a `net.Socket` as a `Transport`, mapping socket EventEmitter events to registered single-handler callbacks. Each `onXxx(fn)` call replaces the prior handler and re-registers on the socket (uses `socket.removeAllListeners(event)` + `socket.on(event, fn)`). #### Example ```typescript const t = new NetTransport(socket); t.onData((chunk) => process(chunk)); t.onError((err) => handleError(err)); ``` #### Implements - [`Transport`](#transport-1) #### Constructors ##### Constructor > **new NetTransport**(`socket`): [`NetTransport`](#nettransport) Wrap an existing `net.Socket` (or `tls.TLSSocket`) as a `Transport`. ###### Parameters ###### socket `Socket` The connected (or connecting) socket to adapt. ###### Returns [`NetTransport`](#nettransport) #### Methods ##### close() > **close**(): `void` See [Transport.close](#close-5). ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`close`](#close-5) ##### destroy() > **destroy**(`reason?`): `void` See [Transport.destroy](#destroy-4). ###### Parameters ###### reason? `Error` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`destroy`](#destroy-4) ##### onClose() > **onClose**(`fn`): `void` See [Transport.onClose](#onclose-2). ###### Parameters ###### fn () => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onClose`](#onclose-2) ##### onConnect() > **onConnect**(`fn`): `void` See [Transport.onConnect](#onconnect-2). ###### Parameters ###### fn () => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onConnect`](#onconnect-2) ##### onData() > **onData**(`fn`): `void` See [Transport.onData](#ondata-2). ###### Parameters ###### fn (`chunk`) => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onData`](#ondata-2) ##### onError() > **onError**(`fn`): `void` See [Transport.onError](#onerror-2). ###### Parameters ###### fn (`err`) => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onError`](#onerror-2) ##### write() > **write**(`buf`): `boolean` See [Transport.write](#write-2). ###### Parameters ###### buf `Buffer` ###### Returns `boolean` ###### Implementation of [`Transport`](#transport-1).[`write`](#write-2) *** ### TlsTransport Wraps a `tls.TLSSocket` as a `Transport`. `onConnect(fn)` is armed on the `'secureConnect'` event (handshake complete, including certificate verification when `rejectUnauthorized` is on) rather than the raw TCP `'connect'` event that `NetTransport` uses. Each `onXxx(fn)` call replaces the prior handler and re-registers on the socket (`socket.removeAllListeners(event)` + `socket.on(event, fn)`), the same set-once semantics as `NetTransport`. #### Example ```typescript const t = new TlsTransport(tlsSocket); t.onConnect(() => logger.info('secure')); t.onError((err) => handleTlsError(err)); ``` #### Implements - [`Transport`](#transport-1) #### Constructors ##### Constructor > **new TlsTransport**(`socket`): [`TlsTransport`](#tlstransport) Wrap an existing `tls.TLSSocket` (connecting or already secure) as a `Transport`. ###### Parameters ###### socket `TLSSocket` The TLS socket to adapt. ###### Returns [`TlsTransport`](#tlstransport) #### Methods ##### close() > **close**(): `void` See [Transport.close](#close-5). ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`close`](#close-5) ##### destroy() > **destroy**(`reason?`): `void` See [Transport.destroy](#destroy-4). ###### Parameters ###### reason? `Error` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`destroy`](#destroy-4) ##### onClose() > **onClose**(`fn`): `void` See [Transport.onClose](#onclose-2). ###### Parameters ###### fn () => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onClose`](#onclose-2) ##### onConnect() > **onConnect**(`fn`): `void` See [Transport.onConnect](#onconnect-2). Armed on `'secureConnect'`, the TLS handshake-complete event, not the raw TCP `'connect'` event. ###### Parameters ###### fn () => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onConnect`](#onconnect-2) ##### onData() > **onData**(`fn`): `void` See [Transport.onData](#ondata-2). ###### Parameters ###### fn (`chunk`) => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onData`](#ondata-2) ##### onError() > **onError**(`fn`): `void` See [Transport.onError](#onerror-2). ###### Parameters ###### fn (`err`) => `void` ###### Returns `void` ###### Implementation of [`Transport`](#transport-1).[`onError`](#onerror-2) ##### write() > **write**(`buf`): `boolean` See [Transport.write](#write-2). ###### Parameters ###### buf `Buffer` ###### Returns `boolean` ###### Implementation of [`Transport`](#transport-1).[`write`](#write-2) ## Interfaces ### AckCorrelationWarning The `'warning'` payload the client emits for an ACK-correlation deviation (`MLLP_ACK_UNMATCHED_CONTROL_ID`, `MLLP_ACK_AFTER_TIMEOUT`). A `MllpWarning` with two extra numeric fields. `message` is a **frozen registry entry**, byte-for-byte identical for a given code no matter what arrived on the wire, and everything input-derived is a number: the control ID's byte length and the elapsed time. A warning is a log line, so it carries no field content. #### Example ```typescript client.on('warning', (w: AckCorrelationWarning) => { logger.warn({ code: w.code, idBytes: w.controlIdBytes, elapsedMs: w.elapsedSinceSendMs }); }); ``` #### Extends - [`MllpWarning`](#mllpwarning) #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Absolute stream byte offset where the anomaly was detected. ###### Inherited from [`MllpWarning`](#mllpwarning).[`byteOffset`](#byteoffset-5) ##### code > `readonly` **code**: [`AckCorrelationCode`](#ackcorrelationcode) The correlation code this warning reports. ###### Overrides [`MllpWarning`](#mllpwarning).[`code`](#code-8) ##### connectionId > `readonly` **connectionId**: `string` \| `undefined` Connection identifier. `undefined` at the framing layer; enriched by `Connection` before emitting upstream. ###### Inherited from [`MllpWarning`](#mllpwarning).[`connectionId`](#connectionid-7) ##### controlIdBytes > `readonly` **controlIdBytes**: `number` \| `null` Byte length of the control ID involved, or `null` when there was none to read. Control IDs are decoded `latin1`, so a code-unit count is a byte count. ##### elapsedSinceSendMs > `readonly` **elapsedSinceSendMs**: `number` Milliseconds between the send's write-flush (or its timeout, for a late ACK) and this warning. ##### message > `readonly` **message**: `string` Stable human-readable description. It carries structural facts (a byte offset, an accumulated size) and **never a run of payload content**. Two codes carry exactly one input byte, and it is worth knowing which: `MLLP_MISSING_LEADING_VT` and `MLLP_FS_WITHOUT_CR` render the hex of the single byte found where a framing byte was expected, and on a stream that omits its leading `VT` that byte is the first byte of the unframed content. One byte, never a run, the same bound `MllpFramingError.snippet` carries. If that is more than your threat model allows, log `code` and `byteOffset` rather than `message`. The ACK-correlation codes are stricter: their text comes from a frozen registry and does not vary with the wire at all. ###### Inherited from [`MllpWarning`](#mllpwarning).[`message`](#message-3) ##### timestamp > `readonly` **timestamp**: `Date` Wall-clock time at point of emission. ###### Inherited from [`MllpWarning`](#mllpwarning).[`timestamp`](#timestamp-3) *** ### AckModeWarning The `'warning'` payload the client emits for an **acknowledgement-mode** deviation: an enhanced-mode send on a client that cannot correlate two acknowledgements, a Table 0155 value it did not recognise, an acknowledgement it could not classify into a mode, and a further acknowledgement for a send that is already settled or failed. It arrives on the same `'warning'` event as a framing warning and an [AckCorrelationWarning](#ackcorrelationwarning), so narrow on `code` before reading its fields. It is **not** an `MllpWarning`: its `code` is an [AckModeCode](#ackmodecode), deliberately a family of its own rather than an addition to the decoder's registry, and it is not counted in `getStats().warningsByCode`, whose keys are the decoder's codes. Everything input-derived on it is a number or a member of the closed six-code Table 0008 set. `message` is a **frozen registry entry**, byte-for-byte identical for a given code no matter what arrived on the wire. A warning is a log line, so it carries no field content: an MSA-1 nobody could classify is reported by its byte length alone. #### Example ```typescript client.on('warning', (w: AckModeWarning) => { if (w.code === 'MLLP_ACK_MSA1_UNCLASSIFIABLE') { logger.warn({ code: w.code, msa1Bytes: w.msa1Bytes, at: w.byteOffset }); } }); ``` #### Properties ##### ackCode > `readonly` **ackCode**: `"AA"` \| `"AE"` \| `"AR"` \| `"CA"` \| `"CE"` \| `"CR"` \| `null` Table 0008 code involved, or `null` when the code reports none. ##### byteOffset > `readonly` **byteOffset**: `number` Inbound frame's stream byte offset; `0` for a warning raised on an outbound send. ##### code > `readonly` **code**: [`AckModeCode`](#ackmodecode) The acknowledgement-mode code this warning reports. ##### connectionId > `readonly` **connectionId**: `string` \| `undefined` Connection identifier, `undefined` before a connection is attached. ##### controlIdBytes > `readonly` **controlIdBytes**: `number` \| `null` Byte length of the control ID involved, or `null` when there was none to read. ##### elapsedSinceSendMs > `readonly` **elapsedSinceSendMs**: `number` Milliseconds since the send's write-flush, or since its disposal for a late ACK. ##### message > `readonly` **message**: `string` Frozen registry text for `code`. Never interpolated. ##### msa1Bytes > `readonly` **msa1Bytes**: `number` \| `null` Byte length of the MSA-1 field involved, or `null` when the code reports none. ##### timestamp > `readonly` **timestamp**: `Date` Wall-clock time at point of emission. *** ### AckModeWarningEvent Payload of the server `'ackModeWarning'` event: something about an inbound message's acknowledgement-mode request was read and reported, and the acknowledgement went out anyway. Emitted when MSH-15 carries a value outside HL7 Table 0155. The acknowledgement is **not** downgraded, negated or otherwise changed for that reason: reading a field is not validating it, and answering a message the handler committed with anything but a positive code would make a sender resend a message that is already stored. **PHI-safe by construction:** a connection ID, a stable code and a frozen registry message, never the payload and never the field's value. The object is `Object.freeze()`'d before emission. #### Example ```typescript server.on('ackModeWarning', ({ code }) => metrics.increment('mllp.ack_mode', { code })); ``` #### Properties ##### code > `readonly` **code**: [`AckModeCode`](#ackmodecode) The acknowledgement-mode code being reported. ##### connectionId > `readonly` **connectionId**: `string` Connection the inbound message arrived on. ##### message > `readonly` **message**: `string` Frozen registry text for `code`. Never interpolated. ##### timestamp > `readonly` **timestamp**: `Date` Wall-clock time at point of emission. *** ### CanonicalExchange One send-and-await-response pair the harness runs against a peer. `payload` is the HL7 v2 message body with **no** MLLP framing on it. The harness frames it canonically (`VT` + payload + `FS` + `CR`) with the package's own strict encoder before it goes on the wire, so what a peer receives is the Release 1 block this package emits for every message. #### Example ```typescript const [first] = canonicalExchanges(); // first?.id === 'adt-a01'; first?.controlId === 'MSG00001' ``` #### Properties ##### controlId > `readonly` **controlId**: `string` The `MSH-10` message control ID this message carries. A conformant peer echoes it in the `MSA-2` of its acknowledgement, which is the correlation the harness checks. ##### description > `readonly` **description**: `string` One-line description of what the message is, for a human reading the report. ##### id > `readonly` **id**: `string` Stable identifier for this exchange, and the identifier the report uses. It names the message, never a patient, so it is safe to log. ##### payload > `readonly` **payload**: `Buffer` The unframed HL7 v2 message bytes. A fresh copy per call; mutating it is harmless. *** ### ClientOptions Options for [createClient](#createclient) and the [MllpClient](#mllpclient) constructor. #### Example ```typescript const opts: ClientOptions = { host: 'localhost', port: 2575, drainTimeoutMs: 10_000 }; ``` #### Properties ##### ackTimeoutMs? > `readonly` `optional` **ackTimeoutMs?**: `number` Per-message ACK timeout in milliseconds. The clock starts at the underlying `write()` flush callback, NOT at the `send()` call, pre-flush queue time is not charged to the peer. Default: `30_000`. ##### applicationAckTimeoutMs? > `readonly` `optional` **applicationAckTimeoutMs?**: `number` Bound, in milliseconds, on the **second** wait an enhanced-mode send can enter: the one that starts when an accept acknowledgement (`CA`) reports that the peer has committed the message, and ends when the application acknowledgement arrives. Measured from the moment that accept acknowledgement was received, not from the send, so a peer that commits at 9 s and applies at 12 s settles the send successfully at 12 s. Defaults to whatever `ackTimeoutMs` is in force for the send, and can be overridden per send. It only ever applies to a send whose MSH-16 asks for an application acknowledgement, so it changes nothing for an original-mode interface. ###### Default the send's own `ackTimeoutMs` ##### autoReconnect? > `readonly` `optional` **autoReconnect?**: `boolean` Auto-reconnect on transient disconnect. Default `false`. When `true`, dropped connections caused by transient errors (per [isTransientConnectionError](#istransientconnectionerror)) trigger the FSM cycle `CONNECTED → DISCONNECTED → RECONNECTING → CONNECTING → CONNECTED` with exponential backoff unless overridden by [ClientOptions.retryStrategy](#retrystrategy). Permanent errors halt and transition directly to `CLOSED`. ##### correlateByControlId? > `readonly` `optional` **correlateByControlId?**: `boolean` If `true`, ACKs are matched against outgoing sends by MSH-10 → MSA-2. Default `false` (FIFO mode). Out-of-order ACKs from the peer are supported in this mode. MSH-10 is extracted from the outbound payload before send; MSA-2 is extracted from the inbound ACK payload. An ACK whose MSA-2 matches no pending send (and is not in the late-ACK graveyard) emits a frozen `MllpFramingError('MLLP_ACK_UNMATCHED_CONTROL_ID')` to the `'error'` event. A late ACK whose MSA-2 matches a graveyard entry emits a `MLLP_ACK_AFTER_TIMEOUT` warning and is dropped. ###### Default ```ts false ``` ##### deadPeerTimeoutMs? > `readonly` `optional` **deadPeerTimeoutMs?**: `number` Application-idle timeout (ms) keyed on last inbound bytes / ACK / warning. On trip, calls `connection.destroy(new Error('dead peer timeout'))` which surfaces as `MllpConnectionError({ phase: 'receive' })`. Trip honors [ClientOptions.autoReconnect](#autoreconnect). Independent of [ClientOptions.keepaliveIntervalMs](#keepaliveintervalms). ###### Default ```ts undefined (off) ``` ##### drainTimeoutMs? > `readonly` `optional` **drainTimeoutMs?**: `number` Bound on the acknowledgement wait [MllpClient.close](#close-1) performs (default: `30_000` ms). Sends already written to the transport are awaited until it elapses; the wait ends early the moment the last of them is answered. ##### framing? > `readonly` `optional` **framing?**: `Omit`\<[`FrameReaderOptions`](#framereaderoptions), `"onFrame"` \| `"onWarning"`\> FrameReader tolerance / size options. `onFrame` and `onWarning` are managed internally. ##### highWaterMark? > `readonly` `optional` **highWaterMark?**: `HighWaterMark` Application-level high-water mark on the in-flight + queued send set. `number` configures a count cap (default 64); `{ bytes }` configures a byte cap; `{ count, bytes }` configures both, with the stricter-of-two trigger winning. When the cap is exceeded, behavior is governed by [ClientOptions.onBackpressure](#onbackpressure). ###### Default ```ts 64 ``` ##### host > `readonly` **host**: `string` Host to connect to (e.g. `'localhost'` or `'mllp.example.com'`). ##### initialDelayMs? > `readonly` `optional` **initialDelayMs?**: `number` First delay (ms) on auto-reconnect; default 100. ##### jitter? > `readonly` `optional` **jitter?**: `number` Jitter fraction, e.g. 0.2 = ±20%; default 0.2. ##### keepaliveIntervalMs? > `readonly` `optional` **keepaliveIntervalMs?**: `number` TCP keepalive interval (ms). Sets `socket.setKeepAlive(true, ms)` on the underlying `net.Socket` BEFORE wrapping in `NetTransport`. OS-level half-open detection (network partitions, NAT-table eviction). Independent of [ClientOptions.deadPeerTimeoutMs](#deadpeertimeoutms). ###### Default ```ts undefined (off) ``` ##### maxDelayMs? > `readonly` `optional` **maxDelayMs?**: `number` Maximum backoff cap (ms); default 30_000. ##### multiplier? > `readonly` `optional` **multiplier?**: `number` Backoff multiplier; default 2. ##### onBackpressure? > `readonly` `optional` **onBackpressure?**: `"reject"` \| `"wait"` Behavior when the high-water mark is exceeded. - `'reject'` (default), `send()` rejects with `MllpBackpressureError`. - `'wait'`, `send()` awaits the `'drain'` event OR the per-message `ackTimeoutMs` OR `signal` abort, whichever fires first. ###### Default ```ts 'reject' ``` ##### pipeline? > `readonly` `optional` **pipeline?**: `boolean` Strict serialization send → await-ACK → send. - `true` (default), concurrent in-flight sends up to [ClientOptions.highWaterMark](#highwatermark-1). - `false`, collapses the in-flight set to ≤1 (the unified Correlator's `maxInFlight=1`); the next `send()` waits for the prior ACK before reaching the wire. ###### Default ```ts true ``` ##### port > `readonly` **port**: `number` TCP port. ##### retryStrategy? > `readonly` `optional` **retryStrategy?**: [`RetryStrategy`](#retrystrategy-2) Custom reconnect-backoff hook. Return `null` to halt reconnection. Receives a frozen [RetryContext](#retrycontext). Defaults to the exponential strategy. ##### tls? > `readonly` `optional` **tls?**: `true` \| [`TlsOptions`](#tlsoptions) Enable TLS (MLLPS) for this connection. `true` enables TLS with all defaults, including certificate verification **on**. Pass a [TlsOptions](#tlsoptions) object to customize (`ca`/`cert`/`key`, minimum version, ciphers, `allowUnverified`, …). Spec anchor: IHE ATNA ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html). ###### Default ```ts undefined (plaintext TCP) ``` *** ### ClientStats Observability snapshot returned by [MllpClient.getStats](#getstats-1). All fields are JSON-serializable, no Buffers, no class instances, no Maps, no circular references. `lastConnectedAt` and `lastAckAt` are **epoch milliseconds** (numbers), NOT `Date` instances, log-pipeline friendly. `warningsByCode` keys are constrained to the public [WarningCode](#warningcode) union, adding/removing a code is a breaking change (CLAUDE.md stable-codes guardrail enforced at the type boundary). #### Example ```typescript const stats = client.getStats(); logger.info(JSON.stringify(stats)); // {"state":"CONNECTED","connectionId":"…","queueDepth":0, … } ``` #### Properties ##### ackedTotal > `readonly` **ackedTotal**: `number` Total ACKs matched + resolved since construction. ##### connectionId > `readonly` **connectionId**: `string` \| `null` Live Connection's id, or `null` before the first connect (or post-CLOSED). ##### inFlight > `readonly` **inFlight**: `number` Entries with `sentAt !== null`, actually written to the wire / awaiting ACK. ##### lastAckAt > `readonly` **lastAckAt**: `number` \| `null` Epoch ms of the most recent successful ACK. `null` until first ACK. ##### lastConnectedAt > `readonly` **lastConnectedAt**: `number` \| `null` Epoch ms of the last `CONNECTED` transition. `null` until first connect. ##### queueBytes > `readonly` **queueBytes**: `number` Sum of `frame.length` across live correlator entries. ##### queueDepth > `readonly` **queueDepth**: `number` Total live correlator entries (in-flight + pre-flush + serialization-queued). ##### reconnectAttempts > `readonly` **reconnectAttempts**: `number` Total reconnect attempts since construction. ##### sentTotal > `readonly` **sentTotal**: `number` Total successful `connection.send()` calls since construction. ##### state > `readonly` **state**: [`ConnectionState`](#connectionstate-1) Current FSM state, mirrors `client.state`. ##### timedOutTotal > `readonly` **timedOutTotal**: `number` Total ACK timeouts since construction. ##### tls > `readonly` **tls**: `boolean` Whether this client is configured for TLS. Mirrors `ClientOptions.tls` being set. ##### totalBytesIn > `readonly` **totalBytesIn**: `number` Bytes received from the peer (current Connection). ##### totalBytesOut > `readonly` **totalBytesOut**: `number` Bytes written to the peer (current Connection). ##### warningsByCode > `readonly` **warningsByCode**: `Partial`\<`Record`\<[`WarningCode`](#warningcode), `number`\>\> Aggregated warning counts. Keys are constrained to the public [WarningCode](#warningcode) union. Connection-level warnings + Correlator `MLLP_ACK_*` warnings are merged. *** ### CommitAckReport Per-send report that the peer has **committed** a message but has not yet said what its application did with it, handed to the `onCommitAck` callback of the `send()` that drew it. It is scoped to one caller's own send, and it arrives at a point where that send has not settled: `send()` is still pending and will resolve on the application acknowledgement, or reject if none arrives. That scoping is the point. The commit disposition belongs to one send, and a caller learns which send it belongs to by being the one that made it, never by anything being logged. #### Example ```typescript const ack = await client.send(payload, { onCommitAck: ({ code }) => logger.info({ commit: code }), // 'CA': the peer has custody }); ``` #### Properties ##### code > `readonly` **code**: `"CA"` The accept-mode Table 0008 code received. Always the positive `CA`. ##### latencyMs > `readonly` **latencyMs**: `number` Milliseconds between the send's write-flush and this acknowledgement. ##### payload > `readonly` **payload**: `Buffer` The accept acknowledgement's own payload bytes, framing stripped. *** ### ConnectionOptions Options for constructing a [Connection](#connection). #### Example ```typescript const opts: ConnectionOptions = { transport: new NetTransport(socket), onMessage: (payload) => handleMessage(payload), onWarning: (w) => logger.warn(w), drainTimeoutMs: 10_000, }; ``` #### Properties ##### drainTimeoutMs? > `optional` **drainTimeoutMs?**: `number` Drain timeout used by close() (default: 30_000 ms). ##### framing? > `optional` **framing?**: `Omit`\<[`FrameReaderOptions`](#framereaderoptions), `"onFrame"` \| `"onWarning"`\> FrameReader options (tolerance, maxFrameSizeBytes). onFrame/onWarning are managed internally. ##### onMessage? > `optional` **onMessage?**: (`payload`) => `void` Called for each decoded MLLP frame (raw payload bytes, framing stripped). ###### Parameters ###### payload `Buffer` ###### Returns `void` ##### onWarning? > `optional` **onWarning?**: (`w`) => `void` Per-connection warning subscriber. Replaces previous subscription. ###### Parameters ###### w [`MllpWarning`](#mllpwarning) ###### Returns `void` ##### transport > **transport**: [`Transport`](#transport-1) The transport this Connection will drive. *** ### ConnectionStats Return type of `connection.getStats()`, JSON-serializable. All timestamps are `Date | null` (not epoch milliseconds). `JSON.stringify()` serialises them to ISO 8601 strings by default with no information loss. #### Example ```typescript const stats = conn.getStats(); logger.info(JSON.stringify(stats)); // safe: all values are JSON-serializable ``` #### Properties ##### bytesIn > `readonly` **bytesIn**: `number` ##### bytesOut > `readonly` **bytesOut**: `number` ##### connectedAt > `readonly` **connectedAt**: `Date` \| `null` ##### connectionId > `readonly` **connectionId**: `string` ##### lastByteInAt > `readonly` **lastByteInAt**: `Date` \| `null` ##### lastByteOutAt > `readonly` **lastByteOutAt**: `Date` \| `null` ##### remoteAddress > `readonly` **remoteAddress**: `string` \| `null` ##### remotePort > `readonly` **remotePort**: `number` \| `null` ##### state > `readonly` **state**: [`ConnectionState`](#connectionstate-1) ##### warningsByCode > `readonly` **warningsByCode**: `Record`\<`string`, `number`\> ##### warningsTruncated > `readonly` **warningsTruncated**: `boolean` *** ### DifferentialDeviation One named deviation, located by offset. #### Example ```typescript const deviation: DifferentialDeviation = { code: 'MLLP_MISSING_LEADING_VT', byteOffset: 0 }; ``` #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset within the peer's response stream where it was detected. ##### code > `readonly` **code**: [`WarningCode`](#warningcode) The package's stable warning code for this deviation. *** ### DifferentialExchangeReport What one canonical exchange produced. #### Example ```typescript for (const exchange of report.exchanges) { console.log(exchange.exchangeId, exchange.outcome, exchange.byteParity); } ``` #### Properties ##### byteParity > `readonly` **byteParity**: [`DifferentialParityOutcome`](#differentialparityoutcome) Whether the response frame matched the canonical Release 1 block byte for byte. ##### correlation > `readonly` **correlation**: [`DifferentialCorrelationOutcome`](#differentialcorrelationoutcome) Whether the response echoed the control ID of the message that was sent. ##### deadlineMs > `readonly` **deadlineMs**: `number` The response deadline this exchange waited, in milliseconds. ##### deviations > `readonly` **deviations**: readonly [`DifferentialDeviation`](#differentialdeviation)[] The same deviations with the byte offset each was detected at. ##### elapsedMs > `readonly` **elapsedMs**: `number` How long the exchange actually took, in milliseconds. ##### exchangeId > `readonly` **exchangeId**: `string` The corpus identifier of the message that was sent. Names a message, never a patient. ##### outcome > `readonly` **outcome**: [`DifferentialExchangeOutcome`](#differentialexchangeoutcome) What became of the exchange. ##### requestByteCount > `readonly` **requestByteCount**: `number` How many bytes were sent to the peer, framing included. A count, never content. ##### responseByteCount > `readonly` **responseByteCount**: `number` How many bytes were read back before the exchange concluded. A count, never content. ##### warningCodes > `readonly` **warningCodes**: readonly [`WarningCode`](#warningcode)[] Every stable warning code observed on this exchange, in the order they were seen. *** ### DifferentialPeer A resolved peer endpoint: where the harness opens its connections. #### Example ```typescript const peer: DifferentialPeer = { host: '127.0.0.1', port: 2575 }; ``` #### Properties ##### host > `readonly` **host**: `string` Host name or address literal, with any IPv6 brackets already removed. ##### port > `readonly` **port**: `number` TCP port, an integer in 1..65535. *** ### DifferentialReport The report a run returns. Frozen, and JSON-serializable throughout. #### Example ```typescript const report = await runDifferential({ peer: '127.0.0.1:2575' }); console.log(JSON.stringify(report, null, 2)); ``` #### Properties ##### deadlineMs > `readonly` **deadlineMs**: `number` The per-exchange response deadline the run used, in milliseconds. ##### exchanges > `readonly` **exchanges**: readonly [`DifferentialExchangeReport`](#differentialexchangereport)[] One entry per canonical exchange attempted, in the order they ran. ##### exchangesAnswered > `readonly` **exchangesAnswered**: `number` How many of them the peer answered with a decodable frame. ##### exchangesAttempted > `readonly` **exchangesAttempted**: `number` How many exchanges were attempted. ##### finishedAt > `readonly` **finishedAt**: `string` When the run finished, as an ISO 8601 string. ##### peer > `readonly` **peer**: [`DifferentialReportPeer`](#differentialreportpeer-1) \| `undefined` The peer that was contacted, or `undefined` when the run was skipped. ##### result > `readonly` **result**: [`DifferentialRunResult`](#differentialrunresult) What the run as a whole observed. Never a conformance verdict. ##### skipReason > `readonly` **skipReason**: `"no-peer-configured"` \| `undefined` Why the run was skipped, or `undefined` when it ran. ##### startedAt > `readonly` **startedAt**: `string` When the run started, as an ISO 8601 string. *** ### DifferentialReportPeer The peer a report describes. Host and port as configured, nothing resolved or probed. #### Example ```typescript const peer: DifferentialReportPeer = { host: '127.0.0.1', port: 2575 }; ``` #### Properties ##### host > `readonly` **host**: `string` Host name or address literal the run connected to. ##### port > `readonly` **port**: `number` TCP port the run connected to. *** ### DifferentialRunOptions Options for [runDifferential](#rundifferential). #### Example ```typescript const report = await runDifferential({ peer: '127.0.0.1:2575', deadlineMs: 5_000 }); ``` #### Properties ##### connect? > `readonly` `optional` **connect?**: [`DifferentialConnect`](#differentialconnect) How to open each connection. Defaults to a plain TCP socket. ##### deadlineMs? > `readonly` `optional` **deadlineMs?**: `number` Per-exchange response deadline in milliseconds. Default 10000. ##### exchanges? > `readonly` `optional` **exchanges?**: readonly [`CanonicalExchange`](#canonicalexchange)[] The exchanges to run. Defaults to the shipped canonical corpus. ##### maxFrameSizeBytes? > `readonly` `optional` **maxFrameSizeBytes?**: `number` Largest response payload the decoder will accumulate before it refuses, in bytes. Defaults to the package's own 16 MiB frame cap. A peer that exceeds it is reported as a deviation named by the oversize warning code, not as an unhandled error. ##### peer? > `readonly` `optional` **peer?**: `string` The peer to run against, as `host:port`. Absent or empty means no peer is configured, and the run skips cleanly. A value that is present and cannot be resolved into a host and a port is refused by name instead, because a silent skip there would read as proof the harness ran. ##### signal? > `readonly` `optional` **signal?**: `AbortSignal` Aborts the run. The rejection carries the signal's own reason. *** ### EncoderOptions Options for [encodeFrame](#encodeframe). #### Example ```typescript const frame = encodeFrame(payload, { allowDelimiterBytesInPayload: true, onWarning: (w) => logger.warn(w), }); ``` #### Properties ##### allowDelimiterBytesInPayload? > `optional` **allowDelimiterBytesInPayload?**: `boolean` When `true`, VT (0x0B) or FS (0x1C) bytes within the payload are preserved verbatim in the output frame instead of throwing `MllpFramingError`. An `MllpWarning` is emitted per offending byte if `onWarning` is provided. Default: `false` (strict, throws on delimiter bytes). ##### onWarning? > `optional` **onWarning?**: (`w`) => `void` Called for each offending delimiter byte when `allowDelimiterBytesInPayload` is `true`. Invocation is wrapped in try/catch, a throwing handler does not interrupt encoding. ###### Parameters ###### w [`MllpWarning`](#mllpwarning) ###### Returns `void` ###### Example ```typescript const frame = encodeFrame(payload, { allowDelimiterBytesInPayload: true, onWarning: (w) => logger.warn({ code: w.code, offset: w.byteOffset }), }); ``` *** ### FrameReaderOptions Options for `FrameReader`. All tolerance opts default to `false`, every framing deviation throws unless explicitly enabled. Server-level defaults (allowFsOnly, allowLfAfterFs, allowLeadingWhitespace) are applied by the server when constructing readers. #### Example ```typescript const opts: FrameReaderOptions = { onFrame: (payload, byteOffset, warnings) => process(payload, byteOffset, warnings), onWarning: (w) => logger.warn(w), maxFrameSizeBytes: 4 * 1024 * 1024, // 4 MiB limit allowFsOnly: true, allowLfAfterFs: true, }; ``` #### Properties ##### allowFsOnly? > `optional` **allowFsOnly?**: `boolean` Tolerate FS without trailing CR; emits `MLLP_FS_WITHOUT_CR`. ##### allowLeadingWhitespace? > `optional` **allowLeadingWhitespace?**: `boolean` Tolerate SP/TAB/LF/CR before VT; emits `MLLP_LEADING_WHITESPACE`. ##### allowLfAfterFs? > `optional` **allowLfAfterFs?**: `boolean` Tolerate FS+LF instead of FS+CR; emits `MLLP_LF_AFTER_FS`. ##### allowMissingLeadingVt? > `optional` **allowMissingLeadingVt?**: `boolean` Tolerate missing leading VT; emits `MLLP_MISSING_LEADING_VT`. ##### maxFrameSizeBytes? > `optional` **maxFrameSizeBytes?**: `number` Maximum accumulated payload bytes before `MllpFramingError('MLLP_FRAME_TOO_LARGE')`. Default: 16 MiB (DoS prevention). ##### onFrame > **onFrame**: (`payload`, `byteOffset`, `warnings`) => `void` Called synchronously during `push()` for each complete MLLP payload. ###### Parameters ###### payload `Buffer` Raw MLLP payload bytes (framing stripped). ###### byteOffset `number` Stream byte offset of the VT byte that opened this frame. Monotonic across the connection lifetime; reset to 0 by `reset()`. ###### warnings readonly [`MllpWarning`](#mllpwarning)[] Framing warnings emitted during decoding of this specific frame. Empty array when no tolerance deviations were detected. ###### Returns `void` ##### onWarning? > `optional` **onWarning?**: (`w`) => `void` Called for each tolerated framing deviation. Wrapped in try/catch. A throwing handler will not corrupt FSM state. ###### Parameters ###### w [`MllpWarning`](#mllpwarning) ###### Returns `void` ##### strict? > `optional` **strict?**: `boolean` When `true`, escalates the following tolerances to thrown `MllpFramingError` even if individual opt-ins (`allowFsOnly`, `allowLfAfterFs`, `allowMissingLeadingVt`, `allowLeadingWhitespace`) are enabled: - `MLLP_MISSING_LEADING_VT` - `MLLP_FS_WITHOUT_CR` - `MLLP_LF_AFTER_FS` - `MLLP_LEADING_WHITESPACE` (leading whitespace escalates as `MLLP_MISSING_LEADING_VT`) `MLLP_EMPTY_PAYLOAD` and `MLLP_TRAILING_BYTES` remain warnings even in strict mode. ###### Example ```typescript // Hardened enforcement, no tolerance, all violations throw const reader = new FrameReader({ onFrame: fn, strict: true }); ``` *** ### MessageMeta Metadata attached to each decoded MLLP message. All fields are `readonly`, the object is `Object.freeze()`'d before emission. #### Example ```typescript server.on('message', ({ payload, meta }) => { console.log(meta.connectionId, '@', meta.byteOffset, 'warnings:', meta.warnings.length); }); ``` #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of the frame start in the connection's data stream. ##### connectionId > `readonly` **connectionId**: `string` Stable UUID identifying the connection that delivered this message. ##### warnings > `readonly` **warnings**: readonly [`MllpWarning`](#mllpwarning)[] Framing warnings emitted during decoding of this frame. *** ### MllpWarning A frozen warning object emitted when the decoder tolerates a framing deviation. `connectionId` is `undefined` when emitted by a standalone `FrameReader`. `Connection` enriches it to the real UUIDv4 before forwarding upstream. #### Example ```typescript const reader = new FrameReader({ onFrame: (p) => process(p), onWarning: (w: MllpWarning) => logger.warn(w), allowFsOnly: true, }); ``` #### Extended by - [`AckCorrelationWarning`](#ackcorrelationwarning) #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Absolute stream byte offset where the anomaly was detected. ##### code > `readonly` **code**: [`WarningCode`](#warningcode) ##### connectionId > `readonly` **connectionId**: `string` \| `undefined` Connection identifier. `undefined` at the framing layer; enriched by `Connection` before emitting upstream. ##### message > `readonly` **message**: `string` Stable human-readable description. It carries structural facts (a byte offset, an accumulated size) and **never a run of payload content**. Two codes carry exactly one input byte, and it is worth knowing which: `MLLP_MISSING_LEADING_VT` and `MLLP_FS_WITHOUT_CR` render the hex of the single byte found where a framing byte was expected, and on a stream that omits its leading `VT` that byte is the first byte of the unframed content. One byte, never a run, the same bound `MllpFramingError.snippet` carries. If that is more than your threat model allows, log `code` and `byteOffset` rather than `message`. The ACK-correlation codes are stricter: their text comes from a frozen registry and does not vary with the wire at all. ##### timestamp > `readonly` **timestamp**: `Date` Wall-clock time at point of emission. *** ### NackEvent Payload of the server `'nack'` event, emitted whenever the server responds with a **negative** acknowledgement instead of `AA` on the auto-ACK path (the fail-safe commit contract). This fires both when a commit-gated `autoAck: 'AA'` handler throws/rejects and when the server **downgrades** a positive auto-ACK because the inbound could not be correlated ([NackReason](#nackreason)). **PHI-safe by construction:** carries only the connection ID, the resolved acknowledgement code, and a static `reason`, never the payload, the inbound control ID, or the thrown error's message (which may carry PHI). The object is `Object.freeze()`'d before emission. #### Example ```typescript server.on('nack', ({ connectionId, ackCode, reason }) => { metrics.increment('mllp.nack', { code: ackCode, reason }); // e.g. reason='discarded-bytes' }); ``` #### Properties ##### ackCode > `readonly` **ackCode**: [`NackAckCode`](#nackackcode) The negative acknowledgement code actually sent to the peer. `AE` or `AR` for a message acknowledged in the original mode, and the accept-mode counterparts `CE` or `CR` where the inbound MSH-15 asked for an accept acknowledgement for this disposition. It is always the code that went on the wire, so an observer counting negative acknowledgements sees what the peer saw. ##### connectionId > `readonly` **connectionId**: `string` Connection that produced the negative acknowledgement. ##### reason > `readonly` **reason**: [`NackReason`](#nackreason) Why the negative acknowledgement was sent (PHI-free). *** ### NegotiatedTlsParameters Frozen payload of the `'tlsNegotiated'` event, emitted by both [MllpClient](#mllpclient) and [MllpServer](#mllpserver) once per completed TLS handshake, including every reconnect. Never emitted on a plaintext connection. Carries no HL7 payload content and no certificate or key material: only the two negotiated names and the same routing metadata a `SecurityWarning` carries. It is emitted at handshake-completion time, before any HL7 byte has crossed the link. #### Example ```typescript server.on('tlsNegotiated', (p: NegotiatedTlsParameters) => { if (p.protocolVersion !== 'TLSv1.3') metrics.increment('mllp.tls12_link'); }); ``` #### Properties ##### cipherSuite > `readonly` **cipherSuite**: `string` Negotiated cipher suite in its **IANA** spelling, e.g. `'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'`. That is the spelling the IHE and IETF documents print, so it can be compared with a conformance claim directly. The OpenSSL spelling, which is what a `ciphers` list is written in, is a different rendering of the same suite. ##### host > `readonly` **host**: `string` Host associated with the link (the target host for clients; the bind host for servers). ##### port > `readonly` **port**: `number` Port associated with the link. ##### protocolVersion > `readonly` **protocolVersion**: `string` Negotiated protocol version as the TLS library reports it, e.g. `'TLSv1.2'` or `'TLSv1.3'`. ##### timestamp > `readonly` **timestamp**: `Date` Wall-clock time at point of emission. *** ### ReconnectingEvent Payload for the 'reconnecting' event. Always `Object.freeze`'d. `attempt` and `delayMs` are populated by `MllpClient` when it schedules a reconnect. A `Connection` emitting on its own supplies `connectionId` only. #### Example ```typescript conn.on('reconnecting', ({ connectionId }) => { logger.info(`Reconnecting connection ${connectionId}`); }); ``` #### Properties ##### attempt? > `readonly` `optional` **attempt?**: `number` ##### connectionId > `readonly` **connectionId**: `string` ##### delayMs? > `readonly` `optional` **delayMs?**: `number` *** ### ResolvedTlsCipherPolicy The TLS-context fields the resolved policy contributes. Empty when the option is off and no `ciphers` passthrough is set: this package then imposes no cipher list at all, exactly as before the option existed. #### Properties ##### ciphers? > `readonly` `optional` **ciphers?**: `string` OpenSSL cipher-list string, or absent to impose none. ##### dhparam? > `readonly` `optional` **dhparam?**: `string` \| `Buffer`\<`ArrayBufferLike`\> Ephemeral Diffie-Hellman parameters, server-side only. Two of the four named suites are DHE, and a server with no DH parameters cannot offer a DHE suite at all: without this it would advertise the list and then fail every DHE handshake in it. Exactly two things reach this field. The literal `'auto'` is the TLS library's own spelling for the selection it makes from the certificate in use, which is what `atnaTransportSecurity: true` alone yields. Anything else is PEM content: the caller's own group, taken verbatim from `ServerTlsOptions.dhParameters` and preferred over the automatic selection. Absent when neither was asked for, so the not-selected path is untouched. Typed `string | Buffer` rather than `'auto' | string | Buffer` because the literal is absorbed by `string` in a union and says nothing to a reader there; it is stated here instead. *** ### RetryContext Context passed to a custom `retryStrategy` hook on each reconnect attempt. Frozen via `Object.freeze` before invocation, handlers cannot mutate. #### Example ```typescript const retryStrategy: RetryStrategy = (ctx) => { if (ctx.attempt >= 5) return null; if (ctx.classifiedAs === 'permanent') return null; return Math.min(30_000, 1000 * (ctx.attempt + 1)); }; ``` #### Properties ##### attempt > `readonly` **attempt**: `number` 0-indexed attempt counter for the current reconnect cycle. ##### classifiedAs > `readonly` **classifiedAs**: `"transient"` \| `"permanent"` How the failure that triggered this reconnect attempt was classified. ##### lastDelayMs > `readonly` **lastDelayMs**: `number` Delay used for the previous attempt (ms). 0 on the first attempt. ##### lastError > `readonly` **lastError**: `Error` The error that triggered the disconnect. ##### signal > `readonly` **signal**: `AbortSignal` The same `AbortSignal` passed into `connect()`. If no signal was supplied, the module-level `NEVER_ABORTING_SIGNAL` sentinel is provided so handlers always have a real `AbortSignal` to inspect. ##### sinceLastSuccessMs > `readonly` **sinceLastSuccessMs**: `number` Ms since the last successful ACK. `Infinity` if no success seen. ##### totalElapsedMs > `readonly` **totalElapsedMs**: `number` Total wall-clock ms elapsed since the disconnect that started this cycle. *** ### SecurityWarning Frozen payload of the `'securityWarning'` event, emitted by both [MllpClient](#mllpclient) (`MLLP_TLS_VERIFY_DISABLED`) and [MllpServer](#mllpserver) (`MLLP_BIND_ALL_INTERFACES`). Never carries payload bytes or PHI, only routing metadata (host/port) and a fixed, static message string. #### Example ```typescript client.on('securityWarning', (w: SecurityWarning) => { logger.warn({ code: w.code, host: w.host, port: w.port }); }); ``` #### Properties ##### code > `readonly` **code**: [`SecurityWarningCode`](#securitywarningcode-1) The stable security-warning code. ##### host > `readonly` **host**: `string` Host associated with the warning (the target host for clients; the bind host for servers). ##### message > `readonly` **message**: `string` Fixed, human-readable description. Never contains payload bytes or PHI. ##### port > `readonly` **port**: `number` Port associated with the warning. ##### timestamp > `readonly` **timestamp**: `Date` Wall-clock time at point of emission. *** ### ServerOptions Options for `createServer()`. #### Example ```typescript const opts: ServerOptions = { onMessage: (payload, meta, conn) => { const ack = buildAck(payload); conn.send(ack); }, framing: { maxFrameSizeBytes: 4 * 1024 * 1024 }, keepaliveIntervalMs: 60_000, deadPeerTimeoutMs: 300_000, drainTimeoutMs: 30_000, }; ``` #### Extended by - [`StarterServerOptions`](#starterserveroptions) #### Properties ##### allowWildcardBind? > `optional` **allowWildcardBind?**: `boolean` Opt-in required to bind a wildcard host, a bind-safety guardrail. Without this flag, `listen()` rejects a wildcard host in two tiers: **literal spellings** (`'0.0.0.0'`, `'::'`, `''`, `'::0'`, `'0:0:0:0:0:0:0:0'`, `'::ffff:0.0.0.0'`, …) reject **pre-bind** (fast path, nothing is ever bound); **resolver-only shorthands** (`'0'`, `'0.0'`, `'0x0.0.0.0'`, hostnames resolving to the unspecified address, …) are caught **post-bind** against the OS-normalized bound address, the just-bound server closes before any connection can be accepted and `listen()` rejects, with no listening state and no `'listening'` event either way. When `true`, binding a wildcard host emits a one-time `'securityWarning'` (`MLLP_BIND_ALL_INTERFACES`) at listen time. ###### Default ```ts false ``` ##### autoAck? > `optional` **autoAck?**: `"AA"` \| ((`payload`, `meta`, `conn`) => `Buffer`\<`ArrayBufferLike`\> \| `Promise`\<`Buffer`\<`ArrayBufferLike`\>\>) Auto-ACK mode. When set, the server builds and sends the ACK for each message. - **`'AA'`**, auto-acknowledge with the fail-safe **commit contract**: - **With an `onMessage` handler ⇒ commit-gated (recommended).** The server awaits `onMessage` (the durable-commit step), then sends `AA` on success or a **negative** ACK on failure (`AE` by default; `AR` via [MllpAckError](#mllpackerror)). The positive ACK **cannot precede a successful commit**, a handler throw can never yield `AA`. - **Without an `onMessage` handler ⇒ transport-accept.** `AA` is sent on frame receipt. This `AA` means only **"bytes received and framed"**, *not* "application-processed". ⚠️ For clinical messages this is unsafe on its own: pair `'AA'` with an `onMessage` handler that durably commits, so the ACK reflects real processing. - **`fn`**, `fn(payload, meta, conn)` builds the ACK bytes the server sends; the caller fully owns MSA-1 (e.g. to emit enhanced-mode `CA`/`CE`/`CR`). The `'message'` event always fires BEFORE the ACK is sent. Do NOT call `conn.send()` in `onMessage` when `autoAck` is set, this results in two ACKs. ##### deadPeerTimeoutMs? > `optional` **deadPeerTimeoutMs?**: `number` Application-level idle close timeout in ms. If no HL7 messages are received on a connection for this interval, the connection is destroyed via `conn.destroy(new Error('idle timeout'))`. Resets on every `'message'` event. Distinct from `keepaliveIntervalMs` (OS TCP probe). ##### drainTimeoutMs? > `optional` **drainTimeoutMs?**: `number` Graceful drain timeout passed to `conn.close()` during `server.close()`. Default: 30 000 ms. ##### framing? > `optional` **framing?**: `Omit`\<[`FrameReaderOptions`](#framereaderoptions), `"onFrame"` \| `"onWarning"`\> FrameReader tolerance options applied to every accepted connection. Merged with SERVER_DEFAULT_FRAMING, caller-supplied values override defaults. `onFrame` and `onWarning` are managed internally and must not be supplied here. ##### keepaliveIntervalMs? > `optional` **keepaliveIntervalMs?**: `number` TCP keepalive probe interval in ms. Calls `socket.setKeepAlive(true, ms)` on each accepted socket. Uses OS TCP stack to detect dead peers (half-open, network partitions). Distinct from `deadPeerTimeoutMs` (application-level idle close). ##### onMessage? > `optional` **onMessage?**: (`payload`, `meta`, `conn`) => `void` \| `Promise`\<`void`\> Called for each decoded MLLP message. Its role depends on `autoAck`, this is the **commit contract** (HL7 v2.5.1 §2.9.2): - **`autoAck: 'AA'` + this handler ⇒ commit-gated (the safe default).** The handler is the durable-commit step. The server **awaits** it and only then sends the ACK: resolve ⇒ `AA`; **throw/reject ⇒ `AE`** (or `AR` via [MllpAckError](#mllpackerror)), a positive ACK can never precede a successful commit. Do **not** call `conn.send()` here in this mode. - **`autoAck` unset ⇒ manual mode.** The handler owns the response; build and send the ACK yourself via `conn.send(encodeFrame(ackPayload))`. Its return value is ignored. - **`autoAck: fn` ⇒ observation only.** `fn` builds the ACK; this handler runs first as a side effect and its return value is ignored. Do not call `conn.send()` here. May be sync or async; an async handler is awaited in commit-gated mode. ###### Parameters ###### payload `Buffer` ###### meta [`MessageMeta`](#messagemeta) ###### conn [`Connection`](#connection) ###### Returns `void` \| `Promise`\<`void`\> ##### onWarning? > `optional` **onWarning?**: (`w`) => `void` Per-connection warning subscriber. Called for every framing warning on every connection. ###### Parameters ###### w [`MllpWarning`](#mllpwarning) ###### Returns `void` ##### tls? > `optional` **tls?**: [`ServerTlsOptions`](#servertlsoptions) Enable TLS (MLLPS) for this server. When set, the server binds a `tls.Server` instead of a plain `net.Server`, consumes `'secureConnection'` (post-handshake sockets) instead of `'connection'`, and surfaces failed handshakes via the `'tlsClientError'` event rather than crashing. Spec anchor: IHE ATNA ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html). ###### Default ```ts undefined (plaintext TCP) ``` *** ### ServerStats Observability snapshot returned by `server.getStats()`. All fields are JSON-serializable. `connections` and `activeConnections` both reflect the current live connection count. #### Example ```typescript const stats = server.getStats(); console.log(JSON.stringify(stats)); // log-pipeline friendly ``` #### Properties ##### acceptedTotal > `readonly` **acceptedTotal**: `number` Total connections accepted since listen() (monotonically increasing). ##### activeConnections > `readonly` **activeConnections**: `number` Current live connection count. Same value as `connections`. ##### closedTotal > `readonly` **closedTotal**: `number` Total connections closed since listen() (monotonically increasing). ##### connections > `readonly` **connections**: `number` Current live connection count. Same value as `activeConnections`. ##### host > `readonly` **host**: `string` \| `null` Bound host, or `null` before listen(). ##### listening > `readonly` **listening**: `boolean` Whether the server is currently accepting connections. ##### port > `readonly` **port**: `number` \| `null` Bound port, or `null` before listen(). ##### tls > `readonly` **tls**: `boolean` Whether this server is configured for TLS. Mirrors `ServerOptions.tls` being set. ##### tlsClientErrorsTotal > `readonly` **tlsClientErrorsTotal**: `number` Total `'tlsClientError'` events (failed TLS handshakes, incl. rejected client certs) since listen(). ##### totalBytesIn > `readonly` **totalBytesIn**: `number` Aggregate bytes received across all current connections. ##### totalBytesOut > `readonly` **totalBytesOut**: `number` Aggregate bytes sent across all current connections. *** ### ServerTlsOptions Server-side TLS options (`ServerOptions.tls`). #### Example ```typescript import { createServer } from '@cosyte/mllp'; const server = createServer({ tls: { cert: certPem, key: keyPem, clientAuth: 'MUST', ca: clientCaPem }, }); ``` #### Properties ##### atnaTransportSecurity? > `readonly` `optional` **atnaTransportSecurity?**: `boolean` Offer exactly the cipher suites the IHE ATNA ITI-19 transport-security option names (ITI TF-2 §3.19.6.2.3), rather than inheriting the runtime's own list. Off by default; turning it on only ever **narrows** what is offered. The server side additionally provides ephemeral Diffie-Hellman parameters, because two of the four named suites are DHE and a server without them cannot actually offer a DHE suite. The group is the one the runtime selects for the certificate in use unless [ServerTlsOptions.dhParameters](#dhparameters) names another, which takes precedence. See [TlsOptions.atnaTransportSecurity](#atnatransportsecurity-2) for the rest, including what this setting does **not** claim on your behalf. Mutually exclusive with [ServerTlsOptions.ciphers](#ciphers-1): setting both rejects `listen()` with a typed `MllpTlsConfigurationError`. ###### Default ```ts false ``` ##### ca? > `readonly` `optional` **ca?**: [`PemInput`](#peminput) Trust anchor(s) for verifying client certificates under `WANT`/`MUST`. ##### cert > `readonly` **cert**: [`PemInput`](#peminput) Server certificate (PEM). Required. ##### ciphers? > `readonly` `optional` **ciphers?**: `string` OpenSSL cipher-list string passthrough. Unset uses Node's defaults (includes both ATNA-mandated ECDHE suites). For the ATNA option's own four suites prefer [ServerTlsOptions.atnaTransportSecurity](#atnatransportsecurity). A list the runtime rejects rejects `listen()` with a typed `MllpTlsConfigurationError`; it never silently falls back to the default list. ###### Default ```ts undefined (Node defaults) ``` ##### clientAuth? > `readonly` `optional` **clientAuth?**: [`ClientAuth`](#clientauth-1) ATNA ITI-19 mutual-authentication mode. `'WANT'` requests a client certificate without rejecting unauthorized/absent ones (surfaced, not enforced); `'MUST'` requests AND enforces verification (ATNA mutual auth). See [ClientAuth](#clientauth-1). ###### Default ```ts 'NONE' ``` ##### dhParameters? > `readonly` `optional` **dhParameters?**: `string` \| `Buffer`\<`ArrayBufferLike`\> Ephemeral Diffie-Hellman parameters this server answers a DHE key exchange with, as **PEM content**. There is deliberately no filesystem-path input: every credential on this type is content, and this package performs no disk IO for any of it. Generate a group with `openssl dhparam` and read it in yourself. A DHE cipher suite is unofferable by a server with no parameters, so this is what makes one reachable through [ServerTlsOptions.ciphers](#ciphers-1): a list restricted to a DHE suite without this advertises that suite and then fails every handshake in it. **It takes precedence over the group [ServerTlsOptions.atnaTransportSecurity](#atnatransportsecurity) selects automatically**, and setting both is not a conflict: the option picks a group when nothing else says which, and this says which. That is how a deployment whose own policy names a group puts it in force while still offering exactly the ITI TF-2 §3.19.6.2.3 suites. Parameters this package or the TLS library cannot use reject `listen()` with a typed `MllpTlsConfigurationError` carrying `MLLP_TLS_DH_PARAMETERS_REJECTED`, before anything is bound. Nothing falls back to a server running without them, which matters because the TLS library's own behaviour for unreadable parameters is to discard them in silence. `'auto'` is not accepted here; that selection is what `atnaTransportSecurity` already makes. There is no counterpart on [TlsOptions](#tlsoptions): the side that answers the key exchange supplies the parameters. ###### Default undefined (no Diffie-Hellman parameters unless `atnaTransportSecurity` selects them) ##### key > `readonly` **key**: [`PemInput`](#peminput) Private key matching [ServerTlsOptions.cert](#cert). Required. ##### maxVersion? > `readonly` `optional` **maxVersion?**: `"TLSv1.2"` \| `"TLSv1.3"` Maximum negotiated TLS protocol version. ##### minVersion? > `readonly` `optional` **minVersion?**: `"TLSv1.2"` \| `"TLSv1.3"` Minimum negotiated TLS protocol version, the IHE ATNA ITI-19 "TLS 1.2 Floor" (BCP195) floor (ITI TF-2 §3.19.6.2.3). ###### Default ```ts 'TLSv1.2' ``` ##### passphrase? > `readonly` `optional` **passphrase?**: `string` Passphrase for an encrypted [ServerTlsOptions.key](#key). *** ### StarterClientOptions Options for [createStarterClient](#createstarterclient). The starter applies opinionated defaults on top of `ClientOptions`, so every override here is optional except `host` + `port`. The starter-specific addition is `handleSignals` (mirrors `createStarterServer`). #### Example ```typescript const opts: StarterClientOptions = { host: 'localhost', port: 2575, onMessage: (payload) => logger.info({ bytes: payload.length }), handleSignals: true, }; ``` #### Properties ##### ackTimeoutMs? > `readonly` `optional` **ackTimeoutMs?**: `number` Override default `30_000`. ##### autoReconnect? > `readonly` `optional` **autoReconnect?**: `boolean` Override default `true` (auto-reconnect on transient errors). ##### correlateByControlId? > `readonly` `optional` **correlateByControlId?**: `boolean` Override default `false` (FIFO mode). ##### deadPeerTimeoutMs? > `readonly` `optional` **deadPeerTimeoutMs?**: `number` Application-idle dead-peer timeout ms. ##### drainTimeoutMs? > `readonly` `optional` **drainTimeoutMs?**: `number` Drain timeout for `close()` (default `30_000`). ##### framing? > `readonly` `optional` **framing?**: `Omit`\<[`FrameReaderOptions`](#framereaderoptions), `"onFrame"` \| `"onWarning"`\> FrameReader options (passthrough). ##### handleSignals? > `readonly` `optional` **handleSignals?**: `boolean` Register process SIGTERM/SIGINT handlers that close the client. Default `false`. When `true`, SIGTERM/SIGINT both call `client.close()` and exit the process. Handlers self-deregister on `'close'`. ##### highWaterMark? > `readonly` `optional` **highWaterMark?**: `HighWaterMark` Override default `64`. ##### host > `readonly` **host**: `string` Host to connect to. ##### keepaliveIntervalMs? > `readonly` `optional` **keepaliveIntervalMs?**: `number` TCP keepalive interval ms. ##### onBackpressure? > `readonly` `optional` **onBackpressure?**: `"reject"` \| `"wait"` Override default `'reject'`. ##### onMessage? > `readonly` `optional` **onMessage?**: (`payload`) => `void` Inbound-message callback (any framed payload from the peer, including non-ACK messages on bidirectional channels). Mirrors the server-side `onMessage` ergonomics. ###### Parameters ###### payload `Buffer` ###### Returns `void` ##### pipeline? > `readonly` `optional` **pipeline?**: `boolean` Override default `true` (parallel up to highWaterMark). ##### port > `readonly` **port**: `number` TCP port. ##### retryStrategy? > `readonly` `optional` **retryStrategy?**: [`RetryStrategy`](#retrystrategy-2) Custom reconnect-backoff hook. ##### tls? > `readonly` `optional` **tls?**: `true` \| [`TlsOptions`](#tlsoptions) Enable TLS (MLLPS) for this connection. Passthrough to `ClientOptions.tls`. *** ### StarterServerOptions Options for `createStarterServer()`, the "three lines of code" factory. Extends `ServerOptions` with `port`, `host`, and `handleSignals`. Defaults: `autoAck: 'AA'`, `drainTimeoutMs: 30_000`, `Symbol.asyncDispose` wired. #### Example ```typescript import { createStarterServer } from '@cosyte/mllp'; const server = await createStarterServer({ port: 2575, onMessage: async (payload) => { await db.commit(payload); // the durable-commit step; a throw answers a negative ACK }, }); // server is listening, auto-ACK enabled, Symbol.asyncDispose wired await using _ = server; // closes on scope exit ``` #### Extends - [`ServerOptions`](#serveroptions) #### Properties ##### allowWildcardBind? > `optional` **allowWildcardBind?**: `boolean` Opt-in required to bind a wildcard host, a bind-safety guardrail. Without this flag, `listen()` rejects a wildcard host in two tiers: **literal spellings** (`'0.0.0.0'`, `'::'`, `''`, `'::0'`, `'0:0:0:0:0:0:0:0'`, `'::ffff:0.0.0.0'`, …) reject **pre-bind** (fast path, nothing is ever bound); **resolver-only shorthands** (`'0'`, `'0.0'`, `'0x0.0.0.0'`, hostnames resolving to the unspecified address, …) are caught **post-bind** against the OS-normalized bound address, the just-bound server closes before any connection can be accepted and `listen()` rejects, with no listening state and no `'listening'` event either way. When `true`, binding a wildcard host emits a one-time `'securityWarning'` (`MLLP_BIND_ALL_INTERFACES`) at listen time. ###### Default ```ts false ``` ###### Inherited from [`ServerOptions`](#serveroptions).[`allowWildcardBind`](#allowwildcardbind) ##### autoAck? > `optional` **autoAck?**: `"AA"` \| ((`payload`, `meta`, `conn`) => `Buffer`\<`ArrayBufferLike`\> \| `Promise`\<`Buffer`\<`ArrayBufferLike`\>\>) Auto-ACK mode. When set, the server builds and sends the ACK for each message. - **`'AA'`**, auto-acknowledge with the fail-safe **commit contract**: - **With an `onMessage` handler ⇒ commit-gated (recommended).** The server awaits `onMessage` (the durable-commit step), then sends `AA` on success or a **negative** ACK on failure (`AE` by default; `AR` via [MllpAckError](#mllpackerror)). The positive ACK **cannot precede a successful commit**, a handler throw can never yield `AA`. - **Without an `onMessage` handler ⇒ transport-accept.** `AA` is sent on frame receipt. This `AA` means only **"bytes received and framed"**, *not* "application-processed". ⚠️ For clinical messages this is unsafe on its own: pair `'AA'` with an `onMessage` handler that durably commits, so the ACK reflects real processing. - **`fn`**, `fn(payload, meta, conn)` builds the ACK bytes the server sends; the caller fully owns MSA-1 (e.g. to emit enhanced-mode `CA`/`CE`/`CR`). The `'message'` event always fires BEFORE the ACK is sent. Do NOT call `conn.send()` in `onMessage` when `autoAck` is set, this results in two ACKs. ###### Inherited from [`ServerOptions`](#serveroptions).[`autoAck`](#autoack) ##### deadPeerTimeoutMs? > `optional` **deadPeerTimeoutMs?**: `number` Application-level idle close timeout in ms. If no HL7 messages are received on a connection for this interval, the connection is destroyed via `conn.destroy(new Error('idle timeout'))`. Resets on every `'message'` event. Distinct from `keepaliveIntervalMs` (OS TCP probe). ###### Inherited from [`ServerOptions`](#serveroptions).[`deadPeerTimeoutMs`](#deadpeertimeoutms-1) ##### drainTimeoutMs? > `optional` **drainTimeoutMs?**: `number` Graceful drain timeout passed to `conn.close()` during `server.close()`. Default: 30 000 ms. ###### Inherited from [`ServerOptions`](#serveroptions).[`drainTimeoutMs`](#draintimeoutms-2) ##### framing? > `optional` **framing?**: `Omit`\<[`FrameReaderOptions`](#framereaderoptions), `"onFrame"` \| `"onWarning"`\> FrameReader tolerance options applied to every accepted connection. Merged with SERVER_DEFAULT_FRAMING, caller-supplied values override defaults. `onFrame` and `onWarning` are managed internally and must not be supplied here. ###### Inherited from [`ServerOptions`](#serveroptions).[`framing`](#framing-2) ##### handleSignals? > `optional` **handleSignals?**: `boolean` Register `process.once('SIGTERM')` and `process.once('SIGINT')` handlers that call `server.close()` then `process.exit(0)`. Default: `false`. Handlers are automatically removed when `server.close()` is called, so `process.listenerCount('SIGTERM') === 0` after close() completes, preventing handler accumulation across test instances or multiple server restarts. ##### host? > `optional` **host?**: `string` Host to bind to. Default `'127.0.0.1'` (bind-safety hardening, was `'0.0.0.0'`; binding all interfaces now requires `ServerOptions.allowWildcardBind: true`). ##### keepaliveIntervalMs? > `optional` **keepaliveIntervalMs?**: `number` TCP keepalive probe interval in ms. Calls `socket.setKeepAlive(true, ms)` on each accepted socket. Uses OS TCP stack to detect dead peers (half-open, network partitions). Distinct from `deadPeerTimeoutMs` (application-level idle close). ###### Inherited from [`ServerOptions`](#serveroptions).[`keepaliveIntervalMs`](#keepaliveintervalms-1) ##### onMessage? > `optional` **onMessage?**: (`payload`, `meta`, `conn`) => `void` \| `Promise`\<`void`\> Called for each decoded MLLP message. Its role depends on `autoAck`, this is the **commit contract** (HL7 v2.5.1 §2.9.2): - **`autoAck: 'AA'` + this handler ⇒ commit-gated (the safe default).** The handler is the durable-commit step. The server **awaits** it and only then sends the ACK: resolve ⇒ `AA`; **throw/reject ⇒ `AE`** (or `AR` via [MllpAckError](#mllpackerror)), a positive ACK can never precede a successful commit. Do **not** call `conn.send()` here in this mode. - **`autoAck` unset ⇒ manual mode.** The handler owns the response; build and send the ACK yourself via `conn.send(encodeFrame(ackPayload))`. Its return value is ignored. - **`autoAck: fn` ⇒ observation only.** `fn` builds the ACK; this handler runs first as a side effect and its return value is ignored. Do not call `conn.send()` here. May be sync or async; an async handler is awaited in commit-gated mode. ###### Parameters ###### payload `Buffer` ###### meta [`MessageMeta`](#messagemeta) ###### conn [`Connection`](#connection) ###### Returns `void` \| `Promise`\<`void`\> ###### Inherited from [`ServerOptions`](#serveroptions).[`onMessage`](#onmessage-1) ##### onWarning? > `optional` **onWarning?**: (`w`) => `void` Per-connection warning subscriber. Called for every framing warning on every connection. ###### Parameters ###### w [`MllpWarning`](#mllpwarning) ###### Returns `void` ###### Inherited from [`ServerOptions`](#serveroptions).[`onWarning`](#onwarning-4) ##### port > **port**: `number` Port to listen on. ##### tls? > `optional` **tls?**: [`ServerTlsOptions`](#servertlsoptions) Enable TLS (MLLPS) for this server. When set, the server binds a `tls.Server` instead of a plain `net.Server`, consumes `'secureConnection'` (post-handshake sockets) instead of `'connection'`, and surfaces failed handshakes via the `'tlsClientError'` event rather than crashing. Spec anchor: IHE ATNA ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html). ###### Default ```ts undefined (plaintext TCP) ``` ###### Inherited from [`ServerOptions`](#serveroptions).[`tls`](#tls-2) *** ### StateChangeEvent Payload for the 'stateChange' event. Always `Object.freeze`'d. #### Example ```typescript conn.on('stateChange', ({ from, to, reason }) => { logger.info({ from, to, reason }); }); ``` #### Properties ##### from > `readonly` **from**: [`ConnectionState`](#connectionstate-1) ##### reason? > `readonly` `optional` **reason?**: `string` ##### to > `readonly` **to**: [`ConnectionState`](#connectionstate-1) *** ### TlsCipherPolicyInput The cipher-suite half of a client's or server's TLS options, the two fields that can declare what is offered. #### Properties ##### atnaTransportSecurity? > `readonly` `optional` **atnaTransportSecurity?**: `boolean` See `TlsOptions.atnaTransportSecurity`. ##### ciphers? > `readonly` `optional` **ciphers?**: `string` See `TlsOptions.ciphers`. ##### dhParameters? > `readonly` `optional` **dhParameters?**: `string` \| `Buffer`\<`ArrayBufferLike`\> See `ServerTlsOptions.dhParameters`. Diffie-Hellman parameters are supplied by the end that answers the key exchange, so this is read when `side` is `'server'` and **ignored, without an error, when `side` is `'client'`**. That asymmetry is stated rather than enforced because the field is unreachable from the client option type in the first place: `TlsOptions` carries no Diffie-Hellman parameter field at all, so a client value can only arrive through a direct call to [resolveTlsCipherPolicy](#resolvetlscipherpolicy). *** ### TlsOptions Client-side TLS options (`ClientOptions.tls`). Passing `true` for `ClientOptions.tls` is equivalent to `{}`, TLS enabled with all defaults, including certificate verification **on**. #### Example ```typescript import { createClient } from '@cosyte/mllp'; const client = createClient({ host: 'mllp.example.com', port: 2575, tls: { ca: caPem, minVersion: 'TLSv1.2' }, }); ``` #### Properties ##### allowUnverified? > `readonly` `optional` **allowUnverified?**: `boolean` Loud, explicit dev opt-out from certificate verification (maps to `tls.connect`'s `rejectUnauthorized: false`). There is deliberately no raw `rejectUnauthorized` surface on this type, this is the only door, and it is loud: every successful connection (initial + every reconnect) emits a `'securityWarning'` (`MLLP_TLS_VERIFY_DISABLED`) event and calls `process.emitWarning`. Never set this in production against an untrusted network. ###### Default ```ts false ``` ##### atnaTransportSecurity? > `readonly` `optional` **atnaTransportSecurity?**: `boolean` Offer exactly the cipher suites the IHE ATNA ITI-19 transport-security option names (ITI TF-2 §3.19.6.2.3), rather than inheriting the runtime's own list. Off by default; turning it on only ever **narrows** what is offered. This is the **cipher-suite half** of that option, and it does not make the declaration on your behalf. Mutual node authentication is `ServerTlsOptions.clientAuth` plus a client `cert`/`key`, and the TLS 1.2 floor is [TlsOptions.minVersion](#minversion-1), which already defaults to it. The four TLS 1.2 suites are offered together with the three TLS 1.3 suites the runtime enables by default, so selecting this never removes a protocol version that was reachable without it. A peer that supports none of them fails the handshake rather than falling back, which is the point. Mutually exclusive with [TlsOptions.ciphers](#ciphers-3): both declare the offered list, so setting both rejects `connect()` with a typed `MllpTlsConfigurationError`. ###### Default ```ts false ``` ##### ca? > `readonly` `optional` **ca?**: [`PemInput`](#peminput) Trust anchor(s) for verifying the server's certificate chain. ##### cert? > `readonly` `optional` **cert?**: [`PemInput`](#peminput) Client certificate presented for mutual TLS (ATNA ITI-19). ##### ciphers? > `readonly` `optional` **ciphers?**: `string` OpenSSL cipher-list string passthrough (`tls.connect`'s `ciphers`). Unset uses Node's compiled-in defaults, which already include both ATNA-mandated ECDHE suites (see the module doc comment). Set this to restrict to a stricter list of your own choosing; for the ATNA option's own four suites prefer [TlsOptions.atnaTransportSecurity](#atnatransportsecurity-2), which is the same declaration made once and reported on `'tlsNegotiated'`. A list the runtime rejects rejects `connect()` with a typed `MllpTlsConfigurationError`; it never silently falls back to the default list. ###### Default ```ts undefined (Node defaults) ``` ##### key? > `readonly` `optional` **key?**: [`PemInput`](#peminput) Private key matching [TlsOptions.cert](#cert-1). ##### maxVersion? > `readonly` `optional` **maxVersion?**: `"TLSv1.2"` \| `"TLSv1.3"` Maximum negotiated TLS protocol version. ##### minVersion? > `readonly` `optional` **minVersion?**: `"TLSv1.2"` \| `"TLSv1.3"` Minimum negotiated TLS protocol version. Default `'TLSv1.2'`, the IHE ATNA ITI-19 "TLS 1.2 Floor" (BCP195) floor (ITI TF-2 §3.19.6.2.3). `'TLSv1.0'`/`'TLSv1.1'` are intentionally **not** expressible by this type, the floor cannot be lowered through this API. ###### Default ```ts 'TLSv1.2' ``` ##### passphrase? > `readonly` `optional` **passphrase?**: `string` Passphrase for an encrypted [TlsOptions.key](#key-1). ##### servername? > `readonly` `optional` **servername?**: `string` SNI hostname and the identity-check target (matched against the server certificate's Subject/SAN). Defaults to `ClientOptions.host` when unset. *** ### Transport Pure callback-bag transport abstraction. Each `onXxx` registration is **set-once**: calling `onData(fn)` a second time replaces the first handler. This prevents listener leaks across reconnect cycles where Connection re-registers handlers on a fresh Transport. #### Example ```typescript function writeAll(t: Transport, chunks: Buffer[]): void { for (const chunk of chunks) t.write(chunk); } ``` #### Methods ##### close() > **close**(): `void` Initiate graceful close of the underlying transport. The registered `onClose` handler fires once the socket is fully closed. ###### Returns `void` ##### destroy() > **destroy**(`reason?`): `void` Abruptly destroy the transport, discarding any pending writes. Fires `onError(reason)` (if provided) then `onClose`. ###### Parameters ###### reason? `Error` Optional error describing why the transport was destroyed. ###### Returns `void` ##### onClose() > **onClose**(`fn`): `void` Register the close handler. Called once when the underlying socket is fully closed. Replaces any previously registered handler (set-once semantics). ###### Parameters ###### fn () => `void` ###### Returns `void` ##### onConnect() > **onConnect**(`fn`): `void` Register the connect handler. Called once after TCP (or TLS) handshake completes. Replaces any previously registered handler (set-once semantics). ###### Parameters ###### fn () => `void` ###### Returns `void` ##### onData() > **onData**(`fn`): `void` Register the data handler. Called synchronously for each received chunk. Replaces any previously registered handler (set-once semantics). ###### Parameters ###### fn (`chunk`) => `void` Called with each raw chunk as it arrives from the OS. ###### Returns `void` ##### onError() > **onError**(`fn`): `void` Register the error handler. Called for socket-level errors (ECONNRESET, ETIMEDOUT, etc.). Replaces any previously registered handler (set-once semantics). ###### Parameters ###### fn (`err`) => `void` Receives the underlying OS or TLS error. ###### Returns `void` ##### write() > **write**(`buf`): `boolean` Write `buf` to the underlying transport. ###### Parameters ###### buf `Buffer` ###### Returns `boolean` `true` if the bytes were flushed to the kernel immediately; `false` if the write was buffered (backpressure, caller should pause sending until the `onDrain` event fires at the Connection layer). ## Type Aliases ### AckCode > **AckCode** = `"AA"` \| `"AE"` \| `"AR"` \| `"CA"` \| `"CE"` \| `"CR"` HL7 Table 0008, Acknowledgment Code. A **stable public API**. Two families, by mode (HL7 v2.5.1 §2.9): - **Original mode (§2.9.2):** `AA` accept, `AE` application error, `AR` application reject. The single ACK reports application-level outcome. - **Enhanced mode (§2.9.3):** `CA` commit accept, `CE` commit error, `CR` commit reject, the *accept* acknowledgement, distinct from a later application ACK. `AE` vs `AR`: `AE` is a processing **error** (the sender may resend later, e.g. a transient downstream outage); `AR` is a **reject** (the sender should not resend the message unchanged, e.g. it is structurally unacceptable). `@cosyte/mllp` builds original-mode ACKs; the `C*` codes are surfaced in the type for completeness and for callers that build their own enhanced-mode ACKs via `autoAck: fn`. #### Example ```typescript import type { AckCode } from '@cosyte/mllp'; const code: AckCode = 'AE'; ``` *** ### AckCorrelationCode > **AckCorrelationCode** = `"MLLP_ACK_UNMATCHED_CONTROL_ID"` \| `"MLLP_ACK_AFTER_TIMEOUT"` The two warning codes emitted by ACK correlation rather than by framing. A subset of the framing `WarningCode` union; named separately so the registry below is exhaustive over exactly the codes it owns and a new correlation code cannot be added without a message for it. *** ### AckModeCode > **AckModeCode** = `"MLLP_ACK_ACCEPT_TYPE_UNRECOGNISED"` \| `"MLLP_ACK_APPLICATION_TYPE_UNRECOGNISED"` \| `"MLLP_ACK_TWO_PHASE_UNAVAILABLE"` \| `"MLLP_ACK_COMMIT_ALREADY_REPORTED"` \| `"MLLP_ACK_MSA1_ABSENT"` \| `"MLLP_ACK_MSA1_UNCLASSIFIABLE"` \| `"MLLP_ACK_SEND_ALREADY_DISPOSED"` The stable codes acknowledgement-mode reporting emits. A **public API**: consumers narrow on them in warning handlers, log pipelines and monitoring. Renaming or removing one is a breaking change. They are deliberately a family of their own rather than additions to the framing warning-code union, because that union is the decoder's registry and these are not decoder events. #### Example ```typescript import type { AckModeCode } from '@cosyte/mllp'; const code: AckModeCode = 'MLLP_ACK_TWO_PHASE_UNAVAILABLE'; ``` *** ### ApplicationAckFailure > **ApplicationAckFailure** = `"timeout"` \| `"connection-lost"` Why a send waiting on its application acknowledgement was failed. - `'timeout'`, the wait that started at the accept acknowledgement expired. - `'connection-lost'`, the link failed or was closed while the send was still pending on it. The send is failed rather than left pending or reported as successful, because nobody can say what the receiving application did with a message whose second acknowledgement never arrived. #### Example ```typescript import { MllpApplicationAckError } from '@cosyte/mllp'; if (err instanceof MllpApplicationAckError && err.reason === 'timeout') { // committed by the peer, application disposition unknown } ``` *** ### ClientAuth > **ClientAuth** = `"NONE"` \| `"WANT"` \| `"MUST"` ATNA ITI-19 mutual-authentication modes for [ServerTlsOptions.clientAuth](#clientauth). Mirrors the IHE ATNA "Authenticate Node" mutual-auth requirement (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html): - `'NONE'`, no client certificate requested (default). - `'WANT'`, client certificate requested but NOT required; an untrusted or absent client certificate does not reject the connection. The peer certificate (if any) is surfaced on the `'connection'` event. - `'MUST'`, client certificate required AND verified against [ServerTlsOptions.ca](#ca); the ATNA mutual-authentication mode. A missing or untrusted client certificate rejects the handshake. #### Example ```typescript import type { ClientAuth } from '@cosyte/mllp'; const mode: ClientAuth = 'MUST'; // ATNA ITI-19 mutual node authentication ``` *** ### ConnectionErrorCause > **ConnectionErrorCause** = `"fifo-unsafe"` \| `"in-flight-orphan"` \| `"tls-verify"` \| `"tls-handshake"` \| `"framing-fatal"` Stable cause codes for `MllpConnectionError`. These codes are a **public API**, they appear in error inspection by callers, log pipelines, and monitoring dashboards. Renaming or removing a member is a breaking change. - `'fifo-unsafe'`, A queued send was rejected during reconnect because FIFO ordering cannot be safely resumed across sessions. - `'in-flight-orphan'`, An in-flight send (already write-flushed, ACK timer started) was rejected during reconnect in FIFO mode because the at-most-once delivery contract cannot be preserved across a socket drop (healthcare medication/orders semantics). - `'tls-verify'`, The TLS handshake failed certificate verification (untrusted chain, expired/not-yet-valid cert, hostname mismatch, revocation, …). See [isTlsVerificationErrorCode](#istlsverificationerrorcode) for the exact underlying error codes. Classified **permanent** by `isTransientConnectionError`, never auto-reconnect-looped into a misconfigured or MITM'd endpoint. - `'tls-handshake'`, A TLS-**protocol**-shaped handshake failure observed before `'secureConnect'`: `ERR_SSL_*` codes, `EPROTO`, or an OpenSSL alert-bearing error (protocol version mismatch, no shared cipher, a required mutual-TLS client certificate rejected by the server, …). See `isTlsProtocolError` for the boundary. Pure TCP-level failures (`ECONNREFUSED`, `ETIMEDOUT`, …) on a TLS-configured connection carry **no** `connectionCause`, the same shape as plaintext. Scope note: the `'tls-verify'`/`'tls-handshake'` values are attached on the client's **initial `connect()` path**. Failures on the auto-reconnect path surface as raw socket errors, their transient/permanent classification still applies, but they do not (yet) carry a `connectionCause`. #### Example ```typescript if (err instanceof MllpConnectionError && err.connectionCause === 'in-flight-orphan') { // Treat as at-most-once: do NOT auto-retry; bubble up to caller for // application-level dedupe / replay decisions. } ``` *** ### ConnectionErrorPhase > **ConnectionErrorPhase** = `"connect"` \| `"send"` \| `"receive"` \| `"close"` \| `"reconnect"` Connection lifecycle phase where the error occurred. All 5 phases are defined even though `'reconnect'` is only exercised by the client. Locking the full union now prevents a breaking type change later. *** ### ConnectionState > **ConnectionState** = `"CONNECTING"` \| `"CONNECTED"` \| `"DRAINING"` \| `"RECONNECTING"` \| `"DISCONNECTED"` \| `"CLOSED"` The 6 connection states. Transitions are validated against the legal-transition edge graph. Illegal transitions are silently ignored to preserve FSM integrity. *** ### DifferentialConfigurationErrorCode > **DifferentialConfigurationErrorCode** = *typeof* [`MLLP_DIFF_PEER_UNPARSEABLE`](#mllp_diff_peer_unparseable) Union of the stable differential-configuration error codes. #### Example ```typescript const code: DifferentialConfigurationErrorCode = 'MLLP_DIFF_PEER_UNPARSEABLE'; ``` *** ### DifferentialConnect > **DifferentialConnect** = (`peer`) => [`Transport`](#transport-1) Opens a connection to the peer. Supply your own to run the harness over something other than a plain TCP socket, a TLS transport for an MLLPS peer being the obvious case. The returned transport must not be connected yet: the harness registers its handlers first and writes only once `onConnect` fires. #### Parameters ##### peer [`DifferentialPeer`](#differentialpeer) #### Returns [`Transport`](#transport-1) #### Example ```typescript const connect: DifferentialConnect = (peer) => new TlsTransport(tls.connect({ host: peer.host, port: peer.port })); ``` *** ### DifferentialCorrelationOutcome > **DifferentialCorrelationOutcome** = `"match"` \| `"mismatch"` \| `"absent"` \| `"not-observed"` Whether the peer's acknowledgement echoed the control ID of the message it answered. `absent` means the response carried no readable acknowledged control ID at all, which is a different failure from echoing the wrong one and is worth telling apart. #### Example ```typescript const correlation: DifferentialCorrelationOutcome = 'match'; ``` *** ### DifferentialExchangeOutcome > **DifferentialExchangeOutcome** = `"answered"` \| `"unanswered"` \| `"undecodable-response"` \| `"connection-refused"` \| `"connection-failed"` \| `"connection-dropped"` What became of one canonical exchange. - `answered`, the peer returned a decodable frame, so parity and correlation are reported for it. - `unanswered`, the connection held and no complete frame arrived before the deadline. - `undecodable-response`, bytes arrived and the decoder could not complete a frame from them even with every tolerance enabled. The deviation is still named by its code. - `connection-refused`, the peer refused the connection outright. - `connection-failed`, the connection could not be established for some other reason. - `connection-dropped`, the connection was established and then went away mid-exchange. #### Example ```typescript const outcome: DifferentialExchangeOutcome = 'answered'; ``` *** ### DifferentialParityOutcome > **DifferentialParityOutcome** = `"match"` \| `"deviation"` \| `"not-observed"` Whether the peer's response frame was byte-identical to the canonical Release 1 block. `not-observed` is the honest answer when no frame arrived: an exchange that was never answered has no parity, and reporting one as a failure would confuse a silent peer with a mis-framing one. #### Example ```typescript const parity: DifferentialParityOutcome = 'match'; ``` *** ### DifferentialRunResult > **DifferentialRunResult** = `"parity-observed"` \| `"deviations-observed"` \| `"no-observation"` \| `"skipped"` The overall shape of a run, in three values plus the skip. - `parity-observed`, every attempted exchange was answered with a byte-identical canonical block and a correlating acknowledgement. - `deviations-observed`, at least one exchange was answered and at least one exchange deviated, went unanswered or failed. - `no-observation`, nothing was answered, so the run observed nothing about the peer. - `skipped`, no peer was configured and nothing was sent. **None of these is a pass**, and `parity-observed` in particular is not one: it says what this corpus saw on this run, not that a peer is conformant. #### Example ```typescript const result: DifferentialRunResult = 'no-observation'; ``` *** ### DifferentialSkipReason > **DifferentialSkipReason** = `"no-peer-configured"` Why a run was skipped, when it was. #### Example ```typescript const reason: DifferentialSkipReason = 'no-peer-configured'; ``` *** ### NackAckCode > **NackAckCode** = `"AE"` \| `"AR"` \| `"CE"` \| `"CR"` The negative acknowledgement codes a [NackEvent](#nackevent) can carry: both halves of the negative side of HL7 Table 0008. #### Example ```typescript import type { NackAckCode } from '@cosyte/mllp'; const code: NackAckCode = 'CR'; ``` *** ### NackReason > **NackReason** = `"handler-rejected"` \| `"uncorrelatable-inbound"` \| `"discarded-bytes"` Why the server sent a **negative** acknowledgement instead of `AA` on the auto-ACK path. A stable, **PHI-free** enum (no payload bytes, no control ID), safe to log and to key metrics on. - `'handler-rejected'`, a commit-gated `onMessage` handler threw/rejected (the commit contract: a positive ACK cannot precede a successful commit). - `'uncorrelatable-inbound'`, the inbound could not carry a correlatable positive ACK (no readable `MSH`, an empty MSH-10, or a batch/concatenated-message shape). A positive `AA` here names a control ID the sender cannot match → timeout → resend → **duplicate clinical message**. See [rawAckUncorrelatable](#rawackuncorrelatable). - `'discarded-bytes'`, the decoder flagged `MLLP_TRAILING_BYTES` for this frame: a mid-payload `VT` made it **discard accumulated bytes** and deliver only the fragment after it. The clinical message was destroyed in transit; a positive `AA` would tell the sender a message we never received was delivered. *** ### NegativeAckCode > **NegativeAckCode** = `"AE"` \| `"AR"` The negative original-mode acknowledgement codes a failed handler can produce. #### Example ```typescript import type { NegativeAckCode } from '@cosyte/mllp'; const code: NegativeAckCode = 'AR'; ``` *** ### PemInput > **PemInput** = `string` \| `Buffer` \| (`string` \| `Buffer`)[] A PEM-encoded credential (certificate, key, or CA), matching Node's `tls.connect`/`tls.createServer` input shape, a single PEM string/Buffer, or an array of them (chain / multiple trust anchors). #### Example ```typescript import { readFileSync } from 'node:fs'; const ca: PemInput = readFileSync('ca.pem'); ``` *** ### RetryStrategy > **RetryStrategy** = (`ctx`) => `number` \| `null` Custom reconnect-backoff hook. Return `null` to halt reconnection, the FSM transitions to `CLOSED`. #### Parameters ##### ctx [`RetryContext`](#retrycontext) #### Returns `number` \| `null` *** ### SecurityWarningCode > **SecurityWarningCode** = *typeof* [`MLLP_TLS_VERIFY_DISABLED`](#mllp_tls_verify_disabled) \| *typeof* [`MLLP_BIND_ALL_INTERFACES`](#mllp_bind_all_interfaces) Union of the two stable security-warning codes. #### Example ```typescript const code: SecurityWarningCode = 'MLLP_TLS_VERIFY_DISABLED'; ``` *** ### TlsConfigurationErrorCode > **TlsConfigurationErrorCode** = *typeof* [`MLLP_TLS_CIPHER_LIST_REJECTED`](#mllp_tls_cipher_list_rejected) \| *typeof* [`MLLP_TLS_CIPHER_OPTION_CONFLICT`](#mllp_tls_cipher_option_conflict) \| *typeof* [`MLLP_TLS_DH_PARAMETERS_REJECTED`](#mllp_tls_dh_parameters_rejected) Union of the stable TLS-configuration error codes. #### Example ```typescript const code: TlsConfigurationErrorCode = 'MLLP_TLS_CIPHER_LIST_REJECTED'; ``` *** ### WarningCode > **WarningCode** = `"MLLP_MISSING_LEADING_VT"` \| `"MLLP_FS_WITHOUT_CR"` \| `"MLLP_LF_AFTER_FS"` \| `"MLLP_LEADING_WHITESPACE"` \| `"MLLP_TRAILING_BYTES"` \| `"MLLP_PAYLOAD_CONTAINS_VT"` \| `"MLLP_PAYLOAD_CONTAINS_FS"` \| `"MLLP_EMPTY_PAYLOAD"` \| `"MLLP_FRAME_TOO_LARGE"` \| `"MLLP_ACK_UNMATCHED_CONTROL_ID"` \| `"MLLP_ACK_AFTER_TIMEOUT"` Union of all stable MLLP warning codes. These codes are a **public API**, they appear in `onWarning` handlers, log pipelines, monitoring dashboards, and error messages. Renaming is a breaking change. #### Example ```typescript const code: WarningCode = 'MLLP_FS_WITHOUT_CR'; ``` ## Variables ### ATNA\_CIPHER\_LIST > `const` **ATNA\_CIPHER\_LIST**: `string` The OpenSSL cipher-list string `tls.atnaTransportSecurity: true` offers: the three TLS 1.3 default suites followed by the four ITI TF-2 §3.19.6.2.3 suites, and nothing else. #### Example ```typescript import { ATNA_CIPHER_LIST } from '@cosyte/mllp'; console.log(ATNA_CIPHER_LIST.split(':').length); // => 7 ``` *** ### ATNA\_CIPHER\_SUITES > `const` **ATNA\_CIPHER\_SUITES**: readonly `string`[] The four TLS 1.2 cipher suites ITI TF-2 §3.19.6.2.3 names, in the OpenSSL spelling a cipher-list string is written in. Their IANA spellings, which are what the standard prints and what the `'tlsNegotiated'` event reports, are the same four prefixed `TLS_` and joined with `_WITH_`. #### Example ```typescript import { ATNA_CIPHER_SUITES } from '@cosyte/mllp'; console.log(ATNA_CIPHER_SUITES.includes('ECDHE-RSA-AES128-GCM-SHA256')); // => true ``` *** ### MLLP\_BIND\_ALL\_INTERFACES > `const` **MLLP\_BIND\_ALL\_INTERFACES**: `"MLLP_BIND_ALL_INTERFACES"` = `"MLLP_BIND_ALL_INTERFACES"` Emitted (server-side) once at `listen()` time when the server binds a wildcard host (`'0.0.0.0'` or `'::'`) with `ServerOptions.allowWildcardBind: true`. Binding all interfaces widens the network exposure of the listener; this is the loud, one-time reminder that the operator opted in. #### Example ```typescript import { MLLP_BIND_ALL_INTERFACES } from '@cosyte/mllp'; server.on('securityWarning', (w) => { if (w.code === MLLP_BIND_ALL_INTERFACES) logger.warn(w.message); }); ``` *** ### MLLP\_DIFF\_PEER\_UNPARSEABLE > `const` **MLLP\_DIFF\_PEER\_UNPARSEABLE**: `"MLLP_DIFF_PEER_UNPARSEABLE"` = `"MLLP_DIFF_PEER_UNPARSEABLE"` A peer address was configured and could not be resolved into a host and a port. Distinct from no peer at all, which is not an error and skips the run. #### Example ```typescript import { MLLP_DIFF_PEER_UNPARSEABLE } from '@cosyte/mllp'; if (err.code === MLLP_DIFF_PEER_UNPARSEABLE) process.exit(78); // EX_CONFIG ``` *** ### MLLP\_TLS\_CIPHER\_LIST\_REJECTED > `const` **MLLP\_TLS\_CIPHER\_LIST\_REJECTED**: `"MLLP_TLS_CIPHER_LIST_REJECTED"` = `"MLLP_TLS_CIPHER_LIST_REJECTED"` The runtime refused the cipher-suite list it was handed: no suite in it is available in this Node build's TLS library. Nothing falls back to the runtime default list, the connection or the bind is refused instead. #### Example ```typescript import { MLLP_TLS_CIPHER_LIST_REJECTED } from '@cosyte/mllp'; if (err.code === MLLP_TLS_CIPHER_LIST_REJECTED) process.exit(78); // EX_CONFIG ``` *** ### MLLP\_TLS\_CIPHER\_OPTION\_CONFLICT > `const` **MLLP\_TLS\_CIPHER\_OPTION\_CONFLICT**: `"MLLP_TLS_CIPHER_OPTION_CONFLICT"` = `"MLLP_TLS_CIPHER_OPTION_CONFLICT"` Two different offered-suite declarations were made at once: `atnaTransportSecurity: true` fixes the list, and `ciphers` replaces it. Honouring either one silently discards the other, so both are refused. #### Example ```typescript import { MLLP_TLS_CIPHER_OPTION_CONFLICT } from '@cosyte/mllp'; if (err.code === MLLP_TLS_CIPHER_OPTION_CONFLICT) logger.error('pick one of the two'); ``` *** ### MLLP\_TLS\_DH\_PARAMETERS\_REJECTED > `const` **MLLP\_TLS\_DH\_PARAMETERS\_REJECTED**: `"MLLP_TLS_DH_PARAMETERS_REJECTED"` = `"MLLP_TLS_DH_PARAMETERS_REJECTED"` The ephemeral Diffie-Hellman parameters a server supplied are not usable: not PEM content, not a Diffie-Hellman parameter block, or a group the TLS library refuses. Distinct from [MLLP\_TLS\_CIPHER\_LIST\_REJECTED](#mllp_tls_cipher_list_rejected) because the remedy is different: one is the offered suite list, the other is the key exchange behind it. Nothing is bound and nothing falls back to a server running with no Diffie-Hellman parameters, which would advertise its DHE suites and then fail every handshake in them. #### Example ```typescript import { MLLP_TLS_DH_PARAMETERS_REJECTED } from '@cosyte/mllp'; if (err.code === MLLP_TLS_DH_PARAMETERS_REJECTED) process.exit(78); // EX_CONFIG ``` *** ### MLLP\_TLS\_VERIFY\_DISABLED > `const` **MLLP\_TLS\_VERIFY\_DISABLED**: `"MLLP_TLS_VERIFY_DISABLED"` = `"MLLP_TLS_VERIFY_DISABLED"` Emitted (client-side) on every successful `secureConnect`, initial connect AND every reconnect, when [TlsOptions.allowUnverified](#allowunverified) is `true`. Certificate verification is disabled for the connection; this is the loud, per-connection reminder that the channel is not authenticated per IHE ATNA ITI-19 (https://profiles.ihe.net/ITI/TF/Volume2/ITI-19.html). #### Example ```typescript import { MLLP_TLS_VERIFY_DISABLED } from '@cosyte/mllp'; client.on('securityWarning', (w) => { if (w.code === MLLP_TLS_VERIFY_DISABLED) metrics.increment('mllp.tls_verify_disabled'); }); ``` *** ### TLS13\_DEFAULT\_CIPHER\_SUITES > `const` **TLS13\_DEFAULT\_CIPHER\_SUITES**: readonly `string`[] The three TLS 1.3 cipher suites the runtime enables by default. They are carried alongside the four above for one reason: a TLS 1.3 suite is enabled only by its full name in the cipher list, so a list holding the four TLS 1.2 suites alone would turn TLS 1.3 **off**. Selecting the option must never remove a protocol version that was reachable without it, so these are restated rather than dropped. #### Example ```typescript import { TLS13_DEFAULT_CIPHER_SUITES } from '@cosyte/mllp'; console.log(TLS13_DEFAULT_CIPHER_SUITES.length); // => 3 ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Package version marker exported from the `@cosyte/mllp` root. Kept in lockstep with `package.json` by `scripts/sync-version.mjs`, which the `version` script runs immediately after `changeset version`. The `: string` annotation is deliberate, without it TypeScript infers the *literal* type (`declare const VERSION = "0.0.0"`), which leaks the current release into consumers' types and makes an equality check against any other version a compile error. #### Example ```typescript import { VERSION } from '@cosyte/mllp'; console.log(VERSION); ``` ## Functions ### ackDiagnosticMessage() > **ackDiagnosticMessage**(`code`): `string` Look up the frozen diagnostic text for an ACK-correlation code. A pure table read. It takes no value, so no caller can widen it into an interpolation site. #### Parameters ##### code [`AckCorrelationCode`](#ackcorrelationcode) The correlation code being reported. #### Returns `string` The registered message for that code, byte-for-byte. #### Example ```typescript const message = ackDiagnosticMessage('MLLP_ACK_AFTER_TIMEOUT'); ``` *** ### ackModeDiagnosticMessage() > **ackModeDiagnosticMessage**(`code`): `string` Look up the frozen diagnostic text for an acknowledgement-mode code. A pure table read. It takes no value, so no caller can widen it into an interpolation site. #### Parameters ##### code [`AckModeCode`](#ackmodecode) The acknowledgement-mode code being reported. #### Returns `string` The registered message for that code, byte-for-byte. #### Example ```typescript const message = ackModeDiagnosticMessage('MLLP_ACK_MSA1_UNCLASSIFIABLE'); ``` *** ### buildRawAck() > **buildRawAck**(`payload`, `code`): `Buffer` Build a minimal original-mode HL7 v2 acknowledgement from raw inbound payload bytes, **without a parser** (parser-driven ACKs are the `@cosyte/mllp/ack-from-hl7` subpath). Locates the `MSH` segment (the first `CR`/`LF`-delimited segment starting with `MSH`, so a leading `CR` or an `FHS`/`BHS` batch header does not hide it), reads the field separator from **MSH-1** and the encoding characters from **MSH-2**, then splits that segment on that separator to read fields. The ACK swaps sender/receiver per HL7 ACK rules, echoes the inbound MSH-10 into MSA-2 (§2.9.2.2), sets MSA-1 to `code`, and, for negative codes, adds a **static, PHI-free** MSA-3 reason. A fresh control ID fills the ACK's own MSH-10. ## Why the delimiters are read, not assumed MSH-1 **is** the field separator (HL7 v2.5.1 §2.5.4): the byte at offset 3 of the MSH segment *defines* it for the whole message. `|` is overwhelmingly common but it is a convention, not the spec. Assuming it was wrong in two compounding ways: 1. **Reading.** Splitting a `!`-delimited message on `|` yields ONE field, so every echoed field, including MSH-10, came back empty. The ACK went out as `MSA|AA|` with **no correlation id at all**: the sender cannot match it, times out, and resends → a duplicate clinical message. That is the exact failure this package's correlator exists to prevent, manufactured by the ACK builder itself. 2. **Writing.** The echoed MSH-3..6 and MSH-10 field *content* is copied verbatim from the inbound, still escaped against the **inbound's** encoding characters. Re- emitting that content under a different delimiter set silently reinterprets it: an inbound whose component separator is `#` and whose MSH-10 is `ID#X` would be re-emitted under `^~\&` as the literal `ID#X`, which the sender then reads as a single component rather than two. Echoing MSH-1 and MSH-2 keeps the content and the delimiters that define it together. ## It reads the MSH through the SHARED scanner The MSH read is `readMshSegment` from `src/internal/control-id.ts`, the same call the client's correlator makes to derive the key it will later match this ACK against. That is deliberate and it is the whole point: this builder used to re-derive the read itself (`payload.toString("latin1").split("\r")`, hunting for an `MSH` anywhere in the payload), and the two disagreed on real inputs. On a **truncated MSH** followed by a `PID` the correlator keyed on the PID's MRN while this builder echoed an empty MSA-2. On a payload with a **leading `CR`**, which the MLLP decoder passes straight through, and which real senders emit, this builder found the `MSH` and echoed MSH-10 correctly while the correlator, requiring `MSH` at byte 0, gave up. Every such disagreement is an ACK the sender cannot match → timeout → resend → **duplicate clinical message**. The fix is one scan, but note *which* scan. The first attempt made them agree by requiring `MSH` at byte 0 everywhere, which "resolved" the leading-`CR` disagreement by degrading the side that had been **right**: `buildRawAck` began emitting a positive `AA` with an empty MSA-2, silently, for a message whose MSH-10 was plainly present. Agreement is not the goal; agreeing on the *correct*, *tolerant* answer is. A lenient reader may never drop data that is there (Postel's Law, CLAUDE.md). ## The fail-safe downgrade: never a positive `AA`/`CA` it cannot correlate A positive acknowledgement is a promise the sender may forget the message. If it names a control ID the sender cannot match, or names one message out of several it never read, the sender times out and resends, committing a **duplicate clinical message**. So a requested positive code is **downgraded** to its non-positive counterpart (`AA`→`AE`, `CA`→`CE`) whenever the payload cannot carry a correlatable positive ACK: no readable `MSH`, an empty MSH-10, or a batch/concatenated-message shape that a single MSA-2 cannot acknowledge. See [rawAckUncorrelatable](#rawackuncorrelatable) for the exact conditions and why each is a refusal rather than a widened reader. A requested **negative** code (`AE`/`AR`/`CE`/`CR`) is never touched. This mirrors the parser-backed `buildMllpAck`, which downgrades and warns on an unparseable inbound, the two builders' fail-safe semantics now agree. **Never throws** and **never copies payload content** beyond the routing/control metadata above, `readMshSegment` stops at the MSH's segment terminator, so no field of any later segment (PID and friends) can be reached, let alone echoed. On a missing or unreadable `MSH` it returns a minimal well-formed ACK carrying the (downgraded) `code` so the caller can still respond. #### Parameters ##### payload `Buffer` Raw decoded HL7 v2 payload bytes (MLLP framing already stripped). ##### code [`AckCode`](#ackcode-3) Requested MSA-1 acknowledgement code. A positive `AA`/`CA` is downgraded to `AE`/`CE` when the message cannot be correlated (see above). #### Returns `Buffer` ACK payload bytes (no framing, the caller wraps with `encodeFrame`). #### Example ```typescript import { buildRawAck } from '@cosyte/mllp'; const ack = buildRawAck(inboundPayload, 'AE'); // MSA|AE||message could not be processed ``` *** ### canonicalAcknowledgement() > **canonicalAcknowledgement**(): `Buffer` The canonical positive acknowledgement, unframed, freshly built on every call. This is what a conformant Release 1 peer's answer to the first canonical exchange looks like on the wire once the framing is stripped. The harness never sends it; it is here so a test double, or a consumer building one, can answer with the shape the package itself emits. #### Returns `Buffer` The unframed acknowledgement bytes. #### Example ```typescript import { canonicalAcknowledgement, encodeFrame } from '@cosyte/mllp'; const wire = encodeFrame(canonicalAcknowledgement()); // wire[0] === 0x0b ``` *** ### canonicalExchanges() > **canonicalExchanges**(): readonly [`CanonicalExchange`](#canonicalexchange)[] The canonical exchange corpus, freshly built on every call so a caller cannot mutate the corpus another caller will send. Two messages, both drawn from the framing goldens this package already pins itself against, so what a consumer sends at their own engine is byte-identical to what this package's own framing tests assert. The positive acknowledgement golden is deliberately NOT one of them: it is the reference ANSWER (see [canonicalAcknowledgement](#canonicalacknowledgement)), and an engine that correctly declines to acknowledge an unsolicited acknowledgement would otherwise be reported as having failed to answer. #### Returns readonly [`CanonicalExchange`](#canonicalexchange)[] One entry per exchange, in the order the harness runs them. #### Example ```typescript const exchanges = canonicalExchanges(); // exchanges.map((e) => e.id) is ['adt-a01', 'oru-r01'] ``` *** ### createClient() > **createClient**(`opts`): [`MllpClient`](#mllpclient) Create an [MllpClient](#mllpclient). Equivalent to `new MllpClient(opts)`. #### Parameters ##### opts [`ClientOptions`](#clientoptions) #### Returns [`MllpClient`](#mllpclient) #### Example ```typescript import { createClient } from '@cosyte/mllp'; const client = createClient({ host: 'localhost', port: 2575 }); await client.connect(); const ack = await client.send(payloadBuffer); await client.close(); ``` *** ### createServer() > **createServer**(`opts?`): [`MllpServer`](#mllpserver) Factory function, creates a new `MllpServer` with the supplied options. Prefer `createServer()` over `new MllpServer()` for forward-compatible construction. #### Parameters ##### opts? [`ServerOptions`](#serveroptions) = `{}` #### Returns [`MllpServer`](#mllpserver) #### Example ```typescript import { createServer } from '@cosyte/mllp'; const server = createServer({ onMessage: (payload, meta, conn) => { console.log('received', payload.length, 'bytes'); }, }); await server.listen(2575); ``` *** ### createStarterClient() > **createStarterClient**(`opts`): `Promise`\<[`MllpClient`](#mllpclient)\> Three-line MLLP client with batteries-included defaults. The returned client is already CONNECTED, `connect()` has been awaited. Defaults: - `autoReconnect: true` - `ackTimeoutMs: 30_000` - `correlateByControlId: false` (FIFO mode, simplest mental model) - `pipeline: true` - `highWaterMark: 64` - `onBackpressure: 'reject'` - `handleSignals: false` (opt-in) The factory is **async**, so the literal three-line north-star snippet has an explicit `await` BEFORE `createStarterClient(...)`, without it, the `using` declaration would receive a `Promise`, not an `MllpClient`, and `Symbol.asyncDispose` would not run at scope exit. #### Parameters ##### opts [`StarterClientOptions`](#starterclientoptions) #### Returns `Promise`\<[`MllpClient`](#mllpclient)\> #### Example ```typescript import { createStarterClient } from '@cosyte/mllp'; await using c = await createStarterClient({ host: 'localhost', port: 2575 }); const ack = await c.send(payloadBuffer); ``` *** ### createStarterServer() > **createStarterServer**(`opts`): `Promise`\<[`MllpServer`](#mllpserver)\> Starter factory, creates, configures, and starts an `MllpServer` in one call. Provides the "three lines of code" north-star experience with sensible defaults: `autoAck: 'AA'`, `drainTimeoutMs: 30_000`, `Symbol.asyncDispose` wired. #### Parameters ##### opts [`StarterServerOptions`](#starterserveroptions) #### Returns `Promise`\<[`MllpServer`](#mllpserver)\> #### Example ```typescript import { createStarterServer } from '@cosyte/mllp'; const server = await createStarterServer({ port: 2575, onMessage: async (payload) => { await db.commit(payload); // the durable-commit step; a throw answers a negative ACK }, }); ``` *** ### createWarning() > **createWarning**(`code`, `byteOffset`, `message`): [`MllpWarning`](#mllpwarning) Create a frozen `MllpWarning` object. The returned object is `Object.freeze()`'d so subscribers cannot mutate shared warning state. `connectionId` is always `undefined` here; `Connection` enriches it via `{ ...w, connectionId: this.connectionId }` then re-freezes. #### Parameters ##### code [`WarningCode`](#warningcode) ##### byteOffset `number` ##### message `string` #### Returns [`MllpWarning`](#mllpwarning) #### Example ```typescript const w = createWarning('MLLP_EMPTY_PAYLOAD', 64, 'Empty payload between VT and FS'); // w.connectionId === undefined ``` *** ### differentialConfigurationMessage() > **differentialConfigurationMessage**(`code`): `string` Fixed, human-readable description for a differential-configuration code. Byte-for-byte identical for a given code, whatever was configured. #### Parameters ##### code `"MLLP_DIFF_PEER_UNPARSEABLE"` The stable configuration-error code. #### Returns `string` The frozen registry text for `code`. #### Example ```typescript import { differentialConfigurationMessage, MLLP_DIFF_PEER_UNPARSEABLE } from '@cosyte/mllp'; logger.error(differentialConfigurationMessage(MLLP_DIFF_PEER_UNPARSEABLE)); ``` *** ### encodeFrame() > **encodeFrame**(`payload`, `opts?`): `Buffer` Encode `payload` in canonical MLLP framing: `VT (0x0B) + payload + FS (0x1C) + CR (0x0D)`. Strict by default: throws `MllpFramingError` if the payload contains VT or FS bytes. Set `{ allowDelimiterBytesInPayload: true }` to pass those bytes through verbatim and receive an `MllpWarning` per offending byte via `onWarning` instead. The returned Buffer has length `payload.length + 3`. Bytes at positions 1 through `payload.length` are an exact copy of the payload (independent of the input buffer). #### Parameters ##### payload `Buffer` ##### opts? [`EncoderOptions`](#encoderoptions) #### Returns `Buffer` #### Throws with code `MLLP_PAYLOAD_CONTAINS_VT` when payload contains byte `0x0B` and `allowDelimiterBytesInPayload` is `false` (default). #### Throws with code `MLLP_PAYLOAD_CONTAINS_FS` when payload contains byte `0x1C` and `allowDelimiterBytesInPayload` is `false` (default). #### Example ```typescript import { encodeFrame } from '@cosyte/mllp'; // Strict (default), throws on delimiter bytes in payload const frame = encodeFrame(Buffer.from('MSH|^~\\&|SEND|FAC|RECV|FAC|...')); socket.write(frame); // Tolerant, passes delimiter bytes through with a warning const frame2 = encodeFrame(dirtyPayload, { allowDelimiterBytesInPayload: true, onWarning: (w) => logger.warn(w), }); ``` *** ### isTlsProtocolError() > **isTlsProtocolError**(`err`): `boolean` Detects **TLS-protocol-shaped** errors, failures of the TLS protocol itself, as opposed to plain TCP-level network failures. Apply this only to errors raised on a **TLS** connection; `MllpClient` does exactly that (the predicate is consulted only when `ClientOptions.tls` is set). The boundary: **TLS-protocol-shaped (`true`):** - `code` starting `ERR_SSL_` (Node's TLS alert codes, e.g. `ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED`, a `clientAuth: 'MUST'` server rejecting the client's certificate). - `code === 'EPROTO'`, on a TLS connection this is OpenSSL failing the handshake (protocol version mismatch, no shared cipher, a TLS ≤1.2 mTLS rejection). - `message` containing `ssl` or `alert` (`/\bssl\b|\balert\b/i`, "SSL routines", "tlsv13 alert certificate required", …). This message check is a **heuristic backstop** over the code-based checks above, not a precise boundary: it exists to catch OpenSSL errors that surface without a usable `code`, and `MllpClient` consults it only on connections where TLS is configured. An arbitrary non-TLS error whose message happens to contain those words would also match. **NOT TLS-protocol-shaped (`false`), plain network failures, which stay transient for the reconnect classifier:** `ECONNREFUSED`, `ETIMEDOUT`, `EHOSTUNREACH`, `ENETUNREACH`, `EPIPE`, and a plain `ECONNRESET` carrying no TLS alert context, a network blip during (or after) a handshake should still auto-heal. Certificate-**verification** failures are a separate class, see [isTlsVerificationErrorCode](#istlsverificationerrorcode); `MllpClient` checks that first and labels those `connectionCause: 'tls-verify'`. Why this matters: under TLS 1.3 (RFC 8446 §4.4.2) a `clientAuth: 'MUST'` server can reject the client's certificate AFTER the client's own `'secureConnect'`, the rejection then surfaces as a post-connect socket error. A misconfigured mTLS client must never auto-reconnect-loop against a server that will always reject it, so `MllpClient` classifies TLS-protocol-shaped errors as **permanent**. #### Parameters ##### err `unknown` #### Returns `boolean` #### Example ```typescript import { isTlsProtocolError, MllpConnectionError } from '@cosyte/mllp'; client.on('error', ({ error }) => { if (error instanceof MllpConnectionError && isTlsProtocolError(error.cause)) { // TLS protocol failure, a configuration problem, not a network blip. } }); ``` *** ### isTlsVerificationErrorCode() > **isTlsVerificationErrorCode**(`code`): `boolean` Set of Node/OpenSSL error codes that indicate a **certificate-verification** failure (as opposed to some other TLS handshake failure), untrusted chain, expired/not-yet-valid certificate, hostname mismatch, revocation, etc. Used by `MllpClient` to classify a TLS connect failure's `connectionCause` as `'tls-verify'` (this set) vs `'tls-handshake'` (everything else observed before `'secureConnect'`). Exported so callers can apply the same classification to their own error handling. #### Parameters ##### code `string` #### Returns `boolean` #### Example ```typescript import { isTlsVerificationErrorCode } from '@cosyte/mllp'; if (isTlsVerificationErrorCode('CERT_HAS_EXPIRED')) { // definitely a verification failure, not a protocol/cipher mismatch } ``` *** ### isTransientConnectionError() > **isTransientConnectionError**(`err`): `boolean` Classifies a connection error as transient (eligible for auto-reconnect) or permanent (halts auto-reconnect, transitions to CLOSED). Used internally by `MllpClient` BEFORE invoking `retryStrategy` (see `RetryContext.classifiedAs`). Re-exported so callers can implement their own retry policies. Classification table: - `ENOTFOUND`, `EACCES` → **permanent** (`false`) - `ECONNREFUSED`, `ECONNRESET`, `ETIMEDOUT`, `EHOSTUNREACH`, `ENETUNREACH`, `EPIPE` → **transient** (`true`) - `CERT_*` and `UNABLE_TO_VERIFY_LEAF_SIGNATURE` / `DEPTH_ZERO_SELF_SIGNED_CERT` / `SELF_SIGNED_CERT_IN_CHAIN` (any [isTlsVerificationErrorCode](#istlsverificationerrorcode) code) → **permanent** (`false`), never auto-reconnect-loop into a misconfigured or MITM'd endpoint. - `ERR_SSL_*` (Node TLS alert codes) → **permanent** (`false`), a TLS protocol failure such as a `clientAuth: 'MUST'` server rejecting the client certificate recurs on every attempt. On TLS-configured connections `MllpClient` additionally consults [isTlsProtocolError](#istlsprotocolerror), which also catches `EPROTO`/alert-bearing OpenSSL errors that this generic classifier (which cannot know the connection was TLS) leaves transient. - `MLLP_*` (any [MllpFramingError](#mllpframingerror) code, a fatal decoder throw, surfaced with `connectionCause: 'framing-fatal'`) → **permanent** (`false`). The peer is not speaking MLLP, an HTTP probe, a health check, a wrong-port misconfiguration, or is emitting frames past `maxFrameSizeBytes`. Every reconnect meets the same bytes, so retrying is an unbounded storm against a peer that is already misconfigured. If a peer's quirk is *expected*, the decoder's tolerance opt-ins are the supported answer, they make it a warning, not a fatal. - non-Error / unknown / no-code → **transient** (`true`), Postel's Law default. Reconnect attempts are bounded by `retryStrategy` and the 30s backoff cap, so the default is safe. #### Parameters ##### err `unknown` #### Returns `boolean` #### Example ```typescript import { isTransientConnectionError } from '@cosyte/mllp'; client.on('error', (err) => { if (isTransientConnectionError(err)) { metrics.increment('mllp.transient_error'); } else { metrics.increment('mllp.permanent_error'); } }); ``` *** ### rawAckUncorrelatable() > **rawAckUncorrelatable**(`payload`): `boolean` True iff a **positive** raw acknowledgement (`AA`/`CA`) cannot be safely correlated to this payload, so [buildRawAck](#buildrawack) (and the server's auto-ACK path) must **downgrade** it to a non-positive `AE`/`CE` rather than tell the sender "I have it." The rule this enforces: **never answer `AA` for a message you could not correlate.** A positive ACK is a promise the sender may forget the message; if it names a control ID the sender cannot match, or names one of several messages it never read, the sender times out and resends, committing a **duplicate clinical message** (or worse, believes a destroyed message was delivered). Three payload-shaped reasons make a positive ACK uncorrelatable, all peer-reachable off the wire: 1. **No readable `MSH`.** `readMshSegment` returns `null`, e.g. a `BOM`/`SP`/`TAB` before `MSH` (which shares the `MSH`'s segment line, so `MSH` heads no segment), or a bare fragment delivered after a mid-payload `VT` discard (`MLLP_TRAILING_BYTES`). MSA-2 would be empty: an ACK that correlates to nothing. 2. **Empty MSH-10.** The `MSH` is readable but carries no message control ID, so there is nothing to echo, again `MSA|AA|` with an empty MSA-2. 3. **A batch or concatenated messages.** An `FHS`/`BHS`/`BTS`/`FTS` envelope (§2.10.3) or a second `MSH` in the same frame: a single MSA-2 can echo only ONE control ID, so a positive ACK naming the first silently drops the rest (see containsBatchOrExtraMessage). Batch ACK is its own feature; until it is designed, a batch must stay a **loud non-positive** answer. This is a **refusal**, not a tolerance widening: it never makes an unreadable message readable, never re-bases on a located `MSH`, never parses a batch. It only recognizes the shapes for which a positive disposition would be a lie, so the builder can fall back to `AE`. A **negative** requested code (`AE`/`AR`/`CE`/`CR`) is unaffected, it is already non-positive, and echoing whatever control ID it can find is still useful. Pure, byte-level, never throws. #### Parameters ##### payload `Buffer` #### Returns `boolean` #### Example ```typescript import { rawAckUncorrelatable } from '@cosyte/mllp'; rawAckUncorrelatable(oneGoodMessage); // false → AA is safe rawAckUncorrelatable(twoConcatenatedMsh); // true → downgrade AA to AE ``` *** ### readNegotiatedTlsParameters() > **readNegotiatedTlsParameters**(`socket`, `host`, `port`): [`NegotiatedTlsParameters`](#negotiatedtlsparameters) \| `null` Read the negotiated parameters off a socket whose handshake has completed. Returns `null` for a socket that is not a TLS socket at all, and for a TLS socket with no session to report. That is what keeps a plaintext connection from producing an event: it is a structural property of the read, not a caller-side condition anyone has to remember. #### Parameters ##### socket `Socket` \| `TLSSocket` The socket whose handshake completed; plaintext is allowed and yields `null`. ##### host `string` Routing metadata for the payload's `host`. ##### port `number` Routing metadata for the payload's `port`. #### Returns [`NegotiatedTlsParameters`](#negotiatedtlsparameters) \| `null` A frozen payload, or `null` when there is nothing negotiated to report. #### Example ```typescript const params = readNegotiatedTlsParameters(tlsSocket, 'mllp.example.com', 2575); if (params !== null) logger.info({ suite: params.cipherSuite }); ``` *** ### resolveDifferentialPeer() > **resolveDifferentialPeer**(`raw`): [`DifferentialPeer`](#differentialpeer) \| `undefined` Resolve a configured peer address into a host and a port. The address is split on its **last** colon, so an IPv6 literal resolves as well as a host name: `[::1]:2575` and `::1:2575` both give host `::1`. A naive split on the first colon mangles the host and produces a NaN port, which is precisely how a live run gets skipped while the operator believes it happened. #### Parameters ##### raw `string` \| `null` \| `undefined` The configured address, or `undefined`/`null`/empty for no peer at all. #### Returns [`DifferentialPeer`](#differentialpeer) \| `undefined` The resolved peer, or `undefined` when no peer is configured. #### Throws MllpDifferentialConfigurationError when an address is present and unusable. #### Example ```typescript import { resolveDifferentialPeer } from '@cosyte/mllp'; const peer = resolveDifferentialPeer('127.0.0.1:2575'); // peer?.host === '127.0.0.1'; peer?.port === 2575 // resolveDifferentialPeer(undefined) === undefined ``` *** ### resolveNackCode() > **resolveNackCode**(`err`): [`NegativeAckCode`](#negativeackcode) Resolve the negative acknowledgement code for a handler failure. An [MllpAckError](#mllpackerror) carries an explicit `ackCode`; any other thrown value maps to `AE` (application error, the default, since most handler failures are transient and a resend may succeed). #### Parameters ##### err `unknown` #### Returns [`NegativeAckCode`](#negativeackcode) #### Example ```typescript import { resolveNackCode, MllpAckError } from '@cosyte/mllp'; resolveNackCode(new Error('boom')); // 'AE' resolveNackCode(new MllpAckError('nope', { ackCode: 'AR' })); // 'AR' ``` *** ### resolveTlsCipherPolicy() > **resolveTlsCipherPolicy**(`opts`, `side`): [`ResolvedTlsCipherPolicy`](#resolvedtlscipherpolicy) Resolve the offered-cipher-suite policy for one side of a connection, and prove the runtime accepts it before any socket is opened. Refuses, rather than picking a winner, when both `atnaTransportSecurity` and `ciphers` are set: each declares the offered list, and honouring one would silently discard the other on a link whose whole point is being able to say what it offered. **Diffie-Hellman parameters are the one place a winner IS picked, and the caller wins.** `atnaTransportSecurity: true` selects a group automatically because two of its four suites are DHE; a caller that also names its own group is stating site policy, and site policy is more specific than a default this package chose. So the two are not in conflict and the configuration is accepted with the caller's group in force. Each input is validated **alone**, with no certificate, key or passphrase in scope, so a rejection can only ever be about that input and the error it raises cannot carry credential material. #### Parameters ##### opts [`TlsCipherPolicyInput`](#tlscipherpolicyinput) The cipher-suite half of the caller's TLS options. ##### side `"client"` \| `"server"` Which end of the connection is being configured. #### Returns [`ResolvedTlsCipherPolicy`](#resolvedtlscipherpolicy) The TLS-context fields to spread into `tls.connect` / `tls.createServer`. #### Throws On a conflicting declaration, a list the runtime rejects, or Diffie-Hellman parameters it cannot use. Never falls back to the runtime default list, and never to a server with no Diffie-Hellman parameters. #### Example ```typescript import { resolveTlsCipherPolicy } from '@cosyte/mllp'; const policy = resolveTlsCipherPolicy({ atnaTransportSecurity: true }, 'server'); console.log(policy.dhparam); // => 'auto' ``` *** ### runDifferential() > **runDifferential**(`options?`): `Promise`\<[`DifferentialReport`](#differentialreport)\> Run the canonical exchanges against a peer and report what was observed. With no peer configured the run skips cleanly and returns a report saying so, so a default verification stays green on a machine that has no engine to point at. With a peer configured that cannot be resolved into a host and a port, the run is refused by name instead of skipped. The returned report is frozen and JSON-serializable, and carries no content read off the peer: deviations are named by stable code and byte offset, never by quoting bytes. **The run sends synthetic patient messages into whatever engine it is aimed at.** Aim it at a test or staging endpoint. #### Parameters ##### options? [`DifferentialRunOptions`](#differentialrunoptions) = `{}` Peer address, deadline, corpus, connection factory and abort signal. #### Returns `Promise`\<[`DifferentialReport`](#differentialreport)\> The report for the whole run, whatever each exchange did. #### Throws MllpDifferentialConfigurationError when a peer address is configured and unusable. #### Example ```typescript import { runDifferential } from '@cosyte/mllp'; const report = await runDifferential({ peer: process.env['MLLP_DIFF_PEER'] }); console.log(JSON.stringify(report, null, 2)); ``` *** ### tlsConfigurationMessage() > **tlsConfigurationMessage**(`code`): `string` Fixed, human-readable description for a TLS-configuration code. Byte-for-byte identical for a given code, whatever the configuration was. #### Parameters ##### code [`TlsConfigurationErrorCode`](#tlsconfigurationerrorcode) The stable configuration-error code. #### Returns `string` The frozen registry text for `code`. #### Example ```typescript import { tlsConfigurationMessage, MLLP_TLS_CIPHER_OPTION_CONFLICT } from '@cosyte/mllp'; logger.error(tlsConfigurationMessage(MLLP_TLS_CIPHER_OPTION_CONFLICT)); ``` --- URL: https://docs.cosyte.com/mllp/limitations Markdown: https://docs.cosyte.com/mllp/limitations.md # Known limitations & non-goals The honest list. A transport that oversells what it guarantees is how messages get lost, so this page is a deliverable, not a footnote. **Do not rely on `@cosyte/mllp` to do any of the following.** ## It does not parse or validate HL7 v2 `@cosyte/mllp` moves bytes. It never inspects the payload, never validates a segment, never checks a code system. Pair it with [`@cosyte/hl7`](https://github.com/cosyte/hl7). The one exception is the optional [`ack-from-hl7`](https://docs.cosyte.com/mllp/acks.md) subpath, which delegates the parsing to `@cosyte/hl7` anyway. ## It cannot guarantee delivery MLLP plus an HL7 ACK is **at-least-once at best**, and the gap is unavoidable: if the receiver commits your message and the ACK is lost on the way back, the sender cannot tell that apart from the receiver never having got it. Both look like a timeout. **The application owns idempotency and de-duplication**: key on `MSH-10` (message control ID) plus `MSH-7` (timestamp). `@cosyte/mllp` surfaces unacked messages to you; it does **not** store them, queue them, or replay them. There is no write-ahead log in this package. If you need one, it lives in your application, not here. ## It drains in-flight messages on `close()`, and still cannot make delivery certain `close()` **waits** for the ACKs of sends already written to the transport. `drainTimeoutMs` is what bounds that wait (default `30_000` ms), and it is the whole bound: the drain ends the moment the last outstanding ACK arrives, so a peer that answers promptly does not cost you the timeout. If the link fails mid-drain, `close()` returns then rather than waiting the bound out. What is still unresolved when the wait ends is reported to its own `send()` caller, and the two populations are **told apart**, because a replay decision turns on which one a message is in: - **Never delivered.** `MllpNeverDeliveredError`: the message was still held inside the client and no bytes were written for it, so the receiver cannot have seen it. Resending it cannot duplicate anything. - **Fate unknown.** `MllpUnknownFateError`: the bytes went out and no ACK came back. It carries `flushedAt` (when they went out), `elapsedMs` and `byteCount`, and **no payload content at all**, not the control ID, only its byte length. The receiver may hold this message, so resending it may commit a clinical message twice. - **Committed, application disposition unknown.** `MllpApplicationAckError({ reason: 'connection-lost' })`, unchanged: an enhanced-mode send whose commit disposition had already arrived keeps the error that names that commit. The receiver's custody of those bytes is a known fact, and a shutdown never downgrades it to an unknown one. Tell them apart with `instanceof`, never by reading an error message. **A drain cannot make delivery certain, and this one does not pretend to.** A commit whose ACK is lost in flight is indistinguishable from a message never received, and always will be: that is what the unknown-fate report says out loud. Nothing is retried automatically, because the accept acknowledgement is what releases a sender from resending, and the standard puts the retransmission decision on the application and on its peer's duplicate detection (`MSH-10` + `MSH-7`). There is still no write-ahead log, queue or replay in this package. `destroy()` is the other half of the pair and is unchanged: it settles every pending send at once, awaits no ACK and honours no drain timeout. Use it when you need the socket gone now. See [Connection, reconnect & backpressure](https://docs.cosyte.com/mllp/reliability.md). ## A fatal framing error drops the connection, and is not retried If the decoder throws (an oversized frame, or a structural violation whose tolerance opt-in is off), that **connection** is destroyed. It is not resynchronized: after a throw the reader's position in the byte stream is untrustworthy, and guessing where the next message starts is how one gets silently mis-split. Bytes already accumulated in that connection's partial frame are **lost**. The failure is contained to the one connection (a server keeps serving every other peer) and it is classified `framing-fatal`, i.e. **permanent**, so a client does not reconnect into it. That is deliberate (a peer speaking the wrong protocol would otherwise be retried forever), but it means a client facing a peer that emits _occasional_ junk will **stop**, not heal. If a peer's quirk is expected, use the tolerance opt-ins so the bytes are a warning rather than a fatal. See [Framing & tolerance](https://docs.cosyte.com/mllp/framing.md). ## It does not complete an enhanced-mode exchange on the server side The client half is built: an enhanced-mode send correlated by control ID draws both its commit disposition and its later application disposition, and neither is dropped ([ACKs](https://docs.cosyte.com/mllp/acks.md#enhanced-mode-one-send-two-acknowledgements)). Three bounds on that: - **This package's own server never sends the second acknowledgement.** It emits exactly one ACK per inbound message and picks the right half of Table 0008 for it. Pointing this client at this server with both MSH-15 and MSH-16 asking for acknowledgement therefore ends at the application-acknowledgement timeout. A consumer that needs the second exchange owns it. - **Two-phase correlation needs `correlateByControlId: true`.** MSA-2 is the only thing that can attribute a second acknowledgement to a send, and the default is FIFO. On FIFO an enhanced-mode send gets the ordinary single-acknowledgement behaviour plus a warning. - **A conditional application condition that is never met ends at that timeout.** MSH-16 `SU` whose peer applies the message unsuccessfully, or `ER` whose peer applies it successfully, never draws a second acknowledgement. The failure carries the commit disposition you did receive, so you know the peer took custody; what its application did is unknown and unknowable from here. Enhanced mode over a **batch** frame stays refused, and no sequence-number protocol (MSH-13/MSA-4), retransmission, queueing or replay is implemented here or planned for here. ## It does not decide clinical acceptance The package builds _conformant_ ACKs and structurally enforces _never-`AA`-without-commit_. It does not, and cannot, decide whether your application should accept a message. `AA` / `AE` / `AR` is your call, from your own processing outcome. See [the commit contract](https://docs.cosyte.com/mllp/acks.md). ## It does not speak MLLP Release 2 Only **Release 1** (framing, with reliability delegated to the HL7 v2 ACK) is implemented: the universal default for HL7 v2. The R2 commit-acknowledgement blocks (`` / ``) and R2's synchronous "no new content until ack" discipline are **not supported**. R2 is used mainly with HL7 v3 and is rarely needed for v2; if it ships, it will be opt-in and off by default, and R1 framing will never silently downgrade to it. ## It is not differentially verified against Epic or Cerner, so verify your own engine Interop is proven here against **freely available** engines only: the Google Cloud Healthcare MLLP adapter and Mirth/NextGen Connect (byte-parity on canonical R1 frames). Neither Epic nor Cerner is part of that. Their behavior is inferred from the spec, not observed, and this package will never tell you otherwise. **What it gives you instead is the harness, so you can observe your own engine before go-live.** `runDifferential` ships in the package. Point it at an engine you name and it reports, per exchange, whether the frame that came back was byte-identical to the canonical Release 1 block (`VT` + payload + `FS` + `CR`) and whether its `MSA-2` echoed the `MSH-10` of the message it answered. What the report carries, what it deliberately does not, and how to aim a run safely are on [Testing & verification](https://docs.cosyte.com/mllp/testing.md#verify-your-own-engine-rundifferential). That harness does not soften the non-goal above. A run is evidence **you** hold about **your** peer, gathered by a corpus of synthetic messages; it is not a conformance verdict, and this package issues none about an engine it has never met. ## It ships no PKI TLS verifies **caller-supplied** certificates. This package bundles no CA, issues nothing, rotates nothing, and has no opinion about your certificate lifecycle. See [MLLPS / TLS](https://docs.cosyte.com/mllp/tls.md). ## It cannot claim an IHE option on your behalf The offered cipher suites are a property of this package once you select `atnaTransportSecurity`, and every completed handshake reports the protocol version and suite it agreed on. That is support and evidence, and it is as far as a transport library can go: an IHE option is claimed by an **actor**, in a conformance statement, and that statement is yours to make. With the option left off, the offered suites are not this package's at all: they are the runtime's, which a Node distribution can configure at build time and an operator can replace from outside the process. See [MLLPS / TLS](https://docs.cosyte.com/mllp/tls.md). What this package _does_ declare about itself, in the actor-and-option wording a Product Registry entry or a Connectathon test is recorded in, is on one page: the [Conformance statement](https://docs.cosyte.com/mllp/conformance.md). It names what this package supplies for each option and what stays yours, and it claims none of them on your behalf. ## It cannot carry charsets that collide with the framing bytes MLLP is **not byte-transparent**. `0x0B` and `0x1C` are structural. A payload encoded in UTF-16 or UTF-32 will contain those bytes inside ordinary characters, and any MLLP implementation (not just this one) will mis-frame it. Use a single-byte encoding, UTF-8, or Shift_JIS. Which charset is in play is the HL7 message's `MSH-18` concern, not the transport's. ## `ack-from-hl7` re-serializes the control ID; it does not copy its bytes MSA-2 must carry the inbound MSH-10 **verbatim** (HL7 v2.5.1 §2.9.2.2), because that is the key the sender correlates its ACK on. `buildMllpAck` (the `/ack-from-hl7` subpath) holds that guarantee byte-for-byte for a plain control ID under the HL7 default delimiters (including a high-bit one under an `MSH-18` of `8859/1`) but it builds through `@cosyte/hl7`, which **re-emits** MSH-10 in its canonical form rather than copying the bytes. Five things that canonical form does not preserve: - **Non-default delimiters.** `@cosyte/hl7` always emits `|^~\&`, so `ID#X` under a `#` component separator is re-delimited to `ID^X`. - **Escape sequences.** Unescaped on read, re-escaped on write: `ID\X` comes back as `ID\E\X`. - **Whitespace.** Fields are trimmed: `MSG42 ` comes back as `MSG42`. - **Trailing empty components/subcomponents.** Canonicalized away: `ID^` and `ID&` both become `ID`. - **A lossy `encoding` override.** Any codec that cannot round-trip the inbound bytes. Each yields a _different_ MSH-10, and so an ACK the sender cannot match. On a **`Buffer`** inbound none of them is silent. The result carries `MLLP_ACK_CONTROL_ID_NOT_VERBATIM`. And all five have the same answer: **`buildRawAck`** (the root export, and what the server's `autoAck` path uses) is parser-free (it copies the MSH-10 bytes) so it holds the verbatim guarantee across escapes, padding, empty components, and **any** delimiter set. It always emits the ACK under the inbound's own field separator (never a substituted one), and MSH-10 provably cannot contain that separator, so the echo round-trips byte-for-byte whatever the control ID contains. See [ACKs](https://docs.cosyte.com/mllp/acks.md). And the _verbatim proof_ is a **`Buffer`** guarantee. On a `string` / `Hl7Message` inbound the wire bytes were decoded before `buildMllpAck` ever saw them, so it re-encodes your text with the same codec it decoded it with: the codec cancels on both sides, and `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` (a byte-for-byte comparison) cannot fire. `buildAckAA(payload.toString("latin1"))` on a high-bit control ID (`0x8B`) emits the two `utf8` bytes `0xC2 0x8B`, a _different_ control ID the sender cannot correlate. The encoding cannot be fixed from here (decoded text does not remember its bytes), and the verbatim check cannot catch it (the bytes are gone), but the API no longer stays **silent** about it. Whenever the emitted MSA-2 holds a non-ASCII byte on a text inbound, `buildMllpAck` emits **`MLLP_ACK_CONTROL_ID_UNVERIFIABLE`**: a distinct "cannot verify this echo; pass a `Buffer`" signal, separate from the `Buffer`-path proof-of-mismatch. An all-ASCII control ID round-trips under every codec and stays quiet. Pass a `Buffer`. That is what the `Buffer`-first API rule is for. A **non-text** `encoding` override is a step past even that. It is **rejected**, not warned, on **every** input shape. `"base64"`/`"base64url"`/`"hex"` reinterpret the ACK _string_ as encoded data, and `"utf16le"`/`"ucs2"` NUL-pad every byte, so the emitted frame is wholesale garbage a receiver cannot parse. Because a garbage frame is a caller mistake rather than a runtime condition, `buildMllpAck` throws a `TypeError` at the boundary for a non-text codec; only text codecs (`"utf8"`/`"ascii"`/`"latin1"`/`"binary"`) are accepted. This includes a `Buffer` inbound: a non-text codec there garbles the _inbound_ decode into the unparseable fallback (empty MSA-2, so the `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` proof never runs) and then serializes the fallback ACK to garbage bytes that ~3-4 % of the time (identically on Node 22 and 24) contain a `VT`/`FS` byte and trip the strict frame encoder with a nondeterministic `MllpFramingError`. It was never the "loud AE" escape hatch it was documented to be. The legitimate byte-level escape hatch is untouched: a **charset** codec on a `Buffer` (`"latin1"` byte-verbatim, or a lossy `"ascii"` that still surfaces as the loud `MLLP_ACK_CONTROL_ID_NOT_VERBATIM` proof) is what serves a peer that demands a specific byte-level codec. Neither builder **ACKs an HL7 batch** (§2.10.3) or a frame of concatenated messages. An `FHS`/`BHS` envelope, or a second `MSH` in the same frame, yields the warned, non-positive `AE`, never a positive `AA` correlated to the first message, which would tell the sender the whole batch was accepted while messages 2..N went unread. `buildMllpAck` refuses via its unparseable fallback; `buildRawAck` and the server's auto-ACK path refuse by downgrading a requested positive code. A raw `VT` inside a payload is the same class of hazard from the transport side: the decoder discards the accumulated bytes (`MLLP_TRAILING_BYTES`) and delivers only the fragment after it, and the auto-ACK path downgrades that frame rather than positively acknowledge a destroyed message. Batch ACK is its own unbuilt feature. ## API stability `@cosyte/mllp` is at `0.1`: it has left the pre-alpha `0.0.x` ladder. The client, the server, the framing codec, the in-memory transport and the optional `ack-from-hl7` bridge are the surface we keep stable. The stable **warning codes** and **security-warning codes** are part of that surface, so renaming or removing one is a breaking change. Below 1.0 a breaking change moves the minor version, and the changelog says how to migrate. --- ## The one thing this package exists to prevent **A sender being told its message was accepted when it was lost.** Everything above is a bound on what the transport promises. That single failure mode is the one it refuses to allow, by construction: the commit contract makes a positive ACK structurally unable to precede a durable commit, and an inbound message that cannot be understood can never produce a positive acknowledgement. The rest of the package hardens the transport around that guarantee. --- # @cosyte/fhir > FHIR R4 resource model, JSON and XML codec, and layered validation for JavaScript and TypeScript. These are the 8 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/fhir/intro Markdown: https://docs.cosyte.com/fhir/intro.md # @cosyte/fhir A developer-focused **FHIR** toolkit for Node.js and TypeScript: an **R4-first** resource model, a JSON **and** XML codec, and layered validation, with the same one-line ergonomics as the rest of the `@cosyte/*` parser suite. It is the FHIR member of that suite and mirrors the API shape of [`@cosyte/hl7`](https://github.com/cosyte/hl7), the reference parser. What it is for, in one sentence: read a real-world FHIR resource, model it with correct primitive semantics, and validate it against US Core, without reading the FHIR specification first. ## Status `@cosyte/fhir` is on the `0.1` line and installs from npm; [installation](https://docs.cosyte.com/fhir/installation.md) has the command. While the package is below 1.0, a breaking change ships in a minor version and is called out in the changelog. What the package does not do is listed in [current limits](https://docs.cosyte.com/fhir/limits.md), which is worth reading before you plan around it. ## Start here - **[Installation](https://docs.cosyte.com/fhir/installation.md)**: runtime requirements, module formats, and how to get the package today. - **[Quickstart](https://docs.cosyte.com/fhir/quickstart.md)**: an unparsed document to a parsed resource, a clinical value and a validation verdict, in three steps. - **[Core concepts](https://docs.cosyte.com/fhir/core-concepts.md)**: the schema-free model, exact primitives, the two codecs and the validation layers. - **[Guides](https://docs.cosyte.com/fhir/guides.md)**: the tasks readers arrive with, from accepting both wire formats to validating against your own profiles. - **[Current limits](https://docs.cosyte.com/fhir/limits.md)**: the deliberate boundaries, including what is checked and what is only reported as unchecked. - **[Troubleshooting](https://docs.cosyte.com/fhir/troubleshooting.md)**: every refusal and degraded reading, with the coded reason it carries and the action to take. ## What it does Against FHIR R4 (`4.0.1`), the package can already: - **Read and round-trip** a resource through a precision-preserving JSON codec and a zero-dependency XML codec that share one schema-free model. A `decimal` or `integer64` value is kept as its exact lexical text and never routed through a JavaScript number, so a dose or an identifier cannot be silently corrupted. The XML reader refuses any `DOCTYPE` or non-predefined entity rather than resolving it. - **Validate** across structural, cardinality and value-domain layers, emitting a value-free `OperationOutcome`: a finding carries a coded reason and a location, never the value it was raised over. - **Surface the safety-critical status and negation model**: it fails closed on an unknown `modifierExtension` and does not drop a status, a modifier or a negation on the paths it reads. - **Report measured values by their true type** with UCUM unit fidelity, never the display string and never converted, validate code systems and binding strength without vendoring any terminology content, validate against caller-supplied US Core or vendor profiles, and evaluate their FHIRPath invariants through a bounded, in-repository expression subset, reporting anything outside that subset as `INVARIANT_UNCHECKED` rather than passing it. Every one of those has a page behind it. The [quickstart](https://docs.cosyte.com/fhir/quickstart.md) is the shortest way to see the whole flow working. --- URL: https://docs.cosyte.com/fhir/installation Markdown: https://docs.cosyte.com/fhir/installation.md # Installation ## Requirements - **Node `>=22.0.0`**, the range the package declares in its `engines` field. Older runtimes are not supported and are not tested against. - **A package manager.** The repository uses **pnpm**, and the instructions below are written for it. npm and yarn work the same way. - **TypeScript 5 or newer**, if you consume the types. The package ships its own declarations, so there is no separate types package to install. ## Availability `@cosyte/fhir` is installable from the public npm registry today: ```sh pnpm add @cosyte/fhir ``` npm and yarn work the same way: `npm install @cosyte/fhir` or `yarn add @cosyte/fhir`. While the package is below 1.0, a breaking change ships in a minor version and is called out in the changelog, so a caret range such as `^0.1.0` takes fixes and never a break. To build it from source instead, clone the repository and depend on the checkout: ```sh git clone https://github.com/cosyte/fhir cd fhir pnpm install pnpm build ``` Then, from your own project: ```sh pnpm add file:../fhir ``` The built output is what the package publishes: an ESM bundle, a CommonJS bundle, and one declaration file for each. `pnpm build` is required before the local dependency resolves, because the repository tracks source, not build output. ## Module formats The package is **ESM** first. Its `package.json` declares `"type": "module"`, and the ESM build is what the `import` condition of its `exports` map resolves to: ```ts import { parseResource } from "@cosyte/fhir"; ``` A **CommonJS** build is published beside it, reached through the `require` condition of the same `exports` map: ```js const { parseResource } = require("@cosyte/fhir"); ``` Types are shipped for both: a `.d.ts` for the ESM entry and a `.d.cts` for the CommonJS one, so a project on either module system gets the same editor completions. ## The public surface is one entry point The `exports` map offers exactly two paths: the package itself, and its `package.json`. There is no supported deep import, so everything in this documentation is reached from a single specifier: ```ts import { parseResource, readSafety, validateResource } from "@cosyte/fhir"; ``` Reaching past that specifier into a build path is unsupported and will break without notice. ## Check the install Put this in a file and run it. It reads a minimal resource and asserts nothing went wrong: ```ts import { VERSION, parseResource } from "@cosyte/fhir"; typeof VERSION; // => "string" const read = parseResource('{"resourceType":"Patient","id":"syn-0001"}'); read.resource.kind; // => "complex" read.issues; // => [] ``` If that runs clean, go to the [quickstart](https://docs.cosyte.com/fhir/quickstart.md), which takes a real resource from an unparsed document to a validation verdict. --- URL: https://docs.cosyte.com/fhir/quickstart Markdown: https://docs.cosyte.com/fhir/quickstart.md # Quickstart Five minutes, one file, and three steps: an unparsed FHIR document read into a resource, one clinically meaningful value read off it, and a validation verdict. Everything below uses the package's public entry point and nothing else. Start from an empty file, `first-read.ts`, in a project that already [has the package](https://docs.cosyte.com/fhir/installation.md). ## 1. Read a document The document below is a synthetic systolic blood pressure, written the way a server would send it. Every value in it is fabricated, and the same document is committed as a synthetic test fixture in this repository. `parseResource` takes the document text and returns two things: the resource model, and the diagnostics the read produced. It never throws for content it can keep, and it never rewrites a value to make it fit. ```ts runnable import { parseResource } from "@cosyte/fhir"; const document = `{ "resourceType": "Observation", "id": "syn-0001", "status": "final", "code": { "coding": [{ "system": "http://loinc.org", "code": "8480-6" }] }, "subject": { "reference": "Patient/syn-0001" }, "effectiveDateTime": "2026-01-05", "valueQuantity": { "value": 120.0, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]" } }`; const { resource, issues } = parseResource(document); resource.kind; // => "complex" issues.map((issue) => issue.code); // => ["DECIMAL_PRECISION_AT_RISK"] ``` That single diagnostic is the point of the codec rather than a complaint about the document. The magnitude was written `120.0`, and a reader that routed it through a JavaScript number would hand you back `120`, silently losing the precision the sender chose to state. This one keeps the exact lexical form and tells you the protection mattered here. ## 2. Read a value off it A blood pressure is only useful as a magnitude with a unit. `readObservationValue` reports which `value[x]` variant is actually present, so a caller can never read a coded or free-text result as if it were a number: ```ts import { parseResource, readObservationValue } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","status":"final","valueQuantity":{"value":120.0,"unit":"mmHg","system":"http://unitsofmeasure.org","code":"mm[Hg]"}}', ); const reading = readObservationValue(resource); reading?.type; // => "Quantity" reading?.quantity?.value?.raw; // => "120.0" reading?.quantity?.code; // => "mm[Hg]" reading?.quantity?.unit; // => "mmHg" ``` Two habits worth forming here. Branch on `type` before you touch `quantity`, because a `valueString` of `"POSITIVE"` is a perfectly ordinary Observation result and has no magnitude at all. And compare on `code`, the machine-actionable UCUM unit, never on `unit`, which is a display string a sender is free to spell however it likes. ## 3. Get a verdict `validateResource` walks the resource once and returns coded findings plus a single `valid` flag. Warnings and informational findings do not make a resource invalid; errors do. ```ts import { parseResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}', ); const outcome = validateResource(resource); outcome.valid; // => true outcome.issues; // => [] ``` Now break it. An unknown `modifierExtension` is the one shape the library refuses to read past: a modifier changes the meaning of the element it sits on, so processing the resource as if it were absent is how a negation gets lost. ```ts import { parseResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Patient","modifierExtension":[{"url":"http://example.org/local-flag","valueBoolean":true}]}', ); const outcome = validateResource(resource); outcome.valid; // => false outcome.issues.map((issue) => issue.code); // => ["UNHANDLED_MODIFIER_EXTENSION"] outcome.issues.map((issue) => issue.expression); // => ["Patient.modifierExtension[0]"] ``` Notice what the finding carries: a coded reason and a location, and no slice of the document. That is a contract, not a coincidence, and [core concepts](https://docs.cosyte.com/fhir/core-concepts.md) explains what rests on it. ## Where to go next - [Core concepts](https://docs.cosyte.com/fhir/core-concepts.md) for the model, the two codecs and the validation layers. - [Guides](https://docs.cosyte.com/fhir/guides.md) for profiles, safety readouts and cross-format work. - [Current limits](https://docs.cosyte.com/fhir/limits.md) for what the package deliberately does not do. - [Troubleshooting](https://docs.cosyte.com/fhir/troubleshooting.md) when a document is refused or read in a degraded way. --- URL: https://docs.cosyte.com/fhir/core-concepts Markdown: https://docs.cosyte.com/fhir/core-concepts.md # Core concepts Four ideas carry the whole library: a schema-free model, exact primitives, two codecs that agree with each other, and diagnostics that never carry a value. ## The schema-free resource model A parsed resource is a small, immutable tree of three node kinds: - a **complex** node, an ordered list of named properties, exactly as the document wrote them; - a **list** node, a repeating element with its positions preserved; - a **primitive** node, one value plus the metadata FHIR allows beside it. There is no generated class per resource type, and no schema is consulted to build the tree. That is a deliberate trade. A schema-free read cannot silently drop the element it has no field for, which is the failure mode that matters when the sender is a production system you do not control. What you give up is the compile-time shape of a typed model, and small helpers earn most of it back: ```ts import { getProperty, isPrimitive, parseResource, resourceType } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","status":"final"}'); resourceType(resource); // => "Observation" const status = getProperty(resource, "status"); const code = status !== undefined && isPrimitive(status) ? status.value : undefined; code; // => "final" ``` Content FHIR gives no meaning to is kept, but never as an element. An array nested inside an array, for instance, is preserved as inert text rather than modeled, so no walker can mistake it for part of the resource while the reader still reports that it was there. ## Numbers stay exactly as they were written `decimal` and `integer64` are kept as their exact lexical strings and are never routed through a JavaScript number. This is the single most consequential decision in the library. A JavaScript number is a binary double. It cannot represent `0.1` exactly, it silently rounds past about 15 significant digits, and it has no concept of trailing zeros. In FHIR those three facts are clinical, not academic: `0.010` and `0.01` are different statements about how precisely a dose was measured, and a 19-digit identifier that comes back changed in its last digits is a different patient. So the codec never converts. A magnitude is carried as the text the sender wrote, and it round-trips byte for byte: ```ts import { parseResource, serializeResource } from "@cosyte/fhir"; const source = '{"resourceType":"Observation","status":"final","valueQuantity":{"value":0.010}}'; const { resource, issues } = parseResource(source); serializeResource(resource); // => '{"resourceType":"Observation","status":"final","valueQuantity":{"value":0.010}}' issues.map((issue) => issue.code); // => ["DECIMAL_PRECISION_AT_RISK"] ``` `DECIMAL_PRECISION_AT_RISK` is informational. It says a value arrived that a naive reader would have corrupted, which is useful to log and never a reason to reject a document. When you need arithmetic, ask for it explicitly. The decimal type exposes its exact text and an exact integer view, and neither is a lossy conversion. ## Two codecs, one model The JSON codec is `parseResource` and `serializeResource`. The XML codec is `parseResourceXml` and `serializeResourceXml`. Both produce the same model, so a consumer written against one wire format already works with the other: ```ts import { nodesEquivalent, parseResource, parseResourceXml } from "@cosyte/fhir"; const fromJson = parseResource('{"resourceType":"Observation","status":"final"}').resource; const fromXml = parseResourceXml( '', ).resource; nodesEquivalent(fromJson, fromXml); // => true ``` The writers are conservative where the readers are liberal. A writer authors no value of its own: if a model carries something the target format cannot spell back without inventing or dropping content, the write is refused with a coded reason rather than emitted as something plausible. ### The XML reader refuses rather than resolves The XML reader is hardened by refusal, not by configuration: - any `DOCTYPE` declaration is refused with `DTD_FORBIDDEN`; - any entity beyond the five predefined ones and numeric character references is refused with `UNDEFINED_ENTITY`; - nesting past a fixed bound is refused with `MAX_DEPTH_EXCEEDED`; - there is no I/O and no URI resolution at any point. Together those close external entity expansion and billion-laughs expansion by construction. There is no option to relax them, because an option is what turns a hardened parser into an exploitable one on somebody else's deployment: ```ts import { FhirXmlError, XML_FATAL_CODES, parseResourceXml } from "@cosyte/fhir"; const read = (input: string): string => { try { parseResourceXml(input); return "read"; } catch (err) { return err instanceof FhirXmlError ? err.code : "unknown"; } }; read(''); // => XML_FATAL_CODES.DTD_FORBIDDEN read(''); // => XML_FATAL_CODES.UNDEFINED_ENTITY ``` ## The validation layers `validateResource` runs a single pass made of layers. Each one is independent, each one adds findings, and none of them can withdraw another's: 1. **Structure and cardinality**: unknown elements, type mismatches, choice ambiguity, and the minimum and maximum occurrences a modeled element declares. 2. **Primitive value domains**: the lexical space of each primitive type, and required code enumerations. 3. **Safety**: unknown `modifierExtension`s, retraction, negation and status codes. Always on, for every resource type, whether or not you supply a profile. 4. **Quantity and units**: which `value[x]` is present, UCUM code shape, and the units a vital sign is required to use. Units are checked, never converted. 5. **Bundle integrity**: agreement between an entry's full URL and the resource id, unresolved references, and a bounded guard against contained-resource cycles. 6. **Terminology bindings**: strength-aware and content-free. Without a terminology service the checks degrade to the system level and never produce a false error. 7. **Base constraints**: the error-severity constraints R4 itself declares on the eight modeled resource types and on the definitions they inherit, such as `pat-1` on a patient contact, `ext-1` on every extension and `ele-1` on every element. Always on for those eight types, whether or not you supply a profile. A contained resource is checked only through the constraints its container declares about it (`dom-2` to `dom-5`), and the narrative best-practice warning `dom-6` is not evaluated without a profile. 8. **Profiles and invariants**: everything a caller-supplied `StructureDefinition` asserts, plus its FHIRPath constraints. A base constraint a profile repeats is reported once per occurrence, not twice. The verdict is one boolean over the findings: `valid` is false when any finding is an error or worse. Warnings and informational findings leave it true. ## The value-free OperationOutcome contract Every finding this library raises carries a coded reason and a FHIRPath location, and never the value it was raised over: ```ts import { parseResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Patient","modifierExtension":[{"url":"http://example.org/local-flag"}]}', ); const finding = validateResource(resource).issues[0]; finding?.code; // => "UNHANDLED_MODIFIER_EXTENSION" finding?.severity; // => "error" finding?.expression; // => "Patient.modifierExtension[0]" ``` The reason this is a contract rather than a style is that findings travel. They go into logs, support tickets, dashboards and bug reports, all of which are less protected than the resource store, and a diagnostic that quotes the value it complained about is a diagnostic that copies patient data into every one of them. `toOperationOutcome` renders the same findings as an `OperationOutcome` resource, which is the shape a FHIR server expects to receive: ```ts import { parseResource, serializeResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","status":"final"}'); const outcome = validateResource(resource).toOperationOutcome(); serializeResource(outcome).includes("OperationOutcome"); // => true ``` One qualification, stated because the opposite is easy to assume: a location is bounded to the published form of a FHIR name, not guaranteed to be free of anything a sender wrote. A name that does not match that form is replaced by a withheld marker rather than echoed. --- URL: https://docs.cosyte.com/fhir/guides Markdown: https://docs.cosyte.com/fhir/guides.md # Guides The tasks readers actually arrive with, each one end to end. ## Accept JSON and XML without writing the code twice Both codecs produce the same model, so the format belongs at the edge of your program and nowhere else. Sniff it once, and everything downstream is format-blind: ```ts import { parseResource, parseResourceXml, readSafety } from "@cosyte/fhir"; import type { ReadResult } from "@cosyte/fhir"; const read = (payload: string): ReadResult => payload.trimStart().startsWith("<") ? parseResourceXml(payload) : parseResource(payload); const fromJson = read('{"resourceType":"Observation","status":"final"}'); const fromXml = read(''); readSafety(fromJson.resource).status; // => "final" readSafety(fromXml.resource).status; // => "final" ``` Both readers hand back diagnostics on the same channel, so one logging path covers both. ## Decide whether a resource is safe to summarize Before a resource is rolled into a summary, a card or a feed, ask whether anything in it changes the meaning of what you are about to show. `readSafety` answers that in one call, over the whole document rather than its root alone: ```ts import { parseResource, readSafety } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","status":"entered-in-error","code":{"text":"synthetic"}}', ); const safety = readSafety(resource); safety.status; // => "entered-in-error" safety.retracted; // => true safety.negations; // => ["entered-in-error"] ``` `negations` is the authoritative field. The single-valued conveniences beside it, such as `status` and `retracted`, describe the resource you handed in, while `negations` covers every resource inside it too, so a retracted entry in a Bundle is visible there and nowhere else. Some elements change the meaning of a record without being a status. A patient recorded as deceased, a patient record replaced by another (`link`), and a subpotent immunization dose are each reported on `modifierElements` with their location, whatever value they carry, and each makes the resource unsafe to summarize. A medication request's `intent` is mandatory, so it is surfaced instead: `intents` pairs the code with its location, and only an `intent` that is not one of the eight R4 codes refuses. ```ts import { parseResource, readSafety } from "@cosyte/fhir"; const patient = readSafety( parseResource('{"resourceType":"Patient","deceasedBoolean":true}').resource, ); patient.modifierElements; // => [{ element: "deceased", location: "Patient.deceasedBoolean" }] patient.safeToSummarize; // => false const request = readSafety( parseResource('{"resourceType":"MedicationRequest","status":"active","intent":"proposal"}') .resource, ); request.intents; // => [{ code: "proposal", location: "MedicationRequest.intent" }] request.safeToSummarize; // => true ``` An identifier, a name, an address or a contact point can be marked `old` or `temp`, and FHIR treats that `use` as a modifier: an old address is not where the patient lives now. `datatypeUses` pairs each `use` with its location, on every `identifier` of the resource types the safety readout models and on a patient's `name`, `telecom` and `address`, including those of each `contact`. A readable code leaves the resource safe to summarize, `old` included, because which entry to show is your decision: nothing is filtered, reordered or marked current for you. A `use` that is not one of the codes FHIR defines for that datatype, such as `"OLD"` or `"maiden"` on an identifier, is located on `unreadableDatatypeUses` instead and the resource is not safe to summarize. ```ts import { parseResource, readSafety } from "@cosyte/fhir"; const person = readSafety( parseResource('{"resourceType":"Patient","address":[{"use":"old","city":"Nowhere"}]}').resource, ); person.datatypeUses; // => [{ code: "old", location: "Patient.address[0].use" }] person.safeToSummarize; // => true const unreadable = readSafety( parseResource('{"resourceType":"Patient","identifier":[{"use":"OLD","value":"SYN-0001"}]}').resource, ); unreadable.unreadableDatatypeUses; // => ["Patient.identifier[0].use"] unreadable.safeToSummarize; // => false ``` What this does not read: a practitioner's identifiers, names and addresses (a practitioner's identifier `use` is reported on `modifierElements` instead, whatever its value), a `use` carried in an extension value, a contained resource of a type the safety readout does not model, such as an organization, and an ended `period` on an entry whose `use` is current. When a summary must refuse rather than warn, `assertSafeToSummarize` is the executable form of the same rule: it throws for an unhandled modifier, a value the document left ambiguous, and the other shapes that make an affirmative summary unsafe. ## Validate against your own profiles No profile content is bundled. US Core and vendor profiles are supplied by the caller, which means the library never claims conformance to a version of a profile you did not choose. Load a published `StructureDefinition` with `loadStructureDefinition`, or author one in code: ```ts import { defineProfile, parseResource, validateResource } from "@cosyte/fhir"; const localObservation = defineProfile({ url: "http://example.org/StructureDefinition/local-observation", name: "LocalObservation", type: "Observation", differential: [{ path: "Observation.subject", min: 1, max: 1, mustSupport: true }], }); const { resource } = parseResource( '{"resourceType":"Observation","status":"final","code":{"text":"synthetic"}}', ); const outcome = validateResource(resource, { profiles: [localObservation] }); outcome.valid; // => false outcome.issues.map((issue) => issue.code); // => ["MUST_SUPPORT_ABSENT", "CARDINALITY_MIN"] ``` Two things to read off that result. Must-support is reported as informational, because must-support is an obligation on a system, not a requirement that an instance carry the element. The cardinality finding is the error, and it is the one that moves the verdict. You do not need a profile for the constraints R4 itself declares. On the eight modeled resource types, `validateResource` evaluates them with no options at all: a patient contact with no name, telecom, address or organization breaks `pat-1`, an extension with both a value and nested extensions breaks `ext-1`, and an element with no value and no children breaks `ele-1`: ```ts import { parseResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","contact":[{"gender":"other"}]}'); const outcome = validateResource(resource); outcome.valid; // => false outcome.issues.map((issue) => issue.constraint); // => ["pat-1"] ``` A profile that carries one of those constraints again does not double the finding: each violation is reported once per occurrence. And a profile carrying R4's `dom-3` as written, as every R4-derived snapshot does, draws no finding for it on a resource with nothing contained, because that constraint holds there by its own terms. A starter kit of small, specification-grounded profiles ships with the package as worked examples. They are built through the same public authoring call you would use, so there is nothing privileged about them: ```ts import { STARTER_PROFILES, parseResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","status":"final","code":{"text":"synthetic"}}', ); const outcome = validateResource(resource, { profiles: [...STARTER_PROFILES] }); outcome.issues.some((issue) => issue.code === "CARDINALITY_MIN"); // => true ``` ## Check codes without a terminology server Terminology checks are content-free by default. The library knows code system identities, not their contents, so with no service supplied it verifies that a coding names a system it recognises and that a binding's strength is respected, and it never invents a membership answer: ```ts import { isKnownSystem, LOINC_SYSTEM } from "@cosyte/fhir"; isKnownSystem(LOINC_SYSTEM); // => true isKnownSystem("http://example.org/not-a-code-system"); // => false ``` Supply a terminology service through the validation options when you need real membership answers. Findings that came from a service can carry the code system release the answer was made against, so a verdict can be re-read later against the version that produced it. ## Preserve what you cannot interpret A read is lenient and a write is conservative, which together give a useful property: a document you do not fully understand still round-trips. An element the reader did not expect is preserved and reported rather than dropped: ```ts import { parseResource, serializeResource, validateResource } from "@cosyte/fhir"; const source = '{"resourceType":"Observation","status":"final","code":{"text":"synthetic"},"vendorField":"kept"}'; const { resource } = parseResource(source); validateResource(resource).issues.map((issue) => issue.code); // => ["UNKNOWN_ELEMENT"] serializeResource(resource) === source; // => true ``` `UNKNOWN_ELEMENT` is a warning on a lenient read and an error in strict mode, which is the mode to use when you are the one emitting a document rather than the one receiving it. If the write cannot be done without inventing or losing content, it is refused with a coded reason instead. [Troubleshooting](https://docs.cosyte.com/fhir/troubleshooting.md) lists the refusals and what each one means. --- URL: https://docs.cosyte.com/fhir/api Markdown: https://docs.cosyte.com/fhir/api.md # @cosyte/fhir ## Classes ### FhirCodecError Thrown by the JSON reader on an unrecoverable structural failure. Carries the coded reason, a FHIRPath `expression` location (for a misalignment) or an `offset` byte position (for malformed JSON), and, by design, **no** slice of the offending input, because that slice could be PHI. #### Example ```ts import { parseResource, FhirCodecError, FATAL_CODES } from "@cosyte/fhir"; try { parseResource(feed); } catch (err) { if (err instanceof FhirCodecError && err.code === FATAL_CODES.PRIMITIVE_EXTENSION_MISALIGNED) { // the `_`-sibling alignment was broken at err.expression } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new FhirCodecError**(`code`, `message`, `location?`): [`FhirCodecError`](#fhircodecerror) **`Internal`** ###### Parameters ###### code [`FatalCode`](#fatalcode) The fatal reason. ###### message `string` A PHI-safe description, must not embed any input value. ###### location? Either a FHIRPath `expression` or a byte `offset`. ###### expression? `string` ###### offset? `number` ###### Returns [`FhirCodecError`](#fhircodecerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`FatalCode`](#fatalcode) ##### expression > `readonly` **expression**: `string` \| `undefined` FHIRPath location of the failure, when it has one (misalignment). ##### offset > `readonly` **offset**: `number` \| `undefined` Byte offset into the input, when it has one (malformed JSON). *** ### FhirDecimal A FHIR `decimal`, backed by its exact lexical source string. Construct one via the [decimal](#decimal) factory (which validates the text) rather than `new`. The value is immutable and carries no `number` field by design, read [FhirDecimal.toString](#tostring) for the exact literal, [FhirDecimal.toBigInt](#tobigint) for an integer-valued decimal, and [FhirDecimal.toNumber](#tonumber) only when you have consciously accepted the precision loss. #### Example ```ts import { decimal } from "@cosyte/fhir"; const dose = decimal("0.010"); dose.toString(); // "0.010", the trailing zero survives dose.equals(decimal("0.01")); // false, different precision (FHIR: 0.010 ≠ 0.01) dose.equalsValue(decimal("0.01")); // true , same quantity ``` #### Constructors ##### Constructor > **new FhirDecimal**(`raw`): [`FhirDecimal`](#fhirdecimal) **`Internal`** ###### Parameters ###### raw `string` A validated JSON-number literal. Prefer the [decimal](#decimal) factory over calling this directly; the factory is where validation lives. ###### Returns [`FhirDecimal`](#fhirdecimal) #### Properties ##### raw > `readonly` **raw**: `string` The exact lexical text as it appeared on the wire (or was supplied to [decimal](#decimal)). #### Methods ##### equals() > **equals**(`other`): `boolean` Precision-sensitive equality, the FHIR-conformant default. Two decimals are equal only when they denote the same quantity **and** carry the same precision, so `0.010` does not equal `0.01` (the trailing zero is significant). Use [FhirDecimal.equalsValue](#equalsvalue) for quantity-only comparison. ###### Parameters ###### other [`FhirDecimal`](#fhirdecimal) ###### Returns `boolean` ##### equalsValue() > **equalsValue**(`other`): `boolean` Quantity equality, precision-insensitive: `0.010` equals `0.01` equals `1e-2`. Computed by aligning scales with BigInt arithmetic, no float is involved. ###### Parameters ###### other [`FhirDecimal`](#fhirdecimal) ###### Returns `boolean` ##### toBigInt() > **toBigInt**(): `bigint` The exact value as a `bigint`, valid only for an integer-valued decimal (no fractional digits after accounting for the exponent). Throws a `RangeError` otherwise, a caller asking for an integer view of `1.5` has a bug, and silently truncating would be a data-integrity hazard. ###### Returns `bigint` ###### Example ```ts import { decimal } from "@cosyte/fhir"; decimal("9223372036854775807").toBigInt(); // 9223372036854775807n, exact past 2^53 ``` ##### toNumber() > **toNumber**(): `number` The value as a JavaScript `number`. **Lossy and deliberately explicit**: this is the one place the float hazard is allowed in, and only because the caller named it. For values with more than ~15 significant digits, trailing-zero precision, or magnitude beyond `Number.MAX_SAFE_INTEGER`, the result is not exact. Prefer [FhirDecimal.toString](#tostring) or [FhirDecimal.toBigInt](#tobigint). ###### Returns `number` ##### toString() > **toString**(): `string` The exact lexical form, the string this decimal was created from, unchanged. This is what the serializer emits, so a spec-clean value round-trips byte-for-byte. ###### Returns `string` *** ### FhirInteger64 A FHIR `integer64`, backed by its exact lexical source string. Construct via the [integer64](#integer64) factory (which validates range and grammar). Immutable; the `bigint` view is computed lazily on first access and cached. #### Example ```ts import { integer64 } from "@cosyte/fhir"; const big = integer64("9223372036854775807"); big.toString(); // "9223372036854775807" big.toBigInt(); // 9223372036854775807n, exact, no 2^53 truncation ``` #### Constructors ##### Constructor > **new FhirInteger64**(`raw`): [`FhirInteger64`](#fhirinteger64) **`Internal`** ###### Parameters ###### raw `string` A validated signed-integer literal within the 64-bit range. Prefer the [integer64](#integer64) factory; validation lives there. ###### Returns [`FhirInteger64`](#fhirinteger64) #### Properties ##### raw > `readonly` **raw**: `string` The exact lexical text as it appeared on the wire (FHIR JSON encodes this as a string). #### Methods ##### equals() > **equals**(`other`): `boolean` Exact equality of two `integer64` values (by numeric value, so `"-0"`-style variants agree). ###### Parameters ###### other [`FhirInteger64`](#fhirinteger64) ###### Returns `boolean` ##### toBigInt() > **toBigInt**(): `bigint` The value as a `bigint`, exact across the whole 64-bit range. Computed once and cached. ###### Returns `bigint` ##### toString() > **toString**(): `string` The exact lexical form, what the serializer emits (as a JSON string, per FHIR). ###### Returns `string` *** ### FhirProfileError Thrown when a snapshot cannot be generated: an unresolvable `baseDefinition`, or a `baseDefinition` cycle. The message is value-free (canonical URLs and structural facts only, never instance data). #### Example ```ts import { FhirProfileError, generateSnapshot } from "@cosyte/fhir"; try { generateSnapshot(differentialOnlyProfile, () => undefined); } catch (e) { if (e instanceof FhirProfileError) console.error(e.message); } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new FhirProfileError**(`message`): [`FhirProfileError`](#fhirprofileerror) ###### Parameters ###### message `string` A value-free description of why snapshot generation failed. ###### Returns [`FhirProfileError`](#fhirprofileerror) ###### Overrides `Error.constructor` *** ### FhirSafetyError A refusal raised when a caller tries to flatten or summarize a resource this library cannot summarize honestly: it carries a `modifierExtension` we do not understand (FHIR's `?!` rule forbids ignoring one), it carries a modifier ELEMENT (an ordinary base element R4 flags `?!`, which the same rule forbids ignoring), a repeated property name left an element holding several values with no rule for choosing between them, a `0..1` safety element arrived wrapped in a JSON array, an array inside an array left content the codec could not read at all, XML character data written on an element was dropped, a boolean-valued safety element carries a written value outside the datatype's lexical space, a `code`-valued negation element carries a value that spells a negation code bar its case or its surrounding whitespace, such an element holds content at a position no `code` read can reach, a `MedicationRequest.intent` is not one of the eight R4 codes, a `use` on an Identifier, a HumanName, an Address or a ContactPoint is not a code of its datatype's R4 value set, an element declares an absence in a reason this library cannot read, or an element declares an absence and carries a value. Every way the safe move is to **refuse**, value-free, carrying only the locations. A **readable, non-conflicting** absence marker is not on that list and never refuses: it is a declaration the caller can now read, so summarizing over it asserts nothing this library cannot establish. Neither does a readable `use`, which is surfaced. #### Example ```ts import { assertSafeToSummarize, FhirSafetyError, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Patient","modifierExtension":[{"url":"http://example.org/x"}]}', ); try { assertSafeToSummarize(resource); } catch (err) { if (err instanceof FhirSafetyError) err.locations; // ["Patient.modifierExtension[0]"] } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new FhirSafetyError**(`locations`): [`FhirSafetyError`](#fhirsafetyerror) ###### Parameters ###### locations readonly `string`[] The FHIRPath locations that forced the refusal (value-free). ###### Returns [`FhirSafetyError`](#fhirsafetyerror) ###### Overrides `Error.constructor` #### Properties ##### locations > `readonly` **locations**: readonly `string`[] FHIRPath locations that forced the refusal (value-free). *** ### FhirSerializeError Thrown by a writer asked to serialize a model it cannot encode without losing a finding. Value-free like every other diagnostic in this library: `locations` carries bounded FHIRPath expressions, never the content that was dropped. #### Example ```ts import { serializeResourceXml, FhirSerializeError, SERIALIZE_ERROR_CODES } from "@cosyte/fhir"; try { serializeResourceXml(resource); } catch (err) { if (err instanceof FhirSerializeError && err.code === SERIALIZE_ERROR_CODES.DROPPED_ELEMENT_TEXT) { console.error("cannot re-emit; text was dropped at", err.locations); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new FhirSerializeError**(`message`, `code`, `locations`): [`FhirSerializeError`](#fhirserializeerror) ###### Parameters ###### message `string` A value-free description of the refusal. ###### code [`SerializeErrorCode`](#serializeerrorcode) Which refusal this is. ###### locations readonly `string`[] The bounded FHIRPath locations it is about. ###### Returns [`FhirSerializeError`](#fhirserializeerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`SerializeErrorCode`](#serializeerrorcode) Which refusal this is. ##### locations > `readonly` **locations**: readonly `string`[] The bounded FHIRPath locations the refusal is about, in walk order. Never document content. *** ### FhirXmlError Thrown by the XML reader on an unrecoverable failure, a well-formedness error, a **refused** DTD or entity (the safety refusals), or nesting past the depth bound. Carries the coded reason and a byte `offset`, and, by design, **no** slice of the offending input, because that slice could be PHI. #### Example ```ts import { parseResourceXml, FhirXmlError, XML_FATAL_CODES } from "@cosyte/fhir"; try { parseResourceXml(' ]>'); } catch (err) { if (err instanceof FhirXmlError && err.code === XML_FATAL_CODES.DTD_FORBIDDEN) { // the DTD was refused before any entity could be declared or expanded } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new FhirXmlError**(`code`, `message`, `offset`): [`FhirXmlError`](#fhirxmlerror) **`Internal`** ###### Parameters ###### code [`XmlFatalCode`](#xmlfatalcode) The fatal reason. ###### message `string` A PHI-safe description, must not embed any input value. ###### offset `number` The byte offset where the failure was detected. ###### Returns [`FhirXmlError`](#fhirxmlerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`XmlFatalCode`](#xmlfatalcode) ##### offset > `readonly` **offset**: `number` Byte offset into the input where the failure was detected. *** ### InvalidProfileError Thrown by [defineProfile](#defineprofile) when a spec is malformed, the conservative-writer guard. The message is value-free: it names profile metadata (a `url`, an element `path`, a cardinality number), never instance data. A profile is not PHI, but the same value-free discipline is kept throughout. #### Example ```ts import { defineProfile, InvalidProfileError } from "@cosyte/fhir"; try { defineProfile({ url: "", type: "Patient" }); } catch (e) { if (e instanceof InvalidProfileError) console.error(e.message); // "profile url is required" } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new InvalidProfileError**(`message`): [`InvalidProfileError`](#invalidprofileerror) ###### Parameters ###### message `string` A value-free description of the authoring error. ###### Returns [`InvalidProfileError`](#invalidprofileerror) ###### Overrides `Error.constructor` *** ### UnsupportedFhirPathError Thrown when the bounded FHIRPath subset cannot **lex, parse, or evaluate** an expression, an unrecognised character, an unsupported function or operator, a construct the evaluator does not implement, or a runtime type it cannot reconcile. It is the seam the fail-safe hangs on: an invariant whose expression raises this is reported **`INVARIANT_UNCHECKED` (information)**, the library never claims such a constraint *passed*, only that it could not evaluate it. Widening the subset means catching one of these cases in the parser/evaluator, never suppressing it at the call site. The message is **value-free**, it names the offending FHIRPath construct or position, never an instance value, so it is safe to surface (PHI discipline). #### Example ```ts import { evaluateInvariant, UnsupportedFhirPathError } from "@cosyte/fhir"; try { // `descendants()` is outside the bounded subset: throw new UnsupportedFhirPathError("unsupported function descendants()"); } catch (e) { if (e instanceof UnsupportedFhirPathError) console.error(e.message); } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new UnsupportedFhirPathError**(`message`): [`UnsupportedFhirPathError`](#unsupportedfhirpatherror) ###### Parameters ###### message `string` A value-free description of the construct the subset does not support. ###### Returns [`UnsupportedFhirPathError`](#unsupportedfhirpatherror) ###### Overrides `Error.constructor` ## Interfaces ### AbsenceMarker One readable declared absence: which reason, and where. Value-free by construction. `code` is one of the fifteen literal strings [ABSENCE\_CODES](#absence_codes) holds, never a string read off the document, and `location` is a FHIRPath location whose every segment is bounded to the published form of a FHIR name. The location names the **marked element**, not the extension that marks it: the caller's question is "what happened to this element", and the extension is the answer's carrier rather than its subject. #### Properties ##### code > `readonly` **code**: `"error"` \| `"unknown"` \| `"asked-unknown"` \| `"temp-unknown"` \| `"not-asked"` \| `"asked-declined"` \| `"masked"` \| `"not-applicable"` \| `"unsupported"` \| `"as-text"` \| `"not-a-number"` \| `"negative-infinity"` \| `"positive-infinity"` \| `"not-performed"` \| `"not-permitted"` The reason the sender spelled, always a member of the closed value set. ##### location > `readonly` **location**: `string` The FHIRPath location of the element the marker sits on, bounded (never a document value). *** ### BundleEntry One entry of a [BundleReadout](#bundlereadout), value-free, structural facts only. `request`/`response` presence and the request method/url are surfaced (a `transaction`/`batch` entry carries a `request`; a `*-response` entry carries a `response`) so a caller can see the *shape* of the entry without this library interpreting or executing it. #### Properties ##### fullUrl > `readonly` **fullUrl**: `string` \| `undefined` The entry `fullUrl`, when present, the identity a reference resolves against. ##### hasResource > `readonly` **hasResource**: `boolean` Whether the entry carries an inline `resource`. ##### index > `readonly` **index**: `number` Zero-based position of the entry in `Bundle.entry`. ##### requestMethod > `readonly` **requestMethod**: `string` \| `undefined` `entry.request.method` (`GET`/`POST`/`PUT`/`DELETE`/`PATCH`/`HEAD`), when present. ##### requestUrl > `readonly` **requestUrl**: `string` \| `undefined` `entry.request.url`, when present. ##### resource > `readonly` **resource**: [`FhirComplex`](#fhircomplex) \| `undefined` The wrapped resource itself, for downstream reference resolution. `undefined` when absent. ##### resourceId > `readonly` **resourceId**: `string` \| `undefined` The wrapped resource's logical `id`, when it has one. ##### resourceType > `readonly` **resourceType**: `string` \| `undefined` The wrapped resource's `resourceType`, when it has one. ##### responseStatus > `readonly` **responseStatus**: `string` \| `undefined` `entry.response.status`, when present (a server-reply entry). *** ### BundleIndex A resolvable index of a Bundle's entries, keyed the two ways a reference can name an entry. #### Properties ##### byFullUrl > `readonly` **byFullUrl**: `ReadonlyMap`\<`string`, [`FhirComplex`](#fhircomplex)\> Entries keyed by their exact `fullUrl` (matches an absolute or `urn:` reference). ##### byTypeId > `readonly` **byTypeId**: `ReadonlyMap`\<`string`, [`FhirComplex`](#fhircomplex)\> Entries keyed by `Type/id` (matches a relative reference, or an absolute one's RESTful tail). *** ### BundleReadout The complete, value-free readout of a `Bundle`: its type, the entry-processing semantics implied by that type, and one [BundleEntry](#bundleentry) per `Bundle.entry` in order. `atomic` restates [entryProcessing](#entryprocessing-1) `=== "atomic"` for ergonomics: **`true` means the entries are all-or-nothing (a `transaction`)**, `false` means they are independent or the type carries no processing contract. Nothing here is executed, see the module doc. #### Properties ##### atomic > `readonly` **atomic**: `boolean` `true` exactly for a `transaction` (all-or-nothing); `false` for `batch` and everything else. ##### entries > `readonly` **entries**: readonly [`BundleEntry`](#bundleentry)[] The entries, in document order. ##### processing > `readonly` **processing**: [`EntryProcessing`](#entryprocessing) The entry-processing semantics for [type](#type). ##### total > `readonly` **total**: `string` \| `undefined` `Bundle.total` (a `searchset` count), kept as its **lexical string**, never a JS `number`. ##### type > `readonly` **type**: `string` \| `undefined` `Bundle.type`, or `undefined` if absent. *** ### Coded A (system, code) pair read out of a `Coding`, either half may be absent on a quirky instance. #### Properties ##### code > `readonly` **code**: `string` \| `undefined` ##### system > `readonly` **system**: `string` \| `undefined` *** ### CodeValidationRequest A value-set membership question: is `(system, code)` a member of the value set identified by `valueSet`? All three are plain identities, no PHI, no resource value. #### Properties ##### code > `readonly` **code**: `string` The coding's `code`. ##### system > `readonly` **system**: `string` The coding's `system` URI. ##### valueSet > `readonly` **valueSet**: `string` The value set's canonical identity (URL / OID form), from the element's binding. *** ### CodeValidationResult The result of a [CodeValidationRequest](#codevalidationrequest). #### Properties ##### membership > `readonly` **membership**: [`CodeMembership`](#codemembership) Whether the code is in the value set, not in it, or undecidable. ##### systemVersion? > `readonly` `optional` **systemVersion?**: `string` **Optional.** The code-system release this answer was made against, exactly as the service names it: an RxNorm monthly drop (`"2026-08-04"`), a SNOMED CT edition (`"http://snomed.info/sct/731000124108/version/20260301"`), a LOINC release (`"2.78"`). It is carried onto the finding the validator emits (`CODE_NOT_IN_VALUESET`) and reaches the `OperationOutcome`, so a consumer reconciling a validation report months later can tell which release the answer was made against without asking the service again. **The library verifies nothing about it.** This is the service's own assertion, preserved **exactly**: never trimmed, case-folded, parsed, truncated or substituted. The library vendors no code-system content, so it has nothing to check the string against and does not pretend to. **Declaring nothing is conformant, and is recorded rather than assumed.** Omit it (or, from untyped JavaScript, hand over a value that is not a non-blank string) and the finding is marked as having an **undeclared** release. No default, "latest" or "current" release is ever substituted, and the answer is otherwise unchanged: this field never affects `membership`, severity, or whether a finding is emitted at all. It is a code-**system** release, not a value-set version, and it must never be read from the instance being validated: a resource's own `Coding.version` is document content, and echoing it onto a finding would publish instance data through the one surface this library keeps value-free. *** ### ContainedIndex A resolvable index of one resource's `contained` resources, for `#fragment` resolution. #### Properties ##### byId > `readonly` **byId**: `ReadonlyMap`\<`string`, [`FhirComplex`](#fhircomplex)\> Contained resources keyed by their logical `id` (matches `#id`). ##### root > `readonly` **root**: [`FhirComplex`](#fhircomplex) The containing resource itself, the target of a bare `#` fragment. *** ### DatatypeUseReport One surfaced `use` on an Identifier, a HumanName, an Address or a ContactPoint: the code exactly as the document wrote it, and the location of the `use` element it was read from, one per covered position. The code is one of the [DatatypeUseCode](#datatypeusecode) values of that position's own value set, matched exactly, so this carries no text the library did not spell itself. The location follows the same bound and the same root rule as [ModifierElementReport](#modifierelementreport)'s. #### Example ```ts import { parseResource, readSafety, type DatatypeUseReport } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Patient","identifier":[{"use":"old","value":"S1"}]}', ); const uses: readonly DatatypeUseReport[] = readSafety(resource).datatypeUses; uses; // [{ code: "old", location: "Patient.identifier[0].use" }] ``` #### Properties ##### code > `readonly` **code**: [`DatatypeUseCode`](#datatypeusecode) The `use` code, exactly as written. ##### location > `readonly` **location**: `string` The FHIRPath location of the `use` element it was read from, bounded. *** ### Discriminator One slicing discriminator: how to tell instances of different slices apart. #### Properties ##### path > `readonly` **path**: `string` The FHIRPath (element path, relative to the sliced element) the discriminator inspects. ##### type > `readonly` **type**: `string` The discriminator kind. A `type` outside [DISCRIMINATOR\_TYPES](#discriminator_types) (e.g. R5 `position`) is unsupported. *** ### ElementBinding An element's terminology binding (strength + value-set identity) as declared by a profile. #### Properties ##### strength > `readonly` **strength**: `string` ##### valueSet? > `readonly` `optional` **valueSet?**: `string` *** ### ElementConstraint One `ElementDefinition.constraint`, a FHIR **invariant**. `key` is the stable identifier (`ait-1`, `us-core-1`), `severity` is `error` | `warning`, and `expression` is the FHIRPath the engine evaluates against an instance. `human` (the prose description) is modeled but never surfaced in a diagnostic (it is spec text, not PHI, but the engine reports the value-free `key`). #### Properties ##### expression > `readonly` **expression**: `string` ##### human? > `readonly` `optional` **human?**: `string` ##### key > `readonly` **key**: `string` ##### severity > `readonly` **severity**: `string` *** ### ElementDefinition The slice of a FHIR `ElementDefinition` the validator acts on. `path` is the dotted element path (e.g. `AllergyIntolerance.clinicalStatus`); `id` additionally encodes slice membership as `path:sliceName` (e.g. `Observation.category:VSCat`). #### Properties ##### binding? > `readonly` `optional` **binding?**: [`ElementBinding`](#elementbinding) The element's terminology binding, when the definition states one. ##### constraint? > `readonly` `optional` **constraint?**: readonly [`ElementConstraint`](#elementconstraint)[] The element's invariant constraints (FHIRPath), when the definition states any. ##### fixed? > `readonly` `optional` **fixed?**: [`TypedValue`](#typedvalue) A `fixed[x]` equality constraint, when present. ##### id > `readonly` **id**: `string` The element id, carries slice names as `:sliceName` segments. Defaults to `path` when absent. ##### max? > `readonly` `optional` **max?**: `number` Maximum cardinality ([UNBOUNDED](#unbounded) for `*`), when the definition states one. ##### min? > `readonly` `optional` **min?**: `number` Minimum cardinality, when the definition states one. ##### mustSupport? > `readonly` `optional` **mustSupport?**: `boolean` Whether the element is flagged must-support. ##### path > `readonly` **path**: `string` The dotted element path from the resource root. ##### pattern? > `readonly` `optional` **pattern?**: [`TypedValue`](#typedvalue) A `pattern[x]` subset constraint, when present. ##### sliceName? > `readonly` `optional` **sliceName?**: `string` The slice this element defines, when it is a slice (from `sliceName` or the id's `:` segment). ##### slicing? > `readonly` `optional` **slicing?**: [`Slicing`](#slicing-2) The slicing declaration, when this element introduces slices. ##### type? > `readonly` `optional` **type?**: readonly [`ElementType`](#elementtype)[] The allowed types, when the definition constrains them. *** ### ElementSchema The definition of one direct element of a resource. #### Properties ##### binding? > `readonly` `optional` **binding?**: [`RequiredBinding`](#requiredbinding) A required-strength enumerated `code` binding, when the element has one. ##### max > `readonly` **max**: `number` Maximum cardinality (`1` for a singleton, [UNBOUNDED](#unbounded) for `*`). ##### min > `readonly` **min**: `number` Minimum cardinality (`0` for optional, `≥ 1` for required). ##### types > `readonly` **types**: readonly `string`[] The allowed datatype name(s). One entry for a normal element; several for a `choice[x]` element (see [isChoice](#ischoice)). Primitive names are validated by ./primitives.js; complex names (e.g. `HumanName`) are validated structurally (cardinality + node shape) only, their internals need the datatype's own definition, which this schema does not carry. *** ### ElementType One allowed type for an element (`code` is the datatype; `profile`/`targetProfile` constrain it). #### Properties ##### code > `readonly` **code**: `string` ##### profile? > `readonly` `optional` **profile?**: readonly `string`[] ##### targetProfile? > `readonly` `optional` **targetProfile?**: readonly `string`[] *** ### FhirComplex An object (complex) element: an ordered list of named properties. Order is preserved from the wire so that a spec-clean document round-trips faithfully; on emit the serializer additionally hoists a string `resourceType` to the front (the one canonical-ordering rule FHIR requires). `properties` holds at most one entry per name. FHIR JSON requires property names to be unique (json.html §2.6.2: "Property names SHALL be unique"), and a repeating element is an array, never a repeated name. A non-conformant document that repeats a name is still read (the reader is lenient), and the members the first-wins rule did not put in `properties` are kept in [duplicates](#duplicates) rather than discarded, so nothing the wire carried is lost and a safety read can see that the document was ambiguous. [duplicates](#duplicates) is absent on every conformant document. #### Properties ##### droppedText? > `readonly` `optional` **droppedText?**: `true` Set when the XML document wrote **character data directly on this element**, which a FHIR element has no slot for (xml.html §2.6.1: a value travels in the `value` attribute, and an element's other content is child elements). The text is dropped, so this node holds only what the attributes and child elements carried. A marker that content was present and could not be placed, not a representation of it. Absent on every conformant document and on every document read from JSON. See [isDroppedText](#isdroppedtext). ##### duplicates? > `readonly` `optional` **duplicates?**: readonly [`FhirProperty`](#fhirproperty)[] The members a repeated property name shadowed, in document order, each still carrying the name it was written under. Present only on a non-conformant document. Read them with [getAllProperties](#getallproperties); a consumer that ignores this field is reading one arbitrary value out of several the sender wrote. ##### foreignRoot? > `readonly` `optional` **foreignRoot?**: `true` Set when the XML document's **root** element resolved to a vocabulary that is neither FHIR's nor none at all: `` or ``. The reader already reports that position (../xml/issues.js ISSUE\_CODES.UNEXPECTED\_XML\_CONTENT), but a report lives in the issue list and the issue list is not carried on the model, so a writer handed the model alone could not tell a vendor-rooted resource from one authored in FHIR. **A marker that the root named another vocabulary, NOT the vocabulary.** It is a literal `true`: the namespace URI is document content and is deliberately not kept, so nothing here can be echoed into a diagnostic and no walker gains a position. What it buys is that ../xml/write.js serializeResourceXml refuses rather than emitting a FHIR-namespaced document whose re-read carries an empty issue list. **Set at a document root and nowhere else.** An element that merely inherits a foreign namespace from its parent is flagged where the document leaves the vocabulary and is not marked here, and a root whose prefix resolves to **nothing** is not marked either: that root reads under its verbatim tag, and its XML write is refused at the tag sites on a code of its own rather than through this marker. A root declaring no namespace at all is read as FHIR, unflagged and unmarked. Absent on every conformant document and on every document read from JSON. See [isForeignRoot](#isforeignroot). ##### kind > `readonly` **kind**: `"complex"` ##### nestedArray? > `readonly` `optional` **nestedArray?**: `true` Set when the JSON document put an **array** at this position, where FHIR JSON gives an array no meaning (an array inside an array). The array's contents are **not** modeled as FHIR: this is a marker that content was present and could not be placed, not a representation of it, and the node stays the empty element it has always been. Absent on every conformant document. See [isNestedArray](#isnestedarray). ##### nestedArraySource? > `readonly` `optional` **nestedArraySource?**: `string` The JSON text of the array the sender wrote at this position, never interpreted as FHIR. See [NestedArrayContent](#nestedarraycontent) for what "the text" is exact about, and [nestedArrayContent](#nestedarraycontent-1) to read it. ##### nonObjectSource? > `readonly` `optional` **nonObjectSource?**: `string` The JSON text of a **non-object, non-array** value the sender wrote at a position where FHIR JSON has an object: a string, a number, a boolean, or `null` (json.html §2.6.2 gives a complex element an object and nothing else). The value is **not** modeled as FHIR, and deliberately not as a primitive either: putting it in the tree would make it visible to every walker at a position that walker reads as a complex element, which is a redefinition of the model rather than a preservation of the document. The node stays the empty element the reader had to produce there, and the text hangs off it where the writer can hand it back. The reader raises [ISSUE\_CODES.UNKNOWN\_PROPERTY](#unknown_property) at the same position. An **array** is the neighbouring case and has its own field ([nestedArraySource](#nestedarraysource)) plus a marker, because it additionally carries [ISSUE\_CODES.NESTED\_ARRAY](#nested_array). Absent on every conformant document and on every document read from XML. **The text is value-exact, not byte-exact**, exactly as [NestedArrayContent](#nestedarraycontent) is: it is the value re-rendered the way this library renders every other JSON value it emits, so a number's exact source survives and a string's escaping does not (`"Jamés"` comes back as `"Jamés"`, `"a/b"` as `"a/b"`). Both denote the same string, so nothing is lost; a caller comparing the writer's output to the input **byte for byte** will still see a difference here. ##### properties > `readonly` **properties**: readonly [`FhirProperty`](#fhirproperty)[] *** ### FhirIssue A single value-free diagnostic accumulated during a lenient read. `expression` is a FHIRPath location into the document (e.g. `Bundle.entry[2].resource.ofType(Patient).name[0].given[1]`, or a simpler `Patient.birthDate`), it says *where* without echoing *what*. It never contains a resource value, so an issue is safe to log. **It is a location, and on non-conformant input it can be a location with a gap.** The segments are the document's own names, and a name is echoed only when it matches the published form of a FHIR name; anything else reads as the ../model/path.js `WITHHELD` marker, `""`. A marker is not a FHIRPath identifier, so such an expression will not resolve against the instance: R4 defines `OperationOutcome.issue.expression` as a FHIRPath subset that SHALL resolve to a single node, and a location with a withheld segment does not. Every segment around the marker is intact, so the nearest addressable ancestor is still there. Test for the marker before handing an expression to a FHIRPath engine. #### Properties ##### code > `readonly` **code**: [`IssueCode`](#issuecode) ##### expression > `readonly` **expression**: `string` ##### severity > `readonly` **severity**: [`IssueSeverity`](#issueseverity) *** ### FhirList A repeating element: an ordered list of item nodes. A primitive list preserves value-absent slots as [FhirPrimitive](#fhirprimitive) nodes with `value: undefined`, so the null-padding alignment between a value array and its `_`-sibling array is captured structurally. #### Properties ##### items > `readonly` **items**: readonly [`FhirNode`](#fhirnode)[] ##### kind > `readonly` **kind**: `"list"` *** ### FhirPrimitive A primitive (leaf) element: a value plus its optional `id` and `extension` metadata. `value` is `undefined` exactly when the element has no value of its own but does carry metadata, the FHIR case where a primitive slot is `null` in the value array but an object in the `_`-sibling array. At least one of `value`, `id`, `extension` is meaningful for the node to exist. #### Example ```ts import { primitive } from "@cosyte/fhir"; const given = primitive("Jacqueline"); // plain value const flagged = primitive(undefined, { extension: [ext] }); // value-absent, extension-only ``` #### Properties ##### droppedText? > `readonly` `optional` **droppedText?**: `true` Set when the XML document wrote **character data directly on this element**, which a FHIR element has no slot for: a primitive's value travels in the `value` attribute (xml.html §2.6.1), so the text is dropped and this node's `value` is whatever the attribute held, which for `entered-in-error` is nothing at all. A marker that content was present and could not be placed, not a representation of it. Absent on every conformant document and on every document read from JSON. See [isDroppedText](#isdroppedtext). ##### extension? > `readonly` `optional` **extension?**: readonly [`FhirComplex`](#fhircomplex)[] ##### id? > `readonly` `optional` **id?**: `string` ##### kind > `readonly` **kind**: `"primitive"` ##### nestedArray? > `readonly` `optional` **nestedArray?**: `true` Set when the JSON document put an **array** at this position, where FHIR JSON gives an array no meaning (an array inside an array). The array's contents are **not** modeled as FHIR: this is a marker that content was present and could not be placed, not a representation of it. Absent on every conformant document. See [isNestedArray](#isnestedarray). ##### nestedArrayMetaSource? > `readonly` `optional` **nestedArrayMetaSource?**: `string` The JSON text of the array the sender wrote at this position in the primitive's `_`-sibling (metadata) channel. A primitive can carry a nested array in either channel or both, so the two are kept apart rather than merged. See [nestedArrayContent](#nestedarraycontent-1). ##### nestedArraySource? > `readonly` `optional` **nestedArraySource?**: `string` The JSON text of the array the sender wrote at this position, when that array sat in the element's own value channel. Never interpreted as FHIR. See [NestedArrayContent](#nestedarraycontent) for what "the text" is exact about, and [nestedArrayContent](#nestedarraycontent-1) to read it. ##### nonObjectMetaSource? > `readonly` `optional` **nonObjectMetaSource?**: `string` The JSON text of a **non-object, non-array** value the sender wrote in this primitive's `_`-sibling (metadata) channel: a string, a number, a boolean, or a `null` at a **singleton** slot. FHIR JSON gives that channel an `Element` object and nothing else (json.html §2.6.2.3, "the `id` and/or `extension`"), so there is no metadata to read out of a scalar and the reader models none. This is the `_`-sibling counterpart of [FhirComplex.nonObjectSource](#nonobjectsource) and it exists for the same reason: without the text the writer has no `_`-sibling to emit, so the member is dropped and a non-conformant document comes back conformant with it simply gone. The reader raises [ISSUE\_CODES.UNKNOWN\_PROPERTY](#unknown_property) at the element's position. **A `null` at any slot of a repeating primitive's `_`-array is never marked here** (§2.6.2.3 fills out *both* arrays), so a conformant document never carries this. The exemption is by **position**, not by whether that slot pads a value, so a `_`-array with no value array beside it keeps the silent drop it had before: declared, not closed. A `null` at a **singleton** `_` slot is never padding, on the same reasoning as [undefinedNull](#undefinednull), and is marked. An **array** in this channel is the neighbouring case with its own field ([nestedArrayMetaSource](#nestedarraymetasource)) and its own code. Absent on every document read from XML. **The text is value-exact, not byte-exact**, exactly as [FhirComplex.nonObjectSource](#nonobjectsource) is. ##### undefinedNull? > `readonly` `optional` **undefinedNull?**: `true` Set when the JSON document wrote a bare `null` in this primitive's **value** channel at a position FHIR JSON does not define one. FHIR JSON uses `null` for exactly one thing, padding a repeating primitive's value array so that it aligns index-by-index with the `_`-sibling array carrying that occurrence's `id`/`extension` (json.html §2.6.2.3); a `null` whose slot carries no such metadata aligns nothing, and the element then has neither a value nor children, which R4 `ele-1` requires. The node stays the value-absent primitive it has always been, so no walker sees anything new; what the marker buys is that ../codec/write.js serializeResource can write the `null` back instead of omitting the member, which is what stops the shape being laundered into a conformant document with the member simply missing. Absent on every conformant document and on every document read from XML. See [isUndefinedNull](#isundefinednull). ##### value > `readonly` **value**: [`PrimitiveValue`](#primitivevalue) \| `undefined` *** ### FhirProperty A single named property of a [FhirComplex](#fhircomplex). #### Properties ##### name > `readonly` **name**: `string` ##### value > `readonly` **value**: [`FhirNode`](#fhirnode) *** ### IntentReport One surfaced `MedicationRequest.intent`: the code exactly as the document wrote it, and the location of the `intent` element it was read from, one per `MedicationRequest` root. The code is one of the eight [MedicationRequestIntent](#medicationrequestintent) values, matched exactly, so this carries no text the library did not spell itself. The location follows the same bound and the same root rule as [ModifierElementReport](#modifierelementreport)'s. #### Properties ##### code > `readonly` **code**: [`MedicationRequestIntent`](#medicationrequestintent) The intent code, exactly as written. ##### location > `readonly` **location**: `string` The FHIRPath location of the `intent` element it was read from, bounded. *** ### InvariantOptions Options for [collectInvariantIssues](#collectinvariantissues). #### Properties ##### resolve? > `readonly` `optional` **resolve?**: [`BaseResolver`](#baseresolver) Base resolver for snapshot generation, needed only when the profile carries no snapshot. *** ### InvariantResult The outcome of evaluating one invariant expression against an instance. #### Properties ##### satisfied > `readonly` **satisfied**: `boolean` Whether the constraint is satisfied. Meaningful only when [unchecked](#unchecked) is `false`. ##### unchecked > `readonly` **unchecked**: `boolean` `true` when the bounded subset could not lex/parse/evaluate the expression. The caller reports this as `INVARIANT_UNCHECKED`, **the invariant is never treated as satisfied when unchecked**. *** ### LocatedDoseQuantity A located `doseQuantity` node: the complex value and its FHIRPath expression for a value-free issue. #### Properties ##### node > `readonly` **node**: [`FhirComplex`](#fhircomplex) The `doseQuantity` complex node. ##### path > `readonly` **path**: `string` The FHIRPath location, e.g. `MedicationRequest.dosageInstruction[0].doseAndRate[0].doseQuantity`. *** ### ModifierElementReport One reported modifier element: which element, and where. Value-free by contract. `element` is one of the literal keys this library spells, never a name read off the document, and `location` is bounded segment by segment with its root restricted to [MODIFIER\_ELEMENT\_ROOT\_TYPES](#modifier_element_root_types). #### Properties ##### element > `readonly` **element**: [`ModifierElementName`](#modifierelementname) The modifier element that is present. ##### location > `readonly` **location**: `string` The FHIRPath location it is present at, bounded (never a document value). *** ### NdjsonError A value-free, isolated failure for a single NDJSON line, carries the line number, never content. #### Properties ##### code > `readonly` **code**: [`NdjsonErrorCode`](#ndjsonerrorcode-1) The coded reason. ##### line > `readonly` **line**: `number` The 1-based line number of the failing line. ##### message > `readonly` **message**: `string` A value-free description of the *kind* of failure, never the line's text. *** ### NdjsonOptions Options for the NDJSON readers. #### Properties ##### maxLineBytes? > `readonly` `optional` **maxLineBytes?**: `number` The maximum bytes a single line may reach before a newline forces a `LINE_TOO_LONG` cut-off (the no-whole-file-load / DoS guard). Default 16 MiB, comfortably above any real resource, far below a memory hazard. *** ### NdjsonRecord One NDJSON line's outcome. Exactly one of `resource` / `error` is present: a good line yields the parsed `resource` (plus any value-free codec `issues`), a bad line yields an isolated `error`. #### Properties ##### error? > `readonly` `optional` **error?**: [`NdjsonError`](#ndjsonerror) The isolated failure, when the line did not read. ##### issues? > `readonly` `optional` **issues?**: readonly [`FhirIssue`](#fhirissue)[] Value-free codec diagnostics gathered reading the line (e.g. `DECIMAL_PRECISION_AT_RISK`). ##### line > `readonly` **line**: `number` The 1-based line number. ##### resource? > `readonly` `optional` **resource?**: [`FhirComplex`](#fhircomplex) The parsed resource, when the line read cleanly. *** ### NestedArrayContent One array the sender wrote where FHIR JSON gives an array no meaning, kept as JSON text. The text is the array re-rendered compactly from what was read: member order and every member of a repeated key are preserved, and number tokens keep their verbatim source, so no value changes. It is not a byte-for-byte slice of the input, because insignificant whitespace is dropped and strings are re-escaped canonically, exactly as everywhere else this library emits JSON. #### Properties ##### channel > `readonly` **channel**: [`NestedArrayChannel`](#nestedarraychannel) The JSON channel the array sat in. ##### json > `readonly` **json**: `string` The array's JSON text, uninterpreted. *** ### ObservationReferenceRange A single `Observation.referenceRange` entry, surfaced (never used to compute an abnormal flag). #### Properties ##### high > `readonly` **high**: [`Quantity`](#quantity-1) \| `undefined` `referenceRange.high`, the inclusive upper bound, when present. ##### low > `readonly` **low**: [`Quantity`](#quantity-1) \| `undefined` `referenceRange.low`, the inclusive lower bound, when present. ##### text > `readonly` **text**: `string` \| `undefined` `referenceRange.text`, a free-text range when the bounds are not machine-comparable. ##### type > `readonly` **type**: readonly [`Coded`](#coded)[] `referenceRange.type` codings (e.g. `normal`, `treatment`), when present. *** ### ObservationValue The discriminated reading of an `Observation.value[x]` (or a `component.value[x]`). `type` names the variant that is present; `quantity` is populated **only** when `type === "Quantity"`, so a caller that wants a number must check the type first. `ambiguous` lists any *additional* variants also present, a `value[x]` is a `0..1` choice, so a non-empty `ambiguous` is a structural defect (the kind the structural validator reports as `CHOICE_AMBIGUOUS` once Observation is a modeled schema). This reader surfaces it here regardless, so the extra variant is never silently dropped. #### Properties ##### ambiguous > `readonly` **ambiguous**: readonly (`"Quantity"` \| `"CodeableConcept"` \| `"String"` \| `"Boolean"` \| `"Integer"` \| `"Range"` \| `"Ratio"` \| `"SampledData"` \| `"Time"` \| `"DateTime"` \| `"Period"`)[] Additional `value[x]` variants also present (a structural ambiguity); empty in a clean resource. ##### encodingIssue > `readonly` **encodingIssue**: [`ValidationCode`](#validationcode) \| `undefined` The published ../validate/issues.js `ValidationCode` for the ENCODING the present variant arrived in, when FHIR JSON gives that shape no meaning at a `0..1` choice, and `undefined` otherwise, which is every conformant document. The one value it takes today is `ARRAY_WRAPPED_CHOICE`: the variant arrived wrapped in a JSON array, which json.html §2.6.2.2 reserves for a repeating element. **This is the channel that distinguishes "the sender wrote a value here and nothing read it" from "the sender wrote no value here"**, which `quantity: undefined` alone cannot: a `valueQuantity` of `[{"value":5, "code":"mg"}]` and one of `{}` both read as a present `Quantity` with no magnitude, and one of them is a 5 mg dose. It is the same code `validateResource` raises at the same location, so a caller switching on this and a caller reading the issue list see one vocabulary. **It says nothing about the value's CONTENT.** A variant that arrived the way FHIR JSON spells it and holds a shape the variant cannot carry is not this channel's business; nothing here is a general "the value was unreadable" report. ##### node > `readonly` **node**: [`FhirNode`](#fhirnode) The raw value node for the present variant. ##### property > `readonly` **property**: `string` The full JSON property name of the present variant (e.g. `"valueQuantity"`). ##### quantity > `readonly` **quantity**: [`Quantity`](#quantity-1) \| `undefined` The parsed [Quantity](#quantity-1), present **only** when `type === "Quantity"`; `undefined` otherwise. ##### type > `readonly` **type**: `"Quantity"` \| `"CodeableConcept"` \| `"String"` \| `"Boolean"` \| `"Integer"` \| `"Range"` \| `"Ratio"` \| `"SampledData"` \| `"Time"` \| `"DateTime"` \| `"Period"` The present variant's type suffix (e.g. `"Quantity"`, `"String"`, `"CodeableConcept"`). *** ### ParsedReference A parsed `Reference.reference` string. `type`, `id`, and `version` are populated only when the form makes them unambiguous (a relative reference always; an absolute RESTful URL when its tail matches `Type/id`). A `logical` reference (e.g. `urn:uuid:…`) exposes only `raw` and `kind`. #### Properties ##### id? > `readonly` `optional` **id?**: `string` The referenced logical id, when the form reveals it. For a fragment this is the anchor. ##### kind > `readonly` **kind**: [`ReferenceKind`](#referencekind) The classified form. ##### raw > `readonly` **raw**: `string` The exact reference string as supplied. ##### type? > `readonly` `optional` **type?**: `string` The referenced resource type, when the form reveals it (e.g. `"Patient"`). ##### version? > `readonly` `optional` **version?**: `string` The version id from a `/_history/{vid}` suffix, when present. *** ### PrimitiveMeta Optional `id` / `extension` metadata for [primitive](#primitive). #### Properties ##### extension? > `readonly` `optional` **extension?**: readonly [`FhirComplex`](#fhircomplex)[] ##### id? > `readonly` `optional` **id?**: `string` *** ### ProfileConstraintSpec One `constraint` (invariant) in a [ProfileElementSpec](#profileelementspec). `severity` defaults to `"error"` (as `loadStructureDefinition` defaults it), mirroring the FHIR `ElementDefinition.constraint` shape. #### Properties ##### expression > `readonly` **expression**: `string` The FHIRPath expression the engine evaluates. ##### human? > `readonly` `optional` **human?**: `string` The prose description (spec text, never surfaced in a diagnostic). ##### key > `readonly` **key**: `string` The stable invariant key (`us-core-1`, `ait-1`). ##### severity? > `readonly` `optional` **severity?**: `string` `error` | `warning`; defaults to `"error"` when omitted. *** ### ProfileElementSpec The ergonomic authoring shape for one element. Mirrors [ElementDefinition](#elementdefinition), but `max` accepts the author-friendly `"*"` (normalized to [UNBOUNDED](#unbounded)) and `constraint` takes [ProfileConstraintSpec](#profileconstraintspec) (whose `severity` defaults). `id` defaults to `path`; `sliceName` is derived from the id's `:` segment when omitted, exactly as `loadStructureDefinition` does. #### Properties ##### binding? > `readonly` `optional` **binding?**: [`ElementBinding`](#elementbinding) The element's terminology binding (strength + value-set identity). ##### constraint? > `readonly` `optional` **constraint?**: readonly [`ProfileConstraintSpec`](#profileconstraintspec)[] The element's invariant constraints (FHIRPath). ##### fixed? > `readonly` `optional` **fixed?**: [`TypedValue`](#typedvalue) A `fixed[x]` equality constraint (a FHIR type name + a model node, build with `complex`/`primitive`/`list`). ##### id? > `readonly` `optional` **id?**: `string` The element id, carries slice names as `:sliceName`. Defaults to `path`. ##### max? > `readonly` `optional` **max?**: `number` \| `"*"` Maximum cardinality: a non-negative integer or `"*"` (→ [UNBOUNDED](#unbounded)). ##### min? > `readonly` `optional` **min?**: `number` Minimum cardinality (a non-negative integer). ##### mustSupport? > `readonly` `optional` **mustSupport?**: `boolean` Whether the element is flagged must-support (a *system obligation*, not instance-presence). ##### path > `readonly` **path**: `string` The dotted element path from the resource root (e.g. `Observation.status`). Required. ##### pattern? > `readonly` `optional` **pattern?**: [`TypedValue`](#typedvalue) A `pattern[x]` subset constraint (a FHIR type name + a model node). ##### sliceName? > `readonly` `optional` **sliceName?**: `string` The slice this element defines. Defaults to the id's `:sliceName` segment when present. ##### slicing? > `readonly` `optional` **slicing?**: [`Slicing`](#slicing-2) The slicing declaration, when this element introduces slices. ##### type? > `readonly` `optional` **type?**: readonly [`ElementType`](#elementtype)[] The allowed types, when the profile constrains them. *** ### ProfileOptions Options for [collectProfileIssues](#collectprofileissues). #### Properties ##### resolve? > `readonly` `optional` **resolve?**: [`BaseResolver`](#baseresolver) Base resolver for snapshot generation, needed only when the profile carries no snapshot. *** ### ProfileSpec The ergonomic authoring shape for a whole profile. Mirrors the modeled slice of a FHIR `StructureDefinition` ([StructureDefinition](#structuredefinition)); `differential` / `snapshot` take [ProfileElementSpec](#profileelementspec)s. #### Properties ##### baseDefinition? > `readonly` `optional` **baseDefinition?**: `string` The canonical URL of the definition this one derives from. ##### derivation? > `readonly` `optional` **derivation?**: [`Derivation`](#derivation-2) Specialization (a base resource) or constraint (a profile). ##### differential? > `readonly` `optional` **differential?**: readonly [`ProfileElementSpec`](#profileelementspec)[] The differential element list (constraints relative to the base). ##### kind? > `readonly` `optional` **kind?**: `string` `resource` | `complex-type` | `primitive-type` | `logical`. ##### name? > `readonly` `optional` **name?**: `string` The computer-friendly name. ##### snapshot? > `readonly` `optional` **snapshot?**: readonly [`ProfileElementSpec`](#profileelementspec)[] A pre-resolved snapshot element list (rare in authoring; usually generated from the differential). ##### type > `readonly` **type**: `string` The resource type this profile constrains (e.g. `"Observation"`). Required. ##### url > `readonly` **url**: `string` The canonical URL, the identity the profile is referenced by. Required. ##### version? > `readonly` `optional` **version?**: `string` The business version (e.g. `"6.1.0"`), when the profile is versioned. *** ### Quantity The value-free reading of a FHIR `Quantity` element. `value` is a [FhirDecimal](#fhirdecimal), the exact lexical number, never routed through a JS float, and `code`/`system` are the machine-actionable unit, kept distinct from the human `unit` string. The magnitude is read from either wire format's model, so an XML-sourced quantity is not a unit with no number. The model records no provenance, so a JSON document that spelled its magnitude as a string is read the same way; FHIR JSON says a decimal is a number, so that document is non-conformant either way. #### Properties ##### code > `readonly` **code**: `string` \| `undefined` `Quantity.code`, the machine-actionable coded unit (UCUM when `system` is [UCUM\_SYSTEM](#ucum_system)). ##### comparator > `readonly` **comparator**: `string` \| `undefined` `Quantity.comparator` (`<` | `<=` | `>=` | `>`), a bound, not an exact value, when present. ##### system > `readonly` **system**: `string` \| `undefined` `Quantity.system`, the unit code system URI (UCUM for a coded quantity). ##### unit > `readonly` **unit**: `string` \| `undefined` `Quantity.unit`, the human-readable display string. **Not** for machine comparison. ##### value > `readonly` **value**: [`FhirDecimal`](#fhirdecimal) \| `undefined` `Quantity.value`, the exact decimal, or `undefined` when this reader found no magnitude it can read: an absent `value`, a `comparator`-only bound, or text outside the R4 `decimal` lexical space. **That last one is a residual, not a guarantee.** `` reads `undefined` beside a unit that reads fine, and nothing is raised on any channel to say a magnitude was written there. *** ### RawArray A JSON array node. #### Properties ##### items > `readonly` **items**: readonly [`RawJson`](#rawjson)[] ##### t > `readonly` **t**: `"arr"` *** ### RawBool A JSON boolean node. #### Properties ##### t > `readonly` **t**: `"bool"` ##### value > `readonly` **value**: `boolean` *** ### RawMember A member of a [RawObject](#rawobject), preserving key and source order. #### Properties ##### key > `readonly` **key**: `string` ##### value > `readonly` **value**: [`RawJson`](#rawjson) *** ### RawNull A JSON null node. #### Properties ##### t > `readonly` **t**: `"null"` *** ### RawNumber A JSON number node, preserved as its **exact** source text, never a JavaScript `number`. #### Properties ##### raw > `readonly` **raw**: `string` ##### t > `readonly` **t**: `"num"` *** ### RawObject A JSON object node, members in source order (duplicate keys preserved as separate members). #### Properties ##### members > `readonly` **members**: readonly [`RawMember`](#rawmember)[] ##### t > `readonly` **t**: `"obj"` *** ### RawString A JSON string node, already unescaped to its logical value. #### Properties ##### t > `readonly` **t**: `"str"` ##### value > `readonly` **value**: `string` *** ### ReadResult The result of reading a FHIR resource: the model plus any value-free issues gathered en route. #### Properties ##### issues > `readonly` **issues**: readonly [`FhirIssue`](#fhirissue)[] Value-free diagnostics accumulated during the lenient read (never contains PHI). ##### resource > `readonly` **resource**: [`FhirComplex`](#fhircomplex) The parsed resource as an immutable model tree. *** ### RequiredBinding A required-strength value-set binding to a fixed set of `code` values. #### Properties ##### codes > `readonly` **codes**: readonly `string`[] The complete enumerated code set. ##### strength > `readonly` **strength**: `"required"` Only `"required"` bindings are enforced here; weaker strengths are the terminology layer's. *** ### ResourceSchema A resource's direct elements, keyed by element name (the `choice[x]` base for choices). #### Properties ##### elements > `readonly` **elements**: `Readonly`\<`Record`\<`string`, [`ElementSchema`](#elementschema)\>\> Direct elements by name. The base-resource elements are merged in by the registry. ##### type > `readonly` **type**: `string` The resource type this schema describes (e.g. `"Patient"`). *** ### SafetyReadout The complete, value-free safety readout of a resource. Every modifier element the safety resource types can carry has a slot here, present or `undefined`, so a consumer building a summary reads them explicitly rather than forgetting one. **`negations` is the authoritative safety read, and it is collected at every resource root** the document carries, so a retracted `Observation`, a not-performed `Procedure` or an order marked "do not give" inside `contained` or a `Bundle.entry` reaches it. The one negation not collected from the walk is `no-known-allergy`, which stays the root, type-scoped read on its own field for the reasons given there. **Two groups of fields, and the difference is which question they answer.** The location channels (`unhandledModifierExtensions`, `modifierElements`, `intents`, `datatypeUses`, `shadowedProperties`, `arrayWrappedScalars`, `nestedArrays`, `droppedText`, `unreadableBooleans`, `nearMissNegationCodes`, `unreadableNegationCodes`, `unreadableIntents`, `unreadableDatatypeUses`, `absenceMarkers`, `unreadableAbsenceMarkers`, `conflictingAbsenceMarkers`) and the `safeToSummarize` derived from them (from all but `absenceMarkers`, `intents` and `datatypeUses`, which disclose rather than refuse) are **document-wide**: they carry FHIRPath locations, so a nested finding has an address to name, and `assertSafeToSummarize` refuses over a `Bundle`'s entries. The **single-valued** fields (`resourceType` / `status` / `clinicalStatus` / `verificationStatus` / `doNotPerform` / `retracted` / `noKnownAllergy`) answer about **the resource handed in** and nothing nested inside it, because one value cannot say which resource it came from. `negations` is the read that crosses that line: it is a document-wide **set** with no locations. Branch on it, not on the single-valued fields, whenever the resource may carry others. The single-code convenience fields (`resourceType` / `status` / `clinicalStatus` / `verificationStatus`) surface one value: the *preferred*-system coding of a `CodeableConcept`, falling back to the first coding when the standard one is absent (which may be a local/translation code), and the first member written when a non-conformant document repeated a property name or wrapped the element in an array. The classified `negations` are derived from **every** coding under any system and **every** value written for the element each negation reads (`resourceType`, `status`, `verificationStatus`, `code`, `doNotPerform`), including the ones a repeated property name shadowed and the ones inside an array wrapper **around the element**, so a refutation or a retraction cannot hide in the value a single-value lookup skipped, and the type gate cannot hide a type-scoped negation behind a type it did not read. That extends **one level down**, to an array around a `Coding.system` / `Coding.code` inside a `CodeableConcept`, where the wrapper holds a single array position; a multi-position one is reported rather than read, because pairing a `system` from one position with a `code` from another would assert a coding the sender never wrote (see [arrayWrappedScalars](#arraywrappedscalars-1)). Read a safety decision off `negations`, not off the raw status string, and check `safeToSummarize` before flattening anything. #### Properties ##### absenceMarkers > `readonly` **absenceMarkers**: readonly [`AbsenceMarker`](#absencemarker)[] Every element the document declares an absence for, with the reason the sender spelled and the FHIRPath location of the element it sits on. This is the read that separates "we asked and nobody knows" from "we never sent this": both leave the element value-absent, and before this channel the two were the same answer to a caller. The marker is the R4 DataAbsentReason **extension**, on a complex element or on a primitive's extension metadata, in either wire format. See ../safety/absence.js for the recognition predicate, for the closed value set a `code` is drawn from, and for the two neighbouring shapes that are deliberately NOT markers (the same code system used as a `Coding` inside a coded element, and the `Observation.dataAbsentReason` ELEMENT with its own `obs-6` invariant). **This channel does NOT lower [SafetyReadout.safeToSummarize](#safetosummarize)**, and it is the only location-bearing channel here that does not. Every other one marks something the library could not read or could not rank; a readable, non-conflicting marker is the opposite, a disclosure the sender made deliberately and the caller can now read. Refusing to summarize over it would withdraw an affirmation from a conformant document, which is the one direction this layer's contract forbids. The two channels beside it, [SafetyReadout.unreadableAbsenceMarkers](#unreadableabsencemarkers) and [SafetyReadout.conflictingAbsenceMarkers](#conflictingabsencemarkers), do lower it, and for the ordinary reason. **Document-wide**, like the location channels beside it: a marker inside `contained` or a `Bundle.entry` is here, with a location that names where it sits. Empty for every document that carries no DataAbsentReason extension, which is where a document carrying none reads exactly as it did before this channel existed. ##### arrayWrappedScalars > `readonly` **arrayWrappedScalars**: readonly `string`[] FHIRPath locations where a `0..1` safety element (or `resourceType`) arrived wrapped in a JSON array, the shape a generic XML-to-JSON converter emits for every element. Empty on every conformant document, since FHIR JSON uses an array only for a repeating element. ##### clinicalStatus > `readonly` **clinicalStatus**: `string` \| `undefined` The `clinicalStatus` code, preferred-system-first (AllergyIntolerance / Condition). Convenience only. ##### conflictingAbsenceMarkers > `readonly` **conflictingAbsenceMarkers**: readonly `string`[] FHIRPath locations of elements that carry an absence marker **and** a value of their own, so the document says both "here is the value" and "there is no value" about one element. Nothing here resolves the contradiction. The value stays in the model where a caller walking it finds it, the marker stays on [SafetyReadout.absenceMarkers](#absencemarkers) when its reason is readable, and this location is what stops a caller silently preferring whichever of the two its own read happened to reach first. A complex element "carries a value" when it holds any member beyond `id`, `url`, `extension`, `modifierExtension` and the JSON encoding's `resourceType`, none of which is the value a marker denies; a primitive carries one when its value channel is filled. Empty for every conformant document: an element the sender has data for is written with the data and no marker. ##### datatypeUses > `readonly` **datatypeUses**: readonly [`DatatypeUseReport`](#datatypeusereport)[] `use` on an Identifier, a HumanName, an Address or a ContactPoint, surfaced at **every covered position** the document carries: the code exactly as written, paired with the location of that `use` element, one pair per position, in walk order. The covered positions sit below a resource root of one of the eight types this library's safety layer models (`AllergyIntolerance`, `Condition`, `DiagnosticReport`, `Immunization`, `MedicationRequest`, `MedicationStatement`, `Observation`, `Patient`), whether that root is the resource handed in, a `contained` one or a `Bundle.entry`: every member named `identifier` or `groupIdentifier` at any depth (a Reference's identifier included), and on a `Patient` its `name`, `telecom` and `address` and the same three on each `contact` entry. A position belongs to its nearest enclosing resource root, and a root of any other type reads nothing here. R4 flags `use` a modifier on all four datatypes, so that an `old` or `temp` entry is not taken for a current one. It is optional and written on most records, so it is surfaced rather than refused on presence, and a readable one **does not lower [safeToSummarize](#safetosummarize)**. Only a code of the value set of the position's OWN datatype is surfaced, matched exactly, so `temp` reads on all four and `maiden` on a HumanName only, and this field carries no text the library did not spell. A `use` that is present and not one of those codes is on [unreadableDatatypeUses](#unreadabledatatypeuses) instead. A position with no `use` surfaces nothing. **Surfaced, never interpreted.** Nothing here maps `old` or `temp` to "not current", and nothing is filtered, reordered or picked as the current entry: which entry to show is the caller's decision. `Practitioner.identifier.use` is not here: it keeps its presence rule on [modifierElements](#modifierelements-1). **Declared limits:** a datatype carried as an extension value (`valueIdentifier`, `valueHumanName`, `valueAddress`, `valueContactPoint`) is not read, nor is a position inside a nested resource root whose type is not one of the eight (a contained `Organization`'s `identifier` or `telecom`); and an ended `period` on an entry is a separate element this does not read. Located by the same bound and the same root rule as [modifierElements](#modifierelements-1). ##### doNotPerform > `readonly` **doNotPerform**: `boolean` \| `undefined` `doNotPerform` **on the resource handed to [readSafety](#readsafety)**, whatever its type, read off either wire format's model: the JSON codec's `boolean`, or the lexical `true` / `false` the schema-free XML reader keeps as the text of the `value` attribute. Text outside that two-word lexical space (`"TRUE"`, `"1"`) still reads as `undefined` (coercing it would author a value the sender did not spell) but no longer silently: the element's location is on [unreadableBooleans](#unreadablebooleans-1) and the resource is not `safeToSummarize`. So `undefined` here means *either* "no boolean was written" or "one was written and could not be read", and the two are told apart by that channel, never by this field. **Convenience only, and root-scoped like `status` beside it.** A `doNotPerform` written on a resource *inside* this one, in `contained` or a Bundle `entry`, leaves this field `undefined` and still puts `do-not-perform` on [negations](#negations), which is the authoritative read. Branch on `negations`, never on this field, when the resource may carry others. ##### droppedText > `readonly` **droppedText**: readonly `string`[] FHIRPath locations where an XML document wrote **character data directly on a FHIR element**, which has no slot for it: a primitive's value travels in the `value` attribute (xml.html §2.6.1), so the reader drops the text. Like `nestedArrays` the content at these locations is **not readable**, and the element then looks exactly like one the sender left out, which is how `entered-in-error` came to read as a live record. Empty on every conformant document and on every document read from JSON. ##### intents > `readonly` **intents**: readonly [`IntentReport`](#intentreport)[] `MedicationRequest.intent`, surfaced at **every `MedicationRequest` root** the document carries (the resource handed in, a `contained` one, a `Bundle.entry`): the code exactly as written, paired with the location of that root's `intent`, one pair per root, in walk order. R4 flags `intent` a modifier (a `proposal` is not an `order`) and makes it mandatory, so it is surfaced the way `status` is rather than refused on presence, and a readable one **does not lower [safeToSummarize](#safetosummarize)**. Only the eight codes of the R4 value set can appear here, matched exactly and case-sensitively, so this field carries no text the library did not spell. An `intent` that is present and not one of them is on [unreadableIntents](#unreadableintents) instead, and its root surfaces nothing here. A root with no `intent` at all surfaces nothing and refuses nothing: a missing mandatory element is the validator's verdict, not this readout's. No meaning is read from the code: nothing here says whether a request is in force. ##### modifierElements > `readonly` **modifierElements**: readonly [`ModifierElementReport`](#modifierelementreport)[] The modifier ELEMENTS present in the document, one entry per distinct location, each naming the element and where it is. R4 flags several ordinary base elements `Is Modifier: true` because they change how the value beside them must be read, and this is the channel that surfaces them: `comparator` wherever the walk reaches a node carrying it, `implicitRules` likewise, `Patient.active`, `Patient.deceased[x]` (element `deceased`, located at the member as written, `deceasedBoolean` or `deceasedDateTime`), `Patient.link` (once, at `link`, however many entries), `Immunization.isSubpotent`, and `use` on a `Practitioner`'s `identifier` entries. Every one of them lowers [safeToSummarize](#safetosummarize) **on presence, at any value**: a bounded quantity summarized as a point value is a wrong clinical number delivered under a clean verdict, a deceased patient or a record replaced by another is not the live record a summary would present, and a subpotent dose does not protect the way the record would otherwise say. `deceasedBoolean: false` and `isSubpotent: false` report too, as `active: true` does, because deciding from the value would be interpreting the modifier; an unreadable value (a `null`, a wrong JSON type, the `_` form alone, an array wrapper, a name written twice, a `deceased` member R4 does not define) is reported the same way and never read. `MedicationRequest.intent` is not here: it is mandatory, so it is surfaced on [intents](#intents) instead. **Distinct from [unhandledModifierExtensions](#unhandledmodifierextensions-1), and the two never double-report.** A modifier EXTENSION stays on that channel and draws nothing here, so one modifier extension is one report. `implicitRules` is a modifier element and reports here rather than there. **Reporting only.** The element is surfaced and never interpreted: no bound, no range, no inequality is read out of a `comparator`, and no value, code or URI is carried. See ../safety/modifier-elements.js for the recognition predicate, for what a location may carry, and for the resource type names that may root one. Empty for a document carrying none of them, where this readout returns exactly what it always did. ##### nearMissNegationCodes > `readonly` **nearMissNegationCodes**: readonly `string`[] FHIRPath locations of a `code`-valued negation element holding a value that differs from a code this layer classifies **only by letter case, only by surrounding whitespace, or by both**, where the exact-string match therefore declined it and **that** negation was not classified. **Nothing is dropped at parse time.** Unlike the channels above it, this one does not mark content the codec could not keep: the value is in the model, at the element this location names, and a caller walking the model finds it there. What did not happen is the *classification*. FHIR `code` is case-sensitive and its lexical space excludes surrounding whitespace (`[^\s]+(\s[^\s]+)*`, datatypes.html), so `"NOT-DONE"` and `" not-done"` are not the code `not-done` and this library will not read them as one. **The value is never coerced, trimmed or case-folded into the negation**: that would accept a non-conformant document as though it were conformant and author a negation the sender did not spell. What this channel fixes is that the refusal used to be *silent*: a caller branching on [negations](#negations), which is what this readout tells it to do, saw an empty list over a document that plainly spelled a negation. **This is NOT a promise that the value appears elsewhere on this readout, and the difference bites in two ordinary shapes.** `status` / `verificationStatus` are **root-scoped** and single-valued, and this channel is document-wide, so a near miss inside `contained` or a `Bundle.entry` leaves them holding the *root's* value or `undefined`; and `verificationStatus` surfaces the preferred-system coding, so a near miss in a second coding is not the code it shows. Walk the model at the location; do not read the value off a convenience field. **A near miss is suppressed where the same element also spells that code exactly**, because the negation is then classified and a caller has it. R4 permits translation codings beside the one from a required binding's value set (terminologies.html), so a `verificationStatus` carrying `refuted` from the standard system and `REFUTED` from a local one is **conformant** and draws nothing here. The suppression is per code, so a near miss of a *different* code at that element still reports. The elements are the `code`-valued ones the negation read looks at, `status` and `verificationStatus`, at **every resource root**, which is [negations](#negations)' window. [arrayWrappedScalars](#arraywrappedscalars-1) reaches every root too, but only for the `Coding` members of the elements that same table marks `codings` (`verificationStatus`, not `status`), and its element-level half is scoped to the resource types whose cardinality this layer knows, so the two windows are **not** the same window. `AllergyIntolerance.code` is **not** among them: SNOMED `716186003` is a *positive* assertion whose read is root- and type-scoped (see [noKnownAllergy](#noknownallergy)), and disclosing a near miss at every root would report the miss where an exact hit is read by nothing. **Value-free, like every location on this readout**: the text that failed to match is not carried here or anywhere else, and neither is the code it resembles. **Empty for every conformant document read from JSON, with one shape admitted rather than claimed away**: a `CodeableConcept` may carry translation codings beside the one a required binding's value set supplies, and R4 asks only that *one* coding come from that set (terminologies.html) while `datatypes.html` asks that each coding represent the same concept **without a SHALL**. Under that permissive reading a document whose translation coding differs from a negation code **only by case** is conformant, and this channel discloses it. (Only the case half can be: a surrounding-whitespace value is outside `code`'s lexical space whatever coding carries it.) Over-disclosure is the fail-safe direction, and the slice's own tests pin the case. **In XML the whitespace half is a further declared limit rather than a claim**: R4 derives `code` from `xs:token` (fhir-base.xsd), whose `whiteSpace=collapse` facet strips surrounding whitespace *before* validation, so `` is schema-valid and a schema-validating consumer reads it as the code. This reader is schema-free and does not collapse, so it discloses rather than reads. That is the fail-safe direction, and it is stated here rather than claimed away. ##### negations > `readonly` **negations**: readonly [`NegationKind`](#negationkind)[] Every negation asserted **anywhere in the document** (from all codings, any system), the authoritative safety read: at the resource handed in and at every resource root inside it, so a `contained` or `Bundle.entry` resource's retraction, refutation, `not-done` / `not-taken` status or "do not perform" instruction is here. Value-free and **unlocated**, so a kind appears once however many resources assert it, in a fixed order that does not depend on entry order. `no-known-allergy` is the one exception and is root-scoped; see [noKnownAllergy](#noknownallergy). ##### nestedArrays > `readonly` **nestedArrays**: readonly `string`[] FHIRPath locations where the document wrote an array inside an array, a shape FHIR JSON gives no meaning at any position. Unlike every other location here the content at these is **not readable**: the codec does not model an inner array, so this is the record that something was written where the model now shows an empty element. Whole resources have been lost this way inside a Bundle entry. Empty on every conformant document. ##### noKnownAllergy > `readonly` **noKnownAllergy**: `boolean` Whether **the resource handed to [readSafety](#readsafety)** is a recorded "no known allergy" (SNOMED `716186003` on an `AllergyIntolerance.code`), not an allergy *to* that code. **Root-scoped and type-scoped, and alone among the negations in being both.** Every other negation is read off an element R4 flags `?!`, where a consumer may not process the element as if it were absent and surfacing one can only make a caller more careful. This one is a *positive* clinical assertion read off an element R4 does not flag at all, and surfacing it from somewhere inside a document could make a caller **less** careful about a patient, while leaving it unsurfaced reads as *unknown*. So a nested `AllergyIntolerance` recording it reaches neither this field nor [negations](#negations): a declared gap in the fail-safe direction, not an oversight. ##### resourceType > `readonly` **resourceType**: `string` \| `undefined` The first `resourceType` the document names, read through a repeated name or an array wrapper, **bounded** to the published form of a resource type name, or `undefined` if the resource names none. Every R4 type is returned exactly as written; anything that is not shaped like a type name reads as the [WITHHELD](#withheld) marker, `""`, because this is the one identifier on this readout that a caller would interpolate to describe a location, and a location is not a place to echo whatever a sender wrote. Convenience only: the type-scoped negation reads consider **every** type named, not just this one, and they read it unbounded. ##### retracted > `readonly` **retracted**: `boolean` Whether **the resource handed to [readSafety](#readsafety)** is marked `entered-in-error` (retracted, not data), read off its own `status` or `verificationStatus`. **Root-scoped, like `status` beside it.** A retracted resource in `contained` or a `Bundle.entry` leaves this `false` (a Bundle is not retracted because one of its entries is) and still puts `entered-in-error` on [negations](#negations), which is the read that covers the whole document. So `retracted` implies `entered-in-error` is on `negations`, never the other way round. ##### safeToSummarize > `readonly` **safeToSummarize**: `boolean` `false` when the resource must not be flattened: an unhandled `modifierExtension` is present, a modifier ELEMENT is present, a repeated property name left an element with more than one value, a `0..1` safety element arrived array-wrapped, an array inside an array left content the codec could not read, XML character data on an element was dropped, a boolean-valued safety element carries a written value this layer cannot read, a `code`-valued negation element carries a value that spells a negation code bar its case or its surrounding whitespace, such an element holds content at a position no `code` read can reach, a `MedicationRequest.intent` is not one of the eight R4 codes, a `use` on an Identifier, a HumanName, an Address or a ContactPoint is not a code of its datatype's R4 value set, an element declares an absence in a reason this library cannot read, or an element declares an absence and carries a value. Each is a case where a summary would have to assert something this library cannot establish (for the negation pair, that no negation was asserted; for the absence pair, which of two contradictory answers the element holds), so it declines instead. **A readable, non-conflicting absence marker does NOT move this**, and that exception is the point of the channel rather than a hole in the rule: the declaration is read, carried and addressable, so nothing about the document is unestablished. See [SafetyReadout.absenceMarkers](#absencemarkers). **Nor does a readable `intent`**, which is surfaced on [SafetyReadout.intents](#intents) for the caller to read, **nor a readable `use`** on the four datatypes, which is surfaced on [SafetyReadout.datatypeUses](#datatypeuses): `old` and `temp` included, because refusing on WHICH code was written would be interpreting the modifier. ##### shadowedProperties > `readonly` **shadowedProperties**: readonly `string`[] FHIRPath locations where the document wrote a property name more than once, so the element has several values and no rule says which one the sender meant (fail-closed). Empty on every conformant document, since FHIR JSON requires unique property names. ##### status > `readonly` **status**: `string` \| `undefined` The `status` code (Observation / Immunization / DiagnosticReport / MedicationRequest·Statement). ##### unhandledModifierExtensions > `readonly` **unhandledModifierExtensions**: readonly `string`[] FHIRPath locations of `modifierExtension`s this library does not understand (fail-closed). ##### unreadableAbsenceMarkers > `readonly` **unreadableAbsenceMarkers**: readonly `string`[] FHIRPath locations of elements carrying an absence marker whose reason this library could not read: no `valueCode`, a `valueCode` holding no readable string, an empty one, one written twice, or a code outside the closed fifteen-concept value set the extension's required binding names. **Nothing is read as `unknown` and nothing is read as populated.** Coercing an unreadable code into the value set's most common member would author a reason the sender did not spell, and treating the element as populated would erase the declaration entirely. The element stays value-absent, the marker stays unread, and this location is the record that a declaration was made and could not be honoured. It is the same disposition [SafetyReadout.nearMissNegationCodes](#nearmissnegationcodes) takes for the same reason. **Value-free**: neither the code that failed to match nor anything else from the element is carried, only the location. Empty for every conformant document, in either wire format: the extension's `value[x]` binds to that value set at required strength, so a member of it is what a conformant document writes. ##### unreadableBooleans > `readonly` **unreadableBooleans**: readonly `string`[] FHIRPath locations where a `boolean`-valued safety element carries a **written value outside the R4 `boolean` lexical space** (`true` / `false`, datatypes.html), so the element is present, the sender filled it in, and the read still returns `undefined`. `` and `value="Y"` are ordinary v2 / C-CDA converter output and land here; without the location they read exactly like `value="0"`, and a "do not administer" is indistinguishable from its opposite. Its content is **not readable**, as [nestedArrays](#nestedarrays-1)' and [droppedText](#droppedtext-3)' is, and like them it carries locations and no values. **How many channels share that property is deliberately not written down**: that census has gone stale here before. The element read is `doNotPerform`, at any resource root, on any type; it is the only `boolean` this readout takes off a document at all, since `retracted` and `noKnownAllergy` come from codes and codings. Empty on every conformant document. ##### unreadableDatatypeUses > `readonly` **unreadableDatatypeUses**: readonly `string`[] FHIRPath locations of a `use` on an Identifier, a HumanName, an Address or a ContactPoint, at a covered position ([datatypeUses](#datatypeuses)), that this library could not read as a code of that position's own R4 value set: a string outside it (a case or surrounding-whitespace variant of a code, the empty string, or a code from a sibling set, such as `maiden` on an Identifier), a JSON `null`, a value of another JSON type, the `_use` form with no value, an array wrapper, or the name written twice. Each `use` is a modifier bound to its value set at required strength, so a value outside it says something this library cannot establish, and **nothing is case-folded, trimmed or mapped to a nearby code**: the position surfaces no code on [datatypeUses](#datatypeuses), its location is here, and the resource is not [safeToSummarize](#safetosummarize). Value-free: the text that failed is carried nowhere. Located by the same bound and the same root rule as [modifierElements](#modifierelements-1). Empty for every conformant document. ##### unreadableIntents > `readonly` **unreadableIntents**: readonly `string`[] FHIRPath locations of a `MedicationRequest.intent` this library could not read as one of the eight R4 codes, at any `MedicationRequest` root: a string outside them (including a case or surrounding-whitespace variant of one, and the empty string), a JSON `null`, a value of another JSON type, the `_` form with no value, an array wrapper, or the name written twice. `intent` is a modifier bound to its value set at required strength, so a value outside it says something this library cannot establish, and **nothing is case-folded, trimmed or mapped to a nearby code**: the root surfaces no code on [intents](#intents), its location is here, and the resource is not [safeToSummarize](#safetosummarize). Value-free: the text that failed is carried nowhere. Located by the same bound and the same root rule as [modifierElements](#modifierelements-1). Empty for every conformant document. ##### unreadableNegationCodes > `readonly` **unreadableNegationCodes**: readonly `string`[] FHIRPath locations of a `code`-valued negation element holding **content at a position no datatype FHIR spells there can hold**: an object carrying **any member outside** `{coding, text, id, extension}` (`{"status":{"value":"not-done"}}` and `{"status":{"id":"s1","value":"not-done"}}`, the members a generic converter makes of FHIR XML's `value` attribute and the primitive's own metadata beside it), an object carrying **no member at all** (`ele-1` forbids an element with no value, children or extension), or a written value that is not a string at all. **`ele-1` grounds that one arm; it is not a rule this channel enforces**, and `{"status":{"id":"s1"}}` / `{"status":{"coding":[]}}` violate it too and are deliberately not reported, their members being ones FHIR spells here. The element is present, and the negation read still returned nothing, so `{"resourceType":"Procedure","status":{"value":"not-done"}}` read `negations: []` under `safeToSummarize: true`, indistinguishable from a procedure that was carried out. **The shape complement of [nearMissNegationCodes](#nearmissnegationcodes-1), and the two do not overlap.** That one covers a *value* the exact match declined; this one covers a *position* the read could take no value from at all, which is the case a value-shaped question cannot see. It is the same distinction that keeps an object at `doNotPerform` off [unreadableBooleans](#unreadablebooleans-1), which asks only about written values. **Nothing reads through this location.** `{"value":"not-done"}` is the *XML* spelling of a primitive; FHIR JSON spells a `code` as a JSON string (json.html §2.6.0), so descending to find the code would resolve a negation out of an encoding no version of FHIR defines for JSON. The value is in the model at the location named here and a caller walking it finds it. **What this fixes is the silence, not the strictness**, the same disposition [nearMissNegationCodes](#nearmissnegationcodes-1) takes. **Two datatypes reach a root `status`, and this clears both.** R4 spells it a `code` on the overwhelming majority of types and a **`CodeableConcept`** on `MedicinalProductAuthorization` and `SubstanceSpecification`; R5 adds several more, including a mandatory `DeviceAssociation.status`; DSTU2 spells every one a `code`. So the question is about the **shape**, not about which read succeeded: a complex **all of whose members** are ones FHIR spells here (`coding`, `text`, `id`, `extension`) is left alone, whether or not a code came out of it, while **any** member outside that set is reported, as is an object with **no** member at all (`ele-1`: an element present in a resource SHALL carry a value, children defined for its type, or an extension). The polarity is load-bearing: exempting a shape for carrying *one* legal member would read `{"status":{"id":"s1","value":"not-done"}}` as clean, and that is the same converter output. Keyed on "no string was read" instead, this would refuse the published R4 `MedicinalProductAuthorization` example, which was measured rather than feared. **The element is `status`, at every resource root**, which is [negations](#negations)' window: the entry node plus every node carrying its own `resourceType`, so a `Bundle.entry` or `contained` resource is covered. **`verificationStatus` is deliberately absent and it is a declared limit**: its shape complement is a *primitive* at the element, and `Condition.verificationStatus` **is** a `code` in DSTU2, a version this reader ingests tolerantly, so the same predicate would report a conformant DSTU2 document. Both directions are pinned rather than described. **Value-free, like every location on this readout**: neither the content at the position nor anything read out of it is carried here. **Empty for every conformant document this library has been measured against, in either wire format**, and the limit is declared rather than claimed away: a version spelling a root `status` as a datatype whose members are none of the above would be reported, and the census found none in R4, R5 or DSTU2. The XML reader models a `value` attribute beside `id` / `extension` children as a primitive, so a conformant `` is read. A primitive whose value is *absent* is not reported either: that is the conformant `data-absent-reason` shape (json.html §2.6.2.3), and it is content the read never stepped over. **The converse limit:** a shape **all of whose members** are ones FHIR spells here is never reported, so a code buried under `{"status":{"coding":{…}}}` at a type whose `status` is a `code` stays silent. One member outside the set is enough to report, so this covers only a shape that is wholly a `CodeableConcept`. ##### verificationStatus > `readonly` **verificationStatus**: `string` \| `undefined` The `verificationStatus` code, preferred-system-first (AllergyIntolerance / Condition). Convenience only. *** ### SliceConstraint One `fixed[x]` / `pattern[x]` constraint a slice imposes, at a path relative to the slice element. #### Properties ##### kind > `readonly` **kind**: `"pattern"` \| `"fixed"` Whether the constraint is `fixed` (exact) or `pattern` (subset). ##### path > `readonly` **path**: `string` The path relative to the sliced element (`"$this"` for the slice element itself). ##### value > `readonly` **value**: [`FhirNode`](#fhirnode) The constraint value node. *** ### SliceDefinition A resolved slice: its name, cardinality, value constraints, and existence expectations. #### Properties ##### constraints > `readonly` **constraints**: readonly [`SliceConstraint`](#sliceconstraint)[] The `fixed`/`pattern` constraints the slice imposes, at paths relative to the sliced element. ##### existsExpectations > `readonly` **existsExpectations**: `ReadonlyMap`\<`string`, `boolean`\> Relative paths whose presence/absence the slice fixes (min ≥ 1 → present; max 0 → absent). ##### max? > `readonly` `optional` **max?**: `number` The slice's maximum cardinality, when stated. ##### min? > `readonly` `optional` **min?**: `number` The slice's minimum cardinality, when stated. ##### sliceName > `readonly` **sliceName**: `string` The slice name (e.g. `"VSCat"`). ##### unsatisfiableExists > `readonly` **unsatisfiableExists**: `ReadonlySet`\<`string`\> Relative paths the slice fixes as present **and** absent at once (`min ≥ 1` beside `max 0`). No instance can meet such an expectation, so an `exists` discriminator on one of these paths assigns no occurrence to this slice. Kept apart from [existsExpectations](#existsexpectations) rather than resolved into a boolean there: neither boolean is true of a contradiction, and picking one admits occurrences the profile forbids. *** ### SliceMatchResult The outcome of matching a sliced element's instance occurrences to its slices. #### Properties ##### assignments > `readonly` **assignments**: readonly (`string` \| `undefined`)[] Per instance occurrence (in order), the matched slice name, or `undefined` when none matched. ##### unchecked > `readonly` **unchecked**: `boolean` `true` when membership could not be evaluated (an unsupported/insufficient discriminator). *** ### Slicing The slicing declaration on an element that introduces slices. #### Properties ##### discriminator > `readonly` **discriminator**: readonly [`Discriminator`](#discriminator)[] The discriminators that distinguish the slices (empty is legal but leaves slices unresolvable). ##### ordered? > `readonly` `optional` **ordered?**: `boolean` Whether slice order is significant (surfaced but not enforced here). ##### rules > `readonly` **rules**: [`SlicingRules`](#slicingrules-1) Whether content outside the defined slices is allowed. Absent defaults to `open` (the R4 default). *** ### StructureDefinition The modeled slice of a FHIR `StructureDefinition`. #### Properties ##### baseDefinition? > `readonly` `optional` **baseDefinition?**: `string` The canonical URL of the definition this one derives from. ##### derivation? > `readonly` `optional` **derivation?**: [`Derivation`](#derivation-2) Specialization (a base resource) or constraint (a profile). ##### differential? > `readonly` `optional` **differential?**: readonly [`ElementDefinition`](#elementdefinition)[] The differential element list (constraints relative to the base). ##### kind? > `readonly` `optional` **kind?**: `string` `resource` | `complex-type` | `primitive-type` | `logical`. ##### name? > `readonly` `optional` **name?**: `string` The computer-friendly name, when stated. ##### snapshot? > `readonly` `optional` **snapshot?**: readonly [`ElementDefinition`](#elementdefinition)[] The fully-resolved snapshot element list, when the definition carries one. ##### type > `readonly` **type**: `string` The resource type this definition constrains (`StructureDefinition.type`, e.g. `"AllergyIntolerance"`). ##### url > `readonly` **url**: `string` The canonical URL, the identity a profile is referenced by (`meta.profile`, `baseDefinition`). ##### version? > `readonly` `optional` **version?**: `string` The business version, when stated (e.g. US Core `"6.1.0"`). Part of the `canonical|version` key. *** ### TerminologyBinding A binding from an element path to a value set, at a given strength. #### Properties ##### path > `readonly` **path**: `string` The element's FHIRPath from the resource root, e.g. `"AllergyIntolerance.code"` or `"MedicationRequest.medicationCodeableConcept"` (the concrete `medication[x]` choice variant). ##### strength > `readonly` **strength**: [`BindingStrength`](#bindingstrength) The binding strength, governs the severity of a non-conforming code. ##### systems? > `readonly` `optional` **systems?**: readonly `string`[] The closed set of code `system`s the value set draws from, when it is known. Present enables the content-free "wrong system for this binding" check; absent, only a terminology service can judge conformance. For an **extensible** binding a code from another system may be a legitimate extension, so a system outside this set is a `warning`, never an `error` (see the layer). ##### valueSet > `readonly` **valueSet**: `string` The bound value set's canonical identity (URL / OID form), passed to a terminology service. *** ### TerminologyOptions Terminology inputs to [collectTerminologyIssues](#collectterminologyissues), both optional (both degrade cleanly). #### Properties ##### bindings? > `readonly` `optional` **bindings?**: readonly [`TerminologyBinding`](#terminologybinding)[] Extra element bindings, overriding the built-ins by path (profiles feed these). ##### terminology? > `readonly` `optional` **terminology?**: [`TerminologyService`](#terminologyservice) A pluggable terminology service for value-set membership. **None is bundled**; with none supplied, membership checks are skipped and the layer degrades to the content-free system checks. *** ### TerminologyService A pluggable terminology service, the one seam through which value-set **content** enters the library. A consumer implements this over a real terminology server; the library bundles none. An implementation MUST be **fail-safe**: when it cannot answer, it returns `{ membership: "unknown" }` rather than throwing or guessing. It MUST be value-free, it receives only identities ([CodeValidationRequest](#codevalidationrequest)), never a resource or a patient value. It MAY declare the code-system release an answer was made against ([CodeValidationResult.systemVersion](#systemversion)); declaring nothing is conformant and is recorded as undeclared rather than read as "current". #### Example ```ts import type { TerminologyService } from "@cosyte/fhir"; // A trivial service that only knows one value set; everything else is "unknown". const svc: TerminologyService = { validateCode({ valueSet, code }) { if (valueSet !== "http://example.org/vs/colors") return { membership: "unknown" }; return { membership: ["red", "green", "blue"].includes(code) ? "in" : "not-in" }; }, }; // The same service, declaring the release each answer was made against. const dated: TerminologyService = { validateCode({ valueSet, code }) { if (valueSet !== "http://example.org/vs/colors") return { membership: "unknown" }; const membership = ["red", "green", "blue"].includes(code) ? "in" : "not-in"; return { membership, systemVersion: "2026-08-04" }; }, }; ``` #### Methods ##### validateCode() > **validateCode**(`request`): [`CodeValidationResult`](#codevalidationresult) Decide whether a coding is a member of a value set. ###### Parameters ###### request [`CodeValidationRequest`](#codevalidationrequest) The value-set identity and the `(system, code)` to check. ###### Returns [`CodeValidationResult`](#codevalidationresult) The membership verdict, `"unknown"` when it cannot decide. *** ### Token One lexical token: its [TokenType](#tokentype-1), its text/value, and its start offset (for diagnostics). #### Properties ##### pos > `readonly` **pos**: `number` ##### type > `readonly` **type**: [`TokenType`](#tokentype-1) ##### value > `readonly` **value**: `string` *** ### TypedValue A value bound to a `fixed[x]` or `pattern[x]` constraint: the FHIR type name plus the value node. #### Properties ##### type > `readonly` **type**: `string` The FHIR datatype suffix, e.g. `"Code"`, `"CodeableConcept"`, `"String"` (as it appears on the property). ##### value > `readonly` **value**: [`FhirNode`](#fhirnode) The constraint value, as a model node. *** ### ValidateOptions Options for [validateResource](#validateresource). #### Properties ##### bindings? > `readonly` `optional` **bindings?**: readonly [`TerminologyBinding`](#terminologybinding)[] Extra terminology bindings, overriding the built-ins by element path. ##### mode? > `readonly` `optional` **mode?**: [`ValidationMode`](#validationmode) Lenient (read, the default) or strict (emit). Only affects the severity of unknown elements. ##### profiles? > `readonly` `optional` **profiles?**: readonly [`StructureDefinition`](#structuredefinition)[] Profiles (`StructureDefinition`s) to validate against. **None is bundled**, a caller supplies the US Core (or vendor) profiles. Every supplied profile whose `type` matches the resource type is applied (fixed/pattern, must-support, slicing, profile cardinality), and the resource's `meta.profile` version pins are checked against the supplied set. ##### resolveBase? > `readonly` `optional` **resolveBase?**: [`BaseResolver`](#baseresolver) A resolver from a `baseDefinition` canonical URL to a loaded `StructureDefinition`, used only to generate a snapshot for a supplied profile that carries a differential but no snapshot. ##### schemas? > `readonly` `optional` **schemas?**: readonly [`ResourceSchema`](#resourceschema)[] Extra resource schemas, overriding the built-ins by type (profiles feed these). ##### terminology? > `readonly` `optional` **terminology?**: [`TerminologyService`](#terminologyservice) A pluggable terminology service for value-set membership. **None is bundled**; without one, terminology binding checks degrade to the content-free system checks and never false-error. *** ### ValidationIssue A single value-free validation finding. `expression` is a FHIRPath location into the document (e.g. `Patient.gender`, `Observation.component[1].valueQuantity.value`), it says *where* without echoing *what*. An issue never contains a resource value, so it is safe to log or return in an `OperationOutcome`. **It is a location, and on non-conformant input it can be a location with a gap.** The segments are the document's own names, and a name is echoed only when it matches the published form of a FHIR name; anything else reads as the ../model/path.js `WITHHELD` marker, `""`. A marker is not a FHIRPath identifier, so such an expression will not resolve against the instance: R4 defines `OperationOutcome.issue.expression` as a FHIRPath subset that SHALL resolve to a single node, and a location with a withheld segment does not. Every segment around the marker is intact, so the nearest addressable ancestor is still there. Test for the marker before handing an expression to a FHIRPath engine. #### Properties ##### code > `readonly` **code**: [`ValidationCode`](#validationcode) ##### codeSystemVersion? > `readonly` `optional` **codeSystemVersion?**: [`CodeSystemVersionRecord`](#codesystemversionrecord) Which code-system release the terminology service's answer was made against, when a service was consulted to produce this finding (today, `CODE_NOT_IN_VALUESET` and only it). `undefined` for every finding no service produced, including the content-free `CODE_SYSTEM_UNKNOWN` / `CODE_SYSTEM_UNEXPECTED` checks, which consult nothing. A declared release is the **caller's own assertion**, a public code-system release identifier on the same footing as the constraint key an invariant finding surfaces, never an instance value, so it is safe to surface. It reaches the `OperationOutcome` as `issue.details` (./operation-outcome.js), and the `diagnostics` string stays derived from the finding code alone. ##### constraint? > `readonly` `optional` **constraint?**: `string` The spec constraint key when the finding is an invariant violation (e.g. `"ait-1"`, `"obs-6"`), a public FHIR identifier, never an instance value, so it is safe to surface. `undefined` for every non-invariant finding. It reaches the `OperationOutcome` as `issue.details.text`. ##### expression > `readonly` **expression**: `string` FHIRPath location of the finding. ##### severity > `readonly` **severity**: [`ValidationSeverity`](#validationseverity) ##### type > `readonly` **type**: [`IssueType`](#issuetype) The R4 `OperationOutcome.issue.code` this finding maps to. *** ### ValidationResult The result of validating a resource: the findings plus an `OperationOutcome` view of them. #### Properties ##### issues > `readonly` **issues**: readonly [`ValidationIssue`](#validationissue)[] The value-free findings, in document order. Empty when the resource validated clean. ##### toOperationOutcome > **toOperationOutcome**: () => [`FhirComplex`](#fhircomplex) Render the findings as an `OperationOutcome` resource model (value-free, serializable). ###### Returns [`FhirComplex`](#fhircomplex) ##### valid > `readonly` **valid**: `boolean` Whether there were no `error`/`fatal` findings (warnings and information do not fail). *** ### XmlAttribute A name/value attribute on an [XmlElement](#xmlelement). Values are already entity-decoded. #### Properties ##### name > `readonly` **name**: `string` ##### value > `readonly` **value**: `string` *** ### XmlElement An XML element node: a tag name, its attributes (source order), and its child nodes (source order). #### Properties ##### attributes > `readonly` **attributes**: readonly [`XmlAttribute`](#xmlattribute)[] ##### children > `readonly` **children**: readonly [`XmlNode`](#xmlnode)[] ##### name > `readonly` **name**: `string` ##### type > `readonly` **type**: `"element"` *** ### XmlText An XML character-data node, already entity-decoded to its logical text. #### Properties ##### type > `readonly` **type**: `"text"` ##### value > `readonly` **value**: `string` ## Type Aliases ### AbsenceCode > **AbsenceCode** = *typeof* [`ABSENCE_CODES`](#absence_codes)\[`number`\] One of the fifteen [ABSENCE\_CODES](#absence_codes). A value of this type is always one of the literal strings this package spells, never a string taken off a document, which is what makes an [AbsenceMarker](#absencemarker) safe to log. *** ### BaseResolver > **BaseResolver** = (`canonicalUrl`) => [`StructureDefinition`](#structuredefinition) \| `undefined` A resolver from a canonical URL to a loaded [StructureDefinition](#structuredefinition) (for `baseDefinition`). #### Parameters ##### canonicalUrl `string` #### Returns [`StructureDefinition`](#structuredefinition) \| `undefined` *** ### BindingRegistry > **BindingRegistry** = (`path`) => [`TerminologyBinding`](#terminologybinding) \| `undefined` A resolver from an element path to its [TerminologyBinding](#terminologybinding), or `undefined`. #### Parameters ##### path `string` #### Returns [`TerminologyBinding`](#terminologybinding) \| `undefined` *** ### BindingStrength > **BindingStrength** = `"required"` \| `"extensible"` \| `"preferred"` \| `"example"` The four FHIR binding strengths (terminologies.html), strongest to weakest. *** ### BundleType > **BundleType** = *typeof* [`BUNDLE_TYPES`](#bundle_types)\[keyof *typeof* [`BUNDLE_TYPES`](#bundle_types)\] One of the [BUNDLE\_TYPES](#bundle_types), the R4 `Bundle.type`. *** ### CodeMembership > **CodeMembership** = `"in"` \| `"not-in"` \| `"unknown"` A membership verdict. `"unknown"` is a first-class answer, not a failure, a conformant service returns it whenever it cannot decide, and the validator degrades cleanly rather than guessing. *** ### CodeSystemVersionRecord > **CodeSystemVersionRecord** = \{ `declared`: `true`; `version`: `string`; \} \| \{ `declared`: `false`; \} Which code-system release a membership answer was made against, as recorded on the finding. Three states, all distinguishable, which is the whole point of the type: - the field is **absent** from a [ValidationIssue](#validationissue): no terminology service was consulted to produce that finding, so there is no release to record (every content-free system check, every non-terminology finding); - `{ declared: true, version }`: the service named the release, and `version` is its string **exactly** as declared, never normalised, trimmed, truncated or substituted; - `{ declared: false }`: the service answered but named no release. The answer stands; its currency is **unknown and said so**, rather than left to a reader to fill in with "current". The release is always the caller's own assertion. It is never read from the instance: a resource's own `Coding.version` is document content and never reaches a finding. *** ### CodeSystemVersionRecordCode > **CodeSystemVersionRecordCode** = *typeof* [`CODE_SYSTEM_VERSION_RECORD_CODES`](#code_system_version_record_codes)\[keyof *typeof* [`CODE_SYSTEM_VERSION_RECORD_CODES`](#code_system_version_record_codes)\] One of the [CODE\_SYSTEM\_VERSION\_RECORD\_CODES](#code_system_version_record_codes). *** ### DatatypeUseCode > **DatatypeUseCode** = `"usual"` \| `"official"` \| `"temp"` \| `"secondary"` \| `"old"` \| `"nickname"` \| `"anonymous"` \| `"maiden"` \| `"home"` \| `"work"` \| `"billing"` \| `"mobile"` A `use` code this library surfaces on an Identifier, a HumanName, an Address or a ContactPoint: the union of the four R4 4.0.1 value sets those elements bind to at required strength, and nothing else. Which of them a given position may carry is decided by that position's own datatype (IdentifierUse `usual` `official` `temp` `secondary` `old`; NameUse `usual` `official` `temp` `nickname` `anonymous` `old` `maiden`; AddressUse `home` `work` `temp` `old` `billing`; ContactPointUse `home` `work` `temp` `old` `mobile`). #### Example ```ts import { parseResource, readSafety, type DatatypeUseCode } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","name":[{"use":"old"}]}'); const codes: DatatypeUseCode[] = readSafety(resource).datatypeUses.map((u) => u.code); codes; // ["old"] ``` *** ### Derivation > **Derivation** = `"specialization"` \| `"constraint"` `StructureDefinition.derivation`, how a definition relates to its base. *** ### DiscriminatorType > **DiscriminatorType** = `"value"` \| `"exists"` \| `"pattern"` \| `"type"` \| `"profile"` The R4 discriminator types (`valueset-discriminator-type`) a slicing may use to tell its slices apart. **`position` is R5-only and is deliberately not a member here**: an R4 profile that carries it is treated as an unsupported discriminator, not silently accepted. *** ### EntryProcessing > **EntryProcessing** = `"atomic"` \| `"independent"` \| `"none"` How a server would process a Bundle's entries, the artifact-level semantic contract. - `"atomic"`, a `transaction`: all-or-nothing, entries may be interdependent. - `"independent"`, a `batch`: each entry on its own, no rollback across entries. - `"none"`, every other type: not a processing request, no entry contract. *** ### Expr > **Expr** = \{ `kind`: `"empty"`; \} \| \{ `kind`: `"bool"`; `value`: `boolean`; \} \| \{ `kind`: `"string"`; `value`: `string`; \} \| \{ `kind`: `"number"`; `value`: `number`; \} \| \{ `kind`: `"envvar"`; `name`: `string`; \} \| \{ `kind`: `"variable"`; `name`: `string`; \} \| \{ `kind`: `"member"`; `name`: `string`; `target`: [`Expr`](#expr) \| `null`; \} \| \{ `args`: readonly [`Expr`](#expr)[]; `kind`: `"call"`; `name`: `string`; `target`: [`Expr`](#expr) \| `null`; \} \| \{ `index`: [`Expr`](#expr); `kind`: `"index"`; `target`: [`Expr`](#expr); \} \| \{ `kind`: `"unary"`; `op`: `string`; `operand`: [`Expr`](#expr); \} \| \{ `kind`: `"binary"`; `left`: [`Expr`](#expr); `op`: `string`; `right`: [`Expr`](#expr); \} \| \{ `kind`: `"typeop"`; `op`: `string`; `operand`: [`Expr`](#expr); `type`: `string`; \} A parsed FHIRPath expression node. #### Union Members ##### Type Literal \{ `kind`: `"empty"`; \} *** ##### Type Literal \{ `kind`: `"bool"`; `value`: `boolean`; \} *** ##### Type Literal \{ `kind`: `"string"`; `value`: `string`; \} *** ##### Type Literal \{ `kind`: `"number"`; `value`: `number`; \} *** ##### Type Literal \{ `kind`: `"envvar"`; `name`: `string`; \} *** ##### Type Literal \{ `kind`: `"variable"`; `name`: `string`; \} *** ##### Type Literal \{ `kind`: `"member"`; `name`: `string`; `target`: [`Expr`](#expr) \| `null`; \} Member access: `name` navigates from `target` (or the current focus when `target` is null). *** ##### Type Literal \{ `args`: readonly [`Expr`](#expr)[]; `kind`: `"call"`; `name`: `string`; `target`: [`Expr`](#expr) \| `null`; \} Function call: `name(args)` invoked on `target` (or the current focus when `target` is null). *** ##### Type Literal \{ `index`: [`Expr`](#expr); `kind`: `"index"`; `target`: [`Expr`](#expr); \} Indexer: `target[index]`. *** ##### Type Literal \{ `kind`: `"unary"`; `op`: `string`; `operand`: [`Expr`](#expr); \} *** ##### Type Literal \{ `kind`: `"binary"`; `left`: [`Expr`](#expr); `op`: `string`; `right`: [`Expr`](#expr); \} *** ##### Type Literal \{ `kind`: `"typeop"`; `op`: `string`; `operand`: [`Expr`](#expr); `type`: `string`; \} Type operator: `operand is Type` / `operand as Type` (the type is a possibly-qualified name). *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] Discriminant union of every [FATAL\_CODES](#fatal_codes) value. *** ### FhirNode > **FhirNode** = [`FhirComplex`](#fhircomplex) \| [`FhirList`](#fhirlist) \| [`FhirPrimitive`](#fhirprimitive) Any node in the model tree. *** ### FpColl > **FpColl** = readonly [`FpItem`](#fpitem)[] A FHIRPath collection, the value every expression evaluates to. *** ### FpItem > **FpItem** = \{ `node`: [`FhirNode`](#fhirnode); `t`: `"node"`; \} \| \{ `t`: `"bool"`; `value`: `boolean`; \} \| \{ `t`: `"str"`; `value`: `string`; \} \| \{ `t`: `"num"`; `value`: `number`; \} One item in a FHIRPath collection: a model node, or an engine-computed primitive. *** ### IssueCode > **IssueCode** = *typeof* [`ISSUE_CODES`](#issue_codes)\[keyof *typeof* [`ISSUE_CODES`](#issue_codes)\] Discriminant union of every [ISSUE\_CODES](#issue_codes) value. *** ### IssueSeverity > **IssueSeverity** = `"warning"` \| `"information"` FHIR issue severities carried by a warning (the recoverable subset of the R4 set). *** ### IssueType > **IssueType** = *typeof* [`ISSUE_TYPES`](#issue_types)\[keyof *typeof* [`ISSUE_TYPES`](#issue_types)\] One of the [ISSUE\_TYPES](#issue_types), the R4 `OperationOutcome.issue.code`. *** ### MedicationRequestIntent > **MedicationRequestIntent** = `"proposal"` \| `"plan"` \| `"order"` \| `"original-order"` \| `"reflex-order"` \| `"filler-order"` \| `"instance-order"` \| `"option"` A `MedicationRequest.intent` code this library surfaces: exactly the eight concepts of the R4 4.0.1 value set the element binds to at required strength, and nothing else. #### Example ```ts import { parseResource, readSafety, type MedicationRequestIntent } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"MedicationRequest","intent":"proposal"}'); const codes: MedicationRequestIntent[] = readSafety(resource).intents.map((i) => i.code); codes; // ["proposal"] ``` *** ### ModifierElementName > **ModifierElementName** = `"comparator"` \| `"implicitRules"` \| `"active"` \| `"use"` \| `"deceased"` \| `"link"` \| `"isSubpotent"` The modifier elements this channel reports, by their R4 element names. `deceased` names the R4 choice element `deceased[x]`; the report's location carries the member as the document wrote it. `modifierExtension` is deliberately absent: it keeps its own fail-closed channel, so a modifier extension yields one report and not two. `MedicationRequest.intent` is absent too: it is surfaced as a code on its own field rather than reported on presence ([IntentReport](#intentreport)). #### Example ```ts import { parseResource, readSafety, type ModifierElementName } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","deceasedBoolean":true}'); const names: ModifierElementName[] = readSafety(resource).modifierElements.map((r) => r.element); names; // ["deceased"] ``` *** ### NdjsonErrorCode > **NdjsonErrorCode** = *typeof* [`NDJSON_ERROR_CODES`](#ndjson_error_codes)\[keyof *typeof* [`NDJSON_ERROR_CODES`](#ndjson_error_codes)\] One of the [NDJSON\_ERROR\_CODES](#ndjson_error_codes). *** ### NegationKind > **NegationKind** = `"refuted"` \| `"no-known-allergy"` \| `"do-not-perform"` \| `"not-taken"` \| `"not-done"` \| `"entered-in-error"` A classified negation, an explicit *negative* assertion that must never collapse into its positive on a summary or a round-trip. One value per distinct FHIR negation mechanism this library covers. *** ### NestedArrayChannel > **NestedArrayChannel** = `"value"` \| `"metadata"` Which JSON channel a preserved array came from: the element's own value, or its `_`-sibling. *** ### ObservationValueType > **ObservationValueType** = *typeof* [`OBSERVATION_VALUE_TYPES`](#observation_value_types)\[`number`\] One of the eleven [OBSERVATION\_VALUE\_TYPES](#observation_value_types) `value[x]` variant suffixes. *** ### PrimitiveType > **PrimitiveType** = *typeof* [`PRIMITIVE_TYPES`](#primitive_types)\[`number`\] A FHIR R4 primitive datatype name. *** ### PrimitiveValue > **PrimitiveValue** = `string` \| `boolean` \| [`FhirDecimal`](#fhirdecimal) The scalar value a [FhirPrimitive](#fhirprimitive) can hold. `decimal` is a [FhirDecimal](#fhirdecimal); every other primitive (`string`, `code`, `uri`, `date`, `boolean`, `integer`, …) reduces to one of these three at the structural level. `undefined` means the value is absent but metadata is present (the `_`-sibling-only case, e.g. an extension on a primitive that carries no value). *** ### RawJson > **RawJson** = [`RawObject`](#rawobject) \| [`RawArray`](#rawarray) \| [`RawString`](#rawstring) \| [`RawNumber`](#rawnumber) \| [`RawBool`](#rawbool) \| [`RawNull`](#rawnull) Any node in the raw JSON tree. *** ### ReferenceKind > **ReferenceKind** = `"fragment"` \| `"relative"` \| `"absolute"` \| `"logical"` Which of the four FHIR reference forms a `Reference.reference` string is. *** ### ReferenceResolution > **ReferenceResolution** = \{ `status`: `"resolved"`; `target`: [`FhirComplex`](#fhircomplex); \} \| \{ `status`: `"unresolved"`; \} \| \{ `status`: `"external"`; \} The outcome of resolving a single reference against a closure. #### Union Members ##### Type Literal \{ `status`: `"resolved"`; `target`: [`FhirComplex`](#fhircomplex); \} The reference named a resource in the closure. *** ##### Type Literal \{ `status`: `"unresolved"`; \} A local reference (fragment, or relative within a Bundle) that named nothing in the closure. *** ##### Type Literal \{ `status`: `"external"`; \} A reference to somewhere outside the closure (an absolute/logical target not in the Bundle). *** ### SchemaRegistry > **SchemaRegistry** = (`resourceType`) => [`ResourceSchema`](#resourceschema) \| `undefined` A resolver from a resource type name to its [ResourceSchema](#resourceschema) (base elements merged in), or `undefined` when the type is not modeled. Built by [buildRegistry](#buildregistry). #### Parameters ##### resourceType `string` #### Returns [`ResourceSchema`](#resourceschema) \| `undefined` *** ### SerializeErrorCode > **SerializeErrorCode** = *typeof* [`SERIALIZE_ERROR_CODES`](#serialize_error_codes)\[keyof *typeof* [`SERIALIZE_ERROR_CODES`](#serialize_error_codes)\] Discriminant union of every [SERIALIZE\_ERROR\_CODES](#serialize_error_codes) value. *** ### SlicingRules > **SlicingRules** = `"closed"` \| `"open"` \| `"openAtEnd"` `ElementDefinition.slicing.rules`, whether content outside the named slices is allowed. *** ### TokenType > **TokenType** = `"string"` \| `"number"` \| `"identifier"` \| `"envvar"` \| `"special"` \| `"symbol"` The kind of a lexed [Token](#token). *** ### UcumShapeVerdict > **UcumShapeVerdict** = `"ok"` \| `"invalid"` The verdict of a UCUM shape check: a well-formed UCUM expression, or a malformed one. *** ### ValidationCode > **ValidationCode** = *typeof* [`VALIDATION_CODES`](#validation_codes)\[keyof *typeof* [`VALIDATION_CODES`](#validation_codes)\] Discriminant union of every [VALIDATION\_CODES](#validation_codes) value. *** ### ValidationMode > **ValidationMode** = `"lenient"` \| `"strict"` How strictly to read: `"lenient"` (warn + preserve unknowns) or `"strict"` (unknowns error). *** ### ValidationSeverity > **ValidationSeverity** = *typeof* [`ISSUE_SEVERITIES`](#issue_severities)\[keyof *typeof* [`ISSUE_SEVERITIES`](#issue_severities)\] One of the four R4 [ISSUE\_SEVERITIES](#issue_severities). *** ### XmlFatalCode > **XmlFatalCode** = *typeof* [`XML_FATAL_CODES`](#xml_fatal_codes)\[keyof *typeof* [`XML_FATAL_CODES`](#xml_fatal_codes)\] Discriminant union of every [XML\_FATAL\_CODES](#xml_fatal_codes) value. *** ### XmlNode > **XmlNode** = [`XmlElement`](#xmlelement) \| [`XmlText`](#xmltext) Any node in the raw XML tree. ## Variables ### ABSENCE\_CODES > `const` **ABSENCE\_CODES**: readonly \[`"unknown"`, `"asked-unknown"`, `"temp-unknown"`, `"not-asked"`, `"asked-declined"`, `"masked"`, `"not-applicable"`, `"unsupported"`, `"as-text"`, `"error"`, `"not-a-number"`, `"negative-infinity"`, `"positive-infinity"`, `"not-performed"`, `"not-permitted"`\] The complete DataAbsentReason value set: fifteen concepts, transcribed from the published R4 expansion in the order it lists them, all drawn from one code system. The extension's `value[x]` binds to this value set at **required** strength, so the set is closed: a `valueCode` outside it is a binding violation, not a local extension of the vocabulary. It is enumerated here for the same reason the required `code` bindings the validator enforces are enumerated in its own source: membership in a closed, published, required-strength set is decided from the set itself, and no terminology service, value-set expansion or vendored terminology resource is involved. This is the whole of the terminology content this channel needs. #### Example ```ts import { ABSENCE_CODES } from "@cosyte/fhir"; ABSENCE_CODES.length; // 15 ABSENCE_CODES.includes("masked"); // true ``` *** ### ALLERGY\_CLINICAL\_SYSTEM > `const` **ALLERGY\_CLINICAL\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"` = `"http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"` AllergyIntolerance `clinicalStatus` code system (`allergyintolerance.html`). *** ### ALLERGY\_SUBSTANCE\_VALUESET > `const` **ALLERGY\_SUBSTANCE\_VALUESET**: `"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1186.8"` = `"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1186.8"` US Core AllergyIntolerance substance value set, VSAC `2.16.840.1.113762.1.4.1186.8`, an **extensible** binding drawing from **RxNorm** (drug) **+ SNOMED CT** (food/environmental and the "no known allergy" negation concepts). The multi-system composition means the validator must accept *both* systems on this one element. *(US Core AllergyIntolerance)* *** ### ALLERGY\_VERIFICATION\_SYSTEM > `const` **ALLERGY\_VERIFICATION\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/allergyintolerance-verification"` = `"http://terminology.hl7.org/CodeSystem/allergyintolerance-verification"` AllergyIntolerance `verificationStatus` code system, the system ait-1/ait-2 pin. *** ### BINDING\_STRENGTHS > `const` **BINDING\_STRENGTHS**: readonly [`BindingStrength`](#bindingstrength)[] The set of [BindingStrength](#bindingstrength) values, for validation/iteration. *** ### BUNDLE\_TYPES > `const` **BUNDLE\_TYPES**: `object` The R4 `Bundle.type` value set (`valueset-bundle-type`), in full. The seven headline types plus the two server-reply variants a real feed carries, so an incoming `transaction-response` / `batch-response` classifies rather than falling through. Frozen via `as const`. #### Type Declaration ##### BATCH > `readonly` **BATCH**: `"batch"` = `"batch"` A set of actions applied **independently**, one failing does not roll back the rest. ##### BATCH\_RESPONSE > `readonly` **BATCH\_RESPONSE**: `"batch-response"` = `"batch-response"` The server's reply to a `batch`. ##### COLLECTION > `readonly` **COLLECTION**: `"collection"` = `"collection"` An arbitrary collection with no processing semantics. ##### DOCUMENT > `readonly` **DOCUMENT**: `"document"` = `"document"` A fully-formed clinical document (first entry is a `Composition`). ##### HISTORY > `readonly` **HISTORY**: `"history"` = `"history"` A list of prior versions of one or more resources. ##### MESSAGE > `readonly` **MESSAGE**: `"message"` = `"message"` A message (first entry is a `MessageHeader`). ##### SEARCHSET > `readonly` **SEARCHSET**: `"searchset"` = `"searchset"` The result set of a search. ##### TRANSACTION > `readonly` **TRANSACTION**: `"transaction"` = `"transaction"` A set of actions applied **atomically**, all-or-nothing. ##### TRANSACTION\_RESPONSE > `readonly` **TRANSACTION\_RESPONSE**: `"transaction-response"` = `"transaction-response"` The server's reply to a `transaction`. *** ### CODE\_SYSTEM\_VERSION\_RECORD\_CODES > `const` **CODE\_SYSTEM\_VERSION\_RECORD\_CODES**: `object` The two-concept vocabulary that says **whether** a membership answer declared the code-system release it was made against. Frozen via `as const`; the set is snapshotted (see `test/validation-codes.test.ts`) because it is a public wire identity. The concept is deliberately separate from the release string itself: the marker rides on `issue.details.coding[0].code` and the declared release rides on `issue.details.text`, so no release a service could declare (`"undeclared"` included) can ever be mistaken for the marker. #### Type Declaration ##### DECLARED > `readonly` **DECLARED**: `"declared"` = `"declared"` The service declared a release; the string it declared travels beside this marker. ##### UNDECLARED > `readonly` **UNDECLARED**: `"undeclared"` = `"undeclared"` The service was consulted, answered definitively, and declared **no** release. It is an applicable, unanswered question, **not** "current" and not "not applicable": the marker is emitted precisely so the silence cannot be read as currency. *** ### CODE\_SYSTEM\_VERSION\_RECORD\_SYSTEM > `const` **CODE\_SYSTEM\_VERSION\_RECORD\_SYSTEM**: `"https://cosyte.com/fhir/CodeSystem/code-system-version-record"` = `"https://cosyte.com/fhir/CodeSystem/code-system-version-record"` The canonical identity of the library's own code system for the **code-system release record** carried by a membership finding. It names the two-concept vocabulary in [CODE\_SYSTEM\_VERSION\_RECORD\_CODES](#code_system_version_record_codes) and nothing else, and it reaches an `OperationOutcome` as `issue.details.coding[0].system`. It is **this library's** canonical, not a third-party terminology identity: the known-systems registry (../terminology/systems.js) stays a frozen set of verified external URIs and gains nothing here. Renaming this is a breaking change. *** ### CONDITION\_CATEGORY\_SYSTEM > `const` **CONDITION\_CATEGORY\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/condition-category"` = `"http://terminology.hl7.org/CodeSystem/condition-category"` Condition `category` code system carrying `problem-list-item` (the con-3 trigger). *** ### CONDITION\_CLINICAL\_SYSTEM > `const` **CONDITION\_CLINICAL\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/condition-clinical"` = `"http://terminology.hl7.org/CodeSystem/condition-clinical"` Condition `clinicalStatus` code system (`condition.html`), the system con-4 pins. *** ### CONDITION\_VERIFICATION\_SYSTEM > `const` **CONDITION\_VERIFICATION\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/condition-ver-status"` = `"http://terminology.hl7.org/CodeSystem/condition-ver-status"` Condition `verificationStatus` code system, the system con-3/con-5 pin. *** ### CPT\_SYSTEM > `const` **CPT\_SYSTEM**: `"http://www.ama-assn.org/go/cpt"` = `"http://www.ama-assn.org/go/cpt"` CPT `system` URI, AMA, procedures/billing. **License-restricted**: identity only, no content. *** ### CVX\_SYSTEM > `const` **CVX\_SYSTEM**: `"http://hl7.org/fhir/sid/cvx"` = `"http://hl7.org/fhir/sid/cvx"` CVX `system` URI, CDC NCIRD, vaccines. *** ### DATA\_ABSENT\_REASON\_URL > `const` **DATA\_ABSENT\_REASON\_URL**: `"http://hl7.org/fhir/StructureDefinition/data-absent-reason"` = `"http://hl7.org/fhir/StructureDefinition/data-absent-reason"` The canonical URL of the R4 DataAbsentReason extension, fixed by its own definition as `Extension.url`. An extension is an absence marker when its `url` is this string and never when it merely resembles it: the code system URI that names the same concepts is a **different** URI and is not matched. #### Example ```ts import { DATA_ABSENT_REASON_URL } from "@cosyte/fhir"; DATA_ABSENT_REASON_URL; // "http://hl7.org/fhir/StructureDefinition/data-absent-reason" ``` *** ### DISCRIMINATOR\_TYPES > `const` **DISCRIMINATOR\_TYPES**: readonly [`DiscriminatorType`](#discriminatortype-1)[] The R4 discriminator types, for iteration / validation. *** ### ENTERED\_IN\_ERROR > `const` **ENTERED\_IN\_ERROR**: `"entered-in-error"` = `"entered-in-error"` The `entered-in-error` code, the universal "this record is retracted, not data" value. *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable string codes for the reader's unrecoverable fatals. Everything less severe is a recoverable [FhirIssue](#fhirissue). #### Type Declaration ##### MALFORMED\_JSON > `readonly` **MALFORMED\_JSON**: `"MALFORMED_JSON"` = `"MALFORMED_JSON"` The input is not well-formed JSON. ##### MAX\_DEPTH\_EXCEEDED > `readonly` **MAX\_DEPTH\_EXCEEDED**: `"MAX_DEPTH_EXCEEDED"` = `"MAX_DEPTH_EXCEEDED"` JSON nested deeper than the reader's fixed bound. Well-formed but pathological input (a tower of `[[[[…]]]]` / `{"a":{"a":…}}`) is refused as a **DoS guard**, turning what would otherwise be a V8 stack overflow (`RangeError`, environment-dependent, untyped) into a typed, value-free fatal carrying a byte `offset`. Mirrors the XML reader's `MAX_DEPTH_EXCEEDED` (fuzzing: deep nesting must never crash/hang/OOM, always a typed error or a bounded rejection). ##### PRIMITIVE\_EXTENSION\_MISALIGNED > `readonly` **PRIMITIVE\_EXTENSION\_MISALIGNED**: `"PRIMITIVE_EXTENSION_MISALIGNED"` = `"PRIMITIVE_EXTENSION_MISALIGNED"` A primitive value array and its `_`-sibling array have different lengths, so the null-padded index alignment is broken and the reader cannot know which value each extension belongs to (cf. HAPI #5738). Fails closed, see the module doc. *** ### FHIR\_XML\_NAMESPACE > `const` **FHIR\_XML\_NAMESPACE**: `"http://hl7.org/fhir"` = `"http://hl7.org/fhir"` The FHIR XML namespace; the default namespace of every FHIR resource element. *** ### ICD10CM\_SYSTEM > `const` **ICD10CM\_SYSTEM**: `"http://hl7.org/fhir/sid/icd-10-cm"` = `"http://hl7.org/fhir/sid/icd-10-cm"` ICD-10-CM `system` URI, NCHS/CMS, encounter diagnosis / billing. *** ### ICD9CM\_SYSTEM > `const` **ICD9CM\_SYSTEM**: `"http://hl7.org/fhir/sid/icd-9-cm"` = `"http://hl7.org/fhir/sid/icd-9-cm"` ICD-9-CM `system` URI, legacy, crosswalk only. *** ### ISSUE\_CODES > `const` **ISSUE\_CODES**: `object` Stable string codes for every warning the JSON reader may emit. Frozen via `as const` so the `IssueCode` union is exact and a comparison is typo-checked. Renaming a code is a breaking change. #### Type Declaration ##### DECIMAL\_PRECISION\_AT\_RISK > `readonly` **DECIMAL\_PRECISION\_AT\_RISK**: `"DECIMAL_PRECISION_AT_RISK"` = `"DECIMAL_PRECISION_AT_RISK"` A numeric primitive whose exact value would have been corrupted by routing it through a JavaScript `number`, trailing-zero precision, more than ~15 significant digits, or magnitude past the safe-integer range. Informational: the reader preserved it losslessly; this flags that the protection mattered here. ##### DUPLICATE\_PROPERTY > `readonly` **DUPLICATE\_PROPERTY**: `"DUPLICATE_PROPERTY"` = `"DUPLICATE_PROPERTY"` A JSON object repeated a property name. FHIR JSON requires unique property names (json.html §2.6.2: "Property names SHALL be unique") and expresses repetition with an array, so a repeated name is a document defect with no defined winner: RFC 8259 §4 says "the behavior of software that receives such an object is unpredictable". The reader keeps the first value in the node's `properties`, keeps the rest in its `duplicates` (nothing is discarded), and raises this. Warning severity: the data survived, but any single-value read of that element is now arbitrary. ##### MISPLACED\_PRIMITIVE\_EXTENSION > `readonly` **MISPLACED\_PRIMITIVE\_EXTENSION**: `"MISPLACED_PRIMITIVE_EXTENSION"` = `"MISPLACED_PRIMITIVE_EXTENSION"` A `_`-sibling appeared beside an element that is **not** a primitive. FHIR JSON defines the `_`-prefixed property as the carrier for a *primitive* element's `id` and `extension` (json.html §2.6.2.3); a complex element carries both inline, and a complex array's members carry their own, so there is no position for a `_`-sibling on either and no defined meaning for one. The reader does not model what was inside it, so (as with [ISSUE\_CODES.NESTED\_ARRAY](#nested_array)) **content the sender wrote is not readable** at this location. That is why it is its own code rather than an [ISSUE\_CODES.UNKNOWN\_PROPERTY](#unknown_property), whose contract is that nothing was lost. The location names the **element**, not the `_`-prefixed member: FHIRPath addresses elements, and `_name` is not an element. It is raised once per misplaced sibling. ##### MIXED\_XML\_SPELLING > `readonly` **MIXED\_XML\_SPELLING**: `"MIXED_XML_SPELLING"` = `"MIXED_XML_SPELLING"` One modeled XML element's occurrences did **not all arrive under the same expanded name**: the namespace and the tag together (Namespaces in XML 1.0 §6.1), not the tag alone. Either half can differ, and the two halves are different situations. - **The tag differs, the namespace does not.** `` beside `` is one element written two ways. Nothing is lost and the reading is the correct one: the occurrences are modeled as repeats of a single element, exactly as the same document spelled one way would be. - **The namespace differs, the tag does not.** Two elements that are not the same element at all reach one model name under one tag. **This is not a closed list of routes and no sentence here or anywhere else may make it one**: the rule is simply that the group's occurrences did not all carry one expanded name. Two worth naming, because they are the ones that read as conformant: a prefix **rebound between siblings** (`` beside ``, where the model name of each is that verbatim tag), and a **`
` in the FHIR namespace beside the narrative**, because the narrative is modeled as `div` under every spelling of the XHTML namespace. The second is the costlier: `Narrative.div` is `0..1`, so the merge turns the narrative into a repeat over an otherwise conformant document. A **foreign** element reached by a **default** `xmlns` re-declaration also keeps its tag verbatim as its model name, so it groups with its FHIR namesake the same way; there the group additionally carries [ISSUE\_CODES.UNEXPECTED\_XML\_CONTENT](#unexpected_xml_content), which is the code to read for it. Whether a group carries that flag as well is decided by whether each occurrence is foreign **to its own parent**, so an element in the parent's namespace does not draw it: inside a FHIR-namespace ``, a `
` beside the real XHTML narrative draws this code and no other, which makes this the only report there is to read for it. It also fires where a prefixed FHIR element groups with an unprefixed one carrying a **foreign** default declaration, because that one is spelled exactly like the FHIR element; there the group additionally carries [ISSUE\_CODES.UNEXPECTED\_XML\_CONTENT](#unexpected_xml_content) at the foreign occurrence, and this code is not the one to read for that. In every case this is raised because the *count* is what changes. An element a consumer expects at most once now presents as a repeat, and a single-value read of a repeated element yields nothing rather than a value, so a check written against `0..1` can skip an element it would otherwise have inspected. Warning severity: it tells a consumer that the number of occurrences here came from the spelling, not just from the content. The location names the element, once per element, not once per occurrence. ##### NESTED\_ARRAY > `readonly` **NESTED\_ARRAY**: `"NESTED_ARRAY"` = `"NESTED_ARRAY"` A JSON array appeared **inside another array**. FHIR JSON uses an array for one thing only, a repeating element (json.html §2.6.2.2), so no element is ever a list of lists and this shape has no meaning at any position. The reader does not model what was inside, so unlike every other warning here **content the sender wrote is not readable** at this location. That is why it has its own code: an unexpected-property warning says a shape was tolerated, this one says something was there and could not be read, which is what must stop a downstream safety verdict from being affirmed over it. Raised in addition to any other warning the position already drew, never instead of one. ##### UNDEFINED\_JSON\_NULL > `readonly` **UNDEFINED\_JSON\_NULL**: `"UNDEFINED_JSON_NULL"` = `"UNDEFINED_JSON_NULL"` A JSON `null` sat at a position FHIR JSON does not define one. FHIR JSON forbids `null` and carves out one exception. json.html §2.6.2.1: "properties never have null values (except for a special case documented below)". The exception is §2.6.2.3, and it is about a **repeating** primitive: "In the case where the primitive element may repeat, it is represented in two arrays. JSON null values are used to fill out both arrays so that the id and/or extension are aligned with the matching value in the first array." So this is raised for a `null` that is **not** that. Two conditions, both required for the exception to apply: the `null` sat **inside a repeating primitive's value array**, and the slot it produced **carries an `id` or a non-empty `extension`** for it to align with. A `null` failing either leaves an element with neither a value nor children, which R4 `ele-1` requires one of. A padding `null` in a conformant document never draws it. **A singleton slot is never padding, whatever sits beside it.** §2.6.2.3 states the singleton encoding positively: "If the primitive has an id attribute or extension, but no value, only the property with the `_` is rendered." So a value-absent singleton is `{"_status":{…}}`, and both `{"status":null}` and `{"status":null,"_status":{…}}` draw this. **The set this walks is what the reader read as a primitive, not what FHIR types as one.** The model is schema-free, so a bare `null` at any **singleton** property reaches the primitive branch whatever that element's FHIR type would be: `{"subject":null}` on an `Observation` draws this code, even though `Observation.subject` is a `Reference`. Only an **array item** and a `_`-sibling's `extension` item reach the complex branch (see below). **Unlike [ISSUE\_CODES.NESTED\_ARRAY](#nested_array), this does not say content was unreadable.** A `null` carries nothing, so nothing was lost; the element really is value-absent. What it says is that the document encoded that absence in a way FHIR JSON does not define, which matters because the absence is otherwise indistinguishable from an element the sender legitimately omitted, and a `Quantity` that arrives as `{"value":null,"unit":"mg"}` would then read back as a conformant quantity with a unit and no magnitude. `serializeResource` writes the `null` back for exactly that reason, so the finding survives a round trip rather than being laundered away. Warning severity. **The neighbouring position has its own code and is not this one.** A `null` the reader takes to the complex branch preserves its text (../model/node.js `nonObjectSource`) and raises [ISSUE\_CODES.UNKNOWN\_PROPERTY](#unknown_property) instead; that behaviour is unchanged and no case moved onto this code. **The predicate, rather than a list of the documents that satisfy it:** a `null` reaches the complex branch when it is an item of an array the reader read as a *complex* array (its first non-`null` item is not a scalar, so an object **or** an inner array puts it there), or an item of a `_`-sibling's `extension` array. A `_`-sibling that is itself not an object (`"_status":null`) is a third position, and it draws [ISSUE\_CODES.UNKNOWN\_PROPERTY](#unknown_property) too: FHIR JSON has an `Element` object in that channel, so a `null` there is the scalar-where-an-object -belongs observation rather than this one. **No case has ever moved between the two codes**; that channel drew nothing at all until it was closed, so a predicate written against either is unchanged by it. ##### UNEXPECTED\_XML\_CONTENT > `readonly` **UNEXPECTED\_XML\_CONTENT**: `"UNEXPECTED_XML_CONTENT"` = `"UNEXPECTED_XML_CONTENT"` The XML reader met content at this position that does not belong to the vocabulary it expected, or that it cannot map to the model. Warning severity: the document is not rejected. **It reports two different observations, and only one of them preserves anything.** - **An element from another vocabulary** (a namespace other than its parent's, or a root declaring a namespace other than FHIR's). The element **is** modeled; this says the document left the vocabulary here. - **Non-whitespace character data directly on an element.** A FHIR element carries its value in the `value` attribute (xml.html), not as text, so there is no slot on the model for it and the text is **dropped**. That is true at every site this fires for text: on a complex element, on a primitive (`entered-in-error` loses the status), and beside the one resource child of a resource-valued element (`…`). The guarantee on offer is that the drop is not silent, and nothing more. The narrative `
` is the one element whose text is expected, and it is carried whole rather than reported here. **Do not write a claim that this code means the content survived.** **More than one of the observations above can be true at one position, and this code does NOT promise one report per location.** Exactly one site takes care not to be the second: the text report beside the one resource child of a resource-valued element, which is the site added last and the only one that checks. Everywhere else, an element that is both in another vocabulary and carrying character data draws the code twice at one expression. That is the behaviour on every release that has had this code, and a consumer keying on `code` + `expression` should treat it as a set. ##### UNKNOWN\_PROPERTY > `readonly` **UNKNOWN\_PROPERTY**: `"UNKNOWN_PROPERTY"` = `"UNKNOWN_PROPERTY"` A property the reader did not expect at this position and preserved verbatim (Postel's Law, lenient read). Warning severity: nothing was lost, but a consumer may want to know. **Two JSON positions raise it for the same observation, and both preserve the text so that re-reading the writer's output raises it again.** FHIR JSON has an object at each of them (json.html §2.6.2), and what arrived was a string, a number, a boolean or a `null`: a **complex** element's own position (the text goes to ../model/node.js `nonObjectSource`), and a primitive's **`_`-sibling**, the channel §2.6.2.3 gives the `id` and/or `extension` (`nonObjectMetaSource`). Neither is modeled as FHIR, at either position, so no walker sees a new element. **A `null` padding a repeating primitive's `_`-array is the one exception §2.6.2.3 defines and never draws this.** #### Example ```ts import { parseResource, ISSUE_CODES } from "@cosyte/fhir"; const { issues } = parseResource(json); if (issues.some((i) => i.code === ISSUE_CODES.DECIMAL_PRECISION_AT_RISK)) { // a value here would have been corrupted by a naive JSON.parse, we preserved it } ``` *** ### ISSUE\_SEVERITIES > `const` **ISSUE\_SEVERITIES**: `object` The R4 issue-severity value set (`valueset-issue-severity`), in full. R4 does **not** include the R5 `success` value, the "all clear" case is expressed as `information` + [ISSUE\_TYPES.INFORMATIONAL](#informational), not a `success` severity. #### Type Declaration ##### ERROR > `readonly` **ERROR**: `"error"` = `"error"` ##### FATAL > `readonly` **FATAL**: `"fatal"` = `"fatal"` ##### INFORMATION > `readonly` **INFORMATION**: `"information"` = `"information"` ##### WARNING > `readonly` **WARNING**: `"warning"` = `"warning"` *** ### ISSUE\_TYPES > `const` **ISSUE\_TYPES**: `object` The subset of the R4 `IssueType` value set (`valueset-issue-type`) that the layers emit. These are the wire `OperationOutcome.issue.code` values; the richer sub-code tree (terminology, invariant, profile) arrives with the layers that emit it. Renaming one is a breaking change. #### Type Declaration ##### BUSINESS\_RULE > `readonly` **BUSINESS\_RULE**: `"business-rule"` = `"business-rule"` A business rule / profile-level assertion failed (e.g. a declared profile version is unknown). ##### CODE\_INVALID > `readonly` **CODE\_INVALID**: `"code-invalid"` = `"code-invalid"` A code is not a member of a required-strength value set binding. ##### INFORMATIONAL > `readonly` **INFORMATIONAL**: `"informational"` = `"informational"` Informational only, carries no defect (e.g. "this resource type has no schema yet"). ##### INVARIANT > `readonly` **INVARIANT**: `"invariant"` = `"invariant"` A content-validation rule (a resource `constraint` / invariant) failed. ##### NOT\_FOUND > `readonly` **NOT\_FOUND**: `"not-found"` = `"not-found"` A referenced resource could not be found within the resolution closure (a Bundle reference). ##### NOT\_SUPPORTED > `readonly` **NOT\_SUPPORTED**: `"not-supported"` = `"not-supported"` The content uses a modifier the processor does not support and cannot safely ignore. ##### REQUIRED > `readonly` **REQUIRED**: `"required"` = `"required"` A required element (min cardinality ≥ 1) is missing. ##### STRUCTURE > `readonly` **STRUCTURE**: `"structure"` = `"structure"` Structural issue, an element that is not allowed here, or a cardinality-max violation. ##### VALUE > `readonly` **VALUE**: `"value"` = `"value"` An element value is invalid against its datatype value-domain (a primitive-regex failure). *** ### KNOWN\_MODIFIER\_EXTENSION\_URLS > `const` **KNOWN\_MODIFIER\_EXTENSION\_URLS**: `ReadonlySet`\<`string`\> The `modifierExtension` URLs this library understands. It is **empty**: no standard `modifierExtension` is handled yet, so *every* `modifierExtension` an instance carries is unknown and the validator fails closed on it (../validate/safety.js). The set exists as the seam a change widens deliberately, a URL is added here only alongside code that actually honors that modifier's meaning. Widening it silently would re-introduce the exact hazard the FHIR `?!` rule exists to prevent. *** ### KNOWN\_SYSTEMS > `const` **KNOWN\_SYSTEMS**: `ReadonlyMap`\<`string`, `string`\> The frozen known-systems registry: each recognized code-`system` URI mapped to its short steward name. This is a **closed set of identities** (like the status codes and the vital-signs table), not a licensed terminology table, it says *which* system a URI names, never *what codes* it contains. A URI absent from this map is "unknown": the validator cannot reason about its codes, and degrades to a non-erroring informational note rather than a false rejection (fail-safe). It is the seam a later change widens as verified URIs are confirmed (ICD-10-PCS / HCPCS remain open). *** ### LOINC\_SYSTEM > `const` **LOINC\_SYSTEM**: `"http://loinc.org"` = `"http://loinc.org"` The LOINC `system` URI, the coding system the vital-signs profile keys its required units on. *** ### MAX\_REFERENCE\_DEPTH > `const` **MAX\_REFERENCE\_DEPTH**: `512` = `512` A hard cap on the depth-first frontier the cycle guard will hold at once. A `contained` fragment graph deeper than this is treated as pathological (reported as a cycle) rather than walked, a belt-and-suspenders bound on memory on top of the three-color visited marking that already guarantees termination. *** ### MEDICATION\_VALUESET > `const` **MEDICATION\_VALUESET**: `"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1010.4"` = `"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1010.4"` US Core medication value set, VSAC `2.16.840.1.113762.1.4.1010.4`, an **extensible** binding to **RxNorm**. Bound on `MedicationRequest`/`MedicationStatement` `medicationCodeableConcept`. *(US Core MedicationRequest)* *** ### MODIFIER\_ELEMENT\_ROOT\_TYPES > `const` **MODIFIER\_ELEMENT\_ROOT\_TYPES**: `ReadonlySet`\<`string`\> The resource type names that may root a modifier-element location, which is every resource type name THIS LIBRARY spells in its own source, and no other. Named concretely rather than left to "known or modeled", because two candidate sets exist here with different memberships and the choice decides what a location reads: - the seven `SAFETY_RESOURCE_TYPES` (./codes.js), the types whose type-scoped safety elements this library surfaces, `Immunization` and `MedicationRequest` among them; - `Patient`, the one type the validator carries a built-in element table for, and one of the types this module's own predicate gates on; - `Practitioner`, another type this module's predicate gates on; - `Bundle`, which the validator branches on by name when it checks entries. The surfaced `MedicationRequest.intent` locations ([IntentReport](#intentreport)) and the unreadable ones are rooted by the same rule, since they are read by this module at the same window, and so are the surfaced and the unreadable `use` locations on the four datatypes ([DatatypeUseReport](#datatypeusereport)). The set is the union, and it is derived from source constants only. **It is never derived from the input**: a type name is a member because this package wrote it down, not because a document looked plausible. That is the whole property, and a "shaped like a resource type" test would defeat it, since a forged name can match a shape. #### Example ```ts import { MODIFIER_ELEMENT_ROOT_TYPES } from "@cosyte/fhir"; MODIFIER_ELEMENT_ROOT_TYPES.has("MedicationRequest"); // true MODIFIER_ELEMENT_ROOT_TYPES.has("Foo"); // false, such a location roots at "" ``` *** ### NDC\_SYSTEM > `const` **NDC\_SYSTEM**: `"http://hl7.org/fhir/sid/ndc"` = `"http://hl7.org/fhir/sid/ndc"` NDC `system` URI, FDA, drug product/package. *** ### NDJSON\_ERROR\_CODES > `const` **NDJSON\_ERROR\_CODES**: `object` Stable, value-free codes for a per-line NDJSON failure. #### Type Declaration ##### LINE\_TOO\_LONG > `readonly` **LINE\_TOO\_LONG**: `"LINE_TOO_LONG"` = `"LINE_TOO_LONG"` The line exceeded [NdjsonOptions.maxLineBytes](#maxlinebytes) with no newline, cut off, not buffered. ##### MALFORMED\_JSON > `readonly` **MALFORMED\_JSON**: `"MALFORMED_JSON"` = `"MALFORMED_JSON"` The line is not well-formed JSON. ##### NOT\_A\_RESOURCE > `readonly` **NOT\_A\_RESOURCE**: `"NOT_A_RESOURCE"` = `"NOT_A_RESOURCE"` The line is valid JSON but not a resource (not a JSON object at the top level). *** ### NO\_KNOWN\_ALLERGY > `const` **NO\_KNOWN\_ALLERGY**: `"716186003"` = `"716186003"` SNOMED CT `716186003` "No known allergy", a **positive** record that the patient has no known allergy. This is a first-class negation: it is *not* an absent AllergyIntolerance (absence = *unknown*), and it must *not* be read as an allergy to code `716186003`. Other "no known X allergy" substance-specific concepts (drug/food/environmental) are recognized by the same mechanism when terminology work lands; only this concept is encoded here. *** ### NOT\_DONE > `const` **NOT\_DONE**: `"not-done"` = `"not-done"` The `not-done` status code, a negation: the event did not happen. R4 spells it **only** as a `status` value and defines it in the `event-status` and `medication-admin-status` code systems, which define it as "terminated prior to any activity beyond preparation" and "terminated prior to any impact on the subject". The R4 resources whose `status` binds a value set containing it are `Procedure`, `Communication`, `Media`, `MedicationAdministration` and `Immunization`. Read off `status` on any resource type, for the reasons on statusSpells. *** ### NOT\_TAKEN > `const` **NOT\_TAKEN**: `"not-taken"` = `"not-taken"` The `not-taken` status code, a negation: "the medication was not consumed by the patient". R4 spells it **only** as a `status` value and defines it in **only** the `MedicationStatement.status` code system (`medication-statement-status`), where that sentence is the code's own definition. Read off `status` on any resource type, for the reasons on statusSpells. *** ### OBSERVATION\_CATEGORY\_SYSTEM > `const` **OBSERVATION\_CATEGORY\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/observation-category"` = `"http://terminology.hl7.org/CodeSystem/observation-category"` The `Observation.category` code system that carries the `vital-signs` slice value. *** ### OBSERVATION\_VALUE\_TYPES > `const` **OBSERVATION\_VALUE\_TYPES**: readonly \[`"Quantity"`, `"CodeableConcept"`, `"String"`, `"Boolean"`, `"Integer"`, `"Range"`, `"Ratio"`, `"SampledData"`, `"Time"`, `"DateTime"`, `"Period"`\] The eleven `Observation.value[x]` variant type suffixes, in FHIR's declared order. A variant's JSON property name is `"value" + ` (e.g. `"Quantity"` → `valueQuantity`, `"String"` → `valueString`). This is the exact choice set from `observation.html`; a rename would be breaking. *** ### PATIENT\_IDENTIFIER\_STARTER > `const` **PATIENT\_IDENTIFIER\_STARTER**: [`StructureDefinition`](#structuredefinition) A **`Patient` identifier** starter, grounded in US Core Patient. It marks `identifier`, `identifier.system`, and `identifier.value` required + must-support, a patient identity is the `(system, value)` tuple. It deliberately does **not** slice an "MRN" slice and does **not** bind `identifier.type`: US Core does neither, and inventing an MRN slice is exactly the wrong-patient-merge hazard. *** ### PRIMITIVE\_TYPES > `const` **PRIMITIVE\_TYPES**: readonly \[`"boolean"`, `"integer"`, `"integer64"`, `"unsignedInt"`, `"positiveInt"`, `"decimal"`, `"string"`, `"code"`, `"markdown"`, `"id"`, `"uri"`, `"url"`, `"canonical"`, `"oid"`, `"uuid"`, `"base64Binary"`, `"instant"`, `"date"`, `"dateTime"`, `"time"`\] The FHIR R4 primitive datatype names. `code`, `id`, `markdown`, `url`, `canonical`, `oid`, `uuid` are string-derived types with their own patterns; the JSON-number family (`integer` …) is stored as ../model/decimal.js. Frozen so the union is exact. *** ### REFUTED > `const` **REFUTED**: `"refuted"` = `"refuted"` `refuted`, an AllergyIntolerance/Condition asserted to be *not* present after investigation. *** ### RXNORM\_SYSTEM > `const` **RXNORM\_SYSTEM**: `"http://www.nlm.nih.gov/research/umls/rxnorm"` = `"http://www.nlm.nih.gov/research/umls/rxnorm"` RxNorm `system` URI, NLM, medications. *** ### SAFETY\_RESOURCE\_TYPES > `const` **SAFETY\_RESOURCE\_TYPES**: `ReadonlySet`\<`string`\> The resource types whose **type-scoped** modifier/status/negation elements this library surfaces and whose invariants it enforces. `MedicationStatement` rides alongside `MedicationRequest`. Count them off the set, never off a sentence: a written-down count here read "six" over a set of seven for days, and reached `dist/` saying so. Several reads are deliberately **not** scoped by it, because they can only add a finding: the `modifierExtension` fail-closed check, the `entered-in-error` retraction, the `refuted` verification status, the `doNotPerform` instruction, and the `not-done` / `not-taken` status negations (statusSpells). `doNotPerform` used to be gated on `MedicationRequest` alone, and `not-done` on `Immunization` alone; the types each gate left out were neither read nor reported, so a conformant `ServiceRequest` carrying an instruction *not* to perform the service, and a conformant `Procedure` recording that it was **not** done, both read as carrying no negation at all. Those same reads run at **every resource root** the document carries (./status.js `checkNegations`), not only the resource handed in. *** ### SERIALIZE\_ERROR\_CODES > `const` **SERIALIZE\_ERROR\_CODES**: `object` Every reason a writer refuses to serialize a model. #### Type Declaration ##### DROPPED\_ELEMENT\_TEXT > `readonly` **DROPPED\_ELEMENT\_TEXT**: `"DROPPED_ELEMENT_TEXT"` = `"DROPPED_ELEMENT_TEXT"` The model carries character data the XML reader dropped, at one or more locations. There is no conformant encoding of it in either wire format, and emitting the element as unfilled would launder the `DROPPED_ELEMENT_TEXT` finding across a round trip. ##### UNSERIALIZABLE\_ARRAY\_WRAPPER > `readonly` **UNSERIALIZABLE\_ARRAY\_WRAPPER**: `"UNSERIALIZABLE_ARRAY_WRAPPER"` = `"UNSERIALIZABLE_ARRAY_WRAPPER"` The model carries, at a location this library already reports as an array-wrapped `0..1` safety element, a wrapper FHIR XML has no repetition to spell back: one holding fewer than two items, or any wrapper at all on `resourceType`. **XML only**: `serializeResource` writes the list back and the re-read reports the same location, so that route stays open. XML spells a repeat by repeating the element and has no other mark for one, so a wrapper of fewer than two items emits at most one element and re-reads as an ordinary single-valued element. The encoding complaint the reader raised is then gone, and with it an `error`-severity `ARRAY_WRAPPED_SCALAR` and a `safeToSummarize: false`. See assertXmlArrayWrapper for the exact predicate and for what it deliberately leaves. ##### UNSERIALIZABLE\_CHOICE\_WRAPPER > `readonly` **UNSERIALIZABLE\_CHOICE\_WRAPPER**: `"UNSERIALIZABLE_CHOICE_WRAPPER"` = `"UNSERIALIZABLE_CHOICE_WRAPPER"` The model carries, at a location this library already reports as an array-wrapped `Observation.value[x]` choice, a wrapper FHIR XML has no repetition to spell back: one holding fewer than two items. **XML only**: `serializeResource` writes the list back and the re-read reports the same location, so that route stays open. The same harm as [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_ARRAY\_WRAPPER](#unserializable_array_wrapper), at the position where the number is a DOSE: `{"resourceType":"Observation","status":"final","valueQuantity": [{"value":5,"system":"http://unitsofmeasure.org","code":"mg"}]}` reads with the encoding reported and no magnitude handed out, and used to come back from XML as an unambiguous 5 mg nothing had complained about. See assertXmlValueChoiceWrapper for the window and for what it deliberately leaves. ##### UNSERIALIZABLE\_DIV\_MARKUP > `readonly` **UNSERIALIZABLE\_DIV\_MARKUP**: `"UNSERIALIZABLE_DIV_MARKUP"` = `"UNSERIALIZABLE_DIV_MARKUP"` A `div` property carries a string the XML writer would emit as raw markup, and that string does not contribute exactly one element named `div` to the document. **XML only**: `serializeResource` carries the string as a string, so this refusal never reaches it and that route stays open. **That is a statement about the `div` string, not about the whole model.** `serializeResource` has its own declared non-spec-clean exceptions, so a model refused here can still route through it and emit one of those: `{"text":{"div":""},"name":[[{"family":"X"}]]}` is refused here and `serializeResource` emits the array inside an array unchanged. See `emitsOneDivElement` in `../xml/write.js` for the exact predicate and what it does not cover. ##### UNSERIALIZABLE\_DIV\_PREFIX > `readonly` **UNSERIALIZABLE\_DIV\_PREFIX**: `"UNSERIALIZABLE_DIV_PREFIX"` = `"UNSERIALIZABLE_DIV_PREFIX"` The model holds, at one or more `div` locations, a string the XML writer would splice in as raw markup that spells the one `div` element the property names, and whose markup names a namespace prefix, on an element or on an attribute, that no declaration inside the string binds. **XML only**: `serializeResource` carries the string as a string, so this refusal never reaches it and that route stays open. Written anyway, `x` put a prefix nothing declared into the output, which a conformant XML parser rejects, and this library's own re-read of that output turned the narrative into a property named `v:div`: the narrative gone after one write and one read, with no diagnostic at either end. **Not [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_PREFIXED\_NAME](#unserializable_prefixed_name)**, whose published meaning is a name at a tag position, and **not [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_DIV\_MARKUP](#unserializable_div_markup)**, whose question is which elements the string contributes: a string that fails that question keeps that code whatever prefix it names. See `bindsEveryPrefix` in `../xml/write.js` for the exact predicate and what it leaves. ###### Example ```ts import { FhirSerializeError, SERIALIZE_ERROR_CODES, parseResource, serializeResourceXml, } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Patient","text":{"status":"generated","div":"x"}}', ); try { serializeResourceXml(resource); } catch (err) { if (err instanceof FhirSerializeError && err.code === SERIALIZE_ERROR_CODES.UNSERIALIZABLE_DIV_PREFIX) { err.locations; // ["Patient.text.div"] } } ``` ##### UNSERIALIZABLE\_ELEMENT\_NAME > `readonly` **UNSERIALIZABLE\_ELEMENT\_NAME**: `"UNSERIALIZABLE_ELEMENT_NAME"` = `"UNSERIALIZABLE_ELEMENT_NAME"` The model carries a name that cannot occupy the `Name` slot of an XML start tag, so writing it would emit markup that does not re-read as the element the model holds. **XML only**: JSON escapes a member name, so this refusal never reaches it and that route stays open. **Narrowed 2026-08-07 from "encodes every one of these correctly", which was false and shipped**: a model refused here can carry one of `serializeResource`'s own declared exceptions and emit that. See breaksTag for the exact predicate and for what is deliberately NOT refused. ##### UNSERIALIZABLE\_FOREIGN\_ROOT > `readonly` **UNSERIALIZABLE\_FOREIGN\_ROOT**: `"UNSERIALIZABLE_FOREIGN_ROOT"` = `"UNSERIALIZABLE_FOREIGN_ROOT"` The model carries, at one or more elements, a root the XML reader read out of a vocabulary that resolved to something other than FHIR's. **XML only**: `serializeResource` spells a member name and never a namespace, so it emits the model exactly as it did before this refusal existed and that route stays open -- which is a statement about that writer's output, **not** a claim that the JSON channel keeps the flag. It does not, and that half is declared open. FHIR XML puts the resource in the FHIR namespace, and the writer has no vendor binding to write instead, so it emitted a document in the FHIR namespace with no trace of the one the sender wrote. That output re-reads with an **empty** issue list, so the one warning that said the document came from another vocabulary is gone after a single trip and the vendor document is indistinguishable from FHIR. See assertXmlForeignRoot for the window, the two routes weighed, and what it costs. ##### UNSERIALIZABLE\_JSON\_ONLY\_SHAPE > `readonly` **UNSERIALIZABLE\_JSON\_ONLY\_SHAPE**: `"UNSERIALIZABLE_JSON_ONLY_SHAPE"` = `"UNSERIALIZABLE_JSON_ONLY_SHAPE"` The model carries, at one or more locations, a shape the JSON reader marked because FHIR JSON gives that position no meaning: an array inside an array, a scalar or `null` where FHIR JSON has an object (a complex element's own position or a primitive's `_`-sibling), or a `null` in a primitive's value channel that padded nothing. **XML only**: this refusal does not reach `serializeResource`, which writes these back from the text the reader preserved at every position that writer walks. **It does not walk a member a repeated property name shadowed, and this refusal does reach one** -- `serializeResource` refuses that model on [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_SHADOWED\_PROPERTY](#unserializable_shadowed_property) rather than emitting it, so it is still not a route the shape survives. The XML writer keeps reporting THIS code on such a model, because this one is raised first. See assertXmlSerializable. XML has no array-of-arrays, no `_`-sibling and no `null`, so the XML writer has nowhere to put any of it and emits the element the reader was left holding: an empty one, or none. That output re-reads with an empty issue list, so the finding the reader raised is gone after one trip. See assertXmlSerializable for the exact set of markers and for what it does NOT cover. ##### UNSERIALIZABLE\_PREFIXED\_NAME > `readonly` **UNSERIALIZABLE\_PREFIXED\_NAME**: `"UNSERIALIZABLE_PREFIXED_NAME"` = `"UNSERIALIZABLE_PREFIXED_NAME"` The model holds, at one or more tag positions, a name carrying a colon. XML reads the colon as a namespace prefix, and the model carries no namespace binding for the writer to declare one with, so the document would not be namespace-well-formed. **XML only**: `serializeResource` spells a member name as a JSON string, so this refusal never reaches it and that route stays open. Written anyway, the output named a prefix nothing declared, which a conformant XML parser rejects, and a prefix rebound between siblings (`` beside ``) lost its `MIXED_XML_SPELLING` report across one write and one re-read. **Not [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_ELEMENT\_NAME](#unserializable_element_name)**, whose line is whether this library's own round trip survives a name: a colon-bearing name survives that round trip, so the two are different questions and keep different codes. A name that trips both keeps that one. See carriesUndeclarablePrefix for the exact predicate, its one exemption, and what it leaves. ##### UNSERIALIZABLE\_RESOURCE\_TYPE > `readonly` **UNSERIALIZABLE\_RESOURCE\_TYPE**: `"UNSERIALIZABLE_RESOURCE_TYPE"` = `"UNSERIALIZABLE_RESOURCE_TYPE"` The model carries, at one or more elements, a first `resourceType` that is not a string. FHIR XML has no `resourceType` element at all -- the type IS the tag -- so the XML writer skipped the property, and at the root, with no string to name the tag, it wrote `Resource` instead: the member deleted and the element named a type nobody wrote. **XML only**: `serializeResource` emits a non-string `resourceType` through its ordinary path, so this refusal never reaches it and that route stays open. See assertXmlResourceType for the window and for what it deliberately leaves. ##### UNSERIALIZABLE\_SHADOWED\_PROPERTY > `readonly` **UNSERIALIZABLE\_SHADOWED\_PROPERTY**: `"UNSERIALIZABLE_SHADOWED_PROPERTY"` = `"UNSERIALIZABLE_SHADOWED_PROPERTY"` The model carries, at one or more object elements, a member a repeated property name shadowed (../model/node.js `duplicates`). **Both writers**, unlike the four refusals above it: each walks `properties` only, so each wrote one member per name and the second value left the document with no diagnostic on it. See assertNoShadowedProperty for the window, the two routes weighed, and what it leaves. ##### UNSERIALIZABLE\_XML\_CHARACTER > `readonly` **UNSERIALIZABLE\_XML\_CHARACTER**: `"UNSERIALIZABLE_XML_CHARACTER"` = `"UNSERIALIZABLE_XML_CHARACTER"` The model holds, at one or more locations, a value the XML writer would emit as an attribute value (a primitive's `value`, an `id` written as an attribute, an `Extension.url`), or a `div` string it would splice in, carrying a code point outside XML 1.0 `Char` (production [2]): U+0000 to U+0008, U+000B, U+000C, U+000E to U+001F, an unpaired surrogate, U+FFFE or U+FFFF. In a `div` string that includes a numeric character reference denoting one (`�`), which the Legal Character constraint makes as fatal as the raw character; each reference is judged on its own, so `��` is two unpaired surrogates, not one character. **XML only**: `serializeResource` writes these values as JSON strings, so this refusal never reaches it and that route stays open. Written anyway, a U+0000 went into the output raw and a conforming processor rejected the document. **Refused, never repaired**: a reference to a non-`Char` is itself a fatal error, and replacing or dropping the character would change a value the sender wrote, so the model is left exactly as it was and no string is returned. Raised after every other refusal, the name code included. See carriesNonXmlCharacter for the exact predicate. ###### Example ```ts import { FhirSerializeError, SERIALIZE_ERROR_CODES, parseResource, serializeResourceXml, } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","gender":"a\\u0000"}'); try { serializeResourceXml(resource); } catch (err) { if (err instanceof FhirSerializeError && err.code === SERIALIZE_ERROR_CODES.UNSERIALIZABLE_XML_CHARACTER) { err.locations; // ["Patient.gender"] } } ``` ##### UNSERIALIZABLE\_XML\_NAME > `readonly` **UNSERIALIZABLE\_XML\_NAME**: `"UNSERIALIZABLE_XML_NAME"` = `"UNSERIALIZABLE_XML_NAME"` The model holds, at one or more tag positions, a name that is not an XML 1.0 `Name` (production [5]): `a&b`, `1abc`, `-x`, a name carrying U+0000 or an unpaired surrogate. A conforming XML processor must reject such a document as a fatal error. **XML only**: `serializeResource` spells a member name as a JSON string, so this refusal never reaches it and that route stays open. Written anyway, `` was a document this library's own reader read back unchanged and a third-party parser rejected, which is why the tag-breaking refusal, whose line is this library's own round trip, never reached it. **Not [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_ELEMENT\_NAME](#unserializable_element_name)** and **not [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_PREFIXED\_NAME](#unserializable_prefixed_name)**: a name that breaks the tag, or carries a colon, keeps the code it drew before this one existed, whether or not it is a `Name`. A name carrying a character outside `Char` is not a `Name`, so it draws this code rather than [SERIALIZE\_ERROR\_CODES.UNSERIALIZABLE\_XML\_CHARACTER](#unserializable_xml_character). See isXmlName for the exact predicate and what it leaves. ###### Example ```ts import { FhirSerializeError, SERIALIZE_ERROR_CODES, parseResource, serializeResourceXml, } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","1abc":"v"}'); try { serializeResourceXml(resource); } catch (err) { if (err instanceof FhirSerializeError && err.code === SERIALIZE_ERROR_CODES.UNSERIALIZABLE_XML_NAME) { err.locations; // ["Patient."] } } ``` *** ### SNOMED\_SCT > `const` **SNOMED\_SCT**: `"http://snomed.info/sct"` = `"http://snomed.info/sct"` The SNOMED CT `system` URI (`terminologies-systems.html`). *** ### STARTER\_PROFILE\_BASE\_URL > `const` **STARTER\_PROFILE\_BASE\_URL**: `"https://cosyte.com/fhir/StructureDefinition"` = `"https://cosyte.com/fhir/StructureDefinition"` The canonical URL prefix the starter-kit profiles are published under. *** ### STARTER\_PROFILES > `const` **STARTER\_PROFILES**: readonly [`StructureDefinition`](#structuredefinition)[] Every starter-kit profile. Iterate this to register the whole kit as a validation profile set, or pick one by `url`, a starting point a consumer extends with their own site/vendor constraints. #### Example ```ts import { STARTER_PROFILES, parseResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource(observationJson); const { issues } = validateResource(resource, { profiles: [...STARTER_PROFILES] }); ``` *** ### TERMINOLOGY\_BINDINGS > `const` **TERMINOLOGY\_BINDINGS**: readonly [`TerminologyBinding`](#terminologybinding)[] The built-in bindings, the multi-system elements. Deliberately minimal: broad US Core element coverage is a profile concern. Each is **extensible**, so a code outside its systems is a `warning` (a possible legitimate extension), never a false `error`. *** ### UCUM\_SYSTEM > `const` **UCUM\_SYSTEM**: `"http://unitsofmeasure.org"` = `"http://unitsofmeasure.org"` The UCUM `system` URI (`terminologies-systems.html`), the one system whose codes are UCUM. *** ### UNBOUNDED > `const` **UNBOUNDED**: `number` = `Number.POSITIVE_INFINITY` `1..1` / `0..*` etc. `max` uses [UNBOUNDED](#unbounded) for `*`. *** ### VALIDATION\_CODES > `const` **VALIDATION\_CODES**: `object` Stable string codes for every validation finding the layers can raise. Frozen via `as const` so the union is exact and a comparison is typo-checked. **Renaming a code is a breaking change**; the set is snapshotted (see `test/validation-codes.test.ts`). #### Type Declaration ##### ABSENCE\_MARKER\_CONFLICT > `readonly` **ABSENCE\_MARKER\_CONFLICT**: `"ABSENCE_MARKER_CONFLICT"` = `"ABSENCE_MARKER_CONFLICT"` Safety, an element carries a DataAbsentReason extension **and a value of its own**, so the document asserts both that the element holds that value and that it holds none. An `error` (`structure`): the two cannot both be true, nothing here ranks them, and a consumer that happened to read one of the two would report the record as though the other had not been written. Both survive on the model and on the safety readout; this reports that they disagree. Value-free, the location of the element, never the value and never the reason. Cannot fire on a conformant document: an element the sender has data for is written with the data and no marker. It is **not** the `Observation.dataAbsentReason` ELEMENT beside a `value[x]`, which is the `obs-6` invariant and reports as [VALIDATION\_CODES.INVARIANT\_VIOLATED](#invariant_violated); the element is not the extension, and the two never report about one another's shape. ##### ABSENCE\_MARKER\_UNREADABLE > `readonly` **ABSENCE\_MARKER\_UNREADABLE**: `"ABSENCE_MARKER_UNREADABLE"` = `"ABSENCE_MARKER_UNREADABLE"` Safety, an element carries a DataAbsentReason extension whose reason this library could not read: no `valueCode`, one holding no readable string, an empty one, one written twice, or a code outside the closed fifteen-concept value set the extension's `value[x]` binds to at **required** strength. An `error` (`code-invalid`), on the same footing as any other required-binding miss: the set is closed and published, so membership is decided from the set itself with no terminology service involved and no value-set expansion. **Nothing is coerced.** The code is not trimmed, case-folded or substituted, and the element is not read as `unknown` and not read as populated: doing either would author a reason the sender did not spell, or erase a declaration the sender did make. The element stays value-absent and this is the record that a declaration was made and could not be honoured. Value-free, the location of the element, never the code that failed to match. ##### ARRAY\_WRAPPED\_CHOICE > `readonly` **ARRAY\_WRAPPED\_CHOICE**: `"ARRAY_WRAPPED_CHOICE"` = `"ARRAY_WRAPPED_CHOICE"` Safety, an `Observation.value[x]` choice element arrived wrapped in a JSON array. The same non-conformant encoding [VALIDATION\_CODES.ARRAY\_WRAPPED\_SCALAR](#array_wrapped_scalar) reports (json.html §2.6.2.2 reserves the array for a repeating element), at the one position whose cardinality is decided by a choice rather than by a per-resource element table, so it needs a code of its own rather than a wider window on that one. An `error`. **Why the value readout needs its own channel.** `readObservationValue` fails safe on this shape, it reports the variant that is present and no `quantity`, so no wrong number is handed out, but "the variant is present and the magnitude is absent" reads identically to an `Observation` whose `valueQuantity` carried no `value`. A dose the sender wrote is then indistinguishable from one it did not, and this is the record that the position held content nothing here would read. **The window is `Observation.value[x]` and `Observation.component.value[x]`, at every Observation resource root the model holds**, which is the window the value readout itself reads (`../quantity/value.js`). Both are `0..1` in R4 (observation.html), so this needs no per-resource cardinality table and cannot fire on a conformant document. It is deliberately not every R4 `0..1` element: that is the per-resource model this library does not have. Value-free, the location of the choice variant, never the magnitude, unit or code inside the wrapper. ##### ARRAY\_WRAPPED\_SCALAR > `readonly` **ARRAY\_WRAPPED\_SCALAR**: `"ARRAY_WRAPPED_SCALAR"` = `"ARRAY_WRAPPED_SCALAR"` Safety, a single-valued (`0..1`) safety element, or `resourceType`, arrived wrapped in a JSON array. FHIR JSON writes a single-valued element as a name/value pair and uses an array only for a repeating element (json.html §2.6.2.2), so this is a non-conformant encoding and an `error`. It is the shape a **generic XML-to-JSON converter** produces for every element, which is how a C-CDA or v2 feed commonly reaches a FHIR surface, and left unreported it reaches the same harm as a repeated property name: a single-value read finds no code in the array, so a retraction or a negation the sender wrote goes unreported and the record reads live. Nothing is lost, the wrapper is preserved and the safety layer reads through it; this reports that the encoding was ambiguous. ##### CARDINALITY\_MAX > `readonly` **CARDINALITY\_MAX**: `"CARDINALITY_MAX"` = `"CARDINALITY_MAX"` Layer 2, an element appears more times than its maximum cardinality allows. ##### CARDINALITY\_MIN > `readonly` **CARDINALITY\_MIN**: `"CARDINALITY_MIN"` = `"CARDINALITY_MIN"` Layer 2, a required element (min ≥ 1) is absent. ##### CHOICE\_AMBIGUOUS > `readonly` **CHOICE\_AMBIGUOUS**: `"CHOICE_AMBIGUOUS"` = `"CHOICE_AMBIGUOUS"` Layer 1, more than one variant of a `choice[x]` element is present. ##### CODE\_INVALID > `readonly` **CODE\_INVALID**: `"CODE_INVALID"` = `"CODE_INVALID"` Layer 3, a `code` value is outside a required-strength enumerated binding. ##### CODE\_NOT\_IN\_VALUESET > `readonly` **CODE\_NOT\_IN\_VALUESET**: `"CODE_NOT_IN_VALUESET"` = `"CODE_NOT_IN_VALUESET"` Terminology, a configured terminology service reported that a bound coding's `(system, code)` is **not a member** of the binding's value set. Severity follows the binding strength (`required`/`extensible` → `error`; `preferred` → `warning`; `example` → `information`, never an error). Emitted **only** when a service definitively answers `not-in`; with no service, or an `unknown` answer, the library degrades to the content-free system checks and never false-errors (fail-safe). Value-free, the coding location, never the code itself. The **only** finding that carries a [ValidationIssue.codeSystemVersion](#codesystemversion) record, because it is the only one a terminology service produces: the release the service declared its answer was made against, or the explicit mark that it declared none. Severity, emission and the fail-safe degrade are all independent of it. ##### CODE\_SYSTEM\_UNEXPECTED > `readonly` **CODE\_SYSTEM\_UNEXPECTED**: `"CODE_SYSTEM_UNEXPECTED"` = `"CODE_SYSTEM_UNEXPECTED"` Terminology, a bound coding uses a **known** code `system` that is not one the binding's value set draws from (e.g. an ICD-10-CM code where the binding expects RxNorm + SNOMED). This is the content-free "wrong system for this binding" check, decided from the `system` alone, with no value-set content. Severity follows the binding strength (`required` → `error`; `extensible`/`preferred` → `warning`, since a different system may be a legitimate extension; `example` → none). Compared on the `system` URI, never a code value. ##### CODE\_SYSTEM\_UNKNOWN > `readonly` **CODE\_SYSTEM\_UNKNOWN**: `"CODE_SYSTEM_UNKNOWN"` = `"CODE_SYSTEM_UNKNOWN"` Terminology, a bound coding's `system` URI is not in the frozen known-systems registry (and not one the binding's value set draws from). Always `information` (`code-invalid`): an unknown system may be a legitimate local/proprietary one, so it is never a defect, it only means the library cannot validate codes drawn from it. Content-free, so it can never flip validity. ##### CONTAINED\_CYCLE > `readonly` **CONTAINED\_CYCLE**: `"CONTAINED_CYCLE"` = `"CONTAINED_CYCLE"` Bundle, the `#fragment` references among a resource's `contained` resources form a **cycle** (a → b → a, or a self-reference). An `error` (`structure`): a containment cycle is malformed and, to a naive transitive resolver, a denial-of-service (an unbounded loop / stack blow-up). The bounded, iterative cycle guard detects it and reports it here rather than looping, DoS-safe by construction. Value-free, the location of the `contained` element, never a value. ##### DROPPED\_ELEMENT\_TEXT > `readonly` **DROPPED\_ELEMENT\_TEXT**: `"DROPPED_ELEMENT_TEXT"` = `"DROPPED_ELEMENT_TEXT"` Safety, an XML document wrote **character data directly on a FHIR element**. FHIR XML carries a primitive's value in the `value` attribute (xml.html §2.6.1), so text written as element content has no slot on the model and the reader drops it: `entered-in-error` yields a `status` with no value. An `error`, and the only code on this list where the content is neither modeled **nor kept**: unlike [VALIDATION\_CODES.NESTED\_ARRAY](#nested_array-1), which preserves the array's JSON text, the character data is discarded outright, because reading it back would be a tolerance for a non-conformant encoding rather than a report of one. Left unreported it reaches the same harm as [VALIDATION\_CODES.NESTED\_ARRAY](#nested_array-1) by the other wire format, because the model is again indistinguishable from an element that was legitimately absent: a retraction, a `refuted` verification status, or a dose *number* beside a surviving unit and UCUM code all read back as a clean document. Value-free, the position the text occupied, never its contents. ##### DUPLICATE\_PROPERTY > `readonly` **DUPLICATE\_PROPERTY**: `"DUPLICATE_PROPERTY"` = `"DUPLICATE_PROPERTY"` Safety, the document wrote a property name more than once, so an element holds several values and nothing says which the sender meant. FHIR JSON requires unique property names (json.html §2.6.2: "Property names SHALL be unique") and expresses repetition with an array, so this is a violated `SHALL` and an `error`. The reader keeps every value (see ../model/node.js `duplicates`), so this reports an ambiguity, never a loss. ##### FULLURL\_ID\_MISMATCH > `readonly` **FULLURL\_ID\_MISMATCH**: `"FULLURL_ID_MISMATCH"` = `"FULLURL_ID_MISMATCH"` Bundle, a Bundle entry's `fullUrl` is a RESTful URL (relative `Type/id` or an absolute URL ending in `Type/id`) whose id disagrees with the entry `resource.id`. An `error` (`business-rule`): FHIR requires a RESTful `fullUrl` to be consistent with the resource it wraps, and a disagreement can cause a reference to resolve to the wrong resource. A `urn:uuid:` (logical) `fullUrl` places no constraint on `resource.id`, so it never triggers this. Value-free, the location of the `fullUrl`, never either id. ##### INVARIANT\_UNCHECKED > `readonly` **INVARIANT\_UNCHECKED**: `"INVARIANT_UNCHECKED"` = `"INVARIANT_UNCHECKED"` Invariant, a `constraint`'s FHIRPath `expression`, from a supplied profile or from the R4 base constraints the base-constraint layer evaluates, is **outside the bounded engine's subset** and could not be evaluated. Always `information` (`informational`): the constraint is reported **unchecked, never assumed to pass** (fail-safe), the library does not claim conformance to an invariant it could not test. The constraint `key` travels in [ValidationIssue.constraint](#constraint-2). Value-free (the location + key, never an instance value). A profile's verbatim copy of an R4 base constraint that the base-constraint layer decided at the same occurrence is not reported this way, because that constraint was evaluated: R4's `dom-3` on a resource with nothing contained draws nothing, with or without a profile. ##### INVARIANT\_VIOLATED > `readonly` **INVARIANT\_VIOLATED**: `"INVARIANT_VIOLATED"` = `"INVARIANT_VIOLATED"` Invariant, a resource constraint failed. Three sources raise it, each with or without the others: the safety layer's named invariants (`ait-1`/`ait-2`, `con-3`/`con-4`/`con-5`, `obs-6`/`obs-7`); the base-constraint layer, which evaluates the other error-severity constraints R4 declares on the eight modeled types and their `DomainResource` / `Element` / `Extension` base (`pat-1`, `obs-3`, `con-1`, `con-2`, `imm-1`, `dom-2` to `dom-5`, `ele-1`, `ext-1`) with no profile supplied; and a supplied profile's own `constraint`s. The specific constraint key travels in [ValidationIssue.constraint](#constraint-2), and the severity mirrors the constraint's own (`error`, except a `warning` constraint such as the best-practice `con-3`). ##### MUST\_SUPPORT\_ABSENT > `readonly` **MUST\_SUPPORT\_ABSENT**: `"MUST_SUPPORT_ABSENT"` = `"MUST_SUPPORT_ABSENT"` Profile, an element the profile marks **must-support** is absent from the instance. **Always `information`, never an error** (the fail-safe, and the single most important must-support rule): must-support is a *system obligation* on the sender to be able to populate the element and on the receiver to tolerate its absence, it is **not** an instance-presence requirement. A strict client that errors on an absent must-support element is the classic bug this code exists to avoid. ##### NESTED\_ARRAY > `readonly` **NESTED\_ARRAY**: `"NESTED_ARRAY"` = `"NESTED_ARRAY"` Safety, the document wrote a JSON array **inside another array**. FHIR JSON uses an array for a repeating element and for nothing else (json.html §2.6.2.2), so a list of lists has no meaning at any position and this is a non-conformant encoding wherever it appears, which is why it needs no cardinality rule and cannot fire on a conformant document. Reported at every position the model has a node for; a `_`-sibling the reader discards whole is the stated exception, and draws a reader warning instead: `UNKNOWN_PROPERTY` for an unrecognised member of a `_`-sibling object, `MISPLACED_PRIMITIVE_EXTENSION` for a sibling on an object or on a non-primitive array, not one code for all three. An `error`, and one of the two on this list where the reader could **not model** what the sender wrote (the other is [VALIDATION\_CODES.DROPPED\_ELEMENT\_TEXT](#dropped_element_text-1)): the codec does not model an inner array, so this reports a loss of *structure* rather than an ambiguity, though the array's JSON text is kept and readable (see ../model/node.js `nestedArrayContent`). Left unreported it is among the worst of the set, because the model then looks exactly like an element that was legitimately absent, and a refuted allergy, a resolved condition, or an entire resource inside a Bundle entry reads back as a clean document. Value-free, the position the inner array occupied, never its contents. ##### PRIMITIVE\_INVALID > `readonly` **PRIMITIVE\_INVALID**: `"PRIMITIVE_INVALID"` = `"PRIMITIVE_INVALID"` Layer 3, a primitive value does not match its datatype's lexical form. ##### PROFILE\_FIXED\_MISMATCH > `readonly` **PROFILE\_FIXED\_MISMATCH**: `"PROFILE_FIXED_MISMATCH"` = `"PROFILE_FIXED_MISMATCH"` Profile, an element carries a value that is not **exactly** the profile's `fixed[x]`. A `value` `error`: `fixed[x]` is an equality constraint (the element SHALL match the fixed value exactly, including every nested property). Compared structurally and precision-exactly (decimals via ../model/decimal.js), never by echoing the value. ##### PROFILE\_PATTERN\_MISMATCH > `readonly` **PROFILE\_PATTERN\_MISMATCH**: `"PROFILE_PATTERN_MISMATCH"` = `"PROFILE_PATTERN_MISMATCH"` Profile, an element does not match the profile's `pattern[x]`. A `value` `error`: `pattern[x]` is a **subset** constraint (the element SHALL contain *at least* the pattern's properties and values, but may carry more), the weaker sibling of `fixed[x]`. Value-free. ##### PROFILE\_SLICE\_UNCHECKED > `readonly` **PROFILE\_SLICE\_UNCHECKED**: `"PROFILE_SLICE_UNCHECKED"` = `"PROFILE_SLICE_UNCHECKED"` Profile, a slicing whose discriminator this library cannot evaluate (a `profile` discriminator, which needs recursive profile resolution, or the R5-only `position`). Emitted as `information` so slice membership is reported **unchecked, never silently passed** (the fail-safe): the library does not guess a slice assignment it cannot justify. ##### PROFILE\_SLICE\_UNMATCHED > `readonly` **PROFILE\_SLICE\_UNMATCHED**: `"PROFILE_SLICE_UNMATCHED"` = `"PROFILE_SLICE_UNMATCHED"` Profile, an instance element is present under a **closed** slicing whose discriminators matched none of the profile's defined slices. A `structure` `error`: `closed` slicing forbids content outside the named slices. (Under `open` slicing an unmatched element is allowed and draws nothing; under `openAtEnd` it is allowed only in the trailing position, this library flags a closed-slicing miss and leaves the ordering nuance unenforced.) ##### PROFILE\_VERSION\_MISMATCH > `readonly` **PROFILE\_VERSION\_MISMATCH**: `"PROFILE_VERSION_MISMATCH"` = `"PROFILE_VERSION_MISMATCH"` Profile, the instance's `meta.profile` declares a profile at a version the supplied profile set does not carry (`canonical|version` with a different version, or an unresolvable canonical). A `warning` (`business-rule`): an unknown profile version is flagged rather than silently best-effort-validating against a different one. ##### REFERENCE\_UNRESOLVED > `readonly` **REFERENCE\_UNRESOLVED**: `"REFERENCE_UNRESOLVED"` = `"REFERENCE_UNRESOLVED"` Bundle, a `Reference` inside a Bundle entry (or a `#fragment` inside a resource's `contained`) that could not be resolved within the resolution closure: a fragment whose target contained resource is absent, or a relative `Type/id` reference naming no entry in the Bundle. A `warning` (`not-found`) and **never fatal**, the target may legitimately live outside the supplied closure (a partial Bundle, an external server), so the reference is **preserved**, only flagged. An absolute/logical reference that is simply external to the Bundle draws **no** finding. Value-free, the FHIRPath location of the reference, never the reference string itself. ##### RESOURCE\_NOT\_MODELED > `readonly` **RESOURCE\_NOT\_MODELED**: `"RESOURCE_NOT_MODELED"` = `"RESOURCE_NOT_MODELED"` Layer 1, no schema is available for this resource type; structural layers were skipped. ##### RESOURCE\_TYPE\_UNKNOWN > `readonly` **RESOURCE\_TYPE\_UNKNOWN**: `"RESOURCE_TYPE_UNKNOWN"` = `"RESOURCE_TYPE_UNKNOWN"` Layer 1, the resource carries no `resourceType`, so it cannot be structurally validated. ##### RETRACTED\_RESOURCE > `readonly` **RETRACTED\_RESOURCE**: `"RETRACTED_RESOURCE"` = `"RETRACTED_RESOURCE"` Safety, the resource is marked `entered-in-error` and is therefore **retracted, not data**. Surfaced as `information` (it is not itself a defect) so a consumer cannot miss it. ##### TYPE\_MISMATCH > `readonly` **TYPE\_MISMATCH**: `"TYPE_MISMATCH"` = `"TYPE_MISMATCH"` Layer 1, an element's node shape (primitive / complex) is not what its datatype expects. ##### UCUM\_UNIT\_UNRECOGNIZED > `readonly` **UCUM\_UNIT\_UNRECOGNIZED**: `"UCUM_UNIT_UNRECOGNIZED"` = `"UCUM_UNIT_UNRECOGNIZED"` Quantity/UCUM, a `Quantity` claims the UCUM `system` but its `code` is absent or not a shape-valid UCUM expression, so the unit cannot be trusted for machine use. A `warning` (`value`): the value is **preserved verbatim and never converted**, the library does not bundle UCUM content, so it cannot assert the code *is* a real unit, only that it is present and well-shaped. ##### UNHANDLED\_MODIFIER\_EXTENSION > `readonly` **UNHANDLED\_MODIFIER\_EXTENSION**: `"UNHANDLED_MODIFIER_EXTENSION"` = `"UNHANDLED_MODIFIER_EXTENSION"` Safety, an element carries a `modifierExtension` this library does not understand. FHIR's `?!` rule forbids ignoring an unknown modifier, so this **fails closed** (an `error`): the element cannot be safely processed. See ./safety.js. ##### UNKNOWN\_ELEMENT > `readonly` **UNKNOWN\_ELEMENT**: `"UNKNOWN_ELEMENT"` = `"UNKNOWN_ELEMENT"` Layer 1, an element the resource's structure does not define at this location. ##### VALUE\_TYPE\_UNEXPECTED > `readonly` **VALUE\_TYPE\_UNEXPECTED**: `"VALUE_TYPE_UNEXPECTED"` = `"VALUE_TYPE_UNEXPECTED"` Quantity/UCUM, an Observation whose profile expects a numeric `Quantity` value carries a different `value[x]` variant instead (e.g. `valueString`). A `warning` (`value`): the value is preserved and surfaced by its real type, a caller must not read it as a number. ##### VITAL\_SIGN\_UNIT\_NONCONFORMANT > `readonly` **VITAL\_SIGN\_UNIT\_NONCONFORMANT**: `"VITAL_SIGN_UNIT_NONCONFORMANT"` = `"VITAL_SIGN_UNIT_NONCONFORMANT"` Quantity/UCUM, a vital-signs Observation's measured value carries a unit the FHIR vital-signs profile forbids for that LOINC code (wrong UCUM `code`, or a non-UCUM `system`). An `error` (`code-invalid`): the vital-signs profile *requires* the unit, so a nonconformant one is a profile violation, compared on the UCUM `code` (case- and bracket-sensitive), never the `unit` string. *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, synced with `package.json#version` at build time by `scripts/sync-version.mjs` (wired into the Changesets `version` script). Exported now so consumers, and the type-check pipeline, have at least one symbol to resolve through the `exports` map. #### Example ```ts import { VERSION } from "@cosyte/fhir"; console.log(VERSION); ``` *** ### VITAL\_SIGN\_OBSERVATION\_STARTER > `const` **VITAL\_SIGN\_OBSERVATION\_STARTER**: [`StructureDefinition`](#structuredefinition) A **vital-sign `Observation`** starter, grounded in `observation-vitalsigns.html` + US Core Vital Signs: `status` is required, `code` is required + must-support, and `category` is **sliced**, a required `VSCat` slice pins the `vital-signs` category coding, while the slicing stays **`open`** so an instance may *also* carry other categories (e.g. `laboratory`). This mirrors how the real vital- signs profile constrains `category` (a slice, **not** a bare pattern on the repeating element, a bare pattern would wrongly require *every* category entry to be `vital-signs` and reject a valid multi-category Observation). It exercises the profile engine's `pattern`/`$this` slicing discriminator. Fixed UCUM units per vital sign are surfaced by the quantity layer (`VITAL_SIGN_UNITS`), not re-encoded here. *** ### VITAL\_SIGN\_UNITS > `const` **VITAL\_SIGN\_UNITS**: `ReadonlyMap`\<`string`, readonly `string`[]\> The FHIR R4 **vital-signs required-unit table** (`observation-vitalsigns.html`): each vital-sign LOINC code and the exact UCUM **codes** its profile requires on `Observation.value[x]` (or the relevant `component.value[x]`). The comparison is against the UCUM `code`, case-sensitive and bracket-literal, never the `unit` display string. Panels (vital-signs panel `85353-1`, blood pressure panel `85354-9`) carry no top-level value and so are not keyed here; their measured components (e.g. systolic `8480-6`) are. This is a **closed, spec-defined** set of stable identifiers (like the status codes), not a licensed terminology table. A LOINC code absent from this table is left unchecked (a clean degrade, never a false error), and the table is the seam a later terminology change widens. *** ### VITAL\_SIGNS\_CATEGORY > `const` **VITAL\_SIGNS\_CATEGORY**: `"vital-signs"` = `"vital-signs"` The `Observation.category` code that marks an observation as a vital sign (its profile trigger). *** ### VITAL\_SIGNS\_PROFILE > `const` **VITAL\_SIGNS\_PROFILE**: `"http://hl7.org/fhir/StructureDefinition/vitalsigns"` = `"http://hl7.org/fhir/StructureDefinition/vitalsigns"` The canonical URL of the FHIR R4 vital-signs `StructureDefinition` (an alternate profile trigger). *** ### WITHHELD > `const` **WITHHELD**: `""` = `""` What a location prints in place of a name it may not echo. Deliberately carries **no length**, because the length of a refused name is itself derivable information about the content that was there. *** ### XHTML\_NAMESPACE > `const` **XHTML\_NAMESPACE**: `"http://www.w3.org/1999/xhtml"` = `"http://www.w3.org/1999/xhtml"` The XHTML namespace of a FHIR narrative `
`, which is carried whole rather than flagged. *** ### XML\_FATAL\_CODES > `const` **XML\_FATAL\_CODES**: `object` Stable string codes for the XML reader's unrecoverable fatals. The first two are the safety refusals (see the module doc); the last two are ordinary well-formedness / DoS bounds. #### Type Declaration ##### DTD\_FORBIDDEN > `readonly` **DTD\_FORBIDDEN**: `"DTD_FORBIDDEN"` = `"DTD_FORBIDDEN"` A `` declaration was present. Refused unconditionally: a DTD is where entities are declared, so refusing it closes the XXE **and** billion-laughs vectors at once (module doc). ##### MALFORMED\_XML > `readonly` **MALFORMED\_XML**: `"MALFORMED_XML"` = `"MALFORMED_XML"` The input is not well-formed XML (bad tag, mismatched close, unterminated string, …). ##### MAX\_DEPTH\_EXCEEDED > `readonly` **MAX\_DEPTH\_EXCEEDED**: `"MAX_DEPTH_EXCEEDED"` = `"MAX_DEPTH_EXCEEDED"` Element nesting deeper than the reader's fixed bound, refused as a DoS guard, never a crash. ##### UNDEFINED\_ENTITY > `readonly` **UNDEFINED\_ENTITY**: `"UNDEFINED_ENTITY"` = `"UNDEFINED_ENTITY"` An entity reference other than the five predefined (`& < > " '`) or a numeric character reference. Undefined by construction (DTDs are refused), so it is refused rather than resolved, never expanded, never fetched, never dropped. ## Functions ### absenceMarkers() > **absenceMarkers**(`resource`, `path`): [`AbsenceMarker`](#absencemarker)[] Every element the document declares an absence for, with the reason the sender spelled: the standalone form of [SafetyReadout.absenceMarkers](#absencemarkers), returning exactly what that channel carries. A source system with no data for a mandatory element cannot omit it, so it writes the element present and value-absent, carrying the R4 DataAbsentReason extension. Without this read that element is indistinguishable from one the sender never wrote: both are present-and-empty to every value reader in the package. This is the read that tells them apart, and it carries the reason, so `unknown` is distinguishable from `masked` and from `not-performed` too. A deep walk of the whole document, so a marker inside `contained` or a Bundle entry is caught with a location that names where it sits, and both wire formats are read by one predicate. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns [`AbsenceMarker`](#absencemarker)[] The readable markers, in walk order, one entry per distinct reason at a location. #### Example ```ts import { absenceMarkers, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","_status":{"extension":[{"url":' + '"http://hl7.org/fhir/StructureDefinition/data-absent-reason","valueCode":"unknown"}]}}', ); absenceMarkers(resource, "Observation"); // [{ code: "unknown", location: "Observation.status" }] ``` *** ### arrayWrappedScalars() > **arrayWrappedScalars**(`resource`, `path`): `string`[] Collect the FHIRPath locations where a `0..1` safety element arrived wrapped in a JSON array, a deep walk of the whole resource (so a resource inside `contained` or a Bundle `entry` is covered). FHIR JSON represents a `0..1` element as a name/value pair and a repeating element as an array (json.html §2.6.2.2), so an array here is non-conformant. It matters far more than that sounds: array-wrapping **every** element is ordinary generic XML-to-JSON converter output, which is exactly the route a C-CDA or v2 feed takes to a FHIR surface, and a single-value read of `{"status":["entered-in-error"]}` finds no string at all. The negation reads now see through the wrapper (../safety/codes.js `primitiveStrings`), so the retraction is no longer missed; this is the second half, the **refusal to affirm** a positive verdict over a document whose safety-bearing element the sender encoded in a shape FHIR does not define. **Scope: a resource root, and three windows that are scoped differently because they are grounded differently.** They are named as three rather than blurred into one, because a sentence that made them one window was false here for as long as the negation read was wider than the report: 1. **The cardinality table**, `SAFETY_SCALAR_ELEMENTS` on a ../safety/codes.js `SAFETY_RESOURCE_TYPES` root, plus `resourceType` on **any** root. The type scoping is not timidity: R4 defines repeating elements under these names elsewhere (`Questionnaire.code`, `ElementDefinition.code`, both `0..*`), so a name-only rule would emit a false error on a conformant document. 2. **One level down from those of them that are `CodeableConcept`-valued** (`SAFETY_CODEABLE_ELEMENTS`), at `Coding.system` / `Coding.code`. 3. **One level down from every element the negation read resolves through a `Coding`** (../safety/codes.js `NEGATION_CODE_READS`, the rows marked `codings`), at **every** resource root of **any** type -- because those reads are not type-scoped either, and a read whose report is narrower than itself resolves a clinical code out of an encoding FHIR JSON does not define with no diagnostic anywhere, or declines one in silence. The `Coding` level needs no cardinality care in either of its two windows, because `Coding` is a datatype whose `system` and `code` are `0..1` wherever it appears -- which is exactly why the un-gated window can exist and the element-level one cannot. Deciding cardinality anywhere else needs a per-resource model, which this library does not have and this layer must not grow. **Why the `Coding` level is reported even though it is now read:** ../safety/codes.js `codingsOf` reads through such a wrapper only where it holds a *single array position*, since `system` and `code` feed its `system` x `code` cross-product and a rule yielding more than one value on either side would pair values from different positions and **assert a coding the sender never wrote**, one of which is a recorded "no known allergy", a *positive* clinical assertion. So a multi-position wrapper is deliberately unread, and its location is the only thing standing between that and a `safeToSummarize: true` over a value the library declined to read. The single-position case is reported for the same reason the element-level wrapper is when its value *is* read: FHIR JSON does not define the shape, so an affirmative verdict over it is not this library's to give. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of the array-wrapped scalar elements, in **walk order**: each resource root as the walk reaches it, and within one root the cardinality table's surviving properties, then the members a repeated property name shadowed, then the negation read's `Coding` locations. **Walk order is not document order**, and no claim is made that it is: do not sort or diff a caller's expectations against the order the document wrote. #### Example ```ts import { arrayWrappedScalars, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","status":["entered-in-error"]}'); arrayWrappedScalars(resource, "Observation"); // ["Observation.status"] ``` *** ### assertSafeToSummarize() > **assertSafeToSummarize**(`resource`): `void` Assert a resource is safe to flatten/summarize, throwing [FhirSafetyError](#fhirsafetyerror) when it carries an unhandled `modifierExtension`, a modifier element, a repeated property name, an array-wrapped single-valued element, an array inside an array, dropped XML element text, a boolean-valued safety element holding a written value outside the datatype's lexical space, or a `code`-valued negation element holding a value that spells a negation code bar its case or its surrounding whitespace, or content at a position no `code` read can reach, or a `MedicationRequest.intent` that is not one of the eight R4 codes, or a `use` on an Identifier, a HumanName, an Address or a ContactPoint that is not a code of its datatype's R4 value set, or an element declaring an absence in a reason this library cannot read, or an element declaring an absence beside a value of its own. A readable, non-conflicting declared absence throws nothing, and neither does a readable `intent` or `use`. This is the executable form of "carries status **or refuses**": a summary helper calls it first, and never silently drops a modifier it cannot honor, nor summarizes an element whose value the document left ambiguous or whose content the codec could not read. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) \| [`SafetyReadout`](#safetyreadout) The resource (or a readout already computed for it). #### Returns `void` #### Throws FhirSafetyError when any of the shapes named above is present. #### Example ```ts import { assertSafeToSummarize, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Condition","clinicalStatus":{}}'); assertSafeToSummarize(resource); // ok, no unhandled modifier ``` *** ### baseSchema() > **baseSchema**(`type`): [`ResourceSchema`](#resourceschema) A base-elements-only schema for a resource type, the universally-true `Resource` / `DomainResource` elements, and nothing resource-specific. Used to validate a resource whose type is not modeled **without** emitting false "unknown element" findings for its own (unmodeled) elements, the safe degrade. #### Parameters ##### type `string` The resource type name. #### Returns [`ResourceSchema`](#resourceschema) A schema carrying only the base elements. #### Example ```ts import { baseSchema } from "@cosyte/fhir"; baseSchema("Device").elements.id; // { min: 0, max: 1, types: ["id"] } ``` *** ### buildBindingRegistry() > **buildBindingRegistry**(`extra?`): [`BindingRegistry`](#bindingregistry) Build a [BindingRegistry](#bindingregistry) from the built-in bindings plus any caller-supplied ones. A caller binding for a path replaces the built-in for that path, so a consumer can override or add element bindings (real profiles feed these). #### Parameters ##### extra? readonly [`TerminologyBinding`](#terminologybinding)[] = `[]` Additional bindings to register (override built-ins by path). #### Returns [`BindingRegistry`](#bindingregistry) A resolver from element path to its binding. #### Example ```ts import { buildBindingRegistry } from "@cosyte/fhir"; const registry = buildBindingRegistry(); registry("AllergyIntolerance.code")?.strength; // "extensible" registry("Patient.gender"); // undefined, not a terminology binding here ``` *** ### buildBundleIndex() > **buildBundleIndex**(`bundle`): [`BundleIndex`](#bundleindex) Build a [BundleIndex](#bundleindex) from a `Bundle`, keying every entry resource by both its `fullUrl` and, where derivable, a `Type/id`, so a relative, absolute, or logical reference can each find it. #### Parameters ##### bundle [`FhirComplex`](#fhircomplex) A `Bundle` resource model. #### Returns [`BundleIndex`](#bundleindex) #### Example ```ts import { parseResource, buildBundleIndex, resolveReference } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Bundle","type":"collection","entry":[' + '{"fullUrl":"https://ex/Patient/1","resource":{"resourceType":"Patient","id":"1"}}]}', ); const index = buildBundleIndex(resource); resolveReference("Patient/1", { bundle: index }).status; // "resolved" ``` *** ### buildRegistry() > **buildRegistry**(`extra?`): [`SchemaRegistry`](#schemaregistry) Build a [SchemaRegistry](#schemaregistry) from the built-in schemas plus any caller-supplied ones. A caller schema for a type replaces the built-in for that type (so a consumer can, for example, provide a resource type this package does not ship). Base elements are always merged in. #### Parameters ##### extra? readonly [`ResourceSchema`](#resourceschema)[] = `[]` Additional resource schemas to register (override built-ins by type). #### Returns [`SchemaRegistry`](#schemaregistry) A resolver from resource type to its merged schema. #### Example ```ts import { buildRegistry } from "@cosyte/fhir"; const registry = buildRegistry(); registry("Patient"); // the built-in Patient schema, base elements merged in registry("Device"); // undefined, not modeled ``` *** ### codeOf() > **codeOf**(`node`, `preferredSystem?`): `string` \| `undefined` The first `code` on a `CodeableConcept` node, preferring a coding in `preferredSystem` when one is given. Used to surface a `clinicalStatus` / `verificationStatus` value without a typed model. #### Parameters ##### node [`FhirNode`](#fhirnode) \| `undefined` A `CodeableConcept` node (or list), or `undefined`. ##### preferredSystem? `string` A system to prefer a coding from, when several are present. #### Returns `string` \| `undefined` The chosen code, or `undefined` when there is none. #### Example ```ts import { codeOf, getProperty, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Condition","clinicalStatus":{"coding":[{"code":"active"}]}}', ); codeOf(getProperty(resource, "clinicalStatus")); // "active" ``` *** ### codingsOf() > **codingsOf**(`node`): [`Coded`](#coded)[] Every `Coding` reachable from a node that is a `CodeableConcept` (or a list of them). Flattens a repeating element (e.g. `Condition.category`) and tolerates a `CodeableConcept` with no `coding`. Read across **every** value a non-conformant document wrote: all `coding` members, and every `system` x `code` combination inside one `Coding` that repeated either name. A conformant `Coding` has one `system` and one `code`, so it yields exactly one pair and this is a no-op there. Each `system` / `code` member contributes **at most one value** (codingScalar), read through a single-position array wrapper but never through a multi-position one. That bound is what makes the wrapper safe to read at all: it holds the cross-product to one pair per (`system` member, `code` member) combination, so this **never invents a `(system, code)` pair the sender did not write**. A multi-position wrapper is left unread on purpose, and reported instead. Precisely: **a single-position wrapper is transparent.** The pairs this yields for a document are exactly the pairs it yields for the same document with those wrappers removed. So unwrapping decides nothing on its own; it restores the reading the sender's pre-conversion document had. Two consequences, both confined to a document that repeated a name, which is a document ../validate/safety.js already reports invalid and ./status.js already refuses to summarize. **(a)** Repeating a name only ever *adds* pairs, so a check asking "is this code present" (a retraction, a refutation) over-reports rather than misses, which is the direction the safety layer wants. A check asking the opposite, "is the required code absent" (`con-3`, `con-4`, `ait-1`), can therefore be *suppressed* by an added pair. **(b)** When a `Coding` repeated **both** names the pairing is genuinely unrecoverable, so a combination the sender never wrote can appear, and [codeOf](#codeof) with a preferred system may select it. Neither is a silent read: the caller already has the `DUPLICATE_PROPERTY` location. Transparency means a wrapper adds no *new* case here: a wrapped repeated name reads as the unwrapped repeated name already did, and the invention is the repetition's, not the wrapper's. Reading a wrapper can also *remove* a finding, and that is the same effect from the other side: a `verificationStatus` of `entered-in-error` written inside a wrapper satisfies `ait-1` and it is the unread version that emitted the false error. It cannot turn a document valid, because the wrapper that made the value readable here is itself an `ARRAY_WRAPPED_SCALAR` error on the very same `Coding` (./status.js `arrayWrappedScalars`). #### Parameters ##### node [`FhirNode`](#fhirnode) \| `undefined` A `CodeableConcept` node, a list of them, or `undefined`. #### Returns [`Coded`](#coded)[] The `(system, code)` pairs, in document order. #### Example ```ts import { codingsOf, getProperty, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Condition","clinicalStatus":{"coding":[{"system":"s","code":"active"}]}}', ); codingsOf(getProperty(resource, "clinicalStatus")); // [{ system: "s", code: "active" }] ``` *** ### collectBundleIssues() > **collectBundleIssues**(`resource`): [`ValidationIssue`](#validationissue)[] Collect the Bundle-integrity findings for a `Bundle` resource. Returns an empty list for a non-Bundle resource (the caller keys this off `resourceType`) and for a clean Bundle. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model (a `Bundle`). #### Returns [`ValidationIssue`](#validationissue)[] The value-free [ValidationIssue](#validationissue)s, in document order. #### Example ```ts import { parseResource, collectBundleIssues } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Bundle","type":"collection","entry":[' + '{"fullUrl":"https://ex/Patient/1","resource":{"resourceType":"Patient","id":"2"}}]}', ); collectBundleIssues(resource).map((i) => i.code); // ["FULLURL_ID_MISMATCH"] ``` *** ### collectInvariantIssues() > **collectInvariantIssues**(`resource`, `profile`, `options?`): [`ValidationIssue`](#validationissue)[] Collect every invariant finding for a resource validated against one profile. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### profile [`StructureDefinition`](#structuredefinition) The profile whose `constraint`s to evaluate. ##### options? [`InvariantOptions`](#invariantoptions) = `{}` Optional base resolver for snapshot generation. #### Returns [`ValidationIssue`](#validationissue)[] The value-free invariant [ValidationIssue](#validationissue)s (`INVARIANT_VIOLATED` / `INVARIANT_UNCHECKED`). Empty when the profile does not apply, carries no constraints, or the resource satisfies them all. #### Example ```ts import { collectInvariantIssues, loadStructureDefinition, parseResource } from "@cosyte/fhir"; const profile = loadStructureDefinition(parseResource(usCoreProfileJson).resource); const issues = collectInvariantIssues(parseResource(instanceJson).resource, profile); ``` *** ### collectProfileIssues() > **collectProfileIssues**(`resource`, `profile`, `options?`): [`ValidationIssue`](#validationissue)[] Collect every profile-conformance finding for a resource validated against one profile. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### profile [`StructureDefinition`](#structuredefinition) The profile (`StructureDefinition`) to validate against. ##### options? [`ProfileOptions`](#profileoptions) = `{}` Optional base resolver for snapshot generation. #### Returns [`ValidationIssue`](#validationissue)[] The value-free profile [ValidationIssue](#validationissue)s. Empty when the profile does not apply to this resource type or the resource conforms. #### Example ```ts import { collectProfileIssues, loadStructureDefinition, parseResource } from "@cosyte/fhir"; const profile = loadStructureDefinition(parseResource(usCoreAllergyJson).resource); const issues = collectProfileIssues(parseResource(allergyJson).resource, profile); ``` *** ### collectProfileVersionIssues() > **collectProfileVersionIssues**(`resource`, `profiles`): [`ValidationIssue`](#validationissue)[] Collect `PROFILE_VERSION_MISMATCH` findings by comparing the resource's declared `meta.profile` canonicals against the supplied profile set. A declared `canonical|version` whose canonical is supplied at a **different** version is flagged (`warning`): flagging an unknown profile version rather than silently validating against a different one. A canonical that is not supplied at all is *not* flagged here (it simply was not validated), and a declaration with no version pin never mismatches. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### profiles readonly [`StructureDefinition`](#structuredefinition)[] The supplied profiles (their `url` + `version` form the known set). #### Returns [`ValidationIssue`](#validationissue)[] The value-free version-mismatch issues. #### Example ```ts import { collectProfileVersionIssues } from "@cosyte/fhir"; // resource.meta.profile = ["http://…/us-core-patient|3.1.1"], supplied profile is version 6.1.0: collectProfileVersionIssues(resource, [usCorePatient610]); // → one PROFILE_VERSION_MISMATCH ``` *** ### collectQuantityIssues() > **collectQuantityIssues**(`resource`, `rt`): [`ValidationIssue`](#validationissue)[] Collect every Quantity/UCUM finding for a resource: UCUM shape on Observation values / dose quantities, and the vital-signs required-unit conformance. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### rt `string` Its resolved `resourceType`. #### Returns [`ValidationIssue`](#validationissue)[] The value-free Quantity [ValidationIssue](#validationissue)s, in document order. #### Example ```ts import { collectQuantityIssues, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","category":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/observation-category","code":"vital-signs"}]}],' + '"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]},' + '"valueQuantity":{"value":120,"system":"http://unitsofmeasure.org","code":"mmHg"}}', ); collectQuantityIssues(resource, "Observation"); // → one VITAL_SIGN_UNIT_NONCONFORMANT ("mmHg" ≠ "mm[Hg]") ``` *** ### collectSafetyIssues() > **collectSafetyIssues**(`resource`, `rt`): [`ValidationIssue`](#validationissue)[] Collect every safety finding for a resource: fail-closed modifier extensions and repeated property names (both universal), the `entered-in-error` retraction note, and the named invariants (for the types `SAFETY_RESOURCE_TYPES` names). #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### rt `string` Its resolved `resourceType` (the caller has already established it is present). #### Returns [`ValidationIssue`](#validationissue)[] The value-free safety [ValidationIssue](#validationissue)s, in a stable order. #### Example ```ts import { collectSafetyIssues, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","status":"entered-in-error"}'); collectSafetyIssues(resource, "Observation"); // → one RETRACTED_RESOURCE issue ``` *** ### collectTerminologyIssues() > **collectTerminologyIssues**(`resource`, `rt`, `options?`): [`ValidationIssue`](#validationissue)[] Collect every terminology binding finding for a resource: content-free system checks on each bound coding, plus value-set membership when a terminology service is supplied. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### rt `string` Its resolved `resourceType`. ##### options? [`TerminologyOptions`](#terminologyoptions) = `{}` The optional terminology service and extra bindings. #### Returns [`ValidationIssue`](#validationissue)[] The value-free terminology [ValidationIssue](#validationissue)s, in document order. #### Example ```ts import { collectTerminologyIssues, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"AllergyIntolerance",' + '"code":{"coding":[{"system":"http://hl7.org/fhir/sid/icd-10-cm","code":"T78.40XA"}]}}', ); // Extensible binding (RxNorm + SNOMED), ICD-10-CM is a known but unexpected system → one warning. collectTerminologyIssues(resource, "AllergyIntolerance"); ``` *** ### complex() > **complex**(`properties`, `duplicates?`): [`FhirComplex`](#fhircomplex) Construct a [FhirComplex](#fhircomplex) from ordered properties, optionally carrying the members a repeated property name shadowed. #### Parameters ##### properties readonly [`FhirProperty`](#fhirproperty)[] The named properties, in wire order, at most one per name. ##### duplicates? readonly [`FhirProperty`](#fhirproperty)[] = `[]` Members shadowed by a repeated name. Omitted (not set to `undefined`) when empty, so a conformant node stays structurally equal to one built without the argument. #### Returns [`FhirComplex`](#fhircomplex) #### Example ```ts import { complex, primitive } from "@cosyte/fhir"; const patient = complex([{ name: "resourceType", value: primitive("Patient") }]); ``` *** ### conflictingAbsenceMarkers() > **conflictingAbsenceMarkers**(`resource`, `path`): `string`[] The locations where an element carries an absence marker and a value of its own: the standalone form of [SafetyReadout.conflictingAbsenceMarkers](#conflictingabsencemarkers). The document says two contradictory things about one element and this library ranks neither. Both survive on the readout, and this location is what stops a caller preferring whichever its own read reached first. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations, in walk order, each once however many markers sit there. #### Example ```ts import { conflictingAbsenceMarkers, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","status":"final","_status":{"extension":[{"url":' + '"http://hl7.org/fhir/StructureDefinition/data-absent-reason","valueCode":"unknown"}]}}', ); conflictingAbsenceMarkers(resource, "Observation"); // ["Observation.status"] ``` *** ### containedIndex() > **containedIndex**(`resource`): [`ContainedIndex`](#containedindex) Build a [ContainedIndex](#containedindex) for `#fragment` resolution against a resource's `contained` set. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The containing resource model. #### Returns [`ContainedIndex`](#containedindex) #### Example ```ts import { parseResource, containedIndex, resolveReference } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","contained":[{"resourceType":"Patient","id":"p1"}],' + '"subject":{"reference":"#p1"}}', ); const contained = containedIndex(resource); resolveReference("#p1", { contained }).status; // "resolved" ``` *** ### convertToBoolean() > **convertToBoolean**(`coll`): `boolean` FHIRPath boolean coercion, matching the reference validator: an empty collection is `false`, a single boolean is itself, any other single item is `true`, and a multi-item collection is `true`. #### Parameters ##### coll [`FpColl`](#fpcoll) The collection to coerce. #### Returns `boolean` The boolean an invariant result (or a `where` criteria) is judged by. #### Example ```ts import { convertToBoolean } from "@cosyte/fhir"; convertToBoolean([]); // false, an empty result fails a constraint, never silently passes ``` *** ### decimal() > **decimal**(`raw`): [`FhirDecimal`](#fhirdecimal) Construct a [FhirDecimal](#fhirdecimal) from its exact lexical text, validating that the text is a JSON number. Throws a `TypeError` on anything else, a decimal primitive can only hold a number literal, and accepting arbitrary text here would defeat the whole point of the type. #### Parameters ##### raw `string` The exact decimal literal, e.g. `"0.010"`, `"-3.14"`, `"1e3"`, `"42"`. #### Returns [`FhirDecimal`](#fhirdecimal) #### Throws TypeError when `raw` is not a valid JSON number. #### Example ```ts import { decimal } from "@cosyte/fhir"; const weight = decimal("70.0"); // one-decimal-place precision, preserved ``` *** ### decimalPrecisionAtRisk() > **decimalPrecisionAtRisk**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.DECIMAL\_PRECISION\_AT\_RISK](#decimal_precision_at_risk) issue at `expression`. #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { decimalPrecisionAtRisk } from "@cosyte/fhir"; const issue = decimalPrecisionAtRisk("Observation.valueQuantity.value"); ``` *** ### defineProfile() > **defineProfile**(`spec`): [`StructureDefinition`](#structuredefinition) Author a [StructureDefinition](#structuredefinition) programmatically from an ergonomic [ProfileSpec](#profilespec). The result is the *same* model the profile engine consumes, pass it straight to `validateResource(resource, { profiles: [defineProfile(spec)] })`. It is identical to what `loadStructureDefinition` produces from the equivalent FHIR `StructureDefinition` JSON: one model, two authoring routes. #### Parameters ##### spec [`ProfileSpec`](#profilespec) The ergonomic profile spec. #### Returns [`StructureDefinition`](#structuredefinition) The modeled [StructureDefinition](#structuredefinition). #### Throws InvalidProfileError when the spec is malformed (missing `url`/`type`/element `path`, a bad cardinality, or a `max` below `min`). #### Example ```ts import { defineProfile, primitive, validateResource, parseResource } from "@cosyte/fhir"; const finalOnly = defineProfile({ url: "http://example.org/StructureDefinition/final-observation", type: "Observation", differential: [{ path: "Observation.status", fixed: { type: "Code", value: primitive("final") } }], }); const { resource } = parseResource('{"resourceType":"Observation","status":"preliminary"}'); validateResource(resource, { profiles: [finalOnly] }); // → one PROFILE_FIXED_MISMATCH ``` *** ### diagnosticFor() > **diagnosticFor**(`code`): `string` The value-free diagnostic line for a code, the only text that reaches an `OperationOutcome`. #### Parameters ##### code [`ValidationCode`](#validationcode) The validation code. #### Returns `string` A description of the *kind* of problem, guaranteed free of any instance value. #### Example ```ts import { diagnosticFor } from "@cosyte/fhir"; diagnosticFor("CARDINALITY_MIN"); // "Required element is missing." ``` *** ### droppedText() > **droppedText**(`resource`, `path`): `string`[] The locations where an XML document wrote **character data directly on a FHIR element**, which the reader drops: a primitive's value travels in the `value` attribute (xml.html §2.6.1), so text written as element content has no slot on the model. A deep walk of the whole resource, so text on an element inside a backbone element or a contained resource is caught too. **This layer reports these; it does not read them.** The dropped text is not kept anywhere, and this location is the only thing that distinguishes such a position from an element the sender genuinely wrote without a value. Without it `entered-in-error` reads back as an ordinary absent `status` and the readout affirms over a retracted record. Reporting is deliberately the entire remedy: reading the text as the element's value would be a **tolerance for a non-conformant encoding**, a decision about what this reader accepts, which is a much larger change than declining to affirm. Empty for every conformant document, and for every document read from JSON, which has no character-data channel. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of the dropped text, in walk order, each location once however many marked nodes sit at it, on the same reasoning as [nestedArrays](#nestedarrays-1). #### Example ```ts import { droppedText, parseResourceXml } from "@cosyte/fhir"; const { resource } = parseResourceXml( 'entered-in-error', ); droppedText(resource, "Observation"); // ["Observation.status"] ``` *** ### duplicateProperty() > **duplicateProperty**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.DUPLICATE\_PROPERTY](#duplicate_property) issue at `expression`. The location names the element, not the individual member: FHIRPath addresses elements, and a repeated JSON name is not addressable, so both the surviving and the shadowed member report here. #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { duplicateProperty } from "@cosyte/fhir"; const issue = duplicateProperty("Observation.status"); ``` *** ### entryProcessing() > **entryProcessing**(`type`): [`EntryProcessing`](#entryprocessing) The entry-processing semantics for a `Bundle.type` (bundle.html). This is the all-or-nothing (`transaction`) vs independent (`batch`) distinction, modeled explicitly so a caller never has to re-derive it, and never conflates the two. #### Parameters ##### type `string` \| `undefined` The `Bundle.type` code (or any string; unknown types are `"none"`). #### Returns [`EntryProcessing`](#entryprocessing) `"atomic"` for `transaction`, `"independent"` for `batch`, `"none"` otherwise. #### Example ```ts import { entryProcessing } from "@cosyte/fhir"; entryProcessing("transaction"); // "atomic" , all-or-nothing entryProcessing("batch"); // "independent", entries stand alone entryProcessing("searchset"); // "none" , not a processing request ``` *** ### evaluateInvariant() > **evaluateInvariant**(`expression`, `focus`, `resource`): [`InvariantResult`](#invariantresult) Evaluate one FHIRPath invariant `expression` against a focus node. The result is judged by [convertToBoolean](#converttoboolean) (empty → not satisfied), matching the reference validator's coercion. **Fail-safe:** any [UnsupportedFhirPathError](#unsupportedfhirpatherror), or any other evaluation error, yields `{ unchecked: true, satisfied: false }`; the engine never reports a constraint *satisfied* on a failure, so an unevaluable expression is surfaced as unchecked, never a false pass. #### Parameters ##### expression `string` The FHIRPath constraint expression (e.g. `dataAbsentReason.empty() or value.empty()`). ##### focus [`FhirComplex`](#fhircomplex) The node the constraint is anchored to (the resource, or an element occurrence). ##### resource [`FhirNode`](#fhirnode) The root resource, bound to `%resource` / `%rootResource` inside the expression. #### Returns [`InvariantResult`](#invariantresult) The [InvariantResult](#invariantresult). #### Example ```ts import { evaluateInvariant, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","valueString":"x","dataAbsentReason":{}}'); evaluateInvariant("dataAbsentReason.empty() or value.empty()", resource, resource); // → { unchecked: false, satisfied: false } (obs-6 violated: both present) ``` *** ### generateSnapshot() > **generateSnapshot**(`profile`, `resolve`, `seen?`): [`ElementDefinition`](#elementdefinition)[] Generate the snapshot element list for a profile: the base resource's snapshot with the profile's differential overlaid. When the profile already carries a snapshot it is returned as-is. #### Parameters ##### profile [`StructureDefinition`](#structuredefinition) The profile (or base resource) StructureDefinition. ##### resolve [`BaseResolver`](#baseresolver) Resolver for `baseDefinition` canonical URLs (a base R4 SD carries its own snapshot). ##### seen? `ReadonlySet`\<`string`\> = `...` Internal cycle guard; omit at the top level. #### Returns [`ElementDefinition`](#elementdefinition)[] The flattened, constraint-applied element list. #### Throws FhirProfileError when the base cannot be resolved, or a `baseDefinition` cycle is found. #### Example ```ts import { generateSnapshot } from "@cosyte/fhir"; // base carries a snapshot; profile carries only a differential tightening one element: const snapshot = generateSnapshot(profile, (url) => (url === base.url ? base : undefined)); ``` *** ### getAllProperties() > **getAllProperties**(`node`, `name`): readonly [`FhirNode`](#fhirnode)[] Every top-level value written under `name`, in document order: the one in `properties` followed by any that a repeated name shadowed. Returns one element for a conformant document, none when the property is absent, and more than one only for a document that broke FHIR's unique-name rule. This is the fail-safe read. A check that must not miss a value the sender wrote (a retraction, a negation) runs over all of them; a convenience read that only needs one uses [getProperty](#getproperty). #### Parameters ##### node [`FhirComplex`](#fhircomplex) ##### name `string` #### Returns readonly [`FhirNode`](#fhirnode)[] #### Example ```ts import { getAllProperties, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","status":"final","status":"entered-in-error"}'); getAllProperties(resource, "status").length; // 2, both values are readable ``` *** ### getProperty() > **getProperty**(`node`, `name`): [`FhirNode`](#fhirnode) \| `undefined` Look up a top-level property on a complex node by name, returning the first match. Returns `undefined` when absent. FHIR JSON requires property names to be unique, so on a conformant document there is exactly one match. On a document that repeats a name this returns the **first** one written and ignores the rest; use [getAllProperties](#getallproperties) wherever reading only one of several written values would be unsafe. #### Parameters ##### node [`FhirComplex`](#fhircomplex) ##### name `string` #### Returns [`FhirNode`](#fhirnode) \| `undefined` #### Example ```ts import { getProperty, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","active":true}'); getProperty(resource, "active"); // the `active` primitive node ``` *** ### hasCodeAnySystem() > **hasCodeAnySystem**(`node`, `code`): `boolean` Whether a `CodeableConcept` node carries the given `code` under **any** system (fail-safe read). #### Parameters ##### node [`FhirNode`](#fhirnode) \| `undefined` A `CodeableConcept` node (or list), or `undefined`. ##### code `string` The code to match, regardless of system. #### Returns `boolean` `true` when any coding carries that code. #### Example ```ts import { ENTERED_IN_ERROR, getProperty, hasCodeAnySystem, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Condition","verificationStatus":{"coding":[{"code":"entered-in-error"}]}}', ); hasCodeAnySystem(getProperty(resource, "verificationStatus"), ENTERED_IN_ERROR); // true ``` *** ### hasCoding() > **hasCoding**(`node`, `system`, `code`): `boolean` Whether a `CodeableConcept` node carries the given `(system, code)` coding exactly. #### Parameters ##### node [`FhirNode`](#fhirnode) \| `undefined` A `CodeableConcept` node (or list), or `undefined`. ##### system `string` The code system URI to match. ##### code `string` The code to match. #### Returns `boolean` `true` when a coding with that exact system and code is present. #### Example ```ts import { getProperty, hasCoding, parseResource, SNOMED_SCT } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"AllergyIntolerance","code":{"coding":[{"system":"http://snomed.info/sct","code":"716186003"}]}}', ); hasCoding(getProperty(resource, "code"), SNOMED_SCT, "716186003"); // true ``` *** ### hasContainedCycle() > **hasContainedCycle**(`resource`): `boolean` Whether a resource's `contained` resources reference each other (or the root) in a **cycle**. Builds the fragment graph, one node per contained resource id plus the root (`""`), an edge for each `#fragment` reference, and runs an iterative three-color DFS. Because the DFS is heap-based (not recursive) and marks visited nodes, it **always terminates**: a cycle is reported, never followed. This is the DoS guard: a reference cycle becomes a typed `CONTAINED_CYCLE` finding, never an infinite loop or a stack overflow. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource whose `contained` set to check. #### Returns `boolean` `true` when a containment cycle exists. #### Example ```ts import { parseResource, hasContainedCycle } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","contained":[' + '{"resourceType":"Observation","id":"a","hasMember":[{"reference":"#b"}]},' + '{"resourceType":"Observation","id":"b","hasMember":[{"reference":"#a"}]}]}', ); hasContainedCycle(resource); // true, a → b → a ``` *** ### integer64() > **integer64**(`raw`): [`FhirInteger64`](#fhirinteger64) Construct a [FhirInteger64](#fhirinteger64) from its lexical text, validating both the signed-integer grammar and the 64-bit range. Throws a `TypeError`/`RangeError` on anything else. #### Parameters ##### raw `string` The exact integer literal, e.g. `"9223372036854775807"`, `"-42"`. #### Returns [`FhirInteger64`](#fhirinteger64) #### Throws TypeError when `raw` is not a signed-integer literal. #### Throws RangeError when `raw` is outside the signed 64-bit range. #### Example ```ts import { integer64 } from "@cosyte/fhir"; const n = integer64("-9223372036854775808"); // the 64-bit minimum, exact ``` *** ### isAbsenceCode() > **isAbsenceCode**(`value`): value is "error" \| "unknown" \| "asked-unknown" \| "temp-unknown" \| "not-asked" \| "asked-declined" \| "masked" \| "not-applicable" \| "unsupported" \| "as-text" \| "not-a-number" \| "negative-infinity" \| "positive-infinity" \| "not-performed" \| "not-permitted" Whether a string is exactly one of the fifteen [ABSENCE\_CODES](#absence_codes). The comparison is exact. A case or whitespace variant is **not** a member and is not made into one: FHIR `code` is case-sensitive and its lexical space excludes surrounding whitespace, so folding `"UNKNOWN"` in would accept a non-conformant document as conformant and author a reason the sender did not spell. #### Parameters ##### value `string` Any string. #### Returns value is "error" \| "unknown" \| "asked-unknown" \| "temp-unknown" \| "not-asked" \| "asked-declined" \| "masked" \| "not-applicable" \| "unsupported" \| "as-text" \| "not-a-number" \| "negative-infinity" \| "positive-infinity" \| "not-performed" \| "not-permitted" `true` when the string is a member of the value set. #### Example ```ts import { isAbsenceCode } from "@cosyte/fhir"; isAbsenceCode("not-performed"); // true isAbsenceCode("UNKNOWN"); // false, never coerced ``` *** ### isAtomicBundle() > **isAtomicBundle**(`type`): `boolean` Whether a `Bundle.type` is applied **all-or-nothing** (a `transaction`). The inverse of "entries are independent"; a convenience over [entryProcessing](#entryprocessing-1). #### Parameters ##### type `string` \| `undefined` #### Returns `boolean` #### Example ```ts import { isAtomicBundle } from "@cosyte/fhir"; isAtomicBundle("transaction"); // true isAtomicBundle("batch"); // false ``` *** ### isChoice() > **isChoice**(`element`): `boolean` Whether an element is a `choice[x]` (more than one allowed datatype). #### Parameters ##### element [`ElementSchema`](#elementschema) An element schema. #### Returns `boolean` `true` when the element allows more than one datatype. #### Example ```ts import { isChoice } from "@cosyte/fhir"; isChoice({ min: 0, max: 1, types: ["boolean", "dateTime"] }); // true ``` *** ### isComplex() > **isComplex**(`node`): `node is FhirComplex` Whether `node` is a [FhirComplex](#fhircomplex). #### Parameters ##### node [`FhirNode`](#fhirnode) #### Returns `node is FhirComplex` #### Example ```ts import { complex, isComplex } from "@cosyte/fhir"; isComplex(complex([])); // true ``` *** ### isDroppedText() > **isDroppedText**(`node`): `boolean` Whether the XML document wrote **character data directly on this element**, content a FHIR element has no slot for and the reader therefore drops. A primitive's value travels in the `value` attribute (xml.html §2.6.1: "values of primitive types in a `value` attribute"), so `entered-in-error` is not a `status` this library can read: the attribute is absent, the model's `value` is `undefined`, and the node is then indistinguishable from an element the sender legitimately wrote without one. That is the harm this marker exists to make reportable. The safety readout collects these locations (`droppedText`), refuses to summarize, and the validator raises an error. The text is **not** kept and **not** interpreted. Reading it as the element's value would be a tolerance for a non-conformant encoding, a separate decision from declining to affirm over it. Always `false` for a document read from JSON, which has no character-data channel, and for every conformant XML document. #### Parameters ##### node [`FhirNode`](#fhirnode) Any model node. #### Returns `boolean` `true` when the reader dropped character data at this node's position. #### Example ```ts import { getProperty, isDroppedText, parseResourceXml } from "@cosyte/fhir"; const { resource } = parseResourceXml( 'entered-in-error', ); isDroppedText(getProperty(resource, "status")!); // true ``` *** ### isForeignRoot() > **isForeignRoot**(`node`): `boolean` Whether the XML document's **root** element resolved to a vocabulary other than FHIR's: a vendor namespace reached by a default declaration or by a bound prefix. The reader models such a root as the FHIR resource its local name spells and reports the position, because it is schema-free and lenient and the local name is the only thing it has to go on. That report is a warning in the issue list and the model carries no issue list, so before this marker existed a caller could write the model back to XML and get a document in the FHIR namespace whose re-read said nothing at all: one round trip and a vendor document was indistinguishable from FHIR. ../xml/write.js serializeResourceXml refuses a marked model rather than emit that. **The namespace itself is NOT kept**, which is the difference between this and recovering the vocabulary. The document's own namespace URI is content, and a marker cannot leak content. `false` for a root that declares no namespace (read as FHIR on purpose), for a root whose prefix resolves to nothing (modeled under its verbatim tag, and refused by the XML writer at its tag sites rather than through this marker), for foreign content below a root, for every document read from JSON, and for every conformant document. #### Parameters ##### node [`FhirNode`](#fhirnode) Any model node. #### Returns `boolean` `true` when this node is a root the XML reader read out of another vocabulary. #### Example ```ts import { isForeignRoot, parseResourceXml } from "@cosyte/fhir"; const { resource } = parseResourceXml(''); isForeignRoot(resource); // true ``` *** ### isKnownSystem() > **isKnownSystem**(`system`): `boolean` Whether a code-`system` URI is in the frozen [KNOWN\_SYSTEMS](#known_systems) registry. An **unknown** system is not an error, it may be a legitimate local/proprietary system, it merely means the library cannot reason about codes drawn from it (fail-safe). #### Parameters ##### system `string` A code-system URI. #### Returns `boolean` `true` when the URI is a recognized code system. #### Example ```ts import { isKnownSystem } from "@cosyte/fhir"; isKnownSystem("http://loinc.org"); // true isKnownSystem("http://example.org/local"); // false, unknown, not invalid ``` *** ### isList() > **isList**(`node`): `node is FhirList` Whether `node` is a [FhirList](#fhirlist). #### Parameters ##### node [`FhirNode`](#fhirnode) #### Returns `node is FhirList` #### Example ```ts import { isList, list } from "@cosyte/fhir"; isList(list([])); // true ``` *** ### isNestedArray() > **isNestedArray**(`node`): `boolean` Whether `node` sits where the JSON document wrote an **array inside an array**, a shape FHIR JSON gives no meaning at any position (json.html §2.6.2.2 uses an array for a repeating element and nothing else, so no element is ever a list of lists). The reader does not model what was inside that array **as FHIR**, so this node is the same empty element it would be without the marker and no walker sees anything there. What the marker buys is that the loss is **reportable**: a document carrying one must never come back with an affirmative safety verdict computed as though nothing had been there. The safety readout collects these locations (`nestedArrays`), refuses to summarize, and the validator raises an error. The content itself is **kept** and is read with [nestedArrayContent](#nestedarraycontent-1). It are deliberately not reachable through `properties`, `items` or `extension`: an array inside an array has no FHIR meaning at any position, so there is no element for it to be, and placing one in the tree would change what a repeating element contains for every consumer that walks one. Always `false` for a document read from XML, which has no way to express the shape, and for every conformant JSON document. #### Parameters ##### node [`FhirNode`](#fhirnode) #### Returns `boolean` #### Example ```ts import { isNestedArray, parseResource, getProperty, isList } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","name":[[{"family":"Roe"}]]}'); const name = getProperty(resource, "name"); isList(name!) && isNestedArray(name.items[0]!); // true ``` *** ### isPrimitive() > **isPrimitive**(`node`): `node is FhirPrimitive` Whether `node` is a [FhirPrimitive](#fhirprimitive). #### Parameters ##### node [`FhirNode`](#fhirnode) #### Returns `node is FhirPrimitive` #### Example ```ts import { isPrimitive, primitive } from "@cosyte/fhir"; isPrimitive(primitive("x")); // true ``` *** ### isPrimitiveType() > **isPrimitiveType**(`name`): name is "string" \| "boolean" \| "integer" \| "integer64" \| "unsignedInt" \| "positiveInt" \| "decimal" \| "code" \| "markdown" \| "id" \| "uri" \| "url" \| "canonical" \| "oid" \| "uuid" \| "base64Binary" \| "instant" \| "date" \| "dateTime" \| "time" Whether `name` is a known FHIR R4 primitive datatype. #### Parameters ##### name `string` A datatype name. #### Returns name is "string" \| "boolean" \| "integer" \| "integer64" \| "unsignedInt" \| "positiveInt" \| "decimal" \| "code" \| "markdown" \| "id" \| "uri" \| "url" \| "canonical" \| "oid" \| "uuid" \| "base64Binary" \| "instant" \| "date" \| "dateTime" \| "time" `true` for a primitive type name, `false` for a complex one. #### Example ```ts import { isPrimitiveType } from "@cosyte/fhir"; isPrimitiveType("date"); // true isPrimitiveType("HumanName"); // false ``` *** ### isRetracted() > **isRetracted**(`resource`): `boolean` Whether a resource is **retracted**, marked `entered-in-error` and therefore not to be treated as active data. Read fail-safe: a `status` primitive of `entered-in-error` (Observation, Immunization, DiagnosticReport, MedicationRequest/Statement) **or** a `verificationStatus` carrying `entered-in-error` under any system (AllergyIntolerance, Condition). Over-surfacing a retraction is safe; missing one is not. "Fail-safe" is read across **every** value the document wrote for those elements, not just the one a single-value lookup returns, and **through an array wrapper around the element** (primitiveStrings). Three documents motivate that and they are one hazard: a `CodeableConcept` legitimately carries several codings and the retraction may not be in the first; a non-conformant document may write `status` twice and put the retraction in the one that lost; and a generic XML-to-JSON converter wraps the `0..1` `status` in an array, where a single-value read finds no string at all. Each ends the same way: reading one of several written values, or none, and reporting the record as live. That includes an array around a `Coding.system` / `Coding.code` *inside* a `CodeableConcept`, which is the same converter shape one level down. It is read where the wrapper holds a single array position, which is the only shape in which the value the sender wrote is recoverable without inventing a `(system, code)` pair; a multi-position wrapper is reported rather than guessed at. See [codingsOf](#codingsof). **It answers about the resource it is handed, never about one nested inside it.** A `Bundle` whose entry is retracted is not itself retracted, so this stays `false` there; the safety walk applies this same read at every resource root and puts `entered-in-error` on `SafetyReadout.negations` (./status.js `checkNegations`), which is the read that covers a whole document. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. #### Returns `boolean` `true` when the resource is marked entered-in-error. #### Example ```ts import { isRetracted, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","status":"entered-in-error"}'); isRetracted(resource); // true ``` *** ### isUndefinedNull() > **isUndefinedNull**(`node`): `boolean` Whether the JSON document wrote a bare `null` in this primitive's **value** channel at a position FHIR JSON does not define one. FHIR JSON defines `null` in one place only: as padding in a repeating primitive's value array, so that the array lines up index-by-index with the `_`-sibling array carrying that occurrence's `id`/`extension` (json.html §2.6.2.3). A `null` outside that array, or one whose slot carries no such metadata, pads nothing, and leaves an element with neither a value nor children, which R4 `ele-1` requires one of. This marks those and only those, so a padding `null` in a conformant document is never marked. The node is the same value-absent primitive it would be without the marker, and nothing is preserved on it, because a `null` carries no content to preserve: it is a non-conformant encoding of an absent value, not content the reader failed to read. That is why this marker does **not** refuse a safety summary the way [isNestedArray](#isnestedarray) and [isDroppedText](#isdroppedtext) do, both of which mark content the reader could not read at all. What it buys is that the writer hands the `null` back rather than omitting the member, so re-reading the output reproduces the ../codec/issues.js ISSUE\_CODES.UNDEFINED\_JSON\_NULL finding instead of losing it. Always `false` for a document read from XML, which has no `null`, and for every conformant JSON document. #### Parameters ##### node [`FhirNode`](#fhirnode) Any model node. #### Returns `boolean` `true` when the document wrote an undefined `null` at this node's position. #### Example ```ts import { getProperty, isUndefinedNull, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","status":null}'); isUndefinedNull(getProperty(resource, "status")!); // true ``` *** ### list() > **list**(`items`): [`FhirList`](#fhirlist) Construct a [FhirList](#fhirlist) from ordered items. #### Parameters ##### items readonly [`FhirNode`](#fhirnode)[] #### Returns [`FhirList`](#fhirlist) #### Example ```ts import { list, primitive } from "@cosyte/fhir"; const given = list([primitive("Jane"), primitive("Q")]); ``` *** ### loadStructureDefinition() > **loadStructureDefinition**(`resource`): [`StructureDefinition`](#structuredefinition) \| `undefined` Load a [StructureDefinition](#structuredefinition) out of a parsed FHIR `StructureDefinition` resource model. Reads the identity, derivation, and the `differential` / `snapshot` element lists. Lenient: a field the validator does not act on is ignored, and a malformed sub-node degrades to `undefined` rather than throwing, a profile a consumer supplies is data, and data is read Postel-style. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) A `StructureDefinition` resource model (e.g. from `parseResource`). #### Returns [`StructureDefinition`](#structuredefinition) \| `undefined` The modeled [StructureDefinition](#structuredefinition), or `undefined` when the resource is not one / lacks a `type`. #### Example ```ts import { parseResource } from "@cosyte/fhir"; import { loadStructureDefinition } from "@cosyte/fhir"; const { resource } = parseResource(usCoreAllergyProfileJson); const sd = loadStructureDefinition(resource); // → { url, type: "AllergyIntolerance", differential, … } ``` *** ### locateDoseQuantities() > **locateDoseQuantities**(`resource`, `rt`): [`LocatedDoseQuantity`](#locateddosequantity)[] Locate every `doseAndRate.doseQuantity` on a medication resource, with its FHIRPath location. Returns `[]` for a non-medication resource or one carrying no dose quantity. Used by the validator to UCUM-check dose units and by [readMedicationDoses](#readmedicationdoses) to surface them. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### rt `string` Its resolved `resourceType`. #### Returns [`LocatedDoseQuantity`](#locateddosequantity)[] The located dose quantities, in document order. #### Example ```ts import { locateDoseQuantities, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"MedicationRequest","dosageInstruction":[{"doseAndRate":[{"doseQuantity":{"value":5,"code":"mg"}}]}]}', ); locateDoseQuantities(resource, "MedicationRequest")[0]?.path; // "MedicationRequest.dosageInstruction[0].doseAndRate[0].doseQuantity" ``` *** ### matchesFixed() > **matchesFixed**(`instance`, `fixed`): `boolean` Whether an instance node **exactly equals** a `fixed[x]` value. #### Parameters ##### instance [`FhirNode`](#fhirnode) \| `undefined` The instance node (or `undefined` when the element is absent). ##### fixed [`FhirNode`](#fhirnode) The profile's fixed value node. #### Returns `boolean` `true` when the instance equals the fixed value exactly (same content, nothing extra). #### Example ```ts import { primitive } from "@cosyte/fhir"; import { matchesFixed } from "@cosyte/fhir"; matchesFixed(primitive("active"), primitive("active")); // true matchesFixed(primitive("inactive"), primitive("active")); // false ``` *** ### matchesPattern() > **matchesPattern**(`instance`, `pattern`): `boolean` Whether an instance node **matches** a `pattern[x]` value, contains at least the pattern's content. #### Parameters ##### instance [`FhirNode`](#fhirnode) \| `undefined` The instance node (or `undefined` when the element is absent). ##### pattern [`FhirNode`](#fhirnode) The profile's pattern value node. #### Returns `boolean` `true` when the instance contains every property/value the pattern names (extras allowed). #### Example ```ts import { complex, list, primitive } from "@cosyte/fhir"; import { matchesPattern } from "@cosyte/fhir"; const instance = complex([ { name: "coding", value: list([complex([ { name: "system", value: primitive("http://terminology.hl7.org/CodeSystem/observation-category") }, { name: "code", value: primitive("vital-signs") }, { name: "display", value: primitive("Vital Signs") }, ])]) }, ]); const pattern = complex([ { name: "coding", value: list([complex([{ name: "code", value: primitive("vital-signs") }])]) }, ]); matchesPattern(instance, pattern); // true, the extra system/display are allowed ``` *** ### matchSlices() > **matchSlices**(`instances`, `slices`, `discriminators`): [`SliceMatchResult`](#slicematchresult) Assign each instance occurrence of a sliced element to a slice (or none), per the discriminators. Returns `unchecked: true`, and no assignments the caller should act on, when membership cannot be evaluated: an empty discriminator set, any discriminator of an unsupported type (`type`, `profile`, R5 `position`, …), or any slice that declares no constraint at a discriminator path. The library does **not** guess a slice assignment it cannot justify. #### Parameters ##### instances readonly [`FhirNode`](#fhirnode)[] The instance occurrences of the sliced element, in order. ##### slices readonly [`SliceDefinition`](#slicedefinition)[] The resolved slice definitions. ##### discriminators readonly `Discriminator`[] The slicing's discriminators. #### Returns [`SliceMatchResult`](#slicematchresult) The per-occurrence assignments and the `unchecked` flag. #### Example ```ts import { matchSlices } from "@cosyte/fhir"; const result = matchSlices(categoryOccurrences, slices, [{ type: "pattern", path: "$this" }]); result.assignments; // e.g. ["VSCat", undefined] ``` *** ### misplacedPrimitiveExtension() > **misplacedPrimitiveExtension**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.MISPLACED\_PRIMITIVE\_EXTENSION](#misplaced_primitive_extension) issue at `expression`. The location is the element the `_`-sibling was written beside (`Patient.name`), because the `_`-prefixed member itself is not addressable in FHIRPath. #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { misplacedPrimitiveExtension } from "@cosyte/fhir"; const issue = misplacedPrimitiveExtension("Patient.name"); ``` *** ### mixedXmlSpelling() > **mixedXmlSpelling**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.MIXED\_XML\_SPELLING](#mixed_xml_spelling) issue at `expression` (XML reader only). The location names the element whose occurrences did not all arrive under one expanded name, raised once for that element rather than once per occurrence. #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { mixedXmlSpelling } from "@cosyte/fhir"; const issue = mixedXmlSpelling("Observation.status"); ``` *** ### modifierElements() > **modifierElements**(`resource`): [`ModifierElementReport`](#modifierelementreport)[] Collect the modifier ELEMENTS a resource carries, a deep walk of the whole document, so one nested in a backbone element, a contained resource or a Bundle entry is caught too. This is the standalone form of [SafetyReadout.modifierElements](#modifierelements) and returns exactly what that channel carries. **It takes no `path`, unlike its siblings on this module, and that is deliberate.** A modifier-element location may root at a resource type name only when the name is one this library defines, so the root is derived here from the document's own type against that fixed set rather than supplied by a caller who could root it at anything. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. #### Returns [`ModifierElementReport`](#modifierelementreport)[] The modifier elements present, in document order, one per distinct location. #### Example ```ts import { modifierElements, parseResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","valueQuantity":{"value":0.01,"comparator":"<","unit":"mg"}}', ); modifierElements(resource); // [{ element: "comparator", location: "Observation.valueQuantity.comparator" }] ``` *** ### nearMissNegationCodes() > **nearMissNegationCodes**(`resource`, `path`): `string`[] The locations where a `code`-valued negation element carries a value that differs from a code this layer classifies **only by letter case or by surrounding whitespace**, so the exact-string match declined it and that negation was not classified. `{"resourceType":"Procedure","status":"NOT-DONE"}` and `{"…","status":" not-done"}` are ordinary output from a system whose codes are upper-cased, or from a fixed-width or CSV feed that padded a field, and both are how a v2 or C-CDA extract reaches a FHIR surface. FHIR `code` is case-sensitive and its lexical space has no room for surrounding whitespace (`[^\s]+(\s[^\s]+)*`, datatypes.html), so neither value **is** the code, and this library does not read either as one. **Nothing here coerces, trims or case-folds a value into a negation**: that would accept a non-conformant document as though it were conformant and hand a caller an assertion its sender never spelled. This is **the record that the value was there**, which is what the read was missing: without it a procedure recorded as `"NOT-DONE"` returns `negations: []` under `safeToSummarize: true`, indistinguishable from one that was carried out. **A near miss is suppressed where the same element also spells that code exactly**, since the negation is then classified and the caller has it. R4 permits translation codings beside the one from a required binding's value set (terminologies.html), so a `verificationStatus` carrying `refuted` from the standard system and `REFUTED` from a local one is **conformant** and draws nothing. Suppressed per code, so a near miss of a *different* code there still reports. **Value-free**: neither the text nor the code it resembles is carried, only the FHIRPath of the element that held it. **The elements are `status` and `verificationStatus`, at every resource root**, which is the negation read's own window and the same window `unreadableBooleans` uses; the pairs come from the table those matches are made from, so this cannot cover a pair the read does not. `arrayWrappedScalars` reaches every root too, but only for the `Coding` members that same table marks -- its element-level half stays on the cardinality table, so the two are **not** one window. `AllergyIntolerance.code` is deliberately outside it (see [SafetyReadout.nearMissNegationCodes](#nearmissnegationcodes)). **Empty for every conformant document read from JSON, bar one admitted shape**: a translation coding beside a required binding's own may differ from a negation code **only by case**, which R4 permits under the reading that only *one* coding SHALL come from the value set (terminologies.html). Only the case half can be conformant, a surrounding-whitespace value being outside `code`'s lexical space whatever coding carries it. Over-disclosure is the fail-safe direction. In XML the whitespace half is a further declared limit: R4 derives `code` from `xs:token` (fhir-base.xsd), whose `whiteSpace=collapse` facet strips surrounding whitespace before validation, and this reader is schema-free and does not collapse. See [SafetyReadout.nearMissNegationCodes](#nearmissnegationcodes). #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of the near-miss negation codes, in walk order. #### Example ```ts import { nearMissNegationCodes, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Procedure","status":"NOT-DONE"}'); nearMissNegationCodes(resource, "Procedure"); // ["Procedure.status"] ``` *** ### nestedArray() > **nestedArray**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.NESTED\_ARRAY](#nested_array) issue at `expression`. The location is the position the inner array occupied, so it indexes into the outer array (`Patient.name[0]`, `Patient.name[0].given[1]`), which is as close as FHIRPath can get to a shape FHIRPath cannot address. #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { nestedArray } from "@cosyte/fhir"; const issue = nestedArray("Patient.name[0]"); ``` *** ### nestedArrayContent() > **nestedArrayContent**(`node`): readonly [`NestedArrayContent`](#nestedarraycontent)[] The arrays the sender wrote at this position that FHIR JSON gives no meaning to, each as JSON text (see [NestedArrayContent](#nestedarraycontent) for what that text preserves). Empty for every conformant document, and for every node the reader did not mark. This is where the content of an array inside an array is preserved. It is **not** modeled as FHIR and never will be: json.html §2.6.2.2 uses an array for a repeating element and for nothing else, so an array inside one is not an element and has no place in the tree. Handing it back as the text the sender wrote keeps it readable without redefining what a repeating element contains. Parse it with `readRawJson` if you need its structure; the library will not decide what it meant. A primitive can carry one in each of the two JSON channels (its value array and its `_`-sibling array), so up to two entries come back, value channel first. #### Parameters ##### node [`FhirNode`](#fhirnode) Any model node. #### Returns readonly [`NestedArrayContent`](#nestedarraycontent)[] The preserved arrays, `[]` when the node carries none. #### Example ```ts import { getProperty, isList, nestedArrayContent, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","name":[[{"family":"Roe"}]]}'); const name = getProperty(resource, "name"); isList(name!) && nestedArrayContent(name.items[0]!); // [{ channel: "value", json: '[{"family":"Roe"}]' }] ``` *** ### nestedArrays() > **nestedArrays**(`resource`, `path`): `string`[] Collect the FHIRPath locations where the document wrote an **array inside an array**, a deep walk of the whole resource. FHIR JSON uses an array for one thing, a repeating element (json.html §2.6.2.2), so no element is ever a list of lists and this shape has no meaning at any position. That is what makes this rule different from the two above it and simpler than either: it needs **no cardinality table and no element list**, because there is no position in a conformant document where it can fire. **Scope: every node the model has.** Every element at every depth, a primitive's `extension` metadata, a resource nested in `contained` or a Bundle entry, and a member a repeated property name shadowed. It is bounded by what the reader modeled rather than by an element list, which is the one qualification worth stating plainly: a `_`-sibling the reader discards *whole* as misplaced or unrecognised (one sitting on an object or on a non-primitive array, or a member of a `_`-sibling object that is neither an `id` **string** nor an `extension` array) leaves no node behind, so an array inside one is reported by the reader against the discarded sibling and is **not** refused here. **Which warning the reader draws is per member, not one code for all three:** an unrecognised member of a `_`-sibling object draws `UNKNOWN_PROPERTY` (`{"birthDate":"1980-01-01","_birthDate":{"foo":[["x"]]}}`, at `Patient.birthDate.foo`), while a `_`-sibling on an object or on a non-primitive array draws `MISPLACED_PRIMITIVE_EXTENSION` for the misplaced sibling and nothing besides. Reaching it would mean reading raw JSON the codec does not model. **This layer reports these; it does not read them.** The codec does not model an inner array, so whatever the sender wrote inside one is not recoverable here, and this location is the only thing that distinguishes such a position from an element that really was empty on the wire. Without it a refuted allergy, a resolved condition, or a whole retracted resource inside a Bundle entry reads back as an ordinary absent element and the readout affirms `safeToSummarize` over content it never saw. Reporting is deliberately the entire remedy: making the inner array readable would change what a repeating element *contains* for every consumer that walks one, which is a much larger and riskier change than declining to affirm. Empty for every conformant document, and for every document read from XML, which has no way to write the shape. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of the nested arrays, in walk order (an element's own properties before any member a repeated name shadowed, which is not the document order that member had), each location once however many marked nodes sit at it. Two elements whose names are both withheld (../model/path.js) share a location and therefore collapse into one entry, on the same reasoning as a repeated name: a location nobody can address twice says nothing twice. The verdict does not move, `safeToSummarize` is `false` either way. #### Example ```ts import { nestedArrays, parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","name":[[{"family":"Roe"}]]}'); nestedArrays(resource, "Patient"); // ["Patient.name[0]"] ``` *** ### nodesEquivalent() > **nodesEquivalent**(`a`, `b`): `boolean` Whether two model nodes are equivalent modulo primitive lexical form and singleton lists, the definition of JSON↔XML model equivalence (see the module doc). Reflexive, symmetric, and transitive over the schema-free model. #### Parameters ##### a [`FhirNode`](#fhirnode) One node (e.g. the model parsed from JSON). ##### b [`FhirNode`](#fhirnode) The other node (e.g. the model parsed from the same resource in XML). #### Returns `boolean` `true` when the two denote the same FHIR content. #### Example ```ts import { parseResource, parseResourceXml, nodesEquivalent } from "@cosyte/fhir"; const json = parseResource('{"resourceType":"Patient","active":true,"name":[{"given":["Jane"]}]}'); const xml = parseResourceXml( '', ); nodesEquivalent(json.resource, xml.resource); // true ``` *** ### parseFhirPath() > **parseFhirPath**(`expression`): [`Expr`](#expr) Parse a FHIRPath expression string into an [Expr](#expr) AST. #### Parameters ##### expression `string` The FHIRPath source (e.g. an `ElementDefinition.constraint.expression`). #### Returns [`Expr`](#expr) The parsed expression tree. #### Throws UnsupportedFhirPathError when the expression is malformed or uses a token the bounded subset does not recognise, the caller's fail-safe reports the invariant *unchecked*, never passed. #### Example ```ts import { parseFhirPath } from "@cosyte/fhir"; const ast = parseFhirPath("dataAbsentReason.empty() or value.empty()"); ``` *** ### parseNdjsonLine() > **parseNdjsonLine**(`line`, `lineNumber?`): [`NdjsonRecord`](#ndjsonrecord) Parse a single NDJSON line into a [NdjsonRecord](#ndjsonrecord), isolating any failure (never throws). The one place read-time exceptions from [parseResource](#parseresource) are caught and turned into a value-free per-line `error`, so a caller iterating lines by hand gets the same isolation the streaming reader provides. #### Parameters ##### line `string` The raw line text (without its trailing newline). A blank/whitespace-only line yields neither a resource nor an error, an empty record, so callers can skip it. ##### lineNumber? `number` = `1` The 1-based line number to stamp on the record. Defaults to 1. #### Returns [`NdjsonRecord`](#ndjsonrecord) The [NdjsonRecord](#ndjsonrecord). #### Example ```ts import { parseNdjsonLine } from "@cosyte/fhir"; parseNdjsonLine('{"resourceType":"Patient","id":"1"}', 1).resource; // the Patient model parseNdjsonLine("{ not json", 2).error?.code; // "MALFORMED_JSON" ``` *** ### parseReference() > **parseReference**(`raw`): [`ParsedReference`](#parsedreference) Classify a `Reference.reference` string into its FHIR form and extract the resource type / id / version where the form allows. #### Parameters ##### raw `string` The reference string, e.g. `"Patient/123"`, `"#p1"`, `"https://ehr/fhir/Observation/9/_history/2"`, `"urn:uuid:…"`. #### Returns [`ParsedReference`](#parsedreference) #### Example ```ts import { parseReference } from "@cosyte/fhir"; parseReference("Patient/123"); // { kind: "relative", type: "Patient", id: "123", ... } parseReference("#p1"); // { kind: "fragment", id: "p1", ... } parseReference("urn:uuid:1-2-3"); // { kind: "logical", ... } ``` *** ### parseResource() > **parseResource**(`input`): [`ReadResult`](#readresult) Read a FHIR resource from JSON text or an already-parsed [RawJson](#rawjson) tree into the immutable model, gathering value-free issues. Throws [FhirCodecError](#fhircodecerror) on malformed JSON or broken `_`-sibling alignment. #### Parameters ##### input `string` \| [`RawJson`](#rawjson) JSON text, or a [RawJson](#rawjson) tree from [readRawJson](#readrawjson). #### Returns [`ReadResult`](#readresult) #### Throws FhirCodecError (`MALFORMED_JSON`) when the input is not a JSON object. #### Throws FhirCodecError (`MAX_DEPTH_EXCEEDED`) when text input nests past the reader's depth bound. #### Throws FhirCodecError (`PRIMITIVE_EXTENSION_MISALIGNED`) when a value/`_`-sibling pair is misaligned. #### Example ```ts import { parseResource } from "@cosyte/fhir"; const { resource, issues } = parseResource('{"resourceType":"Observation","valueQuantity":{"value":0.010}}'); ``` *** ### parseResourceXml() > **parseResourceXml**(`input`): [`ReadResult`](#readresult) Read a FHIR resource from XML text (or an already-parsed [XmlElement](#xmlelement) tree) into the immutable model, gathering value-free issues, the same [ReadResult](#readresult) the JSON ../codec/read.js parseResource returns. Throws ./issues.js FhirXmlError on malformed XML or a refused DTD/entity (XXE / billion-laughs safe). #### Parameters ##### input `string` \| [`XmlElement`](#xmlelement) XML text, or an [XmlElement](#xmlelement) tree from [readRawXml](#readrawxml). #### Returns [`ReadResult`](#readresult) #### Example ```ts import { parseResourceXml, serializeResource } from "@cosyte/fhir"; const { resource } = parseResourceXml( '', ); serializeResource(resource); // → '{"resourceType":"Patient","active":"true"}' // XML carried `active` as attribute text, so it re-emits as a JSON string rather than `true`. ``` *** ### pathExists() > **pathExists**(`node`, `path`): `boolean` Whether an element path selects at least one node on `node`, the `exists` primitive used by the `exists` slicing discriminator and by cardinality checks. #### Parameters ##### node [`FhirNode`](#fhirnode) The starting node. ##### path `string` A dotted element path relative to `node`. #### Returns `boolean` `true` when the path selects one or more nodes. #### Example ```ts import { parseResource } from "@cosyte/fhir"; import { pathExists } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","deceasedBoolean":true}'); pathExists(resource, "deceased[x]"); // true ``` *** ### primitive() > **primitive**(`value`, `meta?`): [`FhirPrimitive`](#fhirprimitive) Construct a [FhirPrimitive](#fhirprimitive). Omits absent optional keys (rather than setting them to `undefined`) so the model satisfies `exactOptionalPropertyTypes` and equality stays structural. #### Parameters ##### value [`PrimitiveValue`](#primitivevalue) \| `undefined` The scalar value, or `undefined` for a metadata-only (value-absent) primitive. ##### meta? [`PrimitiveMeta`](#primitivemeta) = `{}` Optional `id` / `extension`. #### Returns [`FhirPrimitive`](#fhirprimitive) #### Example ```ts import { primitive } from "@cosyte/fhir"; const given = primitive("Jacqueline"); ``` *** ### readBundle() > **readBundle**(`bundle`): [`BundleReadout`](#bundlereadout) Read a `Bundle` resource into a value-free [BundleReadout](#bundlereadout), its type, entry-processing semantics, and one entry per `Bundle.entry`. Lenient: a Bundle with no `type` reads with `type: undefined` / `processing: "none"`, and a malformed entry reads with empty fields rather than throwing (Postel's Law, nothing is dropped, the shape is surfaced). #### Parameters ##### bundle [`FhirComplex`](#fhircomplex) A `Bundle` resource model (typically from `parseResource`). #### Returns [`BundleReadout`](#bundlereadout) The [BundleReadout](#bundlereadout). Nothing is executed, see the module doc. #### Example ```ts import { parseResource, readBundle } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Bundle","type":"transaction","entry":[' + '{"fullUrl":"urn:uuid:1","resource":{"resourceType":"Patient","id":"1"},' + '"request":{"method":"POST","url":"Patient"}}]}', ); const bundle = readBundle(resource); bundle.atomic; // true, a transaction is all-or-nothing bundle.entries[0]?.fullUrl; // "urn:uuid:1" bundle.entries[0]?.requestMethod; // "POST" ``` *** ### readInterpretations() > **readInterpretations**(`observation`): [`Coded`](#coded)[] Surface the `Observation.interpretation` codings (the abnormal flags, H/L/HH/LL/A/N…). Preserved and exposed; never derived from a value and a reference range (this layer does not compute). #### Parameters ##### observation [`FhirComplex`](#fhircomplex) An `Observation` complex node. #### Returns [`Coded`](#coded)[] The interpretation codings across every `interpretation` CodeableConcept (`[]` when none). #### Example ```ts import { parseResource, readInterpretations } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","interpretation":[{"coding":[{"code":"H"}]}]}', ); readInterpretations(resource)[0]?.code; // "H" ``` *** ### readMedicationDoses() > **readMedicationDoses**(`resource`, `rt`): [`Quantity`](#quantity-1)[] Surface every medication **dose `Quantity`** as a [Quantity](#quantity-1), the coded UCUM unit kept distinct from the display string, the value an exact decimal. Reads `MedicationRequest` (`dosageInstruction`) and `MedicationStatement` (`dosage`). #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### rt `string` \| `undefined` #### Returns [`Quantity`](#quantity-1)[] The dose quantities in document order (`[]` when none / not a medication resource). #### Example ```ts import { parseResource, readMedicationDoses, resourceType } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"MedicationRequest","dosageInstruction":[{"doseAndRate":[{"doseQuantity":{"value":5,"system":"http://unitsofmeasure.org","code":"mg"}}]}]}', ); readMedicationDoses(resource, resourceType(resource))[0]?.code; // "mg" ``` *** ### readObservationValue() > **readObservationValue**(`node`): [`ObservationValue`](#observationvalue) \| `undefined` Read the present `value[x]` variant off an Observation (or a `component`), typed by what is actually there. Returns `undefined` when no `value[x]` is present (e.g. a `dataAbsentReason`-only observation). When more than one variant is present, the first in FHIR's declared order is returned and the rest are reported in [ObservationValue.ambiguous](#ambiguous). #### Parameters ##### node [`FhirComplex`](#fhircomplex) An `Observation` or `component` complex node. #### Returns [`ObservationValue`](#observationvalue) \| `undefined` The discriminated [ObservationValue](#observationvalue), or `undefined` when there is no value. #### Example ```ts import { parseResource, readObservationValue } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Observation","valueString":"POSITIVE"}'); const v = readObservationValue(resource); v?.type; // "String", NOT a Quantity; reading it as a number would be wrong v?.quantity; // undefined ``` *** ### readQuantity() > **readQuantity**(`node`): [`Quantity`](#quantity-1) \| `undefined` Read a FHIR `Quantity` (or a specialization: `Age`, `Distance`, `Duration`, `Count`, `SimpleQuantity`) into a [Quantity](#quantity-1), surfacing the coded unit distinct from the display unit. Returns `undefined` for a node that is not a complex element. #### Parameters ##### node [`FhirNode`](#fhirnode) \| `undefined` A `Quantity` node, or `undefined`. #### Returns [`Quantity`](#quantity-1) \| `undefined` The [Quantity](#quantity-1), or `undefined` when the node is not a complex element. #### Example ```ts import { getProperty, parseResource, readQuantity } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","valueQuantity":{"value":120,"unit":"mmHg","system":"http://unitsofmeasure.org","code":"mm[Hg]"}}', ); const q = readQuantity(getProperty(resource, "valueQuantity")); q?.code; // "mm[Hg]" , the machine unit, not the "mmHg" display string ``` *** ### readRawJson() > **readRawJson**(`src`): [`RawJson`](#rawjson) Parse a JSON document into a [RawJson](#rawjson) tree that preserves number literals verbatim and member order. Throws [FhirCodecError](#fhircodecerror) (`MALFORMED_JSON`) on invalid input, or (`MAX_DEPTH_EXCEEDED`) when nesting passes the reader's fixed depth bound, both value-free, with a byte `offset` and no snippet. #### Parameters ##### src `string` The JSON text. #### Returns [`RawJson`](#rawjson) #### Example ```ts import { readRawJson } from "@cosyte/fhir"; const tree = readRawJson('{"v":0.010}'); // the number node carries raw === "0.010", the trailing zero is intact ``` *** ### readRawXml() > **readRawXml**(`src`): [`XmlElement`](#xmlelement) Parse an XML document into a raw [XmlElement](#xmlelement) tree, preserving element/attribute order and decoding the five predefined entities and numeric character references. **Refuses** any DTD (`DTD_FORBIDDEN`) or non-predefined entity (`UNDEFINED_ENTITY`), and bounds nesting depth, so it is XXE- and billion-laughs-safe and never crashes on adversarial input. Throws [FhirXmlError](#fhirxmlerror) on any refusal or well-formedness error, with a byte `offset` and no snippet. #### Parameters ##### src `string` The XML text. #### Returns [`XmlElement`](#xmlelement) #### Example ```ts import { readRawXml } from "@cosyte/fhir"; const root = readRawXml(''); root.name; // "Patient" ``` *** ### readReferenceRanges() > **readReferenceRanges**(`observation`): [`ObservationReferenceRange`](#observationreferencerange)[] Surface every `Observation.referenceRange` entry, population-qualified bounds preserved as [Quantity](#quantity-1)s, not evaluated. A reference range is meaningful only alongside its qualifiers (`appliesTo`, `age`), which are preserved in the model; this reader exposes the bounds and type. #### Parameters ##### observation [`FhirComplex`](#fhircomplex) An `Observation` complex node. #### Returns [`ObservationReferenceRange`](#observationreferencerange)[] The reference ranges in document order (`[]` when none). #### Example ```ts import { parseResource, readReferenceRanges } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","referenceRange":[{"low":{"value":70,"code":"mg/dL"}}]}', ); readReferenceRanges(resource)[0]?.low?.code; // "mg/dL" ``` *** ### readSafety() > **readSafety**(`resource`): [`SafetyReadout`](#safetyreadout) Read the safety-critical modifier / status / negation elements out of a resource, never dropping one. The type-scoped slots are filled for the types `SAFETY_RESOURCE_TYPES` names; for any other type they are `undefined` and only the un-gated reads apply, which are the retraction, the refutation, `doNotPerform`, the `not-done` / `not-taken` status negations, and the modifier-extension check. `noKnownAllergy` is the one negation that stays type-gated, because it asserts something *positive* about a patient. **The un-gated negation reads run at every resource root**, so a `contained` or `Bundle.entry` resource's retraction, refutation, `not-done` / `not-taken` status or "do not perform" instruction is classified on [SafetyReadout.negations](#negations), while the **single-valued** fields (`status`, `retracted`, `doNotPerform`, `noKnownAllergy` and the rest) stay root reads. The readout's location channels and `safeToSummarize` are document-wide too. Branch on `negations` rather than on a single-valued field when the resource may carry others. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model (typically from `parseResource`). #### Returns [`SafetyReadout`](#safetyreadout) The complete [SafetyReadout](#safetyreadout). #### Example ```ts import { parseResource, readSafety } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"MedicationRequest","status":"active","doNotPerform":true,' + '"medicationCodeableConcept":{"text":"amoxicillin"}}', ); const safety = readSafety(resource); safety.doNotPerform; // true safety.negations; // ["do-not-perform"] ``` *** ### requiredVitalSignUnits() > **requiredVitalSignUnits**(`loincCode`): readonly `string`[] \| `undefined` The UCUM codes the vital-signs profile requires for a given LOINC code, or `undefined` when the code is not a table-keyed vital sign (so no required-unit check applies). #### Parameters ##### loincCode `string` A LOINC code (e.g. `"8480-6"`). #### Returns readonly `string`[] \| `undefined` The allowed UCUM codes, or `undefined` when unlisted. #### Example ```ts import { requiredVitalSignUnits } from "@cosyte/fhir"; requiredVitalSignUnits("8480-6"); // ["mm[Hg]"] (systolic blood pressure) ``` *** ### resolveElement() > **resolveElement**(`elements`, `property`): \{ `base`: `string`; `datatype`: `string`; `element`: [`ElementSchema`](#elementschema); \} \| `undefined` Resolve an instance property name against a schema, honoring `choice[x]`. A plain element matches by exact name; a choice element `deceased` (types `boolean | dateTime`) matches the instance property `deceasedBoolean` or `deceasedDateTime`, returning which variant datatype was chosen. #### Parameters ##### elements `Readonly`\<`Record`\<`string`, [`ElementSchema`](#elementschema)\>\> The resource's elements. ##### property `string` The instance property name. #### Returns \{ `base`: `string`; `datatype`: `string`; `element`: [`ElementSchema`](#elementschema); \} \| `undefined` The matched element and (for a choice) the chosen datatype and the choice base name, or `undefined` when nothing matches. #### Example ```ts import { resolveElement } from "@cosyte/fhir"; const elements = { deceased: { min: 0, max: 1, types: ["boolean", "dateTime"] } }; resolveElement(elements, "deceasedBoolean")?.datatype; // "boolean" ``` *** ### resolvePath() > **resolvePath**(`node`, `path`): [`FhirNode`](#fhirnode)[] Resolve an element path against a node, returning every node it selects (empty when nothing matches). `$this` (or the empty path) selects the node itself. A `[x]` segment matches any concrete choice variant (`value[x]` → `valueQuantity`, `valueString`, …). Repeating elements are flattened, so `coding.code` on a `CodeableConcept` with three codings yields three nodes. #### Parameters ##### node [`FhirNode`](#fhirnode) The starting node (a resource, a slice element instance, …). ##### path `string` A dotted element path relative to `node` (`""` / `"$this"` selects `node`). #### Returns [`FhirNode`](#fhirnode)[] The selected nodes, in document order. #### Example ```ts import { parseResource } from "@cosyte/fhir"; import { resolvePath } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","category":[{"coding":[{"code":"vital-signs"}]}]}', ); resolvePath(resource, "category.coding.code"); // → [ the "vital-signs" primitive node ] ``` *** ### resolveReference() > **resolveReference**(`reference`, `options?`): [`ReferenceResolution`](#referenceresolution) Resolve a single `Reference.reference` string against a Bundle and/or a `contained` closure. Resolution is honest about its closure: a `#fragment` resolves only within `contained`; a relative `Type/id` only within the Bundle. A local miss is `"unresolved"` (the caller flags it, `REFERENCE_UNRESOLVED`, and preserves the reference). An absolute or logical reference that is not in the Bundle is `"external"`, it points somewhere this library was never given, which is **not** a defect, so it draws no finding. #### Parameters ##### reference `string` The `Reference.reference` string. ##### options? The closure: a [BundleIndex](#bundleindex) and/or a [ContainedIndex](#containedindex). ###### bundle? [`BundleIndex`](#bundleindex) ###### contained? [`ContainedIndex`](#containedindex) #### Returns [`ReferenceResolution`](#referenceresolution) The [ReferenceResolution](#referenceresolution). #### Example ```ts import { resolveReference } from "@cosyte/fhir"; resolveReference("Patient/1", { bundle }).status; // "resolved" | "unresolved" resolveReference("#p1", { contained }).status; // "resolved" | "unresolved" resolveReference("https://other/fhir/Patient/9", {}).status; // "external" ``` *** ### resolveSlices() > **resolveSlices**(`snapshot`, `slicedElement`): [`SliceDefinition`](#slicedefinition)[] Resolve the slices a sliced element introduces, reading each slice's constraints and existence expectations from the snapshot (the slice element's own `fixed`/`pattern`, plus any descendant element that carries one). #### Parameters ##### snapshot readonly [`ElementDefinition`](#elementdefinition)[] The full snapshot element list. ##### slicedElement [`ElementDefinition`](#elementdefinition) The element carrying the `slicing` declaration. #### Returns [`SliceDefinition`](#slicedefinition)[] The slice definitions, in snapshot order. #### Example ```ts import { resolveSlices } from "@cosyte/fhir"; // snapshot contains `Observation.category` (slicing) + `Observation.category:VSCat` (pattern): const slices = resolveSlices(snapshot, categoryElement); // → [{ sliceName: "VSCat", … }] ``` *** ### resourceType() > **resourceType**(`node`): `string` \| `undefined` The `resourceType` of a complex node, if it carries one as a string primitive. FHIR allows `resourceType` in any position on read; this reads it wherever it sits. #### Parameters ##### node [`FhirComplex`](#fhircomplex) #### Returns `string` \| `undefined` #### Example ```ts import { parseResource, resourceType } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","id":"1"}'); resourceType(resource); // "Patient" ``` *** ### serializeResource() > **serializeResource**(`node`): `string` Serialize a resource (or any [FhirComplex](#fhircomplex)) to compact FHIR JSON text: spec-clean for any model FHIR can express, and faithful rather than spec-clean for the shapes it cannot (see the module comment). #### Parameters ##### node [`FhirComplex`](#fhircomplex) The resource model to serialize. #### Returns `string` Compact JSON text, decimals byte-exact, primitive metadata split back into `_`-siblings with null-padded array alignment, and `resourceType` hoisted to the front where it is a string. A `resourceType` that is anything else keeps its position in the document rather than being dropped, and an array the sender wrote where FHIR gives an array no meaning, a scalar or `null` the sender wrote where FHIR has an object (a complex element's position, or a primitive's `_`-sibling), or a `null` the reader marked in a primitive's value channel, is written back as it was read, so such output is deliberately not spec-clean. #### Throws With `DROPPED_ELEMENT_TEXT` if the model carries a node the XML reader MARKED as having lost character data. JSON has no character-data channel, so the member would simply be absent and the finding would be lost across a round trip. Text the reader drops WITHOUT marking (character data that is `String.trim()`-empty) is not covered, because there is no marker. #### Throws With `UNSERIALIZABLE_SHADOWED_PROPERTY` if the model carries a member a repeated property name shadowed. This writer walks `properties` only, so `{"status":"final","status":"entered-in-error"}` came back as `{"status":"final"}`: the retraction absent, and `valid` and `safeToSummarize` both moved from `false` to `true`. `serializeResourceXml` drops it too, so **this refusal reaches both writers** and there is no route here that keeps the member. A repeated name inside a primitive's `_`-sibling is not modeled, and one inside a complex in a primitive's `extension` is outside the window; see `assertNoShadowedProperty` for both. #### Example ```ts import { parseResource, serializeResource } from "@cosyte/fhir"; const { resource } = parseResource(input); const json = serializeResource(resource); // round-trips a spec-clean input byte-for-byte ``` *** ### serializeResourceXml() > **serializeResourceXml**(`node`): `string` Serialize a resource (or any [FhirComplex](#fhircomplex)) to compact FHIR XML text, the exact inverse of [parseResourceXml](#parseresourcexml) for a model read from a conformant document (**the summary line said "spec-clean" unqualified and the section below has always contradicted it**). Decimals are emitted byte-exact (never through a `number`), primitive metadata is co-located (`id` attribute + child ``s), repeating elements are repeated, and the root carries the FHIR namespace. ## What this output is NOT guaranteed to be, stated rather than implied "Spec-clean" is a claim about the FHIR structure, and the XML 1.0 grammar for a name and a character is now held at every site this writer writes one: a tag name that is not a `Name` (``, `<1abc/>`) is refused rather than written, although it re-reads through [parseResourceXml](#parseresourcexml) exactly as written, because a conforming third-party processor must reject it; and a code point outside `Char` in an attribute value or a spliced `div` string is refused, raw or as a reference (see the `@throws` below). A name carrying a **colon** is refused on a code of its own, because the model carries no binding to declare. **What is still written, each a declared residual rather than an oversight**: - a `Name` that is not namespace-well-formed after `xml:` (``), since only the colon question reads the `xml:` prefix and it exempts that prefix; - an element or attribute name inside a `div` string that is not a `Name` (`

` spelled `<1p>`): the string is asked the `Char` question, never the `Name` question, which is asked at tag positions only; - the three `div` counterexamples below, none of them a name or a character matter. The reader is unchanged and still reads `<1abc>` and `` back, and `validateResource` still returns `valid: true` for a `string` carrying U+0000; both are separate surfaces. ## The `div` branch, which writes markup rather than a name A `div` property is written back as its own raw string, so what that string spells is markup in the output. `emitsOneDivElement` is checked at that branch before the string is spliced in, and the string is written only when it parses as exactly one element whose local name is `div`; a string that fails raises `UNSERIALIZABLE_DIV_MARKUP` below. A string that passes is written only when every prefix its markup names is bound by a declaration inside it (`bindsEveryPrefix`); one that is not raises `UNSERIALIZABLE_DIV_PREFIX` below. A string that passes both is written only when it carries no code point outside XML 1.0 `Char`, neither raw nor denoted by a numeric character reference its parse decoded, each reference judged on its own; one that does raises `UNSERIALIZABLE_XML_CHARACTER` below, and a reference inside a comment, which is never decoded, is not one. The shape the first check exists for is `

ok
…716186003…` on an `AllergyIntolerance`: it used to be spliced in whole, and the emitted document re-read with `noKnownAllergy: true` and a `no-known-allergy` negation over a record that had asserted nothing, with no diagnostic at either end and `readSafety` affirming it. **What passing the three checks does and does not settle, by example rather than by rule.** A string they accept contributes one element, that element is the `div`, every prefix its markup names is bound inside it, and every character it spells or denotes is a `Char`; it is not a claim that the round trip is lossless or that the output is well-formed from there. A comment beside the root (`
`) is accepted and does not survive the re-read. `emitsOneDivElement` carries three more counterexamples, each `PRE-EXISTING` and each asserted rather than argued: a depth bound this check spends from a different starting depth than the re-read, an inserted namespace declaration, and an XML declaration a conformant third-party parser rejects. #### Parameters ##### node [`FhirComplex`](#fhircomplex) The resource model to serialize (must carry a `resourceType` to name the root element). #### Returns `string` Canonical FHIR XML text. #### Throws With `DROPPED_ELEMENT_TEXT` if the model carries a node the reader MARKED as having lost character data. There is no conformant XML for it (§2.6.1: an element present in the resource SHALL have a value attribute, child elements, or extensions), and emitting the element as unfilled would lose the `DROPPED_ELEMENT_TEXT` finding across a round trip. Text the reader drops WITHOUT marking (character data that is `String.trim()`-empty) is not covered, because there is no marker. #### Throws With `UNSERIALIZABLE_ELEMENT_NAME` if any tag position holds a name that cannot be written as a tag without changing which elements the document holds: it would either fail to re-read at all, or re-read as DIFFERENT elements. The second is why this refuses rather than reports. [serializeResource](#serializeresource) escapes a member name, so this refusal never reaches it and that route stays open (which is not the same as saying the JSON output is spec-clean: [serializeResource](#serializeresource)'s own exception list still applies to the rest of the model). #### Throws With `UNSERIALIZABLE_DIV_MARKUP` if a `div` property carries a string that would not be spliced in as the one `div` element the property names. It would carry other elements into the document, or leave markup that does not re-read. Refused rather than repaired for the same reason as a name: escaping it would author a text node where the sender wrote markup, and splicing it authors elements the sender never wrote. [serializeResource](#serializeresource) carries the string as a string, so this refusal never reaches it and that route stays open. #### Throws With `UNSERIALIZABLE_JSON_ONLY_SHAPE` if the model carries a shape the JSON reader marked at a position FHIR JSON gives no meaning to: an array inside an array, a scalar or `null` where FHIR JSON has an object (a complex element's position or a primitive's `_`-sibling), or a `null` in a primitive's value channel that padded nothing. XML has no array of arrays, no `_`-sibling and no `null`, so this writer emitted the empty element the reader was left holding and the finding was gone on the next read; `{"value":null,"unit":"mg"}` came back as a `Quantity` carrying a unit and no magnitude under an empty issue list. This refusal does not reach [serializeResource](#serializeresource), which writes these back from the text the reader preserved at every position that writer walks; **it does not walk a member a repeated property name shadowed, and this refusal does reach one**, so that is the refusal's limit and not a route the shape always survives. The text handed back is value-exact, **not byte-exact**. **Only a model read from JSON reaches this**: XML cannot write any of those shapes, so a document read from XML carries none of the markers. #### Throws With `UNSERIALIZABLE_ARRAY_WRAPPER` if the model carries an array wrapper around a `0..1` element, at a location this library already reports as `ARRAY_WRAPPED_SCALAR`, that XML has no repeated element to spell back: one holding fewer than two items, or any wrapper on `resourceType`, where the type is the tag and a tag cannot be repeated. `{"resourceType":"Observation","status":["entered-in-error"]}` used to come back as `` and re-read with an empty issue list, moving `valid` and `safeToSummarize` both from `false` to `true`. **A wrapper of two or more items elsewhere is left alone rather than refused**, because a model read from JSON writes it as repeated elements that re-read as a list. **That is a statement about a model a reader produced, not about every `FhirComplex` this accepts**, and the difference is reachable: a hand-built `list([list([]), list([])])` at `Observation.status` counts two items here and emits **no** element, so it launders exactly as an empty wrapper does. Neither reader builds one: every list the JSON reader constructs holds primitives or complexes, never lists (a nested array is marked at the item, and which of the two it is depends on the spelling), and XML has no such shape. [serializeResource](#serializeresource) writes the wrapper back, so this refusal does not reach it. See `assertXmlArrayWrapper` for the window this is scoped to and what it does not cover. #### Throws With `UNSERIALIZABLE_SHADOWED_PROPERTY` if the model carries a member a repeated property name shadowed. This writer walks `properties` only, so `{"status":"final","status":"entered-in-error"}` came back as ``: the retraction absent, and `valid` and `safeToSummarize` both moved from `false` to `true`. This refusal also reaches [serializeResource](#serializeresource), because that writer drops the member too and there is no route that keeps it. XML *can* repeat an element, but two repeated elements re-read as a list, which is a repeating element the sender never wrote. See `assertNoShadowedProperty` for the window and for the two positions it leaves. #### Throws With `UNSERIALIZABLE_RESOURCE_TYPE` if the first `resourceType` an element wrote is not a string -- the one `resourceTypeOf` reads, since it is a `find`. FHIR XML has no `resourceType` element -- the type IS the tag -- so this writer skips that property at every element, and with no string to name the tag the root fell back to `Resource`: `{"resourceType":{"modifierExtension":[{"url":"http://example.org/x"}]},"status":"final"}` came back as ``, moving `valid` and `safeToSummarize` both from `false` to `true` and taking the modifier extension with it. An element with **no** `resourceType` is untouched and still named `Resource` by the fallback above, and one whose first is a string keeps its tag and is left to the repeated-property-name case. `serializeResource` emits a non-string `resourceType` through its ordinary path, so this refusal does not reach it. See `assertXmlResourceType` for the window, which reaches every depth, and for the bound that holds only at the root. #### Throws With `UNSERIALIZABLE_FOREIGN_ROOT` if the model holds a resource whose root the XML reader read out of a vocabulary that resolved to something other than FHIR's, by a default declaration or by a bound prefix. FHIR XML puts the resource in the FHIR namespace and this writer has no vendor binding to write instead, so `…` came back as `…` whose re-read carried an **empty** issue list: the one warning saying the document came from elsewhere was gone after a single trip. **Unlike most of the refusals above it this one withdraws a round trip from a document that reads `valid: true`**, because the root flag is a warning; the cost is bounded to this class. A root declaring no namespace at all is untouched, and one carrying a prefix bound to nothing is outside it too, refused on `UNSERIALIZABLE_PREFIXED_NAME` below rather than on this code. `serializeResource` emits the model exactly as it always did, so this refusal does not reach it -- a statement about that writer's output, not a claim that the JSON channel keeps the flag. See `assertXmlForeignRoot` for the window and for the route not taken. #### Throws With `UNSERIALIZABLE_CHOICE_WRAPPER` if the model carries an array wrapper around an `Observation.value[x]` choice, at a location this library already reports as `ARRAY_WRAPPED_CHOICE`, that XML has no repeated element to spell back: one holding fewer than two items. `{"resourceType":"Observation","status":"final","valueQuantity":[{"value":5, "system":"http://unitsofmeasure.org","code":"mg"}]}` reads with the encoding reported and no magnitude handed out, and used to come back as `…` re-reading as an unambiguous 5 mg under an empty issue list: a **dose** a format change made confident. **A wrapper of two or more items is left alone rather than refused**, because it writes as repeated elements that re-read as a list and the location is reported again. Scoped to `value[x]` and `component.value[x]`, `0..1` in R4, at every Observation resource root; the window is the value layer's own rather than a second cardinality table. [serializeResource](#serializeresource) writes the wrapper back, so this refusal does not reach it. See `assertXmlValueChoiceWrapper`. #### Throws With `UNSERIALIZABLE_PREFIXED_NAME` if any tag position holds a name carrying a colon, other than `xml:` followed by a local part with no colon in it. XML reads the colon as a namespace prefix and the model carries no binding for this writer to declare one with, so it used to write the prefix bound to nothing: ``, `<:x/>`, ``, ``, and a root read as `` written back as ``. A conformant parser rejects all of them, and a prefix rebound between siblings lost its `MIXED_XML_SPELLING` report across one write and one re-read. **It withdraws an XML write from models that read `valid: true`**, the cost the foreign-root refusal above already pays: a property named `p:x` reads with zero issues. Checked at every tag position, at every depth, including a resource composed into a `contained` or a `Bundle.entry.resource` after it was read. Raised after every refusal above, so a model carrying one of these names beside anything above keeps the code it already reported, and a name that both carries a colon and breaks the tag stays `UNSERIALIZABLE_ELEMENT_NAME`. **Not covered**: a name with no colon that is not an XML 1.0 `Name`, which is refused on `UNSERIALIZABLE_XML_NAME` below, not on this code; and the local part after `xml:`, which is not checked for namespace well-formedness, so `` is still written. A `div` string whose own markup carries an unbound prefix is written by the `div` branch rather than at a tag position, so this refusal does not reach it either; that branch refuses it on `UNSERIALIZABLE_DIV_PREFIX`, below. [serializeResource](#serializeresource) spells a member name as a JSON string, so this refusal does not reach it and that route stays open. #### Throws With `UNSERIALIZABLE_DIV_PREFIX` if a `div` property carries a string that passes the `UNSERIALIZABLE_DIV_MARKUP` check above and whose markup names, on an element or on an attribute, a namespace prefix no declaration inside the string binds (`xml` is bound by definition, and an `xmlns` or `xmlns:p` attribute is a declaration, not a prefixed name). The branch splices the string in verbatim, so it used to write `x` into a document a conformant parser rejects, and this library's re-read of that document turned the narrative into a property named `v:div`, with no diagnostic at either end. An unbound prefix on an inner element or an attribute (`
x
`) is refused as well: the line is namespace-well-formedness, the one the colon refusal above draws at the tag sites. **It withdraws an XML write from models that read `valid: true`**, and for an inner prefix from a model this library itself round-tripped, the cost that refusal already pays. A prefix a conformant document bound on an ANCESTOR of the `div` is not refused, because the reader writes that declaration into the string it hands back. Checked at the `div` branch at every depth, including a resource composed into a `contained` or a `Bundle.entry.resource` after it was read, and raised after every refusal above, so a model that also trips any of them keeps the code it already reported. The message and the locations carry neither the string nor its prefix. [serializeResource](#serializeresource) carries the string as a string, so this refusal does not reach it and that route stays open. #### Throws With `UNSERIALIZABLE_XML_NAME` if any tag position holds a name that is not an XML 1.0 `Name` (production [5]) and that neither breaks the tag nor carries a colon: `a&b`, `1abc`, `-x`, `.x`, `a"b`, a name beginning U+00B7, a name carrying U+0000 or an unpaired surrogate. Each used to be written verbatim, and this library's own reader read each back unchanged, which is why the tag-breaking line above never reached it; a conforming processor must reject every one. Checked at every tag position at every depth: a property name, a name inside `contained`, `Bundle.entry.resource` or an extension, a resource-valued element's wrapper, and a `resourceType` that names a root or a nested resource's tag, the last reported at the location of the element wrapping it. Every position once, in walk order; a refused name's own segment renders `WITHHELD`, and neither the message nor a location carries the name. **Refused, never repaired**: XML has no escape for a name. **It withdraws an XML write from models that read `valid: true`**, the cost the colon refusal above already pays: a JSON property named `1abc` reads with zero issues. Raised after every refusal above, so a model that trips any of them keeps the code it already reported. [serializeResource](#serializeresource) spells a member name as a JSON string, so this refusal does not reach it and that route stays open. #### Throws With `UNSERIALIZABLE_XML_CHARACTER` if a value this writer emits as an attribute value (a primitive's `value`, an `id` written as an attribute, an `Extension.url`) or a `div` string that passes both `div` checks above carries a code point outside XML 1.0 `Char` (production [2]): U+0000 to U+0008, U+000B, U+000C, U+000E to U+001F, an unpaired surrogate, U+FFFE or U+FFFF. In a `div` string a numeric character reference denoting one (`�`, ``) counts as the raw character does, because the Legal Character constraint makes it as fatal; each reference is judged on its own, so two references to the halves of a surrogate pair (`��`) are refused although they decode to one `Char`, while `😀` is written. A reference inside a comment is never decoded and does not count. A U+0000 used to be written raw into its `value` attribute. **Refused, never repaired**: the character is not written raw, as a reference or replaced, and neither it nor its value is dropped, so the model is left as it was. The location is the value's (`Patient.gender`, `Patient.gender.id`, `Patient.extension[0].url`, `Patient.text.div`), every one once, in walk order; neither the message nor a location carries the value or the character. The discouraged code points `Char` still admits (U+007F to U+009F, U+FDD0 to U+FDEF) are written. Raised last of all, after `UNSERIALIZABLE_XML_NAME`, so a model that trips any refusal above keeps the code it already reported and a model carrying both a name that is not a `Name` and a character that is not a `Char` draws the name code. [serializeResource](#serializeresource) writes these values as JSON strings, so this refusal does not reach it and that route stays open. #### Example ```ts import { parseResource, serializeResourceXml } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","active":true}'); serializeResourceXml(resource); // → '' ``` *** ### shadowedProperties() > **shadowedProperties**(`resource`, `path`): `string`[] Collect the FHIRPath locations where the document wrote a property name more than once, a deep walk of the whole resource. FHIR JSON requires unique property names (json.html §2.6.2: "Property names SHALL be unique") and expresses repetition with an array, so this is empty for every conformant document; a non-empty result means an element carries several values and RFC 8259 §4 gives no rule for choosing between them. The location names the element, not the individual member: FHIRPath has no way to address "the second `status` member", so one object element reports its path once however many members shadowed the name there. Two *different* objects that each repeat the same name still report separately, and where those objects are themselves duplicates of each other the two locations read identically, for the same reason: the path is all FHIRPath can say. **Scope: object elements.** A repeated name inside a primitive's `_`-sibling (its R4 `Element` metadata, which is `id` and `extension`, never `modifierExtension`) is reported by the reader as a `DUPLICATE_PROPERTY` issue but does not appear here: nothing in that metadata feeds a safety verdict, so it cannot make one wrong. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of the shadowed members, in document order. #### Example ```ts import { parseResource, shadowedProperties } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","status":"final","status":"entered-in-error"}', ); shadowedProperties(resource, "Observation"); // ["Observation.status"] ``` *** ### snapshotElements() > **snapshotElements**(`profile`, `resolve?`): readonly [`ElementDefinition`](#elementdefinition)[] The snapshot elements to validate against: the profile's own snapshot when present, else generated. A convenience over [generateSnapshot](#generatesnapshot) that does not require a resolver when the profile is already snapshotted (the common case for a published IG profile). #### Parameters ##### profile [`StructureDefinition`](#structuredefinition) The profile StructureDefinition. ##### resolve? [`BaseResolver`](#baseresolver) = `...` A base resolver, needed only when the profile carries no snapshot. #### Returns readonly [`ElementDefinition`](#elementdefinition)[] The snapshot element list. #### Throws FhirProfileError when generation is required but no (or an insufficient) resolver is given. #### Example ```ts import { snapshotElements } from "@cosyte/fhir"; const elements = snapshotElements(usCoreProfile); // uses the IG-supplied snapshot ``` *** ### starterProfile() > **starterProfile**(`url`): [`StructureDefinition`](#structuredefinition) \| `undefined` Look up a starter profile by its canonical `url`. #### Parameters ##### url `string` The canonical URL (see each profile's `url`, or [STARTER\_PROFILE\_BASE\_URL](#starter_profile_base_url)). #### Returns [`StructureDefinition`](#structuredefinition) \| `undefined` The matching starter profile, or `undefined`. #### Example ```ts import { starterProfile, STARTER_PROFILE_BASE_URL } from "@cosyte/fhir"; const p = starterProfile(`${STARTER_PROFILE_BASE_URL}/starter-patient-identifier`); ``` *** ### streamNdjson() > **streamNdjson**(`source`, `options?`): `AsyncGenerator`\<[`NdjsonRecord`](#ndjsonrecord), `void`, `undefined`\> Stream `application/fhir+ndjson`, yielding one [NdjsonRecord](#ndjsonrecord) per line as bytes arrive, **without ever loading the whole file** and with **per-line error isolation**. The source is any (async or sync) iterable of `string` or `Uint8Array` chunks: a Node `Readable` (async-iterable), a web `ReadableStream` via `for await`, or a hand-rolled generator. Lines are split on `\n` (a trailing `\r` is trimmed) as chunks flow; only the current partial line is held. A blank line is skipped. A malformed line yields an `error` record and the stream continues; a line that exceeds `maxLineBytes` before a newline yields a `LINE_TOO_LONG` record and is drained to the next newline rather than buffered. #### Parameters ##### source `AsyncIterable`\<`string` \| `Uint8Array`\<`ArrayBufferLike`\>, `any`, `any`\> \| `Iterable`\<`string` \| `Uint8Array`\<`ArrayBufferLike`\>, `any`, `any`\> An (async)iterable of UTF-8 text or byte chunks. ##### options? [`NdjsonOptions`](#ndjsonoptions) = `{}` [NdjsonOptions](#ndjsonoptions). #### Returns `AsyncGenerator`\<[`NdjsonRecord`](#ndjsonrecord), `void`, `undefined`\> An async generator of [NdjsonRecord](#ndjsonrecord), one per non-blank line, in order. #### Example ```ts import { streamNdjson } from "@cosyte/fhir"; // e.g. a Node Readable from fs.createReadStream(path) for await (const record of streamNdjson(readable)) { if (record.error) console.warn("bad line", record.error.line); // isolated, stream continues else handle(record.resource); } ``` *** ### tokenize() > **tokenize**(`input`): [`Token`](#token)[] Tokenise a FHIRPath expression into a flat [Token](#token) stream. #### Parameters ##### input `string` The FHIRPath expression source. #### Returns [`Token`](#token)[] The tokens, in order (no end-of-input sentinel, the parser tracks its own position). #### Throws UnsupportedFhirPathError on any character the bounded subset does not recognise, so an out-of-subset construct fails loudly rather than mis-lexing into a wrong parse. #### Example ```ts import { tokenize } from "@cosyte/fhir"; tokenize("clinicalStatus.exists()").map((t) => t.value); // ["clinicalStatus", ".", "exists", "(", ")"] ``` *** ### toOperationOutcome() > **toOperationOutcome**(`issues`): [`FhirComplex`](#fhircomplex) Build an `OperationOutcome` resource model from validation issues. The result is an immutable [FhirComplex](#fhircomplex); serialize it with `serializeResource` to get spec-clean, **value-free** FHIR JSON. Safe to log or return to a caller, it contains locations and coded reasons, never resource values. #### Parameters ##### issues readonly [`ValidationIssue`](#validationissue)[] The validation findings (may be empty → an "all clear" outcome). #### Returns [`FhirComplex`](#fhircomplex) The `OperationOutcome` as a model resource. #### Example ```ts import { validateResource, toOperationOutcome, serializeResource } from "@cosyte/fhir"; const { issues } = validateResource(resource); const outcome = toOperationOutcome(issues); serializeResource(outcome); // → {"resourceType":"OperationOutcome","issue":[…]} ``` *** ### undefinedJsonNull() > **undefinedJsonNull**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.UNDEFINED\_JSON\_NULL](#undefined_json_null) issue at `expression`. The location is the primitive slot the `null` occupied, so it indexes into a repeating element (`Patient.name[0].given[1]`) where the `null` sat in an array, and names the element itself (`Observation.status`) where it did not. #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { undefinedJsonNull } from "@cosyte/fhir"; const issue = undefinedJsonNull("Observation.valueQuantity.value"); ``` *** ### unexpectedXmlContent() > **unexpectedXmlContent**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.UNEXPECTED\_XML\_CONTENT](#unexpected_xml_content) issue at `expression` (XML reader only). #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { unexpectedXmlContent } from "@cosyte/fhir"; const issue = unexpectedXmlContent("Observation.status"); ``` *** ### unhandledModifierExtensions() > **unhandledModifierExtensions**(`resource`, `path`): `string`[] Collect the FHIRPath locations of every `modifierExtension` whose URL this library cannot honor, a deep walk of the whole resource, so a modifier nested in a backbone element or a contained resource is caught too. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of unhandled `modifierExtension`s, in document order. #### Example ```ts import { parseResource, unhandledModifierExtensions } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Patient","modifierExtension":[{"url":"http://example.org/x"}]}', ); unhandledModifierExtensions(resource, "Patient"); // ["Patient.modifierExtension[0]"] ``` *** ### unknownProperty() > **unknownProperty**(`expression`): [`FhirIssue`](#fhirissue) Build a [ISSUE\_CODES.UNKNOWN\_PROPERTY](#unknown_property) issue at `expression`. #### Parameters ##### expression `string` #### Returns [`FhirIssue`](#fhirissue) #### Example ```ts import { unknownProperty } from "@cosyte/fhir"; const issue = unknownProperty("Patient.wibble"); ``` *** ### unreadableAbsenceMarkers() > **unreadableAbsenceMarkers**(`resource`, `path`): `string`[] The locations where an absence marker is present and its reason is not readable: the standalone form of [SafetyReadout.unreadableAbsenceMarkers](#unreadableabsencemarkers). Neither `unknown` nor "populated" is inferred at such an element. The extension's `value[x]` binds to a closed fifteen-concept value set at required strength, so a code outside it is a code this library will not read and will not author, and an element with no readable reason is still an element the sender declared absent. Disclosing the refusal is the whole remedy. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations, in walk order, each once however many unreadable markers sit there. #### Example ```ts import { parseResource, unreadableAbsenceMarkers } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Observation","_status":{"extension":[{"url":' + '"http://hl7.org/fhir/StructureDefinition/data-absent-reason","valueCode":"UNKNOWN"}]}}', ); unreadableAbsenceMarkers(resource, "Observation"); // ["Observation.status"] ``` *** ### unreadableBooleans() > **unreadableBooleans**(`resource`, `path`): `string`[] The locations where a `boolean`-valued safety element carries a **written value this layer could not read as a boolean**, so the element is present, the sender filled it in, and the read returned `undefined` all the same. R4 spells a `boolean` as `true` or `false` and nothing else (datatypes.html), so `` and `` (ordinary output from a v2 or C-CDA converter, which is how a great deal of data reaches a FHIR surface) carry no boolean this library may read. Coercing them would author a value the sender did not spell, and `"1"` and `"Y"` also appear on the wire meaning the opposite of what a naive reading gives them. So the value stays unread, and **this is the record that it was there**: without it, `value="1"` and `value="0"` read identically, and a prescriber's "yes, do not administer" is indistinguishable from "no" with nothing anywhere to say a choice was made. **Value-free, like every location on this readout**: the text that failed to read is not carried here or anywhere else, only the FHIRPath of the element that held it. **The element is `doNotPerform`**, the only `boolean` [readSafety](#readsafety) takes off a document, and there is no resource-type gate on it. **The window is every resource root** (so a `contained` or Bundle-`entry` resource is covered), which is **the negation read's own window**: the value that is read and the value that cannot be read are decided together, in one pass, so neither can cover a document the other does not. It is **not** `arrayWrappedScalars`' whole window, and the difference is a live residual rather than a nicety: that report's *element-level* half is scoped to the resource types the cardinality table knows, so `{"resourceType":"ServiceRequest", "doNotPerform":[true]}` is read here and at [SafetyReadout.negations](#negations), while the wrapper it arrived in draws no `ARRAY_WRAPPED_SCALAR`. Closing that needs a cardinality for the element name on the types outside the table, which is a per-resource question this layer does not answer. It is **not** a report of every unreadable value in a document: the profile booleans, an `ElementDefinition.min` whose text falls outside the lexical space the profile loader reads, and a `Quantity` magnitude's lexical forms are read elsewhere and are still lost silently. Empty for every conformant document, in either wire format. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of the unreadable boolean values, in walk order. #### Example ```ts import { parseResourceXml, unreadableBooleans } from "@cosyte/fhir"; const { resource } = parseResourceXml( '', ); unreadableBooleans(resource, "MedicationRequest"); // ["MedicationRequest.doNotPerform"] ``` *** ### unreadableNegationCodes() > **unreadableNegationCodes**(`resource`, `path`): `string`[] The locations where a `code`-valued negation element holds **content at a position no datatype FHIR spells there can hold**, so the negation read stepped over it and returned nothing. `{"resourceType":"Procedure","status":{"value":"not-done"}}` is ordinary output from a converter that carried FHIR XML's `value` attribute across as a JSON member, and `{"…","status":3}` from a feed whose codes are enumerated numerically. Neither is a `code` (FHIR JSON spells one as a JSON string, json.html §2.6.0) and neither is a `CodeableConcept`, the other datatype a root `status` carries. **Nothing here descends into the object or coerces the number**, which would resolve a negation out of an encoding no version of FHIR defines for JSON. This is **the record that content was there**, which is what the read was missing: without it a procedure recorded as not done returns `negations: []` under `safeToSummarize: true`, indistinguishable from one that was carried out. **The shape complement of [nearMissNegationCodes](#nearmissnegationcodes-1)**, which covers a *value* the exact match declined. A position holding no value at all is invisible to every value-shaped question here, [unreadableBooleans](#unreadablebooleans-1) included, and that is the gap this closes. **The element is `status`, at every resource root**, the negation read's own window. A complex with at least one member, all of them ones FHIR spells there, is left alone, because R4 and R5 both spell some root `status` elements as `CodeableConcept`; one member outside that set is enough to report, and so is carrying no member at all. `verificationStatus` is deliberately outside it, and `AllergyIntolerance.code` is outside it for the reason that keeps `no-known-allergy` root-scoped (see [SafetyReadout.unreadableNegationCodes](#unreadablenegationcodes) for both). **Value-free**: only the FHIRPath of the element is carried. Empty for every conformant document this library has been measured against, in either wire format. See [SafetyReadout.unreadableNegationCodes](#unreadablenegationcodes) for the two declared limits. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) The resource model. ##### path `string` The FHIRPath prefix for the resource root (usually its `resourceType`). #### Returns `string`[] The locations of the unreadable negation-code positions, in walk order. #### Example ```ts import { parseResource, unreadableNegationCodes } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Procedure","status":{"value":"not-done"}}', ); unreadableNegationCodes(resource, "Procedure"); // ["Procedure.status"] ``` *** ### validatePrimitiveValue() > **validatePrimitiveValue**(`value`, `datatype`): `"ok"` \| `"type-mismatch"` \| `"invalid"` Whether a model primitive value is well-formed for a declared FHIR R4 primitive datatype. Returns `"ok"` when the value matches the datatype's value-domain, `"type-mismatch"` when the value's *shape* is wrong for the datatype (e.g. a JS string where a `boolean` is required, or a `boolean` where a numeric type is required), and `"invalid"` when the shape is right but the lexical form fails the datatype's pattern. The caller maps these to `TYPE_MISMATCH` / `PRIMITIVE_INVALID` issues. An unknown (non-primitive) datatype name yields `"ok"`, a complex datatype is validated structurally elsewhere, not here; this function speaks only for the primitives it knows. #### Parameters ##### value [`PrimitiveValue`](#primitivevalue) The model primitive value. ##### datatype `string` The declared FHIR datatype name. #### Returns `"ok"` \| `"type-mismatch"` \| `"invalid"` `"ok"` | `"type-mismatch"` | `"invalid"`. #### Example ```ts import { validatePrimitiveValue } from "@cosyte/fhir"; validatePrimitiveValue("2013-06-08", "date"); // "ok" validatePrimitiveValue("2013-13-40", "date"); // "invalid" validatePrimitiveValue("male", "boolean"); // "type-mismatch" ``` *** ### validateResource() > **validateResource**(`resource`, `options?`): [`ValidationResult`](#validationresult) Validate a FHIR resource model against structural, cardinality, and value-domain rules. #### Parameters ##### resource [`FhirComplex`](#fhircomplex) A resource model (typically from `parseResource`). ##### options? [`ValidateOptions`](#validateoptions) = `{}` Mode and extra schemas. #### Returns [`ValidationResult`](#validationresult) The value-free [ValidationResult](#validationresult). #### Example ```ts import { parseResource, validateResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","gender":"masculine"}'); const { issues } = validateResource(resource); // → one CODE_INVALID at Patient.gender ``` *** ### validateUcumShape() > **validateUcumShape**(`code`): [`UcumShapeVerdict`](#ucumshapeverdict) Whether a string is a **shape-valid** UCUM code. This checks structure only, it does **not** assert the code names a real UCUM unit (that needs the UCUM content, which is not bundled). A code is `"invalid"` when it is empty, contains whitespace (UCUM codes never do), or has unbalanced `[]` / `{}` / `()`. Curly-brace annotations (`{RBC}`) are stripped before the whitespace and bracket checks, since their inner text is unconstrained. Everything else is `"ok"`, a conservative pass, so a well-formed but exotic unit is never wrongly rejected. The only consumer of this is a **warning** (`UCUM_UNIT_UNRECOGNIZED`), never an error, so an occasional lenient pass on a weird annotation cannot flip validity. #### Parameters ##### code `string` A candidate UCUM code (e.g. `"mm[Hg]"`, `"kg/m2"`, `"/min"`). #### Returns [`UcumShapeVerdict`](#ucumshapeverdict) `"ok"` when the shape is well-formed, `"invalid"` otherwise. #### Example ```ts import { validateUcumShape } from "@cosyte/fhir"; validateUcumShape("mm[Hg]"); // "ok" validateUcumShape("mm Hg"); // "invalid", UCUM has no spaces (the code is "mm[Hg]") validateUcumShape("[lb_av"); // "invalid", unbalanced bracket ``` *** ### validationIssue() > **validationIssue**(`code`, `severity`, `expression`, `constraint?`, `codeSystemVersion?`): [`ValidationIssue`](#validationissue) Construct a value-free [ValidationIssue](#validationissue). The `IssueType` is fixed by the code; only the `severity` is caller-chosen (it varies with lenient vs strict mode for some codes). #### Parameters ##### code [`ValidationCode`](#validationcode) The validation code. ##### severity [`ValidationSeverity`](#validationseverity) The R4 severity to record (mode-dependent for some codes). ##### expression `string` The FHIRPath location of the finding, never a value. ##### constraint? `string` The spec constraint key, for an invariant finding only (e.g. `"ait-1"`). ##### codeSystemVersion? [`CodeSystemVersionRecord`](#codesystemversionrecord) The code-system release record, for a finding a terminology service produced only. Omit it for every finding no service was consulted for; passing `{ declared: false }` is the distinct claim that a service answered and named no release. #### Returns [`ValidationIssue`](#validationissue) #### Example ```ts import { validationIssue } from "@cosyte/fhir"; const issue = validationIssue("CODE_INVALID", "error", "Patient.gender"); ``` *** ### wouldLosePrecisionAsDouble() > **wouldLosePrecisionAsDouble**(`raw`): `boolean` Whether a JSON-number literal would lose information if it were routed through a JavaScript `number` (an IEEE-754 double) and back. This is the exact test the codec uses to raise `DECIMAL_PRECISION_AT_RISK`: it is `true` when a naive `JSON.parse`-based reader *would have* corrupted this value, either by changing its quantity (too many significant digits, or magnitude past the safe-integer range) or by dropping trailing-zero precision (`0.010` → `0.01`). #### Parameters ##### raw `string` #### Returns `boolean` #### Example ```ts import { wouldLosePrecisionAsDouble } from "@cosyte/fhir"; wouldLosePrecisionAsDouble("0.010"); // true , trailing zero dropped by a double wouldLosePrecisionAsDouble("0.5"); // false, survives a double exactly ``` --- URL: https://docs.cosyte.com/fhir/troubleshooting Markdown: https://docs.cosyte.com/fhir/troubleshooting.md # Troubleshooting Every refusal and every degraded reading in this library carries a coded reason. Look the code up here: each entry says what you are seeing, why the library behaves that way, and what to do about it. A fatal read is thrown as a typed error carrying a code. A non-fatal finding is returned, either on the read result or in the validation findings, and is never thrown. ## The XML reader refuses a document carrying a DOCTYPE You get a thrown error whose code is `DTD_FORBIDDEN`, and nothing is parsed. The reader refuses every document type declaration outright. Resolving one is how external entity attacks read files off the machine running the parser, and how a small document expands into gigabytes of memory. Refusing is not configurable, because a switch to turn it off is a switch an attacker only has to find once. ```ts import { FhirXmlError, XML_FATAL_CODES, parseResourceXml } from "@cosyte/fhir"; const readXml = (input: string): string => { try { parseResourceXml(input); return "read"; } catch (err) { return err instanceof FhirXmlError ? err.code : "unknown"; } }; readXml(''); // => XML_FATAL_CODES.DTD_FORBIDDEN ``` **What to do**: strip the declaration upstream. A FHIR XML resource has no legitimate use for a DTD, so a sender that emits one has a serialization bug worth reporting back to them. Do not attempt to pre-expand entities yourself before handing the document over: that reintroduces the exact exposure the refusal exists to close. ## The XML reader refuses an entity it does not define You get a thrown error whose code is `UNDEFINED_ENTITY`. Only the five predefined XML entities and numeric character references are accepted. Anything else would have to be looked up, and a lookup is either a local definition (which arrives in a DTD, see above) or a fetch. **What to do**: have the sender emit numeric character references or the literal characters. An accented name spelled with a numeric reference reads fine. ## A document is refused for depth You get a thrown error whose code is `MAX_DEPTH_EXCEEDED`, from either codec. Both readers stop at a fixed nesting bound. Without one, a deeply nested document is a denial of service that arrives as a stack overflow deep inside the runtime, untyped and impossible to handle cleanly. **What to do**: treat it as hostile or broken input rather than as a document to be recovered. If a legitimate sender trips it, the shape of what they are sending is worth a conversation. ## The JSON reader refuses the input outright You get a thrown error whose code is `MALFORMED_JSON`, carrying a byte offset and no slice of the input. **What to do**: check the transport before the document. Truncation at a proxy, a doubled response body and a mis-set encoding all arrive looking like this. The offset tells you where the reader stopped, and the absence of a quoted fragment is deliberate: the bytes around a parse failure are as likely to be patient data as anything else in the document. ## A property appears twice in one JSON object You get a `DUPLICATE_PROPERTY` finding at warning severity. The document is still read. FHIR requires unique property names and expresses repetition with an array, so a repeated name has no defined winner. The reader keeps the first value as the element's value, keeps the shadowed one rather than discarding it, and reports the position: ```ts import { parseResource } from "@cosyte/fhir"; const { issues } = parseResource('{"resourceType":"Patient","gender":"male","gender":"female"}'); issues.map((issue) => issue.code); // => ["DUPLICATE_PROPERTY"] issues.map((issue) => issue.expression); // => ["Patient.gender"] ``` **What to do**: do not resolve it yourself by picking a side, and be careful about summarizing the resource at all. A repeated property is a sender bug, and the two values can differ in ways that matter clinically. Report the location back to the sender, and treat any single-value read of that element as arbitrary until they fix it. Note also that a resource carrying one cannot be written back out unchanged: the writers refuse rather than silently pick a winner. ## A resource carries a modifierExtension the library does not know You get an `UNHANDLED_MODIFIER_EXTENSION` finding at error severity, so `valid` is false. A modifier extension changes the meaning of the element it sits on. An unknown one cannot be ignored, because ignoring it means processing the resource as though the modifier were not there, which is how a negation or a retraction gets lost. ```ts import { parseResource, readSafety, validateResource } from "@cosyte/fhir"; const { resource } = parseResource( '{"resourceType":"Patient","modifierExtension":[{"url":"http://example.org/local-flag","valueBoolean":true}]}', ); validateResource(resource).issues.map((issue) => issue.code); // => ["UNHANDLED_MODIFIER_EXTENSION"] readSafety(resource).safeToSummarize; // => false ``` **What to do**: stop the automated path and route the resource to a human, or teach your integration what that specific URL means and handle it explicitly. Do not filter the finding out to get a green verdict. If your code summarizes resources, call `assertSafeToSummarize` first so this case throws instead of quietly producing a summary that omits a modifier. ## A profile constraint was not evaluated You get an `INVARIANT_UNCHECKED` finding at informational severity, carrying the constraint key. The library evaluates FHIRPath through a bounded, in-repository subset rather than a full engine. An expression outside that subset is reported as unchecked instead of being assumed to pass: ```ts import { defineProfile, parseResource, validateResource } from "@cosyte/fhir"; const profile = defineProfile({ url: "http://example.org/StructureDefinition/local-observation", name: "LocalObservation", type: "Observation", differential: [ { path: "Observation", constraint: [ { key: "local-1", severity: "error", expression: "subject.resolve() is Patient" }, ], }, ], }); const { resource } = parseResource( '{"resourceType":"Observation","status":"final","code":{"text":"synthetic"}}', ); const outcome = validateResource(resource, { profiles: [profile] }); outcome.issues.map((issue) => issue.code); // => ["INVARIANT_UNCHECKED"] outcome.issues.map((issue) => issue.constraint); // => ["local-1"] ``` **What to do**: read it as "unknown", never as "passed". If the constraint matters to you, evaluate it yourself for now, and keep the finding in your logs so the gap is visible rather than assumed away. Informational severity leaves `valid` true, so a pipeline that only checks `valid` will not see it: check the findings. ## A profile slice was not decided You get a `PROFILE_SLICE_UNCHECKED` finding at informational severity. The slicing discriminator on that element is one this library cannot apply. The `position` discriminator is the common cause: it is not part of the R4 discriminator set at all. **What to do**: the same as the previous entry. It means the slice membership was not decided, not that it matched. If the profile is yours, an equivalent slice expressed with a value, pattern, type or profile discriminator will be evaluated. ## An element is not in the base definition You get an `UNKNOWN_ELEMENT` finding: a warning on a lenient read, an error in strict mode. The element was preserved, not dropped. Lenient is the right default for reading somebody else's document, and strict is the right mode for checking your own before you send it. **What to do**: on a read, look at whether the element is a vendor extension you should be handling. On a write, treat it as an error and fix the element before sending. ## XML element text disappeared You get an `UNEXPECTED_XML_CONTENT` finding, and the text is not in the model. FHIR XML carries a primitive's value in a `value` attribute. Character data written directly on an element has no slot in the model, so it is dropped. What the library guarantees is that the drop is reported, and nothing more. This is worth taking seriously: a status or a dose written as element text is a value that a summary will not see. **What to do**: fix the sender. In the meantime, treat a resource carrying this finding as incomplete rather than as read successfully, and do not compare it against the same document in JSON expecting agreement. ## The writer refuses to serialize a resource You get a thrown error whose code is one of `SERIALIZE_ERROR_CODES`. The writers author no value of their own. When a model carries something the target format cannot spell back, the choice is to invent content, drop content, or refuse, and the library refuses. The duplicate property above is one such case, and so is a resource type that cannot be written as an XML tag. **What to do**: read the code to find out which shape stopped it, and fix the model rather than the writer. In almost every case the underlying document was already non-conformant on the read, and the finding raised there names the same position. --- URL: https://docs.cosyte.com/fhir/limits Markdown: https://docs.cosyte.com/fhir/limits.md # Current limits What the package does not do, in its own words. Everything here is a deliberate boundary, and each one is stated because working around a limit you know about is cheap, while discovering one in production is not. ## No terminology content is bundled The library knows code system identities, not their contents. There is no bundled SNOMED CT, LOINC, CPT, RxNorm or ICD content, and there never will be: those are licensed distributions, and vendoring them into a parser is both a licensing problem and a staleness problem. What that means in practice: - a coding is checked for a system this library recognises, and for the binding strength the element declares; - membership of a code in a value set is not answered unless you supply a terminology service; - with no service supplied, the checks degrade to that content-free level and never produce a false error. Silence about membership is not a claim of validity. ## No profile content is bundled US Core and vendor `StructureDefinition`s are caller-supplied. The package validates against whatever profiles you hand it, with snapshot generation, slicing, fixed and pattern values, and must-support treated as a system obligation rather than an instance requirement. The consequence to plan for: "validated" here means "validated against the profiles you supplied". The library never asserts conformance to a version of US Core you did not pass in, because the alternative is a claim about an implementation guide release that may not be the one your trading partner is on. ## No typed per-resource models There is no generated TypeScript interface per resource type, and no `Patient` or `Observation` class. The model is schema-free, and elements are reached by name through small helpers. This is the cost side of the trade that makes a lenient read safe: a schema-free tree cannot drop an element it has no field for, but it also cannot offer compile-time completions for a resource's elements. If you want typed access to a handful of elements, wrap the helpers in your own accessors at the edge of your program. ## Two verdicts that say "not checked" rather than passing Both of these are findings the validator emits rather than silently letting something through. If you treat any non-error finding as a pass, you will misread them. - `INVARIANT_UNCHECKED`: a constraint whose FHIRPath expression is outside the bounded expression subset this library evaluates, whether a profile supplied it or R4 declares it on the resource type. It is reported as informational, and it means the invariant was **not** evaluated, not that it held. With no profile, the one base constraint reported this way is `dom-3` on a resource that carries a contained resource: checking that every contained resource is referenced needs reference resolution the subset does not have. With nothing contained, `dom-3` holds by its own terms and nothing is reported, also when a supplied profile carries R4's `dom-3` as written. A profile's constraint under that key written any other way is its own expression, and is reported unchecked like any other the subset cannot evaluate. - `PROFILE_SLICE_UNCHECKED`: a slice whose discriminator this library cannot apply, including the `position` discriminator, which is not part of the R4 discriminator set. Again, it means the slice membership was not decided, not that it matched. Both exist because the alternative, staying quiet, is indistinguishable from a pass to every consumer downstream. ## R4 first FHIR R4 (`4.0.1`) is the modeled target. R5 and DSTU2 documents can be read, because the model is schema-free, but they are not validated against their own definitions. Treat cross-version reads as data recovery, not conformance. ## Reported, not repaired Several non-conformant shapes are read, reported, and deliberately not fixed. Character data written directly on an element in XML is the clearest example: FHIR carries a value in an attribute, so text in that position has no slot on the model and is dropped, with a finding. The package reports the drop rather than guessing what the sender meant. The same posture governs the writers. When a model cannot be spelled back into the target format without inventing or losing content, the write is refused with a coded reason instead of emitting something plausible. [Troubleshooting](https://docs.cosyte.com/fhir/troubleshooting.md) covers what each refusal means. --- # @cosyte/dicom > DICOM Part 10 parser and metadata toolkit for medical imaging. These are the 15 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/dicom/intro Markdown: https://docs.cosyte.com/dicom/intro.md # @cosyte/dicom Read a real-world, vendor-quirky DICOM Part 10 file and pull metadata fields out in one line, without having read the DICOM standard. `@cosyte/dicom` is a metadata-first TypeScript parser for Node.js: a lenient reader, an immutable dataset with dot-path access, a generated data dictionary, and stable warning codes for the deviations real scanners produce. It is **metadata-first by design**. Pixel data is exposed as a raw `Buffer`, and under the encapsulated (compressed) transfer syntaxes of PS3.5 2026c section A.4 - JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest - the object parses and `readPixelDataFragments` returns its Basic Offset Table and fragments as raw bytes, but in every syntax pixel data is **not decoded**. `serializeDicom` writes those syntaxes back with the fragments untouched, and refuses with `INVALID_ENCAPSULATED_PIXEL_DATA` a fragment stream section A.4 does not allow rather than repairing it. An object under one of the four JPIP Referenced syntaxes (sections A.6, A.7, A.11 and A.12) parses for its metadata too, and its Pixel Data Provider URL is returned as written and **never fetched**; that object is read-only here, refused by both `serializeDicom` and `deidentify`. Pixel decoding, along with DIMSE networking and DICOMweb, is left to future companion packages. :::caution Read this before you point it at real data **[Known limitations](https://docs.cosyte.com/dicom/limitations.md)** is the "do not over-trust" list: what is out of scope, which PHI residuals are measured and still open, and which fields are not safe to log. Mis-reading a patient identifier, an image's signedness, or a rescale slope can cause real clinical harm, so that page is a deliverable of this package rather than a footnote. A de-identified output from `@cosyte/dicom` is **metadata-de-identified only**. ::: ## Install ```bash npm install @cosyte/dicom ``` ## One line to a useful field Every DICOM object on this site is **synthetic**: an invented patient and obviously-fake UIDs, encoded as a small base64 Part 10 buffer so an example needs no file on disk. This one is executed against the built package on every test run, so what it asserts is what the shipped code returns. ```ts runnable import { parseDicom } from "@cosyte/dicom"; // Synthetic Part 10 object (base64): a tiny CT header, invented MRN, fake UIDs. No real PHI. const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAYABDUwIAQ1QQACAATE8GAE1STi00Mg==", "base64", ); const ds = parseDicom(buf); ds.series.modality; // => "CT" ds.patient.id; // => "MRN-42" ``` ## Read a file ```ts import { readFile } from "node:fs/promises"; import { parseDicom, Dictionary } from "@cosyte/dicom"; const ds = parseDicom(await readFile("study.dcm")); ds.get("00100010")?.value; // Patient's Name: a typed PersonName value ds.get("00080020")?.value; // Study Date: a typed DicomDate value ds.get("7FE00010")?.value; // Pixel Data: { kind: "binary", bytes }, raw, never decoded ds.warnings; // stable, byte-offset tolerance warnings // Prefer keywords? Resolve one to its tag through the dictionary: ds.get(Dictionary.byKeyword("PatientName")?.tag ?? ""); ``` Elements are keyed by their 8-character `(group,element)` **tag** (`"00100010"`), and `get` / `has` take that tag form. A keyword like `"PatientName"` is resolved to its tag through `Dictionary.byKeyword`. `get` itself does not take keywords. The safety-critical fields have an even shorter path: the typed `patient` / `study` / `series` / `image` views below. ## Typed values `get` returns an `Element`; its `.value` lazily decodes the raw bytes into a typed, discriminated `DicomValue` and caches the result. Every one of the 34 VRs has a decode: numbers, 64-bit `bigint`s, attribute tags, person names (3-group / 5-component), strings, free text, numeric strings (`DS`/`IS`), temporal values (`DA`/`TM`/`DT`), sequences, and raw `binary` for bulk data. ```ts const rows = ds.get("00280010")?.value; // Rows (US) if (rows?.kind === "numbers") rows.values[0]; // 512 const name = ds.get("00100010")?.value; // Patient's Name (PN) if (name?.kind === "personName") name.values[0]?.alphabetic.givenName; // "Jane" ``` Decode is **fail-safe**: it never throws and never coerces a malformed value to a plausible-but-wrong one (a bad `DS`/`IS` token becomes `null`, never `NaN`→0). Per-value deviations surface on the returned value's own `warnings`. String VRs honor the `(0008,0005)` Specific Character Set, threaded through nested sequence items: UTF-8 and ISO-8859 under one Value, and the code extensions of ISO 2022 (Japanese, Korean and Chinese names among them) under more than one, where a value the decode had to tolerate carries a warning. A single-valued set gets no code-extension handling; the limits are in [Typed values](https://docs.cosyte.com/dicom/spec-notes-values.md#character-sets). ## Safety-critical views Pulling the right field out of raw tags is error-prone in exactly the places that matter most, so the `Dataset` exposes four typed, fail-safe views over the safety-critical attributes: `patient`, `study`, `series`, and `image`: ```ts const p = ds.patient; p.id; // "MRN-42", NOT globally unique on its own… p.issuerOfId; // …pair it with the issuer for cross-system matching p.name?.alphabetic.familyName; // structured PN, never flattened const img = ds.image; img.rescaleSlope; // undefined ⇒ MUST NOT assume 1 img.signed; // undefined ⇒ signedness unknown, never guessed img.pixelSpacing; // patient-plane mm, distinct from imagerPixelSpacing ``` The omissions are deliberate and load-bearing: a missing value is **typed-absent** (`undefined`), never a substituted default, because the dangerous DICOM failure is the confident, wrong image. `rescaleSlope` is absent (not `1`) when the tag is absent; `signed` is absent unless Pixel Representation was present; `photometricInterpretation` is never defaulted to `MONOCHROME2`; and the three pixel-spacing tags are distinct fields, never aliased. For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame-else-Shared (it throws a `DicomValueError` for an out-of-range frame, or a required geometry macro missing from both groups: the message carries only structural facts, never PHI): ```ts if (img.isEnhancedMultiFrame) { const f = img.frame(0); f.planePosition?.imagePositionPatient; // this frame's [x, y, z] f.pixelMeasures?.pixelSpacing; // this frame's [row, col] mm } ``` ## Lenient by default The parser is **lenient by default**: the quirks real scanners emit (odd-length values, missing padding, off-spec VRs) become warnings carrying a stable code and the byte offset where they occurred, not failures. Only an unrecoverable structural condition throws, and `FATAL_CODES` is the whole set of them. When you re-serialize, the writer emits a correct File Meta group length, even-length values and proper padding (Postel's Law). See [Serialization](https://docs.cosyte.com/dicom/serialization.md) for what the writer will and will not do. ## Source profiles Real files come from real vendors, and vendors deviate in documented, predictable ways. A **profile** lets you opt into source-specific tolerance without ever risking a wrong decode. Pass one to `parseDicom`: ```ts import { parseDicom, profiles } from "@cosyte/dicom"; // Resolve Siemens CSA private headers to their real VRs instead of UN. const ds = parseDicom(buf, { profile: profiles.siemens }); ``` A profile bundles three things that only ever **tighten or annotate** a parse, never loosen it past the lenient default: - **Private-dictionary overlay**: resolves the Implicit VR of vendor private data elements by the file's _live_ private-creator string (e.g. `"SIEMENS CSA HEADER"`), never a hard-coded block number. A creator the profile does not know degrades to `UN` plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning. - **Escalations**: Tier-2 warning codes promoted to a thrown `DicomParseError` (a stricter posture). - **Suppressions**: benign, high-volume warning codes silenced for a known-quirky source. Five built-ins ship under the `profiles` namespace: `ge`, `siemens`, `philips` (vendor overlays) and `strict` / `lenient` (posture presets). Build your own with `defineProfile()`. It validates input, composes via `extends`, and returns a frozen profile: ```ts import { defineProfile, profiles } from "@cosyte/dicom"; const acmeStrict = defineProfile({ name: "acme-strict", extends: profiles.strict, privateTags: { "ACME PRIV 01": { "0019XX10": { vr: "DS", keyword: "AcmeDose", name: "ACME Dose" } }, }, }); ``` ## De-identify Before sharing a file, strip the identifying metadata. `deidentify()` applies the PS3.15 Annex E **Basic Application Level Confidentiality Profile** (replacing, emptying, or removing every attribute the standard lists as identifying) and returns a fresh dataset plus a report of what it did. Two of the report's fields carry source bytes rather than table lookups and say so on the type: `report.uidMap` and `report.removedPrivateTags`. ```ts import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom"; const { dataset, report } = deidentify(parseDicom(buf)); const safe = serializeDicom(dataset); // safe to share console.log(report.attributes.length, "attributes acted on"); console.log(report.warnings); // e.g. burned-in pixel annotation that this layer cannot clean ``` It is a pure function. Your input dataset is never mutated. UIDs are remapped to deterministic `2.25` replacements that stay consistent across files, so a de-identified study still hangs together. Opt into the metadata-affecting Annex E Options to keep specific classes of attribute (the two PS3.15 §E.3.6 temporal ones are alternatives, so take at most one of them): ```ts // Keep original UIDs and acquisition dates; clean (rather than drop) free-text descriptions. const { dataset } = deidentify(parseDicom(buf), { retain: ["RetainUIDs", "RetainLongitudinalTemporal", "CleanDescriptors"], }); ``` This is **metadata-level** de-identification. Pixel data is out of scope: when a file carries burned-in annotation that this layer cannot remove, you get a `DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` warning rather than a false sense of safety. Pixel cleaning is deferred to `@cosyte/dicom-pixel`. ## Next - Read the **API reference** for every export, generated from source. - See the data dictionary coverage and the full list of warning codes. --- URL: https://docs.cosyte.com/dicom/installation Markdown: https://docs.cosyte.com/dicom/installation.md # Install and verify `@cosyte/dicom` is a metadata-first DICOM Part 10 parser for Node.js/TypeScript. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration, and it takes **zero runtime dependencies**: the byte-level and character-set work is done in-house (the package budget allows up to three ADR-justified deps; none are currently taken). > **Status:** `0.1`, public, and published on npm. The install command below is live. The exported > functions, the shapes they return and the warning codes are the surface we keep stable; below 1.0 > a breaking change moves the minor version, and the changelog says how to migrate. For the current > version, read `npm view @cosyte/dicom version` rather than a number written in a doc. ## Prerequisites - **Node.js >= 22.** The whole `@cosyte/*` suite targets ES2023 / Node 22+. - A package manager: `pnpm`, `npm`, or `yarn`. - **No runtime dependencies, no native build, no post-install script.** The data dictionary is generated from the official DICOM Part 6 source at build time and committed, so at runtime there is no network or filesystem lookup. ## Install ```bash npm install @cosyte/dicom ``` ## Smoke test Confirm the package resolves and a real entry point is callable: parse the smallest synthetic Part 10 object and read a field back through a typed view: ```ts runnable import { parseDicom } from "@cosyte/dicom"; // Synthetic Part 10 object (base64): a tiny CT header, invented MRN, fake UIDs. No real PHI. const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAYABDUwIAQ1QQACAATE8GAE1STi00Mg==", "base64", ); const ds = parseDicom(buf); ds.series.modality; // => "CT" ds.patient.id; // => "MRN-42" ds.warnings.length; // => 0 ``` If that resolves and returns, the install is good. Head to the [Quickstart](https://docs.cosyte.com/dicom/quickstart.md). ## Module systems `@cosyte/dicom` is `"type": "module"` and exposes both conditions, so both of these resolve to the right build without extra configuration: ```ts // ESM / TypeScript import { parseDicom, serializeDicom, deidentify } from "@cosyte/dicom"; ``` ```js // CommonJS const { parseDicom, serializeDicom, deidentify } = require("@cosyte/dicom"); ``` The single top-level entry point (`@cosyte/dicom`) publishes per-condition types (`.d.ts` for `import`, `.d.cts` for `require`), gated by `attw` on every release. Editor IntelliSense matches the build you actually load. The package also exports `VERSION`, the version constant of the build you have loaded. It is there so a bug report can say which build produced a result, and it is deliberately not reproduced in this documentation: read it off the loaded package, or ask npm, rather than trusting a numeral written in a doc. ## PHI discipline Every example in this documentation is built from a **synthetic** object (an invented patient, obviously-fake UIDs and MRNs) encoded as a small base64 buffer so a snippet needs no file on disk. Do the same in your own tests: a real DICOM object is PHI, and one committed to a repository is a leak the moment it publishes. The parser helps: every warning and error message carries only structural locators (attribute tag, byte offset, code), never a patient name, an identifier, or pixel content. See [Troubleshooting](https://docs.cosyte.com/dicom/troubleshooting.md) for the redaction posture and the explicit non-goals. --- URL: https://docs.cosyte.com/dicom/quickstart Markdown: https://docs.cosyte.com/dicom/quickstart.md # Read your first DICOM object This page gives you a first useful result: read a DICOM Part 10 object and pull out **who the patient is**, **what the study/series is**, and the **image geometry** you need before touching a pixel, in a few lines, without having read the standard. The safety-critical fields come out through four typed, fail-safe views (`patient`, `study`, `series`, `image`); everything else is reachable by its `(group,element)` tag. `@cosyte/dicom` is **metadata-first**: it reads the header, decodes typed values, and exposes pixel data as a raw `Buffer`. It never decodes pixels, and it is not a DIMSE/DICOMweb client. See [Troubleshooting](https://docs.cosyte.com/dicom/troubleshooting.md) for the explicit non-goals. > Every object below is **synthetic**: an invented patient ("Jane Doe"), obviously-fake UIDs and an > invented MRN, encoded as a base64 buffer. A DICOM object is PHI; a fixture must never hold a real > one. ## Parse an object and read the metadata `parseDicom(buffer)` returns an immutable `Dataset`. The typed views answer "whose object, what study, what image" without your having to know which tag holds which field or how to decode its bytes. ```ts runnable import { parseDicom } from "@cosyte/dicom"; // Synthetic CT object (base64): patient + study/series + image geometry. No real PHI. const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg", "base64", ); const ds = parseDicom(buf); // Identity: whose object, matchable across systems. ds.patient.id; // => "MRN-42" ds.patient.issuerOfId; // => "SAMPLE-HOSP" ds.patient.name?.alphabetic?.familyName; // => "Doe" ds.patient.name?.alphabetic?.givenName; // => "Jane" // Study / series context. ds.study.instanceUid; // => "1.2.826.0.1.3680043.8.498.1.1" ds.series.number; // => 2 ds.series.modality; // => "CT" // Image geometry: the numbers you need before you can interpret a pixel. ds.image.rows; // => 512 ds.image.columns; // => 512 ds.image.signed; // => true ds.image.rescaleSlope; // => 1 ds.image.rescaleIntercept; // => -1024 ds.image.pixelSpacing; // => [0.5, 0.5] // Clean, spec-conformant input: nothing tolerated, nothing flagged. ds.warnings.length; // => 0 ``` Every one of those fields is **typed-absent when the tag is absent**, never a substituted default. `ds.image.rescaleSlope` is `undefined` (not `1`) when Rescale Slope is missing; `ds.image.signed` is `undefined` unless Pixel Representation was present; the three pixel-spacing tags are distinct fields, never aliased. The dangerous DICOM failure is the confident, wrong image, so a missing value stays missing. See [Safety-critical views](https://docs.cosyte.com/dicom/spec-notes-safety.md) for the full contract. ## Reach any element by tag For attributes outside the four views, use `get` with the 8-character `(group,element)` tag. It returns an `Element`; `.value` lazily decodes the raw bytes into a typed, discriminated `DicomValue`: ```ts runnable import { parseDicom, Dictionary } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg", "base64", ); const ds = parseDicom(buf); // Rows (0028,0010) is a US: a numeric value. ds.get("00280010")?.value.kind; // => "numbers" // Study Date (0008,0020) decodes to a structured, validated DicomDate. ds.get("00080020")?.value.kind; // => "dates" // Prefer keywords? Resolve one to its tag through the dictionary, then get by tag. const tag = Dictionary.byKeyword("PatientName")?.tag; // "00100010" ds.get(tag ?? "")?.value.kind; // => "personName" // `has` also takes the tag form. ds.has("00100010"); // => true ``` ## Unrecoverable input throws: everything else is a warning Only an unrecoverable structural condition throws a typed `DicomParseError`, and `FATAL_CODES` is the whole set of them (`NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED_TRANSFER_SYNTAX`, `EMPTY_INPUT`). A well-formed object with vendor quirks never throws; the quirks collect on `.warnings` with a stable code and a byte offset. ```ts runnable throws import { parseDicom } from "@cosyte/dicom"; // Not a Part 10 object at all: no preamble/DICM, no File Meta. A structural fatal. parseDicom(Buffer.from("this is not a DICOM file, just some ASCII text here", "ascii")); // throws DicomParseError (NOT_DICOM_PART_10) ``` ## Next - [Core Concepts](https://docs.cosyte.com/dicom/spec-notes-model.md): the Part 10 object model, the tolerance tiers and warning codes, the typed value layer, the safety-critical views, and the source-profile system. - [Cookbook](https://docs.cosyte.com/dicom/cookbook.md): recipes: re-serialize spec-clean bytes, de-identify before sharing, read raw pixel data, and triage warnings. - [Troubleshooting](https://docs.cosyte.com/dicom/troubleshooting.md): symptom to cause, the fatal codes, and what a diagnostic may and may not carry. - [Known limitations](https://docs.cosyte.com/dicom/limitations.md): the "do not over-trust" list. What is out of scope (no pixel decode, no DIMSE, no DICOMweb), which PHI residuals are measured and still open, and what is never defaulted. Read it before you point this package at real data. - [Serialization](https://docs.cosyte.com/dicom/serialization.md) and [De-identification](https://docs.cosyte.com/dicom/deidentification.md): writing spec-clean bytes back out, and stripping identifying metadata before you share an object. > **About runnable examples.** The blocks tagged ` ```ts runnable ` above are extracted by the test > suite, executed against the built package, and their `// =>` results asserted, so a documented > example can never silently drift from the code (`docSnippetSuite()`, the documentation analog of > the parser conformance runners). Blocks shown as plain ` ```ts ` are illustrative. --- URL: https://docs.cosyte.com/dicom/spec-notes-model Markdown: https://docs.cosyte.com/dicom/spec-notes-model.md # The object model: Part 10 framing, File Meta, dataset A DICOM Part 10 object on disk is a **128-byte preamble**, the `DICM` magic, a **File Meta Information** group (always Explicit VR Little Endian) that names the transfer syntax, and then the **dataset**: a flat, ordered list of data elements encoded in that transfer syntax. `parseDicom` frames all of it into one immutable `Dataset`. ## What a data element is Every element is a `(group,element)` **tag**, a **VR** (Value Representation: the two-letter type code), a length, and a value. `@cosyte/dicom` keys elements by the 8-character uppercase hex tag (e.g. `"00100010"` for Patient's Name). The parser supports the four native transfer syntaxes, Implicit VR LE, Explicit VR LE, Explicit VR BE, and Deflated Explicit VR LE, plus every encapsulation syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest), whose Data Set section A.4 makes Explicit VR LE: those are read and written, their pixels are never decoded, and `serializeDicom` refuses with `INVALID_ENCAPSULATED_PIXEL_DATA` a top-level Pixel Data section A.4 does not allow. It also reads the four JPIP Referenced syntaxes of sections A.6, A.7, A.11 and A.12, whose Data Set is Explicit VR LE (deflated for A.7 and A.12, and inflated on parse) and whose pixels are referenced by the Pixel Data Provider URL `(0028,7FE0)`, returned as written and never fetched; those are read only, and `serializeDicom` and `deidentify` both refuse them. In Implicit VR the on-wire VR is absent and is resolved from the dictionary; in Explicit VR the on-wire VR is honored and a disagreement with the dictionary is flagged (`DICOM_VR_MISMATCH`), never silently overridden. ## Reaching elements `ds.get(tag)` returns the `Element` at a tag (or `undefined`); `ds.has(tag)` tests presence; `ds.getAll(tag)` is the always-array complement of `get`, and because a `Dataset` holds at most one element per tag it returns 0 or 1. All three take the **tag** form. `get` does **not** take a keyword. Resolve a keyword to its tag through the generated dictionary: ```ts runnable import { parseDicom, Dictionary } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg", "base64", ); const ds = parseDicom(buf); // File Meta names the transfer syntax the dataset was encoded in. ds.fileMeta?.transferSyntaxUID; // => "1.2.840.10008.1.2.1" // Elements are keyed by (group,element) tag, case-insensitively. // This synthetic object carries no Pixel Data, so that tag is absent. ds.has("00100010"); // => true ds.has("7FE00010"); // => false // A keyword resolves to its tag through the dictionary; `get` then takes the tag. Dictionary.byKeyword("PatientName")?.tag; // => "00100010" Dictionary.lookup("00080060")?.keyword; // => "Modality" ``` ## A DICOMDIR's Directory Record tree When an object's File Meta `(0002,0002)` is `1.2.840.10008.1.3.10` (Media Storage Directory Storage), `ds.directory` is a `DicomDirectory`; for anything else it is `undefined`. Its `records` are every Item of the Directory Record Sequence `(0004,1220)` in order, each a `DirectoryRecord` with its `index`, its `(0004,1430)` `type`, its `(0004,1500)` `referencedFileId` components (verbatim), its `item` for the record keys, and its `lowerLevel` records. `root` is the record `(0004,1200)` names and each `(0004,1400)` successor; a record's `lowerLevel` is the record its `(0004,1420)` names and each successor. Each record is in the tree at most once. ```ts import { parseDicom } from "@cosyte/dicom"; const dir = parseDicom(buf).directory; for (const patient of dir?.root ?? []) { for (const study of patient.lowerLevel) { for (const series of study.lowerLevel) { for (const image of series.lowerLevel) { console.log(patient.type, image.referencedFileId?.join("/")); } } } } ``` An offset names a record only when it is exactly where that record's `(FFFE,E000)` Item tag sits in the file, counted from the first byte of the File Preamble, which is `Item.fileOffset` and how PS3.3's Basic Directory IOD defines these offsets (PS3.3 is not vendored here, so no clause is claimed for it). A file read without a preamble still counts the 132 bytes it lacks. Anything else, an offset inside a record, on an Item of a Sequence nested in a record, on the Sequence's header or past the end, names nothing and raises `DICOM_DIRECTORY_OFFSET_UNRESOLVED`. The limits, what the writer refuses, and what de-identification leaves undone are on [Known limitations](https://docs.cosyte.com/dicom/limitations.md). ## The generated data dictionary The `Dictionary` namespace is generated at build time from the official DICOM Part 6 source and committed, so lookups are in-memory and deterministic: no runtime network or filesystem access. `Dictionary.lookup` accepts either a tag or a keyword; `byKeyword` is keyword-only; `uid` resolves a UID (e.g. a transfer syntax) to its human-readable name. Unknown input returns `undefined`. The dictionary never throws. The element registry, 5,310 tags, comes from **NEMA's PS3.6 2026d DocBook**, the normative publication of the standard, rather than from a third-party mirror of it. That is what the `name`, `keyword`, `vr`, `vm`, and `retired` fields on an entry are: the values PS3.6 prints for that tag, in the edition named above. `retired` in particular is worth reading rather than ignoring, in both directions. `(0010,2160)` `EthnicGroup` is retired and its replacements `EthnicGroupCodeSequence` and `EthnicGroups` are what current instances carry; `(3004,0012)` `DoseValue` is **not** retired, whatever an older dictionary may tell you. A retired entry stays resolvable, because files in the wild outlive the editions that defined them; you are told what the tag is and that it is no longer current. ```ts runnable import { Dictionary } from "@cosyte/dicom"; // Retired in PS3.6 2025a, and still resolvable so an older study can be read. Dictionary.lookup("00102160")?.keyword; // => "EthnicGroup" Dictionary.lookup("00102160")?.retired; // => true // Its replacements, which current instances carry. Dictionary.byKeyword("EthnicGroupCodeSequence")?.tag; // => "00102161" Dictionary.byKeyword("EthnicGroups")?.vm; // => "1-n" // PS3.6 still defines this one. The RET marker belongs to (3004,0010). Dictionary.lookup("30040012")?.keyword; // => "DoseValue" Dictionary.lookup("30040012")?.retired; // => false Dictionary.lookup("30040010")?.retired; // => true ``` UID names come from the same edition, out of **PS3.6 Annex A**: Table A-1 (UID Values) and Table A-2 (Well-known Frames of Reference). That is every UID the registry publishes, transfer syntaxes, SOP and Meta SOP Classes, well-known SOP Instances, coding schemes and the rest, current and retired alike, rather than a hand-picked subset. Two things are deliberately not PS3.6's spelling, and both are conveniences rather than corrections: - **Retirement is the `retired` boolean, not a suffix in the name.** Every retired UID in Annex A carries `(Retired)` at the end of its UID Name; here that moves into a field you can branch on, and the name stays a name. - **Four transfer syntaxes keep the short form every toolkit prints.** PS3.6 gives them a trailing `: Default Transfer Syntax for ...` clause recording which storage class defaults to them, so you get `Implicit VR Little Endian` rather than `Implicit VR Little Endian: Default Transfer Syntax for DICOM`. The other four hundred odd names are the normative text, unchanged. Two rows are absent on purpose: PS3.6 retired them and withdrew their names in the same edition, so there is no name to return and `uid` reports them as unknown rather than answering with an empty string. ```ts runnable import { Dictionary } from "@cosyte/dicom"; // A transfer syntax the current edition defines. Dictionary.uid("1.2.840.10008.1.2.4.203")?.name; // => "High-Throughput JPEG 2000 Image Compression" Dictionary.uid("1.2.840.10008.1.2.4.203")?.type; // => "TransferSyntax" // The short form, not PS3.6's longer name. Dictionary.uid("1.2.840.10008.1.2")?.name; // => "Implicit VR Little Endian" // Retirement is a field, and the name is left alone. Dictionary.uid("1.2.840.10008.1.2.2")?.name; // => "Explicit VR Big Endian" Dictionary.uid("1.2.840.10008.1.2.2")?.retired; // => true ``` ## Immutability A `Dataset` is immutable at the model boundary: `warnings` is frozen, and the element map is not exposed for mutation. Edits go through explicit methods (`setElement`, `addElement`, `removeElement`, and the sequence-item equivalents), each returning results rather than mutating shared parser output. This is the same discipline the serializer relies on. See [Re-serializing](https://docs.cosyte.com/dicom/cookbook.md). ## Where values come from `ds.get(tag)` gives you the raw `Element`; its `.value` decodes the bytes into a typed [`DicomValue`](https://docs.cosyte.com/dicom/spec-notes-values.md). For the safety-critical attributes there is a shorter, typed path (the [`patient` / `study` / `series` / `image` views](https://docs.cosyte.com/dicom/spec-notes-safety.md)) which is what the [Quickstart](https://docs.cosyte.com/dicom/quickstart.md) uses. --- URL: https://docs.cosyte.com/dicom/spec-notes-tolerance Markdown: https://docs.cosyte.com/dicom/spec-notes-tolerance.md # Tolerance & the warning model Real scanners and archives emit objects that deviate from the letter of the standard in documented, recoverable ways: odd-length values with no padding, a missing preamble, an off-spec VR, a group-length that disagrees with reality. `@cosyte/dicom` follows **Postel's Law**: the parser is liberal (it recovers and records a stable-coded warning), and the serializer is conservative. A recoverable quirk is **never** a silent change and never a throw. ## Two tiers plus a small fatal set - **Recoverable deviations → a warning.** The parser recovers, keeps the data, and appends a `DicomParseWarning` to `ds.warnings` carrying a **stable code** and the **byte offset** where it occurred (e.g. `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_MISSING_PREAMBLE`, `DICOM_VR_MISMATCH`, `DICOM_FILE_META_GROUP_LENGTH_MISMATCH`). **No count is written here.** One was, it read 25 while the registry held more, and the README's copy of the same numeral was corrected twice before being deleted. `WARNING_CODES` is the list, and the locked snapshot under `test/property/__snapshots__/` measures it on every run. - **Unrecoverable structural corruption → a throw.** A Tier-3 condition throws a typed `DicomParseError`, and the whole set is `NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED_TRANSFER_SYNTAX` and `EMPTY_INPUT`. **No count is written here either**, for the same reason as above: `FATAL_CODES` is the registry and enumerating it is the honest form. Everything short of "these bytes are not a readable Part 10 object" is a warning. ```ts runnable import { parseDicom, WARNING_CODES } from "@cosyte/dicom"; // Synthetic object with the 128-byte preamble omitted: a recoverable quirk. const buf = Buffer.from( "AgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAYABDUwIAQ1QQACAATE8GAE1STi00Mg==", "base64", ); const ds = parseDicom(buf); // It parsed. The data is intact... ds.series.modality; // => "CT" ds.patient.id; // => "MRN-42" // ...and the deviation is recorded, not hidden. ds.warnings.map((w) => w.code); // => ["DICOM_MISSING_PREAMBLE"] ds.warnings[0]?.code === WARNING_CODES.DICOM_MISSING_PREAMBLE; // => true typeof ds.warnings[0]?.position?.byteOffset; // => "number" ``` ## Fatal input throws a typed error An unreadable object throws `DicomParseError`, whose `.code` is a member of `FATAL_CODES`. Narrow on it with `err instanceof DicomParseError`: ```ts runnable import { parseDicom, DicomParseError, FATAL_CODES } from "@cosyte/dicom"; let code: string | undefined; try { parseDicom(Buffer.alloc(0)); // no bytes at all } catch (err) { if (err instanceof DicomParseError) code = err.code; } code; // => "EMPTY_INPUT" code === FATAL_CODES.EMPTY_INPUT; // => true ``` ## What a diagnostic carries, and what it does not A Tier-2 warning's `message` is looked up in a frozen registry keyed by the warning code, and the only substitutions are structural. No factory takes a string read out of the file, so a value cannot be interpolated even by a future call site that tries. A token that fails its check renders as `` rather than being echoed. `w.code` and `w.position` carry nothing from the document. **The three substitutions are bounded three different ways, and they are not interchangeable.** - **`{tag}` is a MEMBERSHIP test.** `renderTag` renders a tag only when PS3.6's element registry carries a **literal row** for it. A repeating-group family row is not membership: `(50xx,xxxx)` Curve Data leaves the whole 16-bit element number free, so a family test admits 16 x 65,536 tags whose free bits are raw document bytes - `"\fPAR"` composes `500C5241` and returns all four payload bytes with one typed read. - **`{vr}` is a membership test** against the 34 VRs PS3.5 2026c §6.2 defines. - **A raw number a header carries has neither a shape nor a membership to test, so where it is bound at all the bound is the absence of the slot.** There is no `renderLength` and there must not be one. `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_NONZERO_RESERVED_BYTES` and `DICOM_PIXEL_DATA_LENGTH_MISMATCH` cannot be handed one. **And a raw number SHIFTED by a constant the reader can compute is that raw number**, because an addition puts it back, so a bound on a rendering's magnitude bounds nothing about its content: `DICOM_ITEM_CROSSES_SEQUENCE_END` printed the bytes that remained inside the sequence, which is the sequence's own declared Value Length less the bytes of that sequence already consumed. It cannot be handed either number now. **The exceptions are named in ONE place that is not a record of a past change, and are deliberately not restated here** - the `WARNING_MESSAGES` docblock in `src/parser/warnings.ts`. No count of the copies is quoted, here or there: this package deletes a count it has corrected twice rather than incrementing it. **What the membership rule closed.** In an earlier release `renderTag` validated a tag's _shape_, and a shape test admits all 2^32 tags. Measured: a `(0008,4000)` `ST` carrying `"MR BRAIN SMITHSON "` whose Value Length under-declares by 12 desynchronizes the **Explicit VR LE** reader onto a fabricated header whose declared length is odd, and `DICOM_ODD_LENGTH_VALUE_PADDED` rendered four bytes of the payload as its tag - `4E495320`, `"IN S"` in wire order - **and four more as its decimal length**, eight consecutive payload bytes in one message. On six other under-declare deltas `DICOM_NONZERO_RESERVED_BYTES` printed two more bytes of the same name as two decimals. **What it costs, stated rather than minimised.** A tag PS3.6 does not name one at a time stops appearing in every message: **private** tags, **Group Length** `(gggg,0000)` tags, and **repeating-group members** such as `(6000,3000)` Overlay Data. The element is still in the Data Set under that tag, and `position.byteOffset` locates the header. **Which channel carries a message is part of the answer.** The fixtures that produce a desynchronized read mostly die before a `Dataset` exists, so their messages reach a consumer through `onWarning` or through the `{ strict: true }` `DicomParseError` rather than a surviving `ds.warnings`. That no measured fixture put one on a surviving `ds.warnings` is a fact about those fixtures and not a promise about the parser. **Two carriers this page named until recently are now bound, by a signature and not by a branch.** `DICOM_PRIVATE_TAG_NO_CREATOR`, `DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR` and `DICOM_PRIVATE_CREATOR_UNKNOWN` take no tag parameter at all: all three fire only on an **odd** group, and an odd group is the one class of tag no closed table this library holds can vouch for - PS3.6's registry is even-group and a `Profile`'s private dictionary is keyed by a creator string. The element stays in the object under its tag and `position.byteOffset` locates the header. `report.embeddedAttributes[].hidden` is bound too, by membership rather than by signature: an entry is a tag the run's own resolved Annex E action fired on that has a **literal row** in PS3.15 Table E.1-1 - 653 of them. **A repeating-group mask hit is excluded, and that is the load-bearing half**: `(50xx,xxxx)` Curve Data leaves the whole 16-bit element number free, so a mask match proves a rule exists without making the membership finite - a draft that stopped at "an even group" was measured admitting `500C5241`, four payload bytes recoverable with one typed read. `hidden` can be empty on a real finding, and it is still uncapped. **A Tier-3 fatal's `message` is bounded the same way, and it was not always.** Every message `parseDicom` throws now comes from a second frozen registry, keyed by the structural reason for the refusal rather than by the fatal code (`FATAL_CODES` is locked, and several of its members are raised for more than one reason). Until this release several of those messages were assembled at the throw site out of template literals and printed the element's tag, its declared length, or both. That reads as harmless and is not. **The reason is NOT that such a fatal fires only when a length field is lying.** It fires on an honestly truncated file too, where every declared length is correct and the transport simply lost bytes: a spec-clean object cut short by two bytes raises `ELEMENT_LENGTH_EXCEEDS_BUFFER`, and a file cut short inside its File Meta group raises `FILE_META_GROUP_LENGTH_OVERRUNS`, both with nothing fabricated anywhere. Both readings are covered by the same bound because the withheld numbers are four bytes a sender wrote in either of them. What makes the desynchronized reading the sharp one is that those four bytes are then somebody's name: measured on a synthetic `"MR BRAIN SMITHSON "`, one such message printed `Element 41524E49 declared length=1330858068`, which is `"RAIN"` followed by `"THSO"` - eight consecutive bytes of the payload in two fields, each recoverable with a single typed read. The bound is the same one the Tier-2 registry uses, and it is structural rather than a discipline: **the factory signatures take no tag and no wire-length parameter at all**, so there is no slot for one to travel through. `position.byteOffset` identifies the element instead. What a fatal message can still carry is named one entry at a time: a VR checked against the closed 34-VR set, a library constant or the caller's own cap, PS3.6's registry name for an unsupported Transfer Syntax UID, and a zlib error code checked against zlib's own nine-name table. **And a count of the bytes left in the buffer is not on that list any more, which is a change to two message texts in this release.** In an earlier release `ELEMENT_LENGTH_EXCEEDS_BUFFER` and `FILE_META_GROUP_LENGTH_OVERRUNS` each printed `buffer.length - cursor.position`, defended as bounded by bytes actually present. That bounds the number's magnitude. The parser reads a defined-length Sequence Item from a **slice**, so inside one the buffer is that Item and the subtraction publishes the Item's own 32-bit declared length, less an offset the same message prints. That is a raw wire field a sender wrote, which is the class this registry refuses everywhere else. Measured with a declared Item Length of `21320`: the message read `21312`, `21288` or `21272` depending on where inside the Item the offending element sat. **A raw number shifted by an amount the reader can compute is that raw number, and here the amount is variable**, so the slot is gone from both signatures rather than filtered. The cost is stated where it falls: `FILE_META_GROUP_LENGTH_OVERRUNS` is raised at the root, where that count was the caller's own input size and leaked nothing, and it loses the number anyway, because a bound that holds only where a function happens to be called from is not a bound. **A `DicomParseError` is still different from a warning, and this is the one to read carefully.** It carries a `snippet`: up to 16 bytes of the source rendered as hex, attached so a structural failure is debuggable. Those are raw input bytes. On a real clinical file they can be part of a patient name or an identifier, and the library does not redact them. Log `err.code`, `err.byteOffset` and `err.message`; treat `err.snippet` as PHI and redact it at your own boundary if your compliance posture requires it. (The one exception is deliberate: for `UNSUPPORTED_TRANSFER_SYNTAX` the snippet slot carries the dictionary's _name_ for the UID when PS3.6 publishes one, which is a constant, not input.) **The snippet is now cut in the same frame its `byteOffset` is counted in**, on every fatal but the `UNSUPPORTED_TRANSFER_SYNTAX` one noted above, whose slot carries PS3.6's name for the UID rather than bytes. It was not: the offset moved with the frame (file-absolute at the root, relative to the enclosing slice inside a defined-length Sequence or Item, into the inflated stream under Deflated Explicit VR LE) while the cut was always taken from the whole file, so a `{ strict: true }` escalation raised inside a defined-length Item returned the 16 bytes sitting at that item-relative number measured from byte 0 of the file. That is a diagnostic handing back part of an element the reader was never asked about. Fixed. **It does not make the snippet safe**: the bytes are still raw source bytes, and the fix makes them more certainly the element's own content, not less. **And the offset itself now says which frame it is counted in, which the snippet fix did not do.** `err.byteOffset` was left as the sole locator on the two messages that lost their remaining-bytes count, and a number alone does not say where its zero is: the same defect in the same file reports `0`, `24` or `40` depending on where inside the Sequence Item the offending element sits. `err.offsetFrame` names the coordinate system, from a closed three-entry set the parser chooses (`"input"`, `"inflated-dataset"`, `"value-slice"`), and the `Error.message` suffix carries it too, so it now reads `(offset=N frame=F)`. **Only `"input"` means the number indexes the buffer you passed in.** A consumer that cuts its own copy of the file at an offset raised inside an Item is reproducing, in its own code, exactly the defect the snippet fix closed in this library's. **The frame's NAME is published and its ORIGIN is not, deliberately.** Where a slice begins is the sum of declared lengths that reached it, so publishing it would hand back by subtraction the wire field these same messages withhold. **Two residuals stay open and neither is closed here**: `DicomParseWarning.position` carries no frame beyond its `deflated` flag, and `Element.byteOffset` carries none at all - it reads `0` inside a defined-length Item and file-absolute inside an undefined-length one, and has since the parser was written. ### The model fields that are bounded, and the ones that are values A downstream package that reads this model and builds its own diagnostics from it needs to know which strings are identifiers this parser composed and which are bytes the sender wrote. These are identifiers, with the bound each one actually has. Some rows are a length rather than a membership test, and they say so. **And `contextPath` has no bound at all** - it was listed here as structural until it was measured, and it now has its own row saying what it really is. It is not the only entry read off the wire (`removedPrivateTags` and `UnauditableSequenceFinding.tag` are too, and are disclosed as such in [Troubleshooting](https://docs.cosyte.com/dicom/troubleshooting.md)); it is the one that was described as though it were not: | Field | Bound | | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Element.tag` | Composed here: eight uppercase hex digits from the header's four bytes. | | `Element.vr` | Two bytes, taken from the wire as-is. **Checked against the closed 34-VR set only where a message renders it**, so a non-conformant sender's two bytes do reach this field (Postel's Law: the on-wire VR is trusted). Two bytes is a hard length bound, not a membership one. | | `Element.privateCreator` | The active `Profile`'s private dictionary must name it, or it reads ``. With no profile, nothing is recognized. It is populated only from a block reservation made in the element's **own** Data Set, so it is `undefined` on a private element in a Sequence Item that claimed no block. | | `Element.specificCharacterSet` | PS3.3's closed defined-term table must name it, or it reads ``. The exported `parseSpecificCharacterSet` bounds its results the same way. | | `FileMeta.transferSyntaxUID` | A member of a closed set: the four native syntaxes, the PS3.5 2026c section A.4 encapsulation syntaxes, and the four JPIP Referenced syntaxes of sections A.6, A.7, A.11 and A.12. Any other value is a fatal, so no parsed dataset carries one. | | `FileMetaRawElement.tag` / `.vr` | As above. | | `DeidentifiedAttribute.tag` / `.keyword` / `.action` / `.applied` / `.repeatingGroup` | Composed from the Part 6 and Annex E tables. `tag` is bound to a tag those tables carry a row for, which is membership in a closed table; an attribute Annex E has no row for is not audited here at all. | | `DeidentifyReport.removedPrivateTags` / `.retained` | Tags, and the option names you passed in. | | `contextPath`, on `DeidentifiedAttribute` and on all three findings that carry one | **Unbounded, and this row is the correction.** A segment is `TAG[index]`; the tag half is whatever tag the descent walked, read off the wire, with neither a shape test nor a closed table behind it. On a file whose under-declared Value Length desynchronized the reader onto four bytes inside somebody's value, those four bytes are published here. Measured: a `LO` carrier holding `MRS BRAIN SMITHSON` yielded `contextPath: ["53484E4F[0]"]`, which is `HSON` in wire order, with no warning and every finding array empty; that tag has no PS3.6 row, so `deidentify()` now removes that fabricated Sequence whole, and the field is unchanged on every Sequence a run does descend. Treat it as PHI when the source is untrusted. | | `EmbeddedAttributeFinding.tag` / `.vr` / `.hidden` | Composed. `hidden` holds tags this parser built from four bytes each, and the bytes were sitting inside a value - so they are re-composed as `Tag` here, never echoed. | | `UnauditableSequenceFinding.tag` / `.applied` / `.byteLength`, `UndefinedVrFinding.byteLength` | Composed, plus an input-derived number, and **the number is the part this row exists to correct**. `byteLength` is `Element.rawBytes.length`, which EQUALS the declared Value Length read off the element header, so where that header was fabricated by an under-declared length upstream it is four document bytes wearing a decimal: `"SO\0\0"` publishes `20307`, two letters of a surname, put back with one `readUInt32LE`. The bytes are never echoed; the number they decode to is. `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` and `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` rendered the same number in an earlier release and do not any more, which leaves these fields its only publisher. **`applied` is one of two literals this package writes (`"emptied"` / `"kept"`), so it is composed; read it before the other two, because it is what says whether the value is still in your output.** | Everything else on the model is a **value**, and carries whatever the file carried: `Element.rawBytes`, `Element.value`, `FileMeta.mediaStorageSOPClassUID`, `.mediaStorageSOPInstanceUID`, `.implementationClassUID`, `.implementationVersionName`, `.sourceApplicationEntityTitle`, `.fileMetaInformationVersion`, `FileMetaRawElement.value`, and the **keys** of `DeidentifyReport.uidMap` (the source UIDs, kept so replacement stays consistent across a study). Treat those as PHI. They are what the parser is for. See [Troubleshooting](https://docs.cosyte.com/dicom/troubleshooting.md) for the full symptom table and the logging posture. ## Escalate when you want strictness The tolerance posture is not fixed. A [source profile](https://docs.cosyte.com/dicom/spec-notes-profiles.md) can **escalate** chosen warning codes to a thrown error (a stricter gate for a trusted sender) or **suppress** benign, high-volume codes for a known-quirky source, without ever loosening a correct decode. The built-in `profiles.strict` and `profiles.lenient` are the two ends of that dial. `ParseOptions` is the per-call form of the same dial, and every field is optional: | Field | What it does | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `strict` | Escalates **every** Tier-2 warning to a thrown `DicomParseError`. 🛑 That error carries a raw `snippet` the warning does not, so a PHI review of the lenient path does not transfer to the strict one. | | `onWarning` | An `OnWarningCallback`, invoked once per Tier-2 warning as it is emitted. It fires after the warning is recorded, and a throw from it is swallowed rather than failing the parse. | | `stripPreamble` | How to treat an object that arrives without the 128-byte preamble and `DICM`. | | `copyValues` | Whether element values are copied out of the input buffer or held as views into it. | | `profile` | The `Profile` to parse under. | ## The exported diagnostic types | Export | What it is | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `WARNING_CODES` | The frozen Tier-2 registry. **It is the list**, and the locked snapshot under `test/property/__snapshots__/` measures it on every run. Read it rather than a numeral written anywhere. | | `WarningCode` | The discriminant type over that registry, so a `switch` over warning codes is exhaustive. | | `DicomParseWarning` | One recorded deviation: its `code`, its `position`, and a `message` looked up in the frozen registry. | | `FATAL_CODES` | The frozen Tier-3 registry. Same rule: enumerate it, never count it. | | `FatalCode` | The discriminant type over that registry. | | `DicomParseError` | The thrown class. Carries `code`, `byteOffset`, `offsetFrame`, `message` and a raw hex `snippet` of the source. **Treat `snippet` as PHI.** | | `DicomPosition` | Where a diagnostic happened: `byteOffset`, plus `fileMeta` / `deflated` flags and a `contextPath` for a nested Sequence Item. Read the frame note below before indexing anything with it. | | `OFFSET_FRAMES` | The frozen set of frames a `byteOffset` may be counted in: the caller's own input, the inflated Data Set of a Deflated object, or a slice this parser cut from inside a Value Field. | | `OffsetFrame` | The discriminant type over that set. **Only the input frame's offsets index the buffer you passed in**, and `DicomParseError.offsetFrame` is what says which frame you have. | --- URL: https://docs.cosyte.com/dicom/spec-notes-values Markdown: https://docs.cosyte.com/dicom/spec-notes-values.md # Typed values & VR decode `ds.get(tag)` returns an `Element`: the raw bytes and the VR. Its **`.value`** getter lazily decodes those bytes into a typed, discriminated `DicomValue` and caches the result. Every one of the 34 VRs has a decode: integers and floats (`numbers`), 64-bit values (`bigints`), attribute tags, person names (`personName`), strings (`strings`), free text (`text`), numeric strings (`DS`/`IS`), temporal values (`dates` / `times` / `dateTimes`), sequences, and raw `binary` for bulk data. ## The DicomValue union `DicomValue` is a discriminated union. Switch on `.kind` and the payload narrows: ```ts runnable import { parseDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg", "base64", ); const ds = parseDicom(buf); // US (unsigned short) → numbers. Switch on `.kind` to narrow the payload. const rows = ds.get("00280010")?.value; // Rows rows?.kind; // => "numbers" const rowCount = rows?.kind === "numbers" ? rows.values[0] : undefined; rowCount; // => 512 // PN (person name) → structured 3-group / 5-component value const name = ds.get("00100010")?.value; // Patient's Name name?.kind; // => "personName" const family = name?.kind === "personName" ? name.values[0]?.alphabetic?.familyName : undefined; family; // => "Doe" // DA (date) → validated calendar parts, raw preserved const date = ds.get("00080020")?.value; // Study Date const day = date?.kind === "dates" ? date.values[0] : undefined; day?.valid; // => true day?.year; // => 1900 // IS (integer string) → parsed integers const num = ds.get("00200011")?.value; // Series Number num?.kind; // => "integerString" ``` ### The typed payloads Each temporal and name payload keeps the on-wire string beside the parsed parts, so nothing is lost when the parse is partial. `valid` is `true` only when the whole value parsed cleanly; when it is `false`, `raw` is the source of truth and the numeric fields are absent rather than guessed. | Export | The value it carries | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `DicomValue` | The discriminated union itself. Switch on `.kind`. | | `DicomDate` | A `DA`: `raw`, `valid`, and `year` / `month` / `day` when it parsed. | | `DicomTime` | A `TM`: `raw`, `valid`, `hours` / `minutes` / `seconds`, and `fractionalSeconds` as a number in `[0,1)`. | | `DicomDateTime` | A `DT`: the date and time parts together, plus `offsetMinutes` as a signed UTC offset when the value carried one. Without an offset it is a **local** time; decide the offset at your own boundary. | | `PersonName` | A `PN`: up to three component groups, `alphabetic` always present, `ideographic` and `phonetic` only when the value supplied them (PS3.5 §6.2.1.1). | | `PersonNameGroup` | One of those groups: `familyName`, `givenName`, `middleName`, `namePrefix`, `nameSuffix`, never flattened into a display string. | ### Decoding a value yourself The pieces the `.value` getter uses are exported, for the cases where you hold bytes rather than an `Element`, or a string rather than an element at all. None of them throws. | Export | What it does | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `decodeElementValue` | The decode itself: takes an `Element` and returns its `DicomValue`. This is what `.value` calls and caches. | | `parsePersonName` | Parses one `PN` string into a `PersonName`. | | `parseDate` | Parses one `DA` string, and reports whether it used the legacy `YYYY.MM.DD` form. | | `parseTime` | Parses one `TM` string. | | `parseDateTime` | Parses one `DT` string, and reports a non-standard UTC offset. | | `parseSpecificCharacterSet` | Splits a `(0008,0005)` value into its defined terms. A component outside the closed table reads `` rather than being returned. | | `isKnownCharsetTerm` | Whether a defined term is one this build can map to a decoder. | | `resolveDecoderLabel` | The `TextDecoder` label to use for a term list, preferring a multibyte decoder and falling back to the default repertoire. A multi-valued set decodes with code extensions instead (see [Character sets](#character-sets)). | | `decodeText` | Decodes bytes under the terms, never throwing. A multi-valued set decodes with the code extensions of ISO 2022; any other decodes under the resolved label, where an unsupported label falls back to UTF-8, then to Latin-1. | ### Converting a date, a time or a datetime `DicomDate`, `DicomTime` and `DicomDateTime` are three shapes, and the code that consumes them usually wants one. `toObject`, `toISO` and `toDate` take any of the three and project it onto a single surface. Every `@cosyte/*` parser that decodes a date exports the same three names with the same meanings, so a caller that learns them here can read a timestamp out of any of them. | Export | What it does | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `toObject` | The calendar components the value stated, as a frozen `DateParts`. `undefined` for an invalid value. | | `toISO` | An ISO-8601 string truncated to the stated precision. `undefined` for an invalid value. | | `toDate` | An absolute-instant `Date`, but only where the zone is determinate. `undefined` otherwise. | | `DateParts` | The shared result shape: `year` / `month` / `day` / `hour` / `minute` / `second` / `millisecond` / `offsetMinutes`, all optional. | | `ToDateOptions` | `toDate`'s only option: `{ assumeOffsetMinutes }`, and no other key. | Four rules make this surface worth having, and each one is a decision the naive version gets wrong. **The key set is the precision.** A component the value did not state is absent from `DateParts` rather than present and `undefined`, and nothing is zero-filled, so `Object.keys()` of the result recovers exactly what the sender wrote. There is no `precision` key because the key set is one, and no `raw` or `valid` key because parse bookkeeping is not a calendar component. **The names are singular.** `DicomTime` and `DicomDateTime` spell the time fields `hours`, `minutes` and `seconds`; `DateParts` spells them `hour`, `minute` and `second`, and `month` is 1 to 12 rather than the JS `Date` 0 to 11. That is the shape `Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject` accept: delete `offsetMinutes` and either constructor takes the rest with no key rename and no value adjustment. **`toDate` never guesses a zone.** A `DT` that carried an `&ZZXX` offset converts exactly, and that stated offset beats any `assumeOffsetMinutes` the caller passes. A value with no offset converts only when the caller supplies one, an explicit `0` meaning "read this naive value as UTC". With neither, the answer is `undefined`: the host machine's zone is never read, and a `TM` is never an instant at all because it states no year. **A component outside its range is refused, never rolled over.** The decoders bound each component on its own, so a `DA` of `18700230` decodes with `valid: true` and `day: 30`. The conversion surface reads the components together: a month outside 1 to 12, a day the month does not really have (full 4/100/400 leap rule), an hour, minute or second out of range, or a component that is not a whole number, and the whole value converts to `undefined`. Rendering it would produce `"1870-02-30"`, which every ISO-8601 reader silently moves to 2 March, and converting it would produce a date of birth off by a day. `second: 60` is refused with them: `TM` and `DT` permit it for a leap second and the decoders keep it, but there is no ISO string for it a reader does not move, and no instant to build. The refusal is in the projection alone, so `parseDate`, `parseTime` and `parseDateTime` still report exactly what the sender wrote. ```ts runnable import { parseDate, parseDateTime, parseTime, toDate, toISO, toObject } from "@cosyte/dicom"; // One set of names, three decoded shapes. toObject(parseDate("20240115").value); // => { year: 2024, month: 1, day: 15 } toObject(parseTime("133015").value); // => { hour: 13, minute: 30, second: 15 } toISO(parseDateTime("20240115133015-0500").value); // => "2024-01-15T13:30:15-05:00" // The key set is the precision: nothing is zero-filled. const stated = toObject(parseDateTime("202401151330").value); Object.keys(stated ?? {}); // => ["year", "month", "day", "hour", "minute"] // `millisecond` is the first three digits as written, never the float scaled up. toObject(parseTime("133015.123456").value)?.millisecond; // => 123 toISO(parseTime("133015.123456").value); // => "13:30:15.123456" // A time is not an instant, however determinate the caller's zone is. toDate(parseTime("133015").value, { assumeOffsetMinutes: 0 }); // => undefined // No stated offset and no assumption: no instant, and no host zone consulted. toDate(parseDate("20240115").value); // => undefined toDate(parseDate("20240115").value, { assumeOffsetMinutes: 0 })?.toISOString(); // => "2024-01-15T00:00:00.000Z" // A stated offset beats whatever the caller assumed. const shifted = parseDateTime("20240115133015-0500").value; toDate(shifted, { assumeOffsetMinutes: 600 })?.toISOString(); // => "2024-01-15T18:30:15.000Z" // A day the calendar does not have converts to nothing, never to the day after it. toObject(parseDate("18700230").value); // => undefined toISO(parseDate("18700431").value); // => undefined toDate(parseDate("18700230").value, { assumeOffsetMinutes: 0 }); // => undefined toISO(parseTime("133060").value); // => undefined // The decoders are untouched: the bytes the sender wrote are still readable. parseDate("18700230").value.day; // => 30 parseTime("133060").value.seconds; // => 60 ``` Two properties are worth stating because a reader will otherwise assume the opposite. `toISO` renders fractional digits exactly as written, neither padded to three nor rounded, and it appends `Z` for a stated zero offset, so it is deliberately not a byte round-trip of the wire value: `serializeDicom` remains the route that reproduces the original bytes. And because the three names are identical across every `@cosyte/*` parser, a file importing two of them has to alias: ```ts import { toISO as dicomToISO } from "@cosyte/dicom"; import { toISO as hl7ToISO } from "@cosyte/hl7"; ``` ## Decode is fail-safe: a bad token is `null`, never a plausible wrong number The decode never throws and never coerces a malformed value into a plausible-but-wrong one. A bad `DS`/`IS` token becomes `null` (never `NaN`→`0`); an out-of-range date part is flagged rather than silently wrapped. Per-value deviations surface on the returned value's own `warnings`, so a mis-encoded token in one element never poisons the rest of the parse. This is the value-layer form of the "correct, not merely green" rule: the parser would rather tell you a token is unreadable than hand you a confident wrong one. ## Character sets `PN`, `LO`, `SH`, `UC`, `ST`, `LT` and `UT` values honor the object's `(0008,0005)` Specific Character Set. The active charset is threaded through nested sequence items, so a value inside a sequence decodes the same way a top-level one does. An unsupported charset term raises `DICOM_UNSUPPORTED_CHARSET`. **With one Value there are no code extensions.** A single-valued `(0008,0005)` (`ISO_IR 192` UTF-8, the ISO-8859 family, `ISO_IR 13`, `ISO_IR 166`, `GB18030`, `GBK`) decodes each value with one decoder, as PS3.5 section 6.1.2.3 prescribes for a single Value. A multi-byte term sent alone, such as `ISO 2022 IR 149`, is not conformant (PS3.3 makes its multi-byte code-extension terms Value 2 to n), and it gets the same single-decoder treatment: no Value 1 reset and none of the warnings below. **With more than one Value, the code extensions of ISO 2022 are decoded** (PS3.5 2026d section 6.1.2.5), unless Value 1 is `ISO_IR 192`, `GB18030` or `GBK`, which take none (section 6.1.2.4): - Every escape sequence in PS3.3's code-extension tables, single-byte and multi-byte, switches the set it names into G0 or G1: ISO-IR 6, ISO-IR 14 and the two-byte JIS X 0208 (ISO-IR 87) and JIS X 0212 (ISO-IR 159) into G0; the ISO-8859 sets, TIS 620 (ISO-IR 166), JIS X 0201 katakana (ISO-IR 13) and the two-byte KS X 1001 (ISO-IR 149) and GB 2312 (ISO-IR 58) into G1. No escape byte reaches the string. - Value 1 sets the starting designations (an empty Value 1 is ISO-IR 6 with nothing in G1), and they come back at every reset point: the start of the value; after a CR, LF or FF; after the `\` value delimiter of `PN`, `LO`, `SH` and `UC`; and after the `^` and `=` delimiters of `PN`. In `ST`, `LT` and `UT` a `\` is literal. - A delimiter counts only as a single byte, never as half of a two-byte character, so a JIS X 0208 character whose second byte is `0x5C`, `0x5E` or `0x3D` stays one character. - `decodeText(bytes, terms)` applies the same decode with the text reset points (CR, LF and FF) and returns the string alone. PS3.5's own worked Person Names (Examples H.3-1, H.3-2, I.2-1 and K.2-1) decode to the names the standard prints, with no warning. What the decode had to tolerate is flagged on `Element.value`'s `warnings`, each code at most once per value, and never in `Dataset.warnings`, so `{ strict: true }` does not refuse the file. Their messages carry the tag and VR, never a byte of the value. | Code | When | What the value holds | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `DICOM_CHARSET_ESCAPE_UNDECLARED` | An escape sequence switches to a set no Value of `(0008,0005)` declares. | The characters, decoded in the set the escape sequence names. | | `DICOM_CHARSET_BYTES_UNDECODABLE` | An ESC that starts no sequence in either table (with every byte after it up to the next recognized sequence, CR, LF, FF or the end), a GR byte while G1 holds no set, a code the set does not define, or a set this runtime cannot decode. | U+FFFD in place of those bytes, never another set's reading. `Element.rawBytes` keeps them. | | `DICOM_CHARSET_EXTENSION_NOT_RESET` | A line, value or `PN` component ends while G0 still holds a set other than Value 1's. | The characters decoded up to that point; what follows decodes from Value 1. | Limits, stated here because each one is a place the decode is narrower than a reader might assume: - **A single-valued `(0008,0005)` gets no code-extension handling**, as described above. - **ISO-IR 14's `0x5C` and `0x7E` read as `\` and `~`**, not YEN SIGN and OVERLINE, because `0x5C` is also the value delimiter and a consumer splits on `\`. - **Every line starts over from Value 1.** An `LT` or `UT` that switches to KS X 1001 once and then runs over several lines reads its later lines as U+FFFD, with `DICOM_CHARSET_BYTES_UNDECODABLE`, because PS3.5 requires the switch again on each line. The bytes stay on `Element.rawBytes`. - **A two-byte set holds only its own standard's characters.** Node's decoders are vendor supersets (NEC and IBM rows in JIS X 0208, GBK's additions in GB 2312's rows); those codes read as U+FFFD here. - **Only the read path changed.** `serializeDicom` writes the raw bytes back and `deidentify()` acts on raw bytes, so neither decodes code extensions. ## Bulk data stays raw Pixel Data and other bulk elements decode to `{ kind: "binary", bytes }`: the raw `Buffer`, never interpreted. This is the metadata-first boundary in the value layer: the bytes are handed to you exactly as stored (for encapsulated transfer syntaxes, as their fragments), and pixel decoding is out of scope. See [Reading raw pixel data](https://docs.cosyte.com/dicom/cookbook.md) and the non-goals in [Troubleshooting](https://docs.cosyte.com/dicom/troubleshooting.md). --- URL: https://docs.cosyte.com/dicom/spec-notes-safety Markdown: https://docs.cosyte.com/dicom/spec-notes-safety.md # Safety-critical views: patient, study, series, image Pulling the right field out of raw tags is error-prone in exactly the places where a mistake is most dangerous. So the `Dataset` exposes four typed, fail-safe views over the safety-critical attributes (`patient`, `study`, `series`, and `image`) each a plain object of typed fields with one absolute rule: **a missing value is typed-absent (`undefined`), never a substituted default.** The dangerous DICOM failure is the confident, wrong image; these views are built so that absence reads as absence. ## The four views ```ts runnable import { parseDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg", "base64", ); const ds = parseDicom(buf); // Patient: an identifier is not globally unique on its own; pair id with issuer // to match across systems, and the name stays structured, never flattened. ds.patient.id; // => "MRN-42" ds.patient.issuerOfId; // => "SAMPLE-HOSP" ds.patient.name?.alphabetic?.familyName; // => "Doe" // Study / series. ds.study.instanceUid; // => "1.2.826.0.1.3680043.8.498.1.1" ds.series.modality; // => "CT" // Image: the geometry you need before interpreting a pixel. ds.image.rows; // => 512 ds.image.signed; // => true ds.image.rescaleSlope; // => 1 ds.image.pixelSpacing; // => [0.5, 0.5] ``` ## Typed-absent is load-bearing An omitted field is `undefined`, not a guessed default. And the difference is a clinical-safety one: ```ts runnable import { parseDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg", "base64", ); const ds = parseDicom(buf); // This synthetic object carries Pixel Spacing but NOT Imager Pixel Spacing: // the two are distinct fields and are never aliased. ds.image.pixelSpacing; // => [0.5, 0.5] ds.image.imagerPixelSpacing; // => undefined // It is a single-frame object, so multi-frame fields are absent, not defaulted. ds.image.numberOfFrames; // => undefined ds.image.isEnhancedMultiFrame; // => false ``` Concretely: `rescaleSlope` is `undefined` (never `1`) when the tag is absent; `signed` is `undefined` unless Pixel Representation was present; `photometricInterpretation` is never defaulted to `MONOCHROME2`; and `pixelSpacing`, `imagerPixelSpacing`, and `nominalScannedPixelSpacing` are three distinct fields. If you need a fallback, apply it deliberately in your own code. The parser will not apply one for you. ## Coded triplets Coded concepts (PS3.16 code sequences: modality-in-study, units, anatomic region) come out as a `CodedConcept` triplet via `readCode`: `{ codeValue, codingSchemeDesignator, codeMeaning, schemeUid }`, with `codingSchemeOid` / `CODING_SCHEME_OIDS` mapping the common designators (`SCT`, `LN`, `DCM`, …) to their OIDs. As with everything else, a missing component is absent, not invented. ## Enhanced multi-frame For Enhanced multi-frame objects (`ds.image.isEnhancedMultiFrame === true`), `ds.image.frame(i)` resolves each frame's functional-group macros **Per-Frame-else-Shared**. The per-frame group wins, falling back to the shared group: ```ts // Illustrative: an Enhanced multi-frame object. if (ds.image.isEnhancedMultiFrame) { const f = ds.image.frame(0); f.planePosition?.imagePositionPatient; // this frame's [x, y, z] f.pixelMeasures?.pixelSpacing; // this frame's [row, col] mm } ``` `frame(i)` throws a `DicomValueError` for an out-of-range index, or when a required geometry macro is missing from **both** the per-frame and shared groups. And the message carries only structural facts (the frame index, the macro tag), never PHI. An optional macro that is simply absent stays `undefined`; only a *required* one missing from both groups is an error. ## The exported shapes The four views and the types under them are exported, so you can hold, pass and narrow one without re-deriving its shape. Every field on every one of them is optional in the same deliberate way: a value that was not in the object is typed-absent, never substituted. | Export | What it is | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PatientView` | `ds.patient`. Identity, and the wrong-patient failure class. `id` is not globally unique on its own: correct cross-system matching needs `id` with `issuerOfId` and the issuer qualifiers, plus `otherIds`. | | `OtherPatientId` | One entry in `patient.otherIds`: `id` with its `issuer` and `typeCode`. | | `StudyView` | `ds.study`. Study identity, dates, accession, referring physician. | | `SeriesView` | `ds.series`. Modality, series identity and number, and the series-level geometry inputs. | | `ImageView` | `ds.image`. Pixel-interpretation and geometry metadata: the "wrong pixels look fine" and "looks fine, measures wrong" classes. The omissions listed above are on this type. | | `RealWorldValueMap` | A Real World Value Mapping: `slope`, `intercept` and the units `CodedConcept`. Authoritative over the linear `rescale*` pair when the object carries one. | | `CodedConcept` | The coded triplet `readCode` returns, with `schemeUid` where a designator maps to one. | | `FrameFunctionalGroups` | What `image.frame(i)` returns: the functional-group macros resolved for one frame, each typed-absent when present in neither the per-frame nor the shared group. | | `VALUE_ERROR_CODES` | The frozen registry of codes this layer throws: `FRAME_INDEX_OUT_OF_RANGE` and `MISSING_REQUIRED_FUNCTIONAL_GROUP`. | | `ValueErrorCode` | The discriminant type over that registry, so a `switch` on a caught code is exhaustive. | | `DicomValueError` | The thrown class. Its message is built from the code and structural facts alone, so it is not a PHI surface the way a `DicomParseError` snippet is. | Where a view's absence is load-bearing rather than incidental, it is stated once, on [Known limitations](https://docs.cosyte.com/dicom/limitations.md). Read that page before treating an `undefined` here as a default you may fill in. --- URL: https://docs.cosyte.com/dicom/spec-notes-profiles Markdown: https://docs.cosyte.com/dicom/spec-notes-profiles.md # Source & vendor profiles Real objects come from real vendors, and vendors deviate in documented, predictable ways: private data elements with implicit VRs, benign quirks emitted at high volume, deviations you want to treat as hard errors from a trusted sender. A **profile** lets you opt into source-specific tolerance without ever risking a wrong decode. Pass one to `parseDicom`: ```ts runnable import { parseDicom, profiles } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAEABVUwIAAAIoABEAVVMCAAACKAAAAVVTAgAQACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSAoADAARFMIADAuNVwwLjUg", "base64", ); // Selecting a vendor profile never changes a correct decode. It only tightens // or annotates. This clean object reads identically with the Siemens overlay. const ds = parseDicom(buf, { profile: profiles.siemens }); ds.series.modality; // => "CT" ds.warnings.length; // => 0 ``` ## What a profile bundles A profile only ever **tightens or annotates** a parse. It never loosens one past the lenient default: - **Private-dictionary overlay**: resolves the Implicit VR of vendor private data elements by the object's _live_ private-creator string (canonical `"GGGGxxLL"` key, PS3.5 §7.8.1), never a hard-coded block number. A creator the profile does not recognize degrades to `UN` plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning: never a wrong decode. The lookup is **scoped to one Data Set**. PS3.5 §7.8.1 reserves a private block for the Data Set its Private Creator element sits in, and §7.5.1 makes every Sequence Item a Data Set of its own, so a block claimed at the root does not resolve an element inside an item, an item's claim does not resolve one in a sibling item, and neither survives back out to the enclosing Data Set. A private element whose block was never claimed in its own Data Set reads `UN` plus `DICOM_PRIVATE_TAG_NO_CREATOR`, with `Element.privateCreator` left `undefined`: the bytes are still on `Element.rawBytes`, and the profile's typed decode is withheld rather than borrowed from a neighbouring Data Set. If you write private data into sequence items, declare the creator in each item that uses it. Under `{ strict: true }` that warning is promoted to a thrown `DicomParseError` like any other Tier-2 code, so a file whose items borrow an enclosing block parses under the lenient default and throws under `strict`. - **Escalations**: chosen Tier-2 warning codes promoted to a thrown `DicomParseError`, a stricter posture for a trusted sender. - **Suppressions**: benign, high-volume warning codes silenced for a known-quirky source. ## The five built-ins Five profiles ship under the frozen `profiles` namespace: `ge`, `siemens`, `philips` (vendor overlays) and `strict` / `lenient` (posture presets). They are the [tolerance dial](https://docs.cosyte.com/dicom/spec-notes-tolerance.md) made concrete: `strict` escalates, `lenient` suppresses. ## Build your own `defineProfile()` validates its input, composes via `extends`, and returns a **frozen** profile: ```ts runnable import { defineProfile, profiles } from "@cosyte/dicom"; const acmeStrict = defineProfile({ name: "acme-strict", extends: profiles.strict, privateTags: { "ACME PRIV 01": { "0019XX10": { vr: "DS", keyword: "AcmeDose", name: "ACME Dose" }, }, }, }); acmeStrict.name; // => "acme-strict" Object.isFrozen(acmeStrict); // => true ``` A profile is a value, not a side effect: it never mutates a dataset and never changes a decode that was already correct. Selecting the wrong vendor overlay costs you resolved private tags. It can never turn a right answer into a wrong one. ## The exported shapes | Export | What it is | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Profile` | The frozen profile itself, as `parseDicom` and `deidentify` accept it. | | `defineProfile` | Builds one from a validated options object. | | `DefineProfileOptions` | Those options: `name` (the only required field), `description`, `escalate`, `suppress`, `privateTags`, and `extends` (one profile or several, composed in order). | | `ProfilePrivateTags` | The per-creator overlay: a map of canonical `"GGGGxxLL"` key to `PrivateTagDefinition`, keyed under the private-creator string it belongs to. | | `PrivateTagDefinition` | One private element's declaration: its `vr`, `keyword` and human-readable `name`. | | `ProfileDefinitionError` | Thrown by `defineProfile` for an invalid definition (an unknown warning code to escalate, a malformed private-tag key). An author-time error, never a file error. | **A profile's declared VR is a second authority, and it is not a licence over what a value nests.** PS3.15 §E.3.10 lets a de-identifier retain a private *Attribute* it knows to be safe; it does not let one vouch for a Data Set a sender nested inside that attribute's value. See [De-identification](https://docs.cosyte.com/dicom/deidentification.md) for what `RetainSafePrivate` plus a profile does and does not keep, and [Known limitations](https://docs.cosyte.com/dicom/limitations.md) for the residuals around it. --- URL: https://docs.cosyte.com/dicom/cookbook Markdown: https://docs.cosyte.com/dicom/cookbook.md # Cookbook Task-oriented recipes for the DICOM jobs you actually get handed. Each one is: here's the problem, here's the code, here's what you get back. Every symbol below is a real `@cosyte/dicom` export, no pseudo-API. All sample objects are **synthetic** (an invented patient, fake UIDs), encoded as base64 so a recipe needs no file on disk; never paste a real DICOM object into a doc or a test. Read [Getting started](https://docs.cosyte.com/dicom/intro.md) first for the parse model; the recipes here assume you can already get a parsed `Dataset`. Read [Known limitations](https://docs.cosyte.com/dicom/limitations.md) before you trust any of this on real data: it is the one page on this site written to tell you what the library does **not** do. ## How the citations here work Every recipe names the PS3 clause for the attributes it reads, and the rule for which clause it may name is deliberately narrow. - **Attribute identity** (tag, name, keyword, VR, VM) is cited to **PS3.6 2026c**, the Registry of DICOM Data Elements. The edition **vendored in this repository and pinned by SHA-256** (`vendor/nema/part06/`) is **2026d**, which carries every row cited here unchanged and is the copy the shipped data dictionary is generated from, and a test re-reads it to confirm that every `(gggg,eeee) Name` pair written on this site is the registry's own. - **Encoding rules** are cited to **PS3.5 2026c** and **de-identification rules to PS3.15 2026c**, vendored and pinned the same way (`vendor/nema/part05/` at 2026c, `vendor/nema/part15/` at 2026d, whose clauses cited here are unchanged). Two checks of different strength run over those, and **what each one covers is worth stating exactly, because a citation gate that is described as total is a claim of its own**. A clause written in the form `PS3.N §X`, `PS3.N section X` or `PS3.N Annex X`, with the label next to its part, is resolved in the pinned document by collecting all candidate sections carrying that label and requiring exactly one. **A label the text writes away from its part is not covered**: a second label in a list (`§7.5.1, §7.8.1`) and a bare `section X` whose part was named a sentence earlier are both read by a human, not by the gate. On top of that, each clause the text leans on for a normative statement is required to carry that sentence **in its own body, not in a subsection**, because a label proves the label exists and proves nothing about what the body says. - **No numbered clause of PS3.3, PS3.4, PS3.10 or PS3.16 is cited anywhere on this site.** Those parts are not vendored here, so a clause number for one could not be checked against anything, and a citation nobody can check is worse than none. Where an IOD-level or file-format rule matters, the text names the part and says what it relies on, without a clause number. The date in every sample object below is `19000101`. That is not a style choice: this repository's PHI scanner reads doc fixtures as well as test fixtures, and it rejects any `DA` or `DT` value inside the last 120 years wherever it finds one, because a plausible real study date is exactly the shape it exists to catch. --- ## 1. Extract metadata and index a folder of studies **The problem:** you have a directory of objects and you need a searchable index: who, which study, which series, which instance, what kind of image. The four views (`patient`, `study`, `series`, `image`) pull the right field out of the right tag, and they are fail-safe: an absent attribute reads `undefined`, never a substituted default. **Attributes read** (PS3.6 2026c, Registry of DICOM Data Elements): `(0010,0020)` Patient ID, `(0010,0021)` Issuer of Patient ID, `(0008,0050)` Accession Number, `(0020,000D)` Study Instance UID, `(0020,000E)` Series Instance UID, `(0008,0018)` SOP Instance UID, `(0008,0016)` SOP Class UID, `(0008,0060)` Modality, `(0008,0020)` Study Date, `(0028,0010)` Rows, `(0028,0011)` Columns. ```ts runnable import { parseDicom, Dictionary } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const ds = parseDicom(buf); const row = { patientId: ds.patient.id, // (0010,0020) issuer: ds.patient.issuerOfId, // (0010,0021) accession: ds.study.accessionNumber, // (0008,0050) studyUid: ds.study.instanceUid, // (0020,000D) seriesUid: ds.series.instanceUid, // (0020,000E) sopInstanceUid: ds.image.sopInstanceUid, // (0008,0018) modality: ds.series.modality, // (0008,0060) rows: ds.image.rows, // (0028,0010) columns: ds.image.columns, // (0028,0011) }; row.patientId; // => "MRN-42" row.accession; // => "ACC-0001" row.modality; // => "CT" row.rows; // => 512 // A date is structured rather than a string, and `valid` is answered rather than assumed. ds.study.date?.year; // => 1900 ds.study.date?.valid; // => true // SOP Class UID says what KIND of object this is. Resolve the UID to its registered name // through the dictionary rather than hard-coding a table of your own. const sopClass = ds.get("00080016")?.value; const sopClassUid = sopClass?.kind === "strings" ? sopClass.values[0] : undefined; Dictionary.uid(sopClassUid ?? "")?.name; // => "CT Image Storage" // The parser recorded no deviation. That is a statement about what THIS reader tolerated, // not a conformance verdict: PS3.10 governs the file format and is not vendored here. ds.warnings.length; // => 0 ``` Over a real folder, wrap the parse per file. A quirky object is tolerated rather than rejected and absent fields come back `undefined`, and a JPEG, JPEG 2000, RLE or other PS3.5 2026c section A.4 compressed object indexes like any other, its pixels never decoded (see [section 7](#7-read-raw-pixel-data-without-decoding-it)), as does a JPIP Referenced object, whose Pixel Data Provider URL is read and never fetched. But **a folder walk still needs a `try`/`catch`**, because the Tier-3 conditions throw and a real archive meets every one of them: `UNSUPPORTED_TRANSFER_SYNTAX` for an object under a syntax this parser refuses (SMPTE ST 2110, or a retired UID); `INVALID_FILE_META` for a truncated or partly-copied file; `NOT_DICOM_PART_10` for whatever non-DICOM file wandered into the folder; and `EMPTY_INPUT` for a zero-byte one. Each throws the one class, so catch `DicomParseError` per file and skip. ```ts import { readFile } from "node:fs/promises"; import { parseDicom, DicomParseError } from "@cosyte/dicom"; async function indexFile(path: string) { try { const ds = parseDicom(await readFile(path)); return { ok: true as const, studyUid: ds.study.instanceUid, warnings: ds.warnings.length }; } catch (err) { if (err instanceof DicomParseError) return { ok: false as const, code: err.code }; throw err; } } ``` Log `err.code`, `err.byteOffset` and `err.offsetFrame`. Do **not** log `err.snippet`: it is up to 16 raw source bytes and the library does not redact it. **`err.byteOffset` indexes the buffer you passed in only when `err.offsetFrame` is `"input"`** - inside a Sequence Item it counts from that Item, so cutting your own copy of the file at it returns a different element. See [Keeping PHI out of logs](https://docs.cosyte.com/dicom/troubleshooting.md#keeping-phi-out-of-logs). --- ## 2. Build routing keys **The problem:** you are filing objects into an archive, reconciling them against an order, or matching them to a patient record, and you need the identifiers that make each of those joins sound. There are three different keys here and they are not interchangeable. **Attributes read** (PS3.6 2026c): `(0020,000D)` Study Instance UID, `(0020,000E)` Series Instance UID, `(0008,0018)` SOP Instance UID, `(0008,0050)` Accession Number, `(0010,0020)` Patient ID, `(0010,0021)` Issuer of Patient ID, `(0010,1002)` Other Patient IDs Sequence. ```ts runnable import { parseDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const ds = parseDicom(buf); // 1. Hierarchy keys. A UID is globally unique by construction, so these need no issuer. const studyKey = ds.study.instanceUid; const seriesKey = ds.series.instanceUid; const instanceKey = ds.image.sopInstanceUid; studyKey; // => "1.2.826.0.1.3680043.8.498.1.1" seriesKey; // => "1.2.826.0.1.3680043.8.498.1.2" instanceKey; // => "1.2.826.0.1.3680043.8.498.111" // 2. Order key. The Accession Number is the HIS-to-PACS join and is NOT globally unique: // it is unique only within the system that issued it. ds.study.accessionNumber; // => "ACC-0001" // 3. Patient key. A Patient ID ALONE is ambiguous across systems. The issuer is a separate // attribute and the library surfaces it separately rather than folding it in for you. const p = ds.patient; const patientKey = `${p.issuerOfId ?? ""}|${p.id ?? ""}`; patientKey; // => "SAMPLE-HOSP|MRN-42" // Additional {id, issuer} pairs, when the sender wrote any, arrive as a list. p.otherIds.length; // => 0 ``` Three things worth being explicit about, because getting any of them wrong is a real-world outage: - **`ds.patient.id` is a string the sending system chose.** It is not globally unique and this library does no patient matching. Pair it with `ds.patient.issuerOfId` and treat the pair as the key, and read `ds.patient.otherIds` for any further assigning authorities the object carried. The Issuer of Patient ID attribute is `(0010,0021)` in PS3.6 2026c; the module that requires the two to be read together is in PS3.3, which is not vendored here, so no clause is claimed for it. - **`instanceKey` and `seriesKey` have no representation in HL7 v2.** Image-level and series-level identity live only in DICOM (see recipe 5). - **Do not derive a routing key from a de-identified object and expect it to match the original.** `deidentify()` replaces UIDs by default. The mapping it used is on `report.uidMap`, whose **keys are the source UIDs** and are therefore not safe to log. --- ## 3. Read pixel-interpretation metadata safely **The problem:** you (or a downstream renderer) will touch the pixels, and the interpretation attributes decide what the stored numbers _mean_. The dangerous DICOM failure is the confident, wrong image, so none of these views defaults a missing value. **Attributes read** (PS3.6 2026c): `(0028,0002)` Samples per Pixel, `(0028,0004)` Photometric Interpretation, `(0028,0010)` Rows, `(0028,0011)` Columns, `(0028,0030)` Pixel Spacing, `(0018,1164)` Imager Pixel Spacing, `(0028,0100)` Bits Allocated, `(0028,0101)` Bits Stored, `(0028,0102)` High Bit, `(0028,0103)` Pixel Representation, `(0028,1052)` Rescale Intercept, `(0028,1053)` Rescale Slope. ```ts runnable import { parseDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const img = parseDicom(buf).image; img.samplesPerPixel; // => 1 img.photometricInterpretation; // => "MONOCHROME2" img.bitsAllocated; // => 16 img.bitsStored; // => 12 img.highBit; // => 11 // (0028,0103) Pixel Representation is 0 for unsigned and 1 for two's complement. // `signed` is a boolean ONLY when the attribute was present; otherwise it is undefined. img.pixelRepresentation; // => 1 img.signed; // => true // Modality LUT: stored value -> real-world value is `stored * slope + intercept`. // An ABSENT slope reads `undefined`, and you MUST NOT read that as 1. img.rescaleSlope; // => 1 img.rescaleIntercept; // => -1024 // Pixel Spacing is [row spacing, column spacing] in mm, in the patient plane. img.pixelSpacing; // => [0.5, 0.5] // Imager Pixel Spacing is a DIFFERENT attribute at the detector plane and is absent here. // The view does not fall back from one to the other. img.imagerPixelSpacing; // => undefined ``` The two failure modes this shape exists to prevent: - **A defaulted rescale.** `undefined` means the object did not carry the attribute. Substituting `1`/`0` turns stored values into confidently wrong Hounsfield units. Decide what to do about an absent Modality LUT in your own code, where the decision is visible. - **A defaulted signedness.** Reading a two's-complement image as unsigned inverts the dark end of the range. `img.signed` is `undefined` rather than `false` when `(0028,0103)` was absent. `(0028,0030)` and `(0018,1164)` are different measurements and this library never substitutes one for the other. Which one a given IOD requires is a PS3.3 question, and PS3.3 is not vendored here, so no clause is claimed; what is claimed is that both attributes are surfaced separately and neither is invented. For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame first and Shared second. It throws a `DicomValueError` (carrying only structural facts, never a value) for an out-of-range frame, or when a required geometry macro is missing from both groups. ```ts if (img.isEnhancedMultiFrame) { const f = img.frame(0); f.planePosition?.imagePositionPatient; // this frame's [x, y, z] f.pixelMeasures?.pixelSpacing; // this frame's [row, col] mm f.pixelValueTransformation?.rescaleSlope; // this frame's slope, if the macro is present } ``` > **Vendor note.** Philips writes private rescale attributes `(2005,1409/140A/140B)` that shadow the > standard `(0028,1052/1053)`; using the standard attributes alone can yield non-quantitative values. > This parser **preserves** the private attributes so you can prefer them. Reach them with > `ds.get("20051409")`, optionally under `profiles.philips`. --- ## 4. De-identify before sharing **The problem:** you need to strip identifying metadata before an object leaves your control, and you need a record of what was done, without mutating the original. `deidentify(ds)` applies the PS3.15 2026d Annex E **Basic Application Level Confidentiality Profile** (§E.2), replacing, emptying or removing every attribute Table E.1-1 lists as identifying, and returns a fresh `Dataset` plus a `DeidentifyReport`. It is a **pure function**: your input dataset is never mutated. Several report fields are **not** value-free and are named on the type: `uidMap`, whose keys are the source UIDs the file carried, and `removedPrivateTags`, `unauditableSequences[].tag` and `contextPath`, whose entries are four source bytes each, and `unauditableSequences[].byteLength` / `undefinedVrElements[].byteLength`, each the declared Value Length off an element header that may itself have been fabricated. `embeddedAttributes[].hidden` **left that list**: an entry is now a tag this run acted on that PS3.15 Table E.1-1 gives a literal row, with repeating-group mask hits excluded - which also means it can be empty on a real finding. **Read the list on the type rather than a count quoted anywhere**, this one included: the number has been corrected twice. ```ts runnable import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const ds = parseDicom(buf); const { dataset, report } = deidentify(ds); // The original is untouched; the identifying fields are gone from the copy. ds.patient.id; // => "MRN-42" dataset.patient.id; // => undefined // Patient's Name is emptied (Annex E action "Z"), not left in place. dataset.get("00100010")?.value.kind; // => "empty" // The Accession Number is emptied too: it identifies the order, and through it the patient. dataset.study.accessionNumber; // => undefined // UIDs are remapped to deterministic 2.25 replacements that stay consistent across files. dataset.study.instanceUid?.startsWith("2.25."); // => true // The object says what this run did to its DATES, so a recipient does not have to // guess. PS3.15 2026c section E.2 requires REMOVED when no Retain Longitudinal // Temporal Information Option is applied, which is this call. dataset.get("00280303")?.vr; // => "CS" dataset.get("00280303")?.value; // => { kind: "strings", values: ["REMOVED"] } // Section E.3.6's Full Dates branch requires UNMODIFIED, and the real dates are // then in the output. const dated = deidentify(ds, { retain: ["RetainLongitudinalTemporal"] }).dataset; dated.get("00280303")?.value; // => { kind: "strings", values: ["UNMODIFIED"] } // And its Modified Dates branch requires MODIFIED. That resolves Table E.1-1's // modified-dates column; YOU perform the date transformation section E.3.6 also // asks for, and the run says so on its report. const shifted = deidentify(ds, { retain: ["RetainLongitudinalTemporalModifiedDates"], }); shifted.dataset.get("00280303")?.value; // => { kind: "strings", values: ["MODIFIED"] } shifted.report.warnings.map((w) => w.code); // => ["DICOM_DEIDENT_DATES_NOT_TRANSFORMED"] // The report lists what was acted on. It is NOT value-free: see the fields // named on DeidentifyReport before logging one whole. report.attributes.length > 0; // => true report.warnings.length; // => 0 // The de-identified copy serializes to bytes you can share. Buffer.isBuffer(serializeDicom(dataset)); // => true ``` ### The header stops naming the sending site §E.1.1 does not stop at Table E.1-1, and two of its unconditional bullets are about the parts of a file that are not the Data Set. `deidentify()` discharges both, on every run and under every Option. **The File Meta group is replaced rather than carried through.** §E.1.1 requires it "replaced with a description of the de-identifying application", because otherwise "identity information may leak through unmodified File Meta Information ... includ[ing] information regarding Application Entity Titles, Presentation Addresses, implementation information, and private information". So: - `(0002,0016)` Source Application Entity Title is **gone** from the output. - `(0002,0012)` Implementation Class UID and `(0002,0013)` Implementation Version Name **name `@cosyte/dicom`** and hold none of the source's values. - Every non-modeled `(0002,xxxx)` element is **dropped**, not filtered: `(0002,0017)` Sending Application Entity Title, `(0002,0018)` Receiving Application Entity Title, `(0002,0100)` Private Information Creator UID, `(0002,0102)` Private Information, and anything a vendor invented. **This deliberately breaks the byte-for-byte File Meta round trip** for de-identified output; it still holds for parse-then-serialize. `report.fileMetaElementsDropped` names what went and `report.fileMetaElementsDroppedCount` counts it, complete at any input size. - What survives identifies the **object**, not the sender: File Meta Information Version, Media Storage SOP Class UID, Transfer Syntax UID, and the Media Storage SOP Instance UID (remapped unless `RetainUIDs`). The 128-byte preamble was already written as zeros on every path. **Group 0004 is removed**, at every depth: §E.1.1 requires it "removed from any SOP Instance or DICOM File other than a DICOMDIR File". `report.group0004Removals` names them (capped, with a context path for the nested ones) and `report.group0004RemovalCount` is the complete total. **The DICOMDIR carve-out is honoured and is not silent.** If `(0002,0002)` is `1.2.840.10008.1.3.10` (Media Storage Directory Storage) the `(0004,xxxx)` elements stay, the Directory Records are de-identified as Data Sets, and `serializeDicom` rewrites each record offset to where its record lands in the written bytes. The run raises `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED` for the two File-set clauses of that bullet it does **not** discharge: creating a DICOMDIR from the de-identified files it references, and removing the non-de-identified DICOMDIR from the File-set. Referenced File IDs are kept verbatim. Do not treat such output as a File-set-conformant DICOMDIR. This is **metadata-level** de-identification. Pixel data is out of scope: when an object carries burned-in annotation this layer cannot remove, you get a `DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` warning on the report rather than a false sense of safety. Pixel cleaning is deferred to `@cosyte/dicom-pixel`. Opt into the metadata-affecting Annex E Options (e.g. `RetainUIDs`, `RetainLongitudinalTemporal`, `CleanDescriptors`) via `deidentify(ds, { retain: [...] })`. **The de-identified object declares what happened to its dates, in the three states PS3.15 defines.** `(0028,0303) Longitudinal Temporal Information Modified` is written on every run: **`REMOVED`** when no Retain Longitudinal Temporal Information Option was active, **`UNMODIFIED`** when `RetainLongitudinalTemporal` was, and **`MODIFIED`** when `RetainLongitudinalTemporalModifiedDates` was. PS3.15 2026c §E.2 states the first ("The Attribute Longitudinal Temporal Information Modified (0028,0303) shall be added to the Data Set with a Value of `REMOVED` if none of the Retain Longitudinal Temporal Information Options is applied") and §E.3.6 the other two, for its Full Dates and Modified Dates branches. That is the whole point of the attribute: without it, a recipient holding dates cannot tell real ones from scrubbed ones, and a study with no dates to begin with is indistinguishable from one this run emptied. **It is replaced, not added to** - the attribute is `VM 1`, so a `(0028,0303)` the source already carried is discarded rather than joined, which is the opposite of what `(0012,0063)` does with a prior method text and is the standard's own asymmetry. **The two §E.3.6 Options are mutually exclusive and the second one's limit rides with it.** A call naming both is rejected with a `DeidentifyError`. `RetainLongitudinalTemporalModifiedDates` resolves Table E.1-1's modified-dates column and writes `MODIFIED`; **this library performs no date transformation**, so the modification §E.3.6 also requires is yours, and every such run carries `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on `report.warnings` saying so. See [Known limitations](https://docs.cosyte.com/dicom/limitations.md) for the whole of that residual, and for what to do if you shift dates yourself after the call. **Before you rely on this on real data, read [Known limitations](https://docs.cosyte.com/dicom/limitations.md).** Several routes are measured, disclosed and open, and the report reading clean is not by itself proof that nothing survived. The attribute-action table behind all of this is generated from **NEMA's PS3.15 2026d DocBook**, the normative publication of the standard, rather than from a third-party mirror of it. That matters because an attribute the table does not list is an attribute `deidentify()` keeps, silently, whenever the PS3.6 registry carries its tag (one the registry does not carry either is removed; see [Attributes neither table carries](https://docs.cosyte.com/dicom/deidentification.md#attributes-neither-table-carries)): the current edition's patient attributes, including the `(0010,00xx)` preferred-name and pronoun block, the `(0010,004x)` gender-identity and sex-parameters attributes, and `EthnicGroupCodeSequence` / `EthnicGroups`, are all removed because the table is the edition's, not a snapshot of it. Four rows of Table E.1-1 name a family rather than a single tag (`(50xx,xxxx)` Curve Data, `(60xx,3000)` and `(60xx,4000)` Overlay Data and Comments, and the odd-group private-attribute row). All four are acted on: private attributes are removed through their own path, and the three repeating-group rows are matched by mask across the sixteen even groups the standard bounds them to, removed, and reported with the mask that matched. **That bound is stated across two editions, and citing only the current one would be wrong**: PS3.5 2026c §7.6 gives the overlay range (`6000`-`601E`) and, in the same clause, retires curve encoding and delegates the curve range to PS3.5-2004 by URL. Both editions are vendored and SHA-pinned here, and the generator proves the delegation link rather than assuming it. ```ts const { report } = deidentify(parseDicom(buf)); report.attributes .filter((a) => a.repeatingGroup !== undefined) .forEach((a) => console.log(a.tag, a.keyword, a.repeatingGroup)); // 60004000 Overlay Comments 60xx4000 ``` --- ## 5. Bridge to FHIR `ImagingStudy` and HL7 v2 **The problem:** the imaging study has to join the rest of the record, and someone has asked you for a FHIR resource or an HL7 v2 field mapping. The crosswalk to work from is the FHIR [`ImagingStudy` "Mappings for DICOM"](https://build.fhir.org/imagingstudy-mappings.html) tab, which maps each `ImagingStudy` element to the DICOM attribute it comes from. **Say which FHIR you mean:** that URL is `build.fhir.org`, the continuous build, not a balloted release, so pin the mappings page for the FHIR version your integration targets (R4 / R4B / R5) rather than treating the build as stable. Nothing in this package is versioned against FHIR; this library's job is to surface the DICOM attributes correctly, and it does not build FHIR resources for you. **Attributes read** (PS3.6 2026c): `(0020,000D)` Study Instance UID, `(0020,000E)` Series Instance UID, `(0008,0018)` SOP Instance UID, `(0008,0016)` SOP Class UID, `(0008,0060)` Modality, `(0008,0020)` Study Date, `(0008,0030)` Study Time, `(0010,0020)` Patient ID, `(0010,0021)` Issuer of Patient ID, `(0008,0050)` Accession Number. ```ts runnable import { parseDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const ds = parseDicom(buf); // --- FHIR ImagingStudy --------------------------------------------------- // ImagingStudy.identifier carries the Study Instance UID as a `urn:oid:` value. const identifier = `urn:oid:${ds.study.instanceUid ?? ""}`; identifier; // => "urn:oid:1.2.826.0.1.3680043.8.498.1.1" // ImagingStudy.series.uid and .series.modality come straight off the series view. const series = { uid: ds.series.instanceUid, modality: ds.series.modality }; series.modality; // => "CT" // ImagingStudy.series.instance.uid and .sopClass, per instance. const instance = { uid: ds.image.sopInstanceUid }; instance.uid; // => "1.2.826.0.1.3680043.8.498.111" // ImagingStudy.started is a FHIR dateTime, so Study Date and Study Time compose into one // value. Both are structured here, so you build the string rather than concatenating raw // bytes and hoping. const d = ds.study.date; const t = ds.study.time; const started = d?.valid === true ? `${String(d.year).padStart(4, "0")}-${String(d.month).padStart(2, "0")}-${String(d.day).padStart(2, "0")}` + (t?.valid === true ? `T${String(t.hours).padStart(2, "0")}:${String(t.minutes ?? 0).padStart(2, "0")}:${String(t.seconds ?? 0).padStart(2, "0")}` : "") : undefined; started; // => "1900-01-01T09:00:00" // ImagingStudy.subject resolves to a Patient. The DICOM side of that join is the Patient ID // AND its issuer, never the id alone. const subject = { id: ds.patient.id, assigner: ds.patient.issuerOfId }; subject.assigner; // => "SAMPLE-HOSP" // --- HL7 v2 --------------------------------------------------------------- // The Accession Number is the HIS-to-PACS workhorse and is conventionally carried in // OBR-18 (Placer Field 1) or OBR-19 in a radiology ORM/ORU. Which one your interface uses // is a site agreement, not a DICOM rule. const accession = ds.study.accessionNumber; accession; // => "ACC-0001" // The Study Instance UID has no standard HL7 v2 field. Sites that carry it use a Z-segment // or an agreed OBR/ZDS slot; there is nothing to read out of the standard here. ``` Three cautions that matter more than the field list: - **`ImagingStudy.started` needs a time zone and DICOM may not carry one.** `(0008,0201)` Timezone Offset From UTC exists but is frequently absent, and this library never invents one. A FHIR `dateTime` without an offset is a local time; decide the offset at your boundary, explicitly. - **Series and SOP Instance UIDs are not represented in HL7 v2.** Image-level identity lives only in DICOM. If someone asks you to "send the image ID in the ORU", the honest answer is that there is no standard field for it. - **A `urn:oid:` identifier is only correct for a genuine OID.** The Study Instance UID is a UID by construction; do not wrap an Accession Number the same way. --- ## 6. Re-serialize a parsed object to spec-clean bytes **The problem:** you parsed an object (perhaps a quirky one) and need Part 10 bytes back out for storage or forwarding, with a guarantee that nothing was silently lost. `serializeDicom(ds)` writes a `Dataset` back to a Part 10 `Buffer`: preamble and `DICM`, File Meta always Explicit VR LE with a recomputed group length, and the dataset body **in the source transfer syntax, no transcode**. It obeys the conservative half of Postel's Law: every Value Field padded to an even length (PS3.5 2026c §7.1.1 defines the Value Field as "An even number of bytes containing the Value(s) of the Data Element"), correct headers, byte-for-byte encapsulated-pixel-data passthrough, and every Data Set it can walk (the root and each Sequence Item, at every depth up to `NESTING_DEPTH_LIMIT`) written in ascending tag order with Items kept in order and no value changed. A tag repeated inside an Item, a Sequence that cannot be walked, is nested past the bound or whose parsed `items` do not match its bytes, and any `UN`-carried Sequence are written as read, unordered; an element whose own bytes do not show where a reader ends it (an undefined-length `UN` the parser could not read as a Sequence, for one) is written after the ascending rest of its Data Set; see [Serialization](https://docs.cosyte.com/dicom/serialization.md#what-the-writer-guarantees). Serializing an already-serialized object is a fixed point. ```ts runnable import { parseDicom, serializeDicom } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const ds = parseDicom(buf); const out = serializeDicom(ds); Buffer.isBuffer(out); // => true // Re-parsing the bytes yields the same data... parseDicom(out).patient.name?.alphabetic?.familyName; // => "Doe" // ...and re-serializing is byte-identical (a fixed point). serializeDicom(parseDicom(out)).equals(out); // => true ``` Only the typed `FileMeta` fields round-trip through the model; the rest of File Meta is recomputed spec-clean on emit. Non-modeled `(0002,xxxx)` elements the source carried are preserved verbatim and re-emitted in ascending tag order. **That is the parse-then-serialize path, and it is not what `deidentify()` output does.** A de-identified File Meta group describes this de-identifying application instead of the source, so the byte-for-byte round trip above does not hold for it. See [De-identify before sharing](#4-de-identify-before-sharing). --- ## 7. Read raw pixel data without decoding it **The problem:** you need the pixel bytes (to hand to an imaging pipeline, to hash, to forward) but `@cosyte/dicom` deliberately does not decode them. Pixel Data `(7FE0,0010)` decodes to a `{ kind: "binary", bytes }` value: the raw `Buffer`, exactly as stored. PS3.5 2026c §8.2 is the clause that governs how those bytes are encoded, native or encapsulated. You read the geometry from the [`image` view](https://docs.cosyte.com/dicom/spec-notes-safety.md) and the bytes from the element; interpreting them is your pipeline's job. ```ts runnable import { parseDicom } from "@cosyte/dicom"; // Synthetic 2x2, 16-bit object with a tiny raw Pixel Data element. const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xACgAEABVUwIAAgAoABEAVVMCAAIAKAAAAVVTAgAQAOB/EABPQgAACAAAAAEAAgADAAQA", "base64", ); const ds = parseDicom(buf); ds.image.rows; // => 2 ds.image.columns; // => 2 const pixels = ds.get("7FE00010")?.value; // Pixel Data pixels?.kind; // => "binary" const byteLength = pixels?.kind === "binary" ? pixels.bytes.length : undefined; byteLength; // => 8 ds.warnings.length; // => 0 ``` The bytes are never windowed, rescaled, or color-transformed here. That is [out of scope](https://docs.cosyte.com/dicom/limitations.md). **Encapsulated (compressed) objects.** Every transfer syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE, MPEG, HEVC, JPEG XL, Deflated Image Frame and Encapsulated Uncompressed) parses: section A.4 makes the whole Data Set Explicit VR Little Endian with only Pixel Data encapsulated, so the metadata reads exactly as it would under Explicit VR LE. The Pixel Data element's `value` is still the whole on-wire span as `binary`. `readPixelDataFragments(ds)` returns a `PixelDataFragments` with the Basic Offset Table and every fragment as raw bytes, in file order, with no Item header in any of them, and `undefined` when Pixel Data is absent or native. ```ts runnable import { parseDicom, readPixelDataFragments } from "@cosyte/dicom"; // Synthetic 2x2, 8-bit object under JPEG Baseline (Process 1), with an empty Basic Offset // Table and two fragments of fill bytes. The fragments are not a JPEG codestream, and nothing // here needs them to be: they are never decoded. const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAeAAAAAgAQAFVJFgAxLjIuODQwLjEwMDA4LjEuMi40LjUwKAAQAFVTAgACACgAEQBVUwIAAgAoAAABVVMCAAgA4H8QAE9CAAD//////v8A4AAAAAD+/wDgCAAAAAECAwQFBgcI/v8A4AQAAAAJCgsM/v/d4AAAAAA=", "base64", ); const ds = parseDicom(buf); ds.fileMeta?.transferSyntaxUID; // => "1.2.840.10008.1.2.4.50" ds.image.rows; // => 2 const pixels = readPixelDataFragments(ds); pixels?.basicOffsetTable?.length; // => 0 pixels?.fragments.length; // => 2 pixels?.fragments[0]?.toString("hex"); // => "0102030405060708" ds.warnings.length; // => 0 ``` **The limits sit beside that capability.** No pixel is decompressed, no frame is assembled from fragments, and neither the Basic Offset Table nor an Extended Offset Table `(7FE0,0001)` is interpreted. `serializeDicom` writes each section A.4 syntax back under its own UID, a de-identified object included, with the Basic Offset Table and fragment Items byte for byte, and **refuses with `INVALID_ENCAPSULATED_PIXEL_DATA`**, never repairing, a top-level Pixel Data section A.4 does not allow: absent, native, beside Float or Double Float Pixel Data, a stream not ended by its Sequence Delimitation Item, or an odd or empty Item. A fragment stream that ends before its Sequence Delimitation Item still parses, and `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` on `ds.warnings` says the fragment list may be short, which is why the writer will not write it. **JPIP Referenced objects carry a URL, not fragments.** Under the four JPIP Referenced syntaxes (PS3.5 2026c sections A.6, A.7, A.11 and A.12) the metadata parses as it would under Explicit VR LE, the two Deflate ones inflated first. A conformant one has no Pixel Data to read, so `readPixelDataFragments` returns `undefined`; its pixels are referenced by Pixel Data Provider URL `(0028,7FE0)`, which `ds.get("00287FE0")?.value` returns as `{ kind: "text", value }` exactly as the file carries it. It is **never fetched**; retrieving it is your call and your network. Such an object is read-only here: `serializeDicom` refuses it with `UNSUPPORTED_TRANSFER_SYNTAX`, and `deidentify()` refuses it with a `DeidentifyError` whose code is `UNSUPPORTED_TRANSFER_SYNTAX`, because the URL has no PS3.15 Table E.1-1 action and would otherwise be kept. The SMPTE ST 2110 syntaxes and every retired UID stay the fatal `UNSUPPORTED_TRANSFER_SYNTAX`. See [Known limitations](https://docs.cosyte.com/dicom/limitations.md). --- ## 8. Triage warnings: the lenient, never-throw contract **The problem:** you want to log or triage every tolerated deviation without your pipeline throwing on a vendor quirk. Every recoverable deviation collects on `ds.warnings` with a stable code and a byte offset; only a `FATAL_CODES` member throws. Each message is looked up in a frozen registry keyed by the code, with only structural substitutions (a tag, a VR, a number). **What a message may contain is a mechanism, not a verdict**, and the verdict form of this paragraph was corrected twice, so it is gone rather than tried a third time. `{tag}` renders only a tag PS3.6's element registry carries a **literal row** for and `` otherwise; `{vr}` renders only one of the 34 VRs PS3.5 defines; and a raw number a header carries is bound out of the factory signature where it is bound at all, so `DICOM_ODD_LENGTH_VALUE_PADDED` no longer prints the odd length and `DICOM_NONZERO_RESERVED_BYTES` no longer prints its two reserved bytes. **Where a `deidentify()` code still prints a length that is the header's own, that is `PRE-EXISTING` and disclosed rather than closed** - see the troubleshooting page. The cost is that a message about a private, Group Length or repeating-group element no longer names its tag. A thrown `DicomParseError`'s `message` comes from its own frozen registry, whose factories have no tag slot at all, but the error still carries a 16-byte hex `snippet` of the source. All of it is measured and covered in [Keeping PHI out of logs](https://docs.cosyte.com/dicom/troubleshooting.md#keeping-phi-out-of-logs). ```ts runnable import { parseDicom, WARNING_CODES } from "@cosyte/dicom"; // Synthetic object with the preamble omitted: a tolerated quirk. const buf = Buffer.from( "AgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAYABDUwIAQ1QQACAATE8GAE1STi00Mg==", "base64", ); const ds = parseDicom(buf); // The object parsed; the deviation is recorded, not hidden. ds.series.modality; // => "CT" ds.warnings.some((w) => w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE); // => true // Every warning carries a stable code and a byte offset, and a registry message. ds.warnings.every((w) => typeof w.code === "string"); // => true ``` **Escalate when you want strictness.** A [source profile](https://docs.cosyte.com/dicom/spec-notes-profiles.md) can promote chosen warning codes to a thrown `DicomParseError` (a stricter gate for a trusted sender) or suppress benign, high-volume codes for a known-quirky source. Note that `{ strict: true }` turns every Tier-2 warning into a `DicomParseError`, which carries the raw `snippet` the warning does not, so a PHI review of the lenient path does not transfer to the strict one. --- URL: https://docs.cosyte.com/dicom/serialization Markdown: https://docs.cosyte.com/dicom/serialization.md # Serializing Part 10 `serializeDicom(ds)` writes a `Dataset` back to a Part 10 `Buffer`. It is the conservative half of Postel's Law: the parser accepts what real scanners emit, and the writer applies the guarantees below. Every DICOM object on this page is **synthetic** (an invented patient, obviously-fake UIDs) and encoded as a base64 buffer, so an example needs no file on disk. Never paste a real object into a doc or a test: a real object is PHI, and one committed to a repository is a leak the moment it publishes. ## What the writer guarantees - **Preamble and `DICM`.** A 128-byte preamble and the magic, always, even for an object parsed from bytes that had neither (`DICOM_MISSING_PREAMBLE` on the way in). - **A recomputed File Meta group.** Always Explicit VR LE, with `(0002,0000)` File Meta Information Group Length recomputed from what is actually written rather than copied from the source. - **The dataset body in the source transfer syntax.** The Transfer Syntax UID on the object decides every byte of the encoding, and the writer does not transcode. Under every encapsulation syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest), the Data Set is Explicit VR LE and the top-level Pixel Data is `OB` of undefined length: the source's Basic Offset Table and fragment Items byte for byte and in order, then a zero-length Sequence Delimitation Item. A top-level Pixel Data section A.4 does not allow is refused, not repaired (below). - **Even-length Value Fields.** PS3.5 2026c §7.1.1 defines a Value Field as "an even number of bytes containing the Value(s) of the Data Element", so odd values are padded with the pad byte their VR specifies. An odd length that arrived tolerated goes out even. - **Every Data Set in ascending tag order.** PS3.5 2026c §7.1 says the Data Elements in a Data Set "shall be ordered by increasing Data Element Tag Number", and §7.5.1 says the same inside every Item. The writer emits the root that way, and the Data Set of every Item of every Sequence it can walk on the wire, at every depth up to `NESTING_DEPTH_LIMIT`, whatever order the source file, your code or `deidentify()` left it in. Items stay in their order (§7.5), only whole elements move, so no value byte changes, and nothing is written that the Sequence's own bytes did not carry. A Sequence is re-ordered only where its parsed `items` match its bytes, so the output reads back as the source did. - **Not ordered, and said here rather than found later:** a tag repeated inside an Item is kept twice, in source order, so that output still breaks the "at most once" of PS3.5 2026c §7.1; a Sequence whose Item stream cannot be walked to exactly its end (a length that runs past its container, an undefined-length Item with no Item Delimitation Item, bytes that are not an Item stream), one nested past the bound, one whose parsed `items` do not match its bytes (a Sequence the parser did not descend, `DICOM_SQ_NOT_DESCENDED`, for one), and any `UN`-carried Sequence (under Implicit VR LE that includes a private Sequence inside an Item, even one a `Profile` resolved to `SQ`, since a default read resolves its tag to `UN`) are written as read, unordered; and an element the parser relocated because a length field lied is ordered where the parser placed it, since ordering cannot recover an order the source destroyed. - **Written last, even when its tag sorts earlier:** an element whose own bytes do not show where a reader ends it, such as an undefined-length `UN` the parser could not read as a Sequence, or a Sequence, `UN` or Pixel Data value missing its Sequence Delimitation Item. A reader takes whatever follows such a value into it, so it keeps its place after the ascending rest of its Data Set, at the root and inside an Item, rather than losing the elements after it on the next read. A Sequence nested past the bound goes after the ascending rest of its Item too, unless it is a defined-length Sequence under Implicit VR LE: seeing where a reader ends any other would take the walk past the bound. - **Byte-for-byte passthrough of what it must not touch.** Encapsulated Pixel Data fragments and `UN` values are re-emitted as read; the writer never re-encodes pixels. - **A fixed point.** Serializing an already-serialized object returns the same bytes. ```ts runnable import { parseDicom, serializeDicom } from "@cosyte/dicom"; // Synthetic object with the preamble omitted: a quirk the parser tolerates. const buf = Buffer.from( "AgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAYABDUwIAQ1QQACAATE8GAE1STi00Mg==", "base64", ); const ds = parseDicom(buf); ds.warnings.length > 0; // => true // What comes out is spec-clean regardless: preamble, DICM, recomputed File Meta. const out = serializeDicom(ds); out.subarray(128, 132).toString("ascii"); // => "DICM" // The data survives the round trip... parseDicom(out).patient.id; // => "MRN-42" // ...the output no longer carries the deviation... parseDicom(out).warnings.length; // => 0 // ...and re-serializing is byte-identical: the writer is a fixed point. serializeDicom(parseDicom(out)).equals(out); // => true ``` ## When it throws, and what the error carries The writer has its own error taxonomy, separate from the parser's `FATAL_CODES` (which are locked to read-side structural corruption) and from the value layer's `DicomValueError`. It throws only with a code listed below. | Export | What it is | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `SERIALIZE_ERROR_CODES` | The frozen registry of the writer's codes: `MISSING_TRANSFER_SYNTAX`, `UNSUPPORTED_TRANSFER_SYNTAX`, `INVALID_ENCAPSULATED_PIXEL_DATA`, `DIRECTORY_OFFSET_UNRESOLVED`, `DIRECTORY_OFFSET_DEFLATED`. | | `SerializeErrorCode` | The discriminant type over that registry, so a `switch` on a caught code is exhaustive. | | `DicomSerializeError` | The thrown class. Narrow with `err instanceof DicomSerializeError`, then on `err.code`. | `MISSING_TRANSFER_SYNTAX` means the `Dataset` carries no `fileMeta`, or its `transferSyntaxUID` is empty. The Transfer Syntax UID is the dispatch input that decides every byte of the encoding, so there is no safe default to fall back to and none is invented. `UNSUPPORTED_TRANSFER_SYNTAX` means the UID is outside the set this package writes, the four native syntaxes and the section A.4 ones; the writer never transcodes, so it cannot emit a syntax it does not understand. That includes the four JPIP Referenced syntaxes, which `parseDicom` reads for metadata and this writer refuses. `INVALID_ENCAPSULATED_PIXEL_DATA` means the UID is a PS3.5 2026c section A.4 one and the top-level Data Set is not one that syntax may carry, so the writer returns nothing rather than repairing it: no top-level Pixel Data `(7FE0,0010)`; Float or Double Float Pixel Data `(7FE0,0008)` / `(7FE0,0009)` present; Pixel Data with a defined Value Length (Native Format); a fragment stream not ended by its Sequence Delimitation Item, including one `parseDicom` read with `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` (appending a delimiter to a stream that may be short would write a well-formed file missing frames); anything other than an Item of defined length before that delimiter; an Item of odd length; a fragment Item of length zero after the Basic Offset Table; or no Basic Offset Table Item or no fragment Item. Pixel Data inside a Sequence Item, such as an Icon Image Sequence, is written as read and not checked. `DIRECTORY_OFFSET_UNRESOLVED` and `DIRECTORY_OFFSET_DEFLATED` are the DICOMDIR codes. For a `Dataset` whose `(0002,0002)` is `1.2.840.10008.1.3.10`, the writer writes `(0004,1200)`, `(0004,1202)` and each Directory Record's `(0004,1400)` and `(0004,1420)` as the byte offset, counted from the first byte of the preamble it writes, of the record each named when the file was read, as PS3.3's Basic Directory IOD defines them (PS3.3 is not vendored here, so no clause is claimed for it). That is how a record stays named after the File Meta group is rebuilt, a record's elements are put in ascending order, or `deidentify()` re-encodes every record at a new length. A record's identity is `Item.fileOffset`, where the parser found its Item tag, which `deidentify()` carries to the Item it rebuilds; an offset is tied to the Directory Record Sequence Item whose `fileOffset` it equals and to nothing found by scanning for an Item tag. `DIRECTORY_OFFSET_UNRESOLVED` means an offset cannot be tied: a non-zero value naming no Item of the Directory Record Sequence the `Dataset` holds (one `parseDicom` warned about, one left after the Sequence was removed or emptied, one on a hand-built `Dataset` with no records) or a value that is not one 32-bit unsigned integer. It is refused rather than written stale, which would name the wrong bytes, or written as zero, which would silently prune the tree. `DIRECTORY_OFFSET_DEFLATED` means the DICOMDIR is written under Deflated Explicit VR LE with a non-zero offset, which names no Item a reader of a deflated stream can seek to; one whose offsets are all zero is written. The limits of the record model are on [Known limitations](https://docs.cosyte.com/dicom/limitations.md). A `DicomSerializeError` message is built from the code and fixed structural text and nothing else: no Transfer Syntax UID, length or byte from the `Dataset` reaches it, so unlike a `DicomParseError` (which carries a raw `snippet`) it holds no source bytes. ## What the writer will not do These are boundaries, not defects. The full list of package non-goals is on [Known limitations](https://docs.cosyte.com/dicom/limitations.md); what follows is the part that belongs to the writer. - **No transcode.** The body is re-emitted in the transfer syntax the object arrived in. Converting between syntaxes is a different operation with different failure modes, and this package does not perform it. - **No pixel re-encoding.** Encapsulated fragments pass through byte for byte. Pixel decode and encode are `@cosyte/dicom-pixel`. - **Only the typed `FileMeta` fields round-trip through the model.** The group is recomputed spec-clean on emit. Non-modeled `(0002,xxxx)` elements the source carried are preserved verbatim and re-emitted in ascending tag order, so a repeated non-modeled element is re-emitted repeated; that is unchanged behavior and is recorded here rather than fixed silently. - **The byte-for-byte File Meta round trip is scoped to parse-then-serialize.** It does **not** hold for [de-identified](https://docs.cosyte.com/dicom/deidentification.md) output, because a de-identified File Meta group describes the de-identifying application instead of the source (PS3.15 §E.1.1). That is a deliberate fidelity loss and the report records it. - **A repeated tag is not recoverable here.** A parsed Data Set is a `Map`, so where the source wrote one tag twice the earlier element was already gone before the writer saw it (`DICOM_DUPLICATE_TAG_IN_DATA_SET` on the parse). Serializing does not restore it, and no value is invented for it. ## Where to go next - A worked recipe with a fuller object: [Cookbook](https://docs.cosyte.com/dicom/cookbook.md#6-re-serialize-a-parsed-object-to-spec-clean-bytes). - What the parser tolerated on the way in: [Tolerance & warnings](https://docs.cosyte.com/dicom/spec-notes-tolerance.md). - Stripping identifying metadata before you share the bytes: [De-identification](https://docs.cosyte.com/dicom/deidentification.md). --- URL: https://docs.cosyte.com/dicom/deidentification Markdown: https://docs.cosyte.com/dicom/deidentification.md # Metadata de-identification `deidentify(ds)` applies the PS3.15 2026d Annex E **Basic Application Level Confidentiality Profile** (§E.2) to a parsed object: it replaces, empties or removes every attribute Table E.1-1 lists as identifying, and returns a fresh `Dataset` plus a `DeidentifyReport` of what it did. It is a **pure function**; your input dataset is never mutated. :::caution This is metadata-level de-identification and nothing more A de-identified output from `@cosyte/dicom` is **metadata-de-identified only**. Pixel data is out of scope: where a file carries burned-in annotation this layer cannot remove, you get a `DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` warning rather than a false sense of safety, and pixel cleaning is deferred to `@cosyte/dicom-pixel`. **A report that reads clean does not close a residual.** [Known limitations](https://docs.cosyte.com/dicom/limitations.md) is the measured, still-open list, and it is required reading before you route any of this at real data. ::: Every DICOM object on this page is **synthetic** (an invented patient, obviously-fake UIDs) and encoded as a base64 buffer, so an example needs no file on disk. Never paste a real object into a doc or a test. ## The shape of a call ```ts runnable import { parseDicom, deidentify, serializeDicom, DEFAULT_UID_ROOT } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const ds = parseDicom(buf); const { dataset, report } = deidentify(ds); // Pure: the input is untouched, the copy is scrubbed. ds.patient.id; // => "MRN-42" dataset.patient.id; // => undefined // Patient's Name is emptied (Annex E action "Z"), not left in place. dataset.get("00100010")?.value.kind; // => "empty" // UIDs are remapped under the default root, deterministically. DEFAULT_UID_ROOT; // => "2.25" dataset.study.instanceUid?.startsWith(`${DEFAULT_UID_ROOT}.`); // => true // The audit trail says what was acted on, and the object says of itself // that dates were not retained on this call. report.attributes.length > 0; // => true dataset.get("00280303")?.value; // => { kind: "strings", values: ["REMOVED"] } // The de-identified copy serializes to bytes you can share. Buffer.isBuffer(serializeDicom(dataset)); // => true ``` ## The options surface | Export | What it is | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DeidentifyOptions` | The options object: `retain`, `uidRoot`, `uidMap`, `profile`, `deidentificationMethod`. Every field is optional; the default is the Basic Profile with no Option active. | | `DeidentifyOption` | One Annex E option-set name. The two pixel-facing Options (`CleanPixelData`, `CleanRecognizableVisual`) are excluded by type, because this layer does not touch pixels. | | `DEIDENTIFY_OPTIONS` | The frozen registry of the metadata option-set names, validated at runtime. **It is the list**: read it rather than a count written anywhere. | | `DeidentifyResult` | What the call returns: `{ dataset, report }`, generic in the dataset type so the parsed type flows through. | | `AppliedAction` | What actually happened to one attribute: `removed` (`X`), `emptied` (`Z`), `dummied` (`D`), `uid-remapped` (`U`), `cleaned` (`C`), or `kept`. The resolved outcome, not the table's code. | An Option is opt-in and each one keeps a class of attribute the Basic Profile would otherwise strip: ```ts runnable import { parseDicom, deidentify, DEIDENTIFY_OPTIONS } from "@cosyte/dicom"; const buf = Buffer.from( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABESUNNAgAAAFVMBAAcAAAAAgAQAFVJFAAxLjIuODQwLjEwMDA4LjEuMi4xAAgAFgBVSRoAMS4yLjg0MC4xMDAwOC41LjEuNC4xLjEuMgAIABgAVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMTExAAgAIABEQQgAMTkwMDAxMDEIADAAVE0GADA5MDAwMAgAUABTSAgAQUNDLTAwMDEIAGAAQ1MCAENUEAAQAFBOCABEb2VeSmFuZRAAIABMTwYATVJOLTQyEAAhAExPDABTQU1QTEUtSE9TUCAgAA0AVUkeADEuMi44MjYuMC4xLjM2ODAwNDMuOC40OTguMS4xACAADgBVSR4AMS4yLjgyNi4wLjEuMzY4MDA0My44LjQ5OC4xLjIAIAARAElTAgAyICgAAgBVUwIAAQAoAAQAQ1MMAE1PTk9DSFJPTUUyICgAEABVUwIAAAIoABEAVVMCAAACKAAwAERTCAAwLjVcMC41ICgAAAFVUwIAEAAoAAEBVVMCAAwAKAACAVVTAgALACgAAwFVUwIAAQAoAFIQRFMGAC0xMDI0ICgAUxBEUwIAMSA=", "base64", ); const ds = parseDicom(buf); // The registry is the source of truth for what may be retained. DEIDENTIFY_OPTIONS.includes("RetainLongitudinalTemporal"); // => true DEIDENTIFY_OPTIONS.includes("CleanPixelData"); // => false // Default: dates go, and the object declares that. deidentify(ds).dataset.get("00280303")?.value.kind; // => "strings" deidentify(ds).dataset.study.date?.raw; // => undefined // With the full-dates Option: the real dates are in the output, and the // declaration changes with them. const dated = deidentify(ds, { retain: ["RetainLongitudinalTemporal"] }).dataset; dated.get("00280303")?.value; // => { kind: "strings", values: ["UNMODIFIED"] } dated.study.date?.raw; // => "19000101" // With the modified-dates Option: the other E.3.6 column, and the third state. const shifted = deidentify(ds, { retain: ["RetainLongitudinalTemporalModifiedDates"], }); shifted.dataset.get("00280303")?.value; // => { kind: "strings", values: ["MODIFIED"] } shifted.report.warnings.map((w) => w.code); // => ["DICOM_DEIDENT_DATES_NOT_TRANSFORMED"] ``` **`RetainLongitudinalTemporal` carries the less protective of the two PS3.15 §E.3.6 columns, and `RetainLongitudinalTemporalModifiedDates` carries the other.** The standard defines a full-dates and a modified-dates Option and Table E.1-1 gives them separate columns; the first name is the full-dates branch, so on the attributes where the columns disagree you keep the real value where modified-dates would have cleaned it. They are **mutually exclusive** and a call naming both is rejected with a `DeidentifyError`. **Date shifting is not performed at this layer, on either branch**: §E.3.6 also requires the dates themselves to be modified and the manner described in a Conformance Statement, and both are yours, so every modified-dates run raises `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on its report. Activate the full-dates Option only when real dates are genuinely required, and see [Known limitations](https://docs.cosyte.com/dicom/limitations.md) for the whole of that residual. ## What the object records about the method PS3.15 2026c §E.1.1 asks a de-identifier to record how it ran, in codes from CID 7050 "De-identification Method" "corresponding to the Profile and Options used", which "shall be added to De-identification Method Code Sequence (0012,0064), and/or a text string describing the method used shall be inserted in or added to De-identification Method (0012,0063)". `deidentify()` writes both, on every run. The text in `(0012,0063)` was already there; `(0012,0064)` is the machine-readable half, so you can switch on a code instead of parsing English, and tell the two §E.3.6 branches apart by `113106` and `113107` rather than only by the `(0028,0303)` value. `(0012,0064)` is a top-level `SQ` with an Item for each code from PS3.16 2026d CID 7050 (context group version `20170914`, UID `1.2.840.10008.6.1.925`): `113100` for the Profile first, then an Item for each active Option in `DEIDENTIFY_OPTIONS` order, whatever order `retain` names them in. Each Item carries exactly Code Value `(0008,0100)`, Coding Scheme Designator `(0008,0102)` = `DCM` and Code Meaning `(0008,0104)`, and nothing else. ```ts runnable import { Dataset, deidentify, DEIDENTIFICATION_METHOD_CODES } from "@cosyte/dicom"; // An empty Data Set keeps this short: the codes follow the options, not the content. const empty = new Dataset({ warnings: [], elements: new Map() }); const { dataset } = deidentify(empty, { retain: ["RetainUIDs", "CleanDescriptors"] }); // The Profile, then each active Option in DEIDENTIFY_OPTIONS order. const codeValues = (dataset.get("00120064")?.items ?? []).map((item) => { const value = item.get("00080100")?.value; return value?.kind === "strings" ? value.values[0] : undefined; }); codeValues; // => ["113100", "113105", "113110"] // The same rows, as data you can compare against. DEIDENTIFICATION_METHOD_CODES.options.RetainUIDs.codeValue; // => "113110" DEIDENTIFICATION_METHOD_CODES.profile.codeMeaning; // => "Basic Application Confidentiality Profile" // Running it again adds nothing, and says it kept what was already there. const again = deidentify(dataset, { retain: ["RetainUIDs", "CleanDescriptors"] }); again.dataset.get("00120064")?.items?.length; // => 3 again.report.warnings.map((w) => w.code); // => ["DICOM_DEIDENT_METHOD_PRIOR_RETAINED", "DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED"] ``` | Code Value | Code Meaning | Written when | | ---------- | -------------------------------------------------------------- | ----------------------------------------- | | `113100` | Basic Application Confidentiality Profile | every run | | `113101` | Clean Pixel Data Option | never | | `113102` | Clean Recognizable Visual Features Option | never | | `113103` | Clean Graphics Option | `CleanGraphics` | | `113104` | Clean Structured Content Option | `CleanStructuredContent` | | `113105` | Clean Descriptors Option | `CleanDescriptors` | | `113106` | Retain Longitudinal Temporal Information Full Dates Option | `RetainLongitudinalTemporal` | | `113107` | Retain Longitudinal Temporal Information Modified Dates Option | `RetainLongitudinalTemporalModifiedDates` | | `113108` | Retain Patient Characteristics Option | `RetainPatientCharacteristics` | | `113109` | Retain Device Identity Option | `RetainDeviceIdentity` | | `113110` | Retain UIDs Option | `RetainUIDs` | | `113111` | Retain Safe Private Option | `RetainSafePrivate` | | `113112` | Retain Institution Identity Option | `RetainInstitutionIdentity` | **Added to, never replaced.** When the source already carries a top-level `(0012,0064)`, every Item in it is kept, in its original order and byte for byte, and this run's Items follow. A code a prior Item already records under the same Coding Scheme Designator and Code Value is not added again (Code Meaning is not part of the match, and trailing padding is ignored), so de-identifying an output again with the same options is a fixed point, and a run that adds one further Option appends that Option's Item and nothing else. The limits of what these codes say, stated here rather than on a later page: - **`113101` and `113102` are never written.** They name the two pixel-level Options, and this layer does not touch pixels. - **`113107` says the modified-dates column was resolved, not that any date was shifted.** It is the same limit `(0028,0303) = MODIFIED` carries: this library transforms no date, and every run that writes `113107` still carries `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on `report.warnings`. - **An Option's code means the Option was active for the run.** It is written whether or not the object carried an attribute that Option acts on, and your `deidentificationMethod` text never changes the codes: they follow the options that ran. - **Prior Items are carried, not inspected.** Neither `(0012,0064)` nor the code attributes in its Items has a Table E.1-1 row, so what a sender wrote there reaches your output under `(0012,0062) = YES`, and no prior code is checked against CID 7050 or any terminology. Every run that keeps a prior Item says so with `DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED`. - **A prior that is not a Sequence of Items is replaced.** A `(0012,0064)` encoded under a VR other than `SQ`, or an `SQ` whose items were not parsed, cannot be added to, so this run's Items stand alone and `DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED` says so (a padding-only prior raises nothing, because nothing was lost). - **Top level only.** A copy nested inside a Sequence Item is neither written nor read, the same reach `(0012,0062)` and `(0012,0063)` have. | Export | What it is | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `DEIDENTIFICATION_METHOD_CODES` | The CID 7050 rows as frozen data, with the context group UID and version, the Profile's code and the code each `DeidentifyOption` writes. | | `DeidentificationMethodCode` | One row: `codeValue`, `codingSchemeDesignator` (`DCM`) and `codeMeaning`, spelled exactly as CID 7050 spells them. | ## UID remapping Action `U` replaces a UID with an internally-consistent one. The replacement is a pure function of the source UID and the root, so the same source UID maps to the same replacement across calls and across files in a study set, with no shared state: cross-instance referential integrity (Study to Series to SOP, Frame of Reference, referenced instances) survives the scrub. | Export | What it is | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DEFAULT_UID_ROOT` | The DICOM-sanctioned UUID-derived arc (PS3.5 §B.2), which needs no registration. The default `uidRoot`. | | `makeUidRemapper` | Builds a remapper over a root and an optional caller-owned cache, for remapping UIDs outside a `deidentify` call. Throws `DeidentifyError` on a bad root. | | `UidRemapper` | What it returns: `map(sourceUid)` plus the `cache` it fills, exposed for reporting and reuse. | ```ts runnable import { makeUidRemapper, DEFAULT_UID_ROOT } from "@cosyte/dicom"; const remap = makeUidRemapper(DEFAULT_UID_ROOT); const replaced = remap.map("1.2.826.0.1.3680043.8.498.1.1"); // Content-derived, so it is stable without any shared state... replaced.startsWith("2.25."); // => true remap.map("1.2.826.0.1.3680043.8.498.1.1") === replaced; // => true // ...and a fresh remapper on the same root agrees with it. makeUidRemapper().map("1.2.826.0.1.3680043.8.498.1.1") === replaced; // => true // The cache is the source-to-replacement map, exposed for your own reporting. remap.cache.get("1.2.826.0.1.3680043.8.498.1.1") === replaced; // => true ``` Pass your own `uidMap` through `DeidentifyOptions` if you would rather share one map explicitly across a whole archive; the mapping is consistent either way, and a shared map only makes repeats cheaper. ## Attributes neither table carries Table E.1-1 lists the attributes the Basic Profile judged, and an attribute it does not list is kept. That is right for an attribute the PS3.6 2026d data dictionary registers, which the Profile has considered, and it is how every unlisted attribute was handled before this rule. It is not right for an attribute **nobody** considered: the notes to PS3.15 2026d Table E.1-1 name "new Standard Attributes" among the places identifying information may be, and say that removing only the known risks "may fail when the Standard is extended, or when a vendor adds unanticipated Standard Attributes". So `deidentify()` **removes a non-private attribute that neither this build's PS3.6 2026d registry nor Table E.1-1 carries**, at the top level and inside every Sequence Item it walks, whatever its VR (`UN`, a VR outside the set PS3.5 §6.2 defines, and `SQ` included; a Sequence goes whole and nothing inside it is walked). "Registered" means a literal PS3.6 row, or a masked row the tag matches: the `(50xx,....)` and `(60xx,....)` families only across the even groups PS3.5 §7.6 bounds them to, so `(6002,0010)` Overlay Rows is kept and `(6020,0010)` is removed, and every other masked row, such as `(0028,04x0)`, as PS3.6 prints it. Private attributes, `(0004,xxxx)`, `(0002,xxxx)`, group lengths `(gggg,0000)` and every tag Table E.1-1 lists keep the handling the rest of this page describes. Each removal is recorded on `report.unregisteredElementRemovals` by its **byte offset**, and by the Sequences the run descended to reach it, **never by tag or VR**: a tag no registry row carries is exactly what four bytes read out of the middle of some element's value look like, so the tag may itself be document content. The list is capped per run and `report.unregisteredElementRemovalCount` carries the complete total, and `DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED` is raised once per run that removed anything, with no tag, VR or count in its message. **The limit, stated beside the capability: a conformant attribute from a PS3.6 edition newer than this build is removed as well**, because nothing on the wire separates it from one a sender invented. That is over-redaction, it is deliberate, and there is no option in this release to keep such an attribute. It also narrows the older gap rather than closing it: an attribute PS3.6 registers and the pinned Table E.1-1 does not list is still kept exactly as the source wrote it, which is the Profile's own design. ## The report, and what it is not safe to log `DeidentifyReport` is an audit trail, not a redacted surface. **Several of its fields carry source bytes rather than table lookups, and they are named on the type. Read that list before logging a report whole**, and read [Keeping PHI out of logs](https://docs.cosyte.com/dicom/troubleshooting.md#keeping-phi-out-of-logs) for the mechanism. An emptied audit is not a performed one: a report that reads as a complete scrub it did not perform is the worse half of every residual on the limitations page. | Export | What it records | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DeidentifiedAttribute` | One audited attribute: its tag, keyword, resolved Annex E action and the `AppliedAction` that action produced. | | `EmbeddedAttributeFinding` | A carrier whose over-declared Value Length swallowed the element after it, emptied rather than kept. `hidden` lists only tags this run acted on that Table E.1-1 gives a literal row, so it can be empty on a real finding. | | `UnauditableSequenceFinding` | An `SQ` that reached the run with no items, so its item stream could not be walked and it was emptied rather than passed through. | | `UndefinedVrFinding` | An element whose on-wire VR is outside the set PS3.5 §6.2 defines, emptied because no Table E.1-1 row can say what its bytes mean. It names a byte offset and deliberately no tag. | | `UnenumerablePrivateRemoval` | A private attribute a `Profile` vouched for under `RetainSafePrivate` whose value this run did not enumerate, so §E.3.10's "known to be safe" was never established and it was **removed**, not emptied. | | `FileMetaDroppedElement` | A non-modeled `(0002,xxxx)` element the source carried that is not in the output, with the VR and byte length that went with it. A deliberate fidelity loss, recorded because what was dropped is the audit value. | | `Group0004Removal` | A `(0004,xxxx)` element removed under §E.1.1's unconditional group-0004 rule. No Option brings one back; the DICOMDIR carve-out is the one object it does not apply to. | | `UnregisteredElementRemoval` | A non-private element removed because neither this build's PS3.6 registry nor Table E.1-1 carries its tag. It names a byte offset and deliberately no tag or VR, and a newer edition's conformant attribute lands here too. | ## Errors | Export | What it is | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | `DEIDENTIFY_ERROR_CODES` | The frozen registry of codes this layer throws: `INVALID_OPTIONS` for an author-time misconfiguration of the call, and `UNSUPPORTED_TRANSFER_SYNTAX` for a `Dataset` under one of the four JPIP Referenced syntaxes, which is refused whatever the options (see Scope limits below). | | `DeidentifyErrorCode` | The discriminant type over that registry. | | `DeidentifyError` | The thrown class, distinct from `DicomParseError`, `DicomValueError` and `DicomSerializeError`. Its message carries only option names and the UID root, or a fixed string for `UNSUPPORTED_TRANSFER_SYNTAX`, never a decoded value. | ```ts runnable throws import { makeUidRemapper } from "@cosyte/dicom"; // A UID root that is not a dotted-decimal OID prefix is an author error, not a file error. makeUidRemapper("not-an-oid"); // throws DeidentifyError (INVALID_OPTIONS) ``` ## Scope limits on this page's subject These are boundaries, not defects; the full list is on [Known limitations](https://docs.cosyte.com/dicom/limitations.md). - **Metadata only.** Burned-in annotation is warned, never removed. Pixel scrubbing is `@cosyte/dicom-pixel`. - **A JPIP Referenced object is refused, not de-identified.** When `ds.fileMeta.transferSyntaxUID` is one of the four JPIP Referenced syntaxes (PS3.5 2026c sections A.6, A.7, A.11 and A.12), `deidentify()` throws a `DeidentifyError` with code `UNSUPPORTED_TRANSFER_SYNTAX` before it reads your options or the Data Set, and returns no dataset or report. Such an object's Pixel Data Provider URL `(0028,7FE0)` has no Table E.1-1 row, so a de-identified copy would keep it by omission. **The refusal keys on the File Meta Transfer Syntax alone**: a non-JPIP object that carries `(0028,7FE0)` anyway is de-identified like any other object, and that URL is kept, as every registered attribute Table E.1-1 does not list is kept. - **A standard attribute newer than this build is removed, conformant or not.** An attribute with no row in this build's PS3.6 registry and none in Table E.1-1 is removed and recorded without its tag (see [Attributes neither table carries](#attributes-neither-table-carries)), so an object from a later edition loses its new attributes. There is no switch to keep them in this release. - **Conditional Annex E codes collapse to their most protective branch.** There is no IOD Type-1 analysis here, so where the table's action depends on the object's IOD this run takes the branch that removes rather than the one that keeps. - **`RetainSafePrivate` keeps only what the run could account for.** Two routes write a private value into de-identified output: a `Profile` you pass, and the **file's own** Private Data Element Characteristics Sequence `(0008,0300)`, which needs no profile at all. Everything else private is removed and recorded. The cost is over-redaction on the profile route, and it is nearly all of that route; on the declaration route the cost runs the other way, because what is kept rests on the **sender's** assertion that the block carries no identifying information rather than on anything this run examined. If you do not trust the sender, leave the Option off: PS3.15 §E.3.10 offers no other mitigation. - **The byte-for-byte File Meta round trip does not hold for this output.** The group is replaced with a description of the de-identifying application (§E.1.1), so the Source AE Title and the source's implementation identity go with it. That loss is recorded, and it is not recoverable from the output. If you need the source group verbatim, read it off the parsed dataset before the call, and see [Serialization](https://docs.cosyte.com/dicom/serialization.md) for the round trip that does hold. - **A de-identified DICOMDIR is not File-set conformant.** Its Directory Records are de-identified as Data Sets and `serializeDicom` rewrites their offsets to where they land, but the run creates no DICOMDIR from the de-identified files and removes no non-de-identified DICOMDIR from the File-set, and `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED` names exactly those two clauses. Referenced File IDs `(0004,1500)` are kept verbatim. Build a de-identified File-set from the de-identified files, not from this output; see [Known limitations](https://docs.cosyte.com/dicom/limitations.md). ## Where to go next - A worked recipe, including what the File Meta group stops naming: [Cookbook](https://docs.cosyte.com/dicom/cookbook.md#4-de-identify-before-sharing). - What a diagnostic may and may not carry: [Tolerance & warnings](https://docs.cosyte.com/dicom/spec-notes-tolerance.md). - Vouching for known-safe private attributes: [Source profiles](https://docs.cosyte.com/dicom/spec-notes-profiles.md). - The measured, still-open residuals: [Known limitations](https://docs.cosyte.com/dicom/limitations.md). --- URL: https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary Markdown: https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md # Dictionary Public Dictionary namespace for `@cosyte/dicom`. - [lookup](#lookup) - accepts either an 8-char hex tag (`"00100010"`) or a keyword (`"PatientName"`) and returns the typed [DictionaryEntry](#dictionaryentry) or `undefined`. - [byKeyword](#bykeyword) - keyword-only lookup; for cases where the caller has already validated the input shape and wants a narrower call. - [uid](#uid-1) - UID lookup. No function throws on miss: each returns the typed entry or `undefined`. Returned entries are deeply frozen - mutation attempts throw `TypeError` in strict mode (which all of `@cosyte/dicom`'s emitted code runs under, given `"use strict"` is implicit for ES modules). ## Example ```ts import { Dictionary } from "@cosyte/dicom"; // Tag or keyword, either way round, and `undefined` rather than a throw on a miss. Dictionary.lookup("00100020")?.name; // "Patient ID" Dictionary.byKeyword("Modality")?.tag; // "00080060" // UIDs resolve to their registered name, so nobody hard-codes a SOP Class table. Dictionary.uid("1.2.840.10008.5.1.4.1.1.2")?.name; // "CT Image Storage" ``` ## Interfaces ### DictionaryEntry One DICOM attribute as published in PS3.6 (Data Dictionary). #### Example ```ts import { Dictionary } from "@cosyte/dicom"; const entry = Dictionary.lookup("00100010"); if (entry) { // entry.keyword === "PatientName" // entry.vr === ["PN"] } ``` #### Properties ##### keyword > `readonly` **keyword**: `string` ##### name > `readonly` **name**: `string` ##### repeatingGroup? > `readonly` `optional` **repeatingGroup?**: `boolean` `true` for repeating-group families (`(50xx,xxxx)`, `(60xx,xxxx)`: curves/overlays); the [DictionaryEntry.tag](#tag) field for these contains lowercase `x` placeholders, NOT a concrete 8-hex-char tag. Repeating-group entries are not surfaced by `Dictionary.lookup(tag)` for concrete tags. ##### retired > `readonly` **retired**: `boolean` ##### tag > `readonly` **tag**: `string` ##### vm > `readonly` **vm**: `string` ##### vr > `readonly` **vr**: readonly [`VR`](#vr-1)[] *** ### UidEntry One DICOM UID as published in PS3.6 Annex A (Tables A-1 and A-2). `name` is the normative UID Name, with two deliberate conveniences. Retirement is carried in `retired` rather than as the trailing `(Retired)` every retired Annex A row carries in its UID Name, and four transfer syntaxes carry the short form every DICOM toolkit prints rather than PS3.6's longer `...: Default Transfer Syntax for ...` form. Every other name is the normative text. #### Example ```ts import { Dictionary } from "@cosyte/dicom"; const ts = Dictionary.uid("1.2.840.10008.1.2.1"); if (ts) { // ts.name === "Explicit VR Little Endian" // ts.type === "TransferSyntax" } ``` #### Properties ##### name > `readonly` **name**: `string` ##### retired > `readonly` **retired**: `boolean` ##### type > `readonly` **type**: `"TransferSyntax"` \| `"SOPClass"` \| `"MetaSOPClass"` \| `"WellKnownFrameOfReference"` \| `"WellKnownSOPInstance"` \| `"CodingScheme"` \| `"ApplicationContext"` \| `"ServiceClass"` \| `"Other"` ##### uid > `readonly` **uid**: `string` ## Type Aliases ### Tag > **Tag** = `string` 8-character uppercase hex DICOM tag, e.g., `"00100010"` (Patient's Name). For repeating-group attribute families (`(50xx,xxxx)` curves, `(60xx,xxxx)` overlays, etc.), the tag string preserves lowercase `x` placeholders verbatim - those entries cannot be looked up by concrete tag and are flagged via [DictionaryEntry.repeatingGroup](#repeatinggroup). #### Example ```ts const tag: Tag = "00100010"; // Patient's Name ``` *** ### VR > **VR** = `"AE"` \| `"AS"` \| `"AT"` \| `"CS"` \| `"DA"` \| `"DS"` \| `"DT"` \| `"FL"` \| `"FD"` \| `"IS"` \| `"LO"` \| `"LT"` \| `"OB"` \| `"OD"` \| `"OF"` \| `"OL"` \| `"OV"` \| `"OW"` \| `"PN"` \| `"SH"` \| `"SL"` \| `"SQ"` \| `"SS"` \| `"ST"` \| `"SV"` \| `"TM"` \| `"UC"` \| `"UI"` \| `"UL"` \| `"UN"` \| `"UR"` \| `"US"` \| `"UT"` \| `"UV"` The 33 standard DICOM Value Representations from PS3.5 §6.2 plus the 64-bit additions (`OV`, `SV`, `UV`) introduced in DICOM 2018. Note that some attributes in the data dictionary list MULTIPLE possible VRs - see [DictionaryEntry.vr](#vr) (which is always an array, possibly empty for retired entries with no VR or special "See Note" entries). #### Example ```ts const vr: VR = "PN"; // Person Name ``` ## Functions ### byKeyword() > **byKeyword**(`keyword`): [`DictionaryEntry`](#dictionaryentry) \| `undefined` Look up a DICOM attribute strictly by keyword (`"PatientName"`). Returns `undefined` for unknown keywords or malformed input. Use [lookup](#lookup) if the input could be either a tag or a keyword. #### Parameters ##### keyword `string` #### Returns [`DictionaryEntry`](#dictionaryentry) \| `undefined` #### Example ```ts import { Dictionary } from "@cosyte/dicom"; const e = Dictionary.byKeyword("StudyInstanceUID"); // e?.tag === "0020000D" ``` *** ### lookup() > **lookup**(`tagOrKeyword`): [`DictionaryEntry`](#dictionaryentry) \| `undefined` Look up a DICOM attribute by tag (`"00100010"`) or keyword (`"PatientName"`). Hex tag input is normalized to uppercase; keyword input is case-sensitive. Returns `undefined` for unknown tags, unknown keywords, malformed input, or tags from repeating-group families (those resolve via the family entry's `repeatingGroup` flag - see [DictionaryEntry](#dictionaryentry)). #### Parameters ##### tagOrKeyword `string` #### Returns [`DictionaryEntry`](#dictionaryentry) \| `undefined` #### Example ```ts import { Dictionary } from "@cosyte/dicom"; const a = Dictionary.lookup("00100010"); // by tag const b = Dictionary.lookup("PatientName"); // by keyword const c = Dictionary.lookup("not-real"); // undefined, never a throw ``` *** ### uid() > **uid**(`uidValue`): [`UidEntry`](#uidentry) \| `undefined` Look up a DICOM UID by its dotted-decimal value. Covers PS3.6 Annex A whole: transfer syntaxes, SOP and Meta SOP Classes, well-known SOP Instances and Frames of Reference, coding schemes and the rest, current and retired alike. Returns `undefined` for unknown UIDs or malformed input. Used by `parseDicom()` to render human-readable Transfer Syntax names. #### Parameters ##### uidValue `string` #### Returns [`UidEntry`](#uidentry) \| `undefined` #### Example ```ts import { Dictionary } from "@cosyte/dicom"; const ts = Dictionary.uid("1.2.840.10008.1.2.1"); // ts?.name === "Explicit VR Little Endian" // ts?.type === "TransferSyntax" ``` --- URL: https://docs.cosyte.com/dicom/api Markdown: https://docs.cosyte.com/dicom/api.md # @cosyte/dicom ## Namespaces - [Dictionary](https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md) ## Classes ### Dataset One parsed DICOM dataset - the structural shell. Public data: `fileMeta`, `warnings`. The internal element map is stored on `_elements` (protected) and reached through the public `get` / `has` / `elements` / `getAll` navigation API. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); console.log(ds.fileMeta?.transferSyntaxUID); for (const w of ds.warnings) { console.warn(w.code, w.message); } ``` #### Extended by - [`Item`](#item) #### Constructors ##### Constructor > **new Dataset**(`init`): [`Dataset`](#dataset) **`Internal`** Construct a new structural `Dataset`. The warnings array is frozen at the model boundary. ###### Parameters ###### init `DatasetInit` ###### Returns [`Dataset`](#dataset) #### Properties ##### \_elements > `protected` `readonly` **\_elements**: `ReadonlyMap`\<`string`, [`Element`](#element)\> **`Internal`** Element map, keyed by uppercase 8-hex tag. Reached publicly through the navigation surface; kept `protected` here so only subclasses (`Item`, and any later extension) can introspect it directly. ##### fileMeta > `readonly` **fileMeta**: [`FileMeta`](#filemeta-3) \| `undefined` ##### warnings > `readonly` **warnings**: readonly [`DicomParseWarning`](#dicomparsewarning)[] #### Accessors ##### directory ###### Get Signature > **get** **directory**(): [`DicomDirectory`](#dicomdirectory) \| `undefined` The DICOMDIR Directory Record tree, or `undefined` when this Dataset is not a DICOMDIR (its File Meta Media Storage SOP Class UID is not `1.2.840.10008.1.3.10`). Read from the four offset attributes (PS3.3 2026d Table F.3-3): `root` is the record `(0004,1200)` names and each `(0004,1400)` successor, and each record's `lowerLevel` is the record its `(0004,1420)` names and each successor. An offset resolves only when it is exactly the file offset of an Item of the Directory Record Sequence `(0004,1220)`; one that is not names no record, and `parseDicom` warns (`DICOM_DIRECTORY_OFFSET_UNRESOLVED`, `DICOM_DIRECTORY_OFFSET_MALFORMED`, `DICOM_DIRECTORY_RECORD_REVISITED`, `DICOM_DIRECTORY_OFFSET_DEFLATED`). Memoised on first read. Limits: record keys are not checked against PS3.3 F.5, the File-set Consistency Flag and `(0004,1202)`'s agreement with the end of the root chain are not checked, and under Deflated Explicit VR LE no offset resolves, so every record is listed in `records` and none is linked. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const dir = parseDicom(buf).directory; for (const patient of dir?.root ?? []) { for (const study of patient.lowerLevel) { for (const series of study.lowerLevel) { for (const image of series.lowerLevel) console.log(image.referencedFileId); } } } ``` ###### Returns [`DicomDirectory`](#dicomdirectory) \| `undefined` ##### image ###### Get Signature > **get** **image**(): [`ImageView`](#imageview) Pixel-interpretation + geometry view. Surfaces exactly what a renderer needs without guessing: `rescaleSlope`/`signed`/ `photometricInterpretation` stay absent rather than defaulted, and the three pixel-spacing tags are distinct. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const img = parseDicom(buf).image; img.rescaleSlope; // undefined ⇒ MUST NOT assume 1 ``` ###### Returns [`ImageView`](#imageview) ##### patient ###### Get Signature > **get** **patient**(): [`PatientView`](#patientview) Patient-identity view. Fail-safe typed-absent fields; `id` is **not** globally unique - match on the `{id, issuerOfId, ...}` tuple plus `otherIds`, never on a bare `(0010,0020)`. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const p = parseDicom(buf).patient; p.name?.alphabetic.familyName; // structured PN, never flattened ``` ###### Returns [`PatientView`](#patientview) ##### series ###### Get Signature > **get** **series**(): [`SeriesView`](#seriesview) Series-identity & co-registration view. A shared `frameOfReferenceUid` means images are spatially co-registered. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const s = parseDicom(buf).series; s.modality; // "CT" ``` ###### Returns [`SeriesView`](#seriesview) ##### study ###### Get Signature > **get** **study**(): [`StudyView`](#studyview) Study-identity view. `instanceUid` is the cross-system study key; `accessionNumber` ties it to the order. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const s = parseDicom(buf).study; s.instanceUid; // "1.2.840.113619..." ``` ###### Returns [`StudyView`](#studyview) #### Methods ##### elements() > **elements**(): readonly [`Element`](#element)[] All elements in this dataset, in parse (insertion) order. ###### Returns readonly [`Element`](#element)[] ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); for (const el of ds.elements()) console.log(el.tag, el.vr); ``` ##### get() > **get**(`tag`): [`Element`](#element) \| `undefined` Look up a single element by tag. Tags are normalised to 8-char uppercase hex, so `"7fe00010"` and `"7FE00010"` resolve to the same element. Returns `undefined` when the tag is absent. ###### Parameters ###### tag `string` ###### Returns [`Element`](#element) \| `undefined` ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); const rows = ds.get("00280010"); // Rows if (rows?.value.kind === "numbers") console.log(rows.value.values[0]); ``` ##### getAll() > **getAll**(`tag`): readonly [`Element`](#element)[] All elements matching a tag as an array (never `undefined`). A dataset holds at most one element per tag, so this returns a 0- or 1-length array - the convenience complement of [Dataset.get](#get) for callers that prefer an always-array shape. ###### Parameters ###### tag `string` ###### Returns readonly [`Element`](#element)[] ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); for (const el of ds.getAll("00080060")) console.log(el.value); ``` ##### has() > **has**(`tag`): `boolean` `true` when an element with the given tag is present (case-insensitive). ###### Parameters ###### tag `string` ###### Returns `boolean` ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); if (ds.has("00100010")) console.log("has Patient's Name"); ``` *** ### DeidentifyError Thrown by [deidentify](#deidentify) when it will not de-identify, for one of two reasons a caller tells apart by `code` (see [DEIDENTIFY\_ERROR\_CODES](#deidentify_error_codes)): an author-time misconfiguration of the call (`INVALID_OPTIONS`: an unknown Retain option, a malformed UID root), or a Dataset under a Transfer Syntax this de-identifier refuses (`UNSUPPORTED_TRANSFER_SYNTAX`: the four JPIP Referenced syntaxes). Distinct from the parser's fatal codes, the value layer's `DicomValueError`, and the serializer's `DicomSerializeError`. The message carries only structural facts (option names, the UID root) or a fixed string - never a decoded value, and for `UNSUPPORTED_TRANSFER_SYNTAX` nothing read from the Dataset at all. #### Example ```ts import { deidentify, DeidentifyError } from "@cosyte/dicom"; try { // @ts-expect-error - not a valid option deidentify(ds, { retain: ["RetainEverything"] }); } catch (e) { if (e instanceof DeidentifyError) console.error(e.code); // "INVALID_OPTIONS" } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new DeidentifyError**(`message`, `code`): [`DeidentifyError`](#deidentifyerror) ###### Parameters ###### message `string` Human-readable, PHI-free description. ###### code [`DeidentifyErrorCode`](#deidentifyerrorcode-1) One of [DEIDENTIFY\_ERROR\_CODES](#deidentify_error_codes). ###### Returns [`DeidentifyError`](#deidentifyerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`DeidentifyErrorCode`](#deidentifyerrorcode-1) *** ### DicomParseError Thrown by `parseDicom` when the input violates one of the four unrecoverable Tier-3 structural rules - or, under `{ strict: true }`, when any Tier-2 warning is escalated through the single `emit` chokepoint. Carries byte-offset positional context plus a short source snippet so consumers can log actionable errors. Message format: `[CODE] msg (offset=N frame=F)`, with `… in a/b/c` appended when `contextPath` is provided. #### Remarks Snippets may contain PHI when parsing real clinical files - redact at the call site if required by your compliance posture. The library does not redact snippets itself. **`byteOffset` is only an index into your own buffer when `offsetFrame` is `"input"`.** This parser reads a Data Set out of a slice in two situations - a defined-length Sequence Item, and an `SQ`/`UN` descent - and out of an inflated stream in a third, and an offset raised in any of them counts from that buffer's byte 0. [OFFSET\_FRAMES](#offset_frames) names which; where a slice begins is deliberately not published, because the distance between two frames is a declared length off the wire. **`snippet` is cut in the frame `offsetFrame` names, on every fatal but one.** The exception is `UNSUPPORTED_TRANSFER_SYNTAX`, whose snippet slot carries PS3.6's own NAME for the unsupported UID when the registry publishes one (`"SMPTE ST 2110-20 Uncompressed Progressive Active Video"`), and 16 raw bytes only when it does not. That is deliberate and predates the frame; it is named here because a universal about `snippet` written without it is false on the code an object under a refused Transfer Syntax reaches first. Everywhere else the two agree, so a consumer that only wants the bytes at the offset already has them. **The frame is what a consumer needs before indexing anything of its OWN by `byteOffset`**, which is the case no field on this class used to cover. #### Example ```ts import { parseDicom, DicomParseError, OFFSET_FRAMES } from "@cosyte/dicom"; try { parseDicom(buffer); } catch (err) { if (err instanceof DicomParseError && err.code === "NOT_DICOM_PART_10") { // err.byteOffset, err.offsetFrame, err.snippet, err.contextPath if (err.offsetFrame !== OFFSET_FRAMES.INPUT) { // `byteOffset` counts from somewhere inside the file, not from its start. } } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new DicomParseError**(`code`, `message`, `byteOffset`, `offsetFrame`, `snippet`, `contextPath?`): [`DicomParseError`](#dicomparseerror) **`Internal`** Construct a new `DicomParseError`. All fields except `contextPath` are required so every thrower populates positional context - and `offsetFrame` is required for the same reason `byteOffset` is, because an offset whose frame is optional is an offset whose frame is usually missing. ###### Parameters ###### code [`FatalCode`](#fatalcode) ###### message `string` ###### byteOffset `number` ###### offsetFrame [`OffsetFrame`](#offsetframe-1) ###### snippet `string` ###### contextPath? readonly `string`[] ###### Returns [`DicomParseError`](#dicomparseerror) ###### Overrides `Error.constructor` #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` ##### code > `readonly` **code**: [`FatalCode`](#fatalcode) ##### contextPath > `readonly` **contextPath**: readonly `string`[] \| `undefined` ##### offsetFrame > `readonly` **offsetFrame**: [`OffsetFrame`](#offsetframe-1) The coordinate system [DicomParseError.byteOffset](#byteoffset) is counted in. See [OFFSET\_FRAMES](#offset_frames). ##### snippet > `readonly` **snippet**: `string` *** ### DicomSerializeError Thrown by `serializeDicom` when a `Dataset` cannot be emitted as spec-clean Part 10. Never carries a decoded value, so it is safe to log without leaking PHI: the `message` is built only from the code and structural constants, never from the Transfer Syntax UID, a length or a byte the `Dataset` holds. #### Example ```ts import { parseDicom, serializeDicom, DicomSerializeError } from "@cosyte/dicom"; const ds = parseDicom(buf); try { serializeDicom(ds); } catch (err) { if (err instanceof DicomSerializeError && err.code === "MISSING_TRANSFER_SYNTAX") { // dataset is missing the File Meta Transfer Syntax UID } if (err instanceof DicomSerializeError && err.code === "INVALID_ENCAPSULATED_PIXEL_DATA") { // a section A.4 object whose top-level Pixel Data is not a fragment stream } if (err instanceof DicomSerializeError && err.code === "DIRECTORY_OFFSET_UNRESOLVED") { // a DICOMDIR offset the writer cannot tie to a Directory Record } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new DicomSerializeError**(`code`, `message`): [`DicomSerializeError`](#dicomserializeerror) **`Internal`** Construct a new `DicomSerializeError`. The `message` MUST be built only from structural facts (never a decoded attribute value) so the error is always safe to log. ###### Parameters ###### code [`SerializeErrorCode`](#serializeerrorcode) ###### message `string` ###### Returns [`DicomSerializeError`](#dicomserializeerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`SerializeErrorCode`](#serializeerrorcode) *** ### DicomValueError Thrown by the domain helpers for a structural contract violation that cannot be answered safely (see module doc). Never carries a decoded value, so it is safe to log without leaking PHI: the `message` is built only from the code and structural facts (indices, tag/macro names). #### Example ```ts import { parseDicom, DicomValueError } from "@cosyte/dicom"; const img = parseDicom(buf).image; try { img.frame(9999); } catch (err) { if (err instanceof DicomValueError && err.code === "FRAME_INDEX_OUT_OF_RANGE") { // handle the out-of-range request } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new DicomValueError**(`code`, `message`): [`DicomValueError`](#dicomvalueerror) **`Internal`** Construct a new `DicomValueError`. The `message` MUST be built only from structural facts (never a decoded attribute value) so the error is always safe to log. ###### Parameters ###### code [`ValueErrorCode`](#valueerrorcode) ###### message `string` ###### Returns [`DicomValueError`](#dicomvalueerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`ValueErrorCode`](#valueerrorcode) *** ### Element One DICOM Data Element as parsed. Structural surface: `tag`, `vr`, `vm`, `length`, `rawBytes`, `byteOffset`, `privateCreator`. On top of it sits a lazy, memoized `.value` getter backed by the VR-aware decoders under `src/dataset/vr/`. `rawBytes` retention behaviour: zero-copy `Buffer.subarray` view by default (pins source ArrayBuffer); pass `{ copyValues: true }` to `parseDicom` for `Buffer.from(slice)` per element when the source needs to be released. #### Example ```ts import { Buffer } from "node:buffer"; import { Element } from "@cosyte/dicom"; const el = new Element({ tag: "00100010", vr: "PN", vm: 1, length: 8, rawBytes: Buffer.from("DOE^JANE"), byteOffset: 200, littleEndian: true, }); // el.tag === "00100010"; el.rawBytes is a Buffer of 8 bytes. // el.value.kind === "personName". ``` #### Constructors ##### Constructor > **new Element**(`init`): [`Element`](#element) **`Internal`** Construct a new structural `Element`. The parser builds these directly from on-wire bytes; consumers receive them via `Dataset` after `parseDicom`. ###### Parameters ###### init `ElementInit` ###### Returns [`Element`](#element) #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` ##### cp246Promoted > `readonly` **cp246Promoted**: `boolean` \| `undefined` **`Internal`** Hint for the lazy SQ decoder. `true` when this Element was promoted from `VR=UN` with undefined length to `VR=SQ` via the CP-246 fallback, which is what selects the Implicit VR LE inner decoder. Always `undefined` on standard SQ elements. ##### items > `readonly` **items**: readonly [`Item`](#item)[] \| `undefined` Parsed items for an `SQ` element; `undefined` otherwise. ##### length > `readonly` **length**: `number` ##### littleEndian > `readonly` **littleEndian**: `boolean` Value byte order (per transfer syntax). See ElementInit.littleEndian. ##### privateCreator > `readonly` **privateCreator**: `string` \| `undefined` ##### rawBytes > `readonly` **rawBytes**: `Buffer` ##### specificCharacterSet > `readonly` **specificCharacterSet**: readonly `string`[] \| `undefined` In-effect `(0008,0005)` terms, or `undefined` for the Default Repertoire. ##### tag > `readonly` **tag**: `string` ##### vm > `readonly` **vm**: `number` ##### vr > `readonly` **vr**: [`VR`](https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md#vr-1) #### Accessors ##### value ###### Get Signature > **get** **value**(): [`DicomValue`](#dicomvalue) The decoded value of this element, by VR. Decode is lazy (the structural parse stays eager; field decode runs on first access) and memoized - the documented ~30× win on large studies where most fields are never read. Fail-safe: never throws; a malformed value surfaces as typed-absent with the deviation on the returned value's `warnings`. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); const v = ds.get("00100010")?.value; // Patient's Name (PN) if (v?.kind === "personName") console.log(v.values[0]?.alphabetic.familyName); ``` ###### Returns [`DicomValue`](#dicomvalue) *** ### Item One sequence item. Inherits `fileMeta` (always `undefined` for nested items), `warnings`, and the protected element map from `Dataset`. `Item.get(...)` / `Item.has(...)` and the rest come from the `Dataset` superclass. #### Example ```ts import { Item } from "@cosyte/dicom"; // The parser constructs items as follows: // const item = new Item({ index: 0, warnings: [], elements: new Map() }); ``` #### Extends - [`Dataset`](#dataset) #### Constructors ##### Constructor > **new Item**(`init`): [`Item`](#item) **`Internal`** Construct a new structural `Item`. Producers are the SQ / FFFE marker parsers. ###### Parameters ###### init `ItemInit` ###### Returns [`Item`](#item) ###### Overrides [`Dataset`](#dataset).[`constructor`](#constructor) #### Properties ##### \_elements > `protected` `readonly` **\_elements**: `ReadonlyMap`\<`string`, [`Element`](#element)\> **`Internal`** Element map, keyed by uppercase 8-hex tag. Reached publicly through the navigation surface; kept `protected` here so only subclasses (`Item`, and any later extension) can introspect it directly. ###### Inherited from [`Dataset`](#dataset).[`_elements`](#_elements) ##### fileMeta > `readonly` **fileMeta**: [`FileMeta`](#filemeta-3) \| `undefined` ###### Inherited from [`Dataset`](#dataset).[`fileMeta`](#filemeta) ##### fileOffset > `readonly` **fileOffset**: `number` \| `undefined` Byte offset of this Item's `(FFFE,E000)` Item tag in the Part 10 file it was read from, counted from the first byte of the File Preamble, which is how PS3.3 2026d Table F.3-3 counts a DICOMDIR's offsets. For a file read without a preamble, the 132 bytes of File Preamble and `DICM` prefix it lacks are still counted, so the File Meta group's first byte is offset 132. `undefined` for an Item with no position in a file: one inside a Deflated Data Set (a byte of a deflated stream is not a byte of the file), and one built by hand. `deidentify()` carries it from each source Item to the Item it rebuilds, as that Item's identity: it is how `serializeDicom` ties a DICOMDIR offset to the Directory Record it named. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); const first = ds.get("00041220")?.items?.[0]; first?.fileOffset; // where that Directory Record Item starts in the file ``` ##### index > `readonly` **index**: `number` ##### warnings > `readonly` **warnings**: readonly [`DicomParseWarning`](#dicomparsewarning)[] ###### Inherited from [`Dataset`](#dataset).[`warnings`](#warnings) #### Accessors ##### directory ###### Get Signature > **get** **directory**(): [`DicomDirectory`](#dicomdirectory) \| `undefined` The DICOMDIR Directory Record tree, or `undefined` when this Dataset is not a DICOMDIR (its File Meta Media Storage SOP Class UID is not `1.2.840.10008.1.3.10`). Read from the four offset attributes (PS3.3 2026d Table F.3-3): `root` is the record `(0004,1200)` names and each `(0004,1400)` successor, and each record's `lowerLevel` is the record its `(0004,1420)` names and each successor. An offset resolves only when it is exactly the file offset of an Item of the Directory Record Sequence `(0004,1220)`; one that is not names no record, and `parseDicom` warns (`DICOM_DIRECTORY_OFFSET_UNRESOLVED`, `DICOM_DIRECTORY_OFFSET_MALFORMED`, `DICOM_DIRECTORY_RECORD_REVISITED`, `DICOM_DIRECTORY_OFFSET_DEFLATED`). Memoised on first read. Limits: record keys are not checked against PS3.3 F.5, the File-set Consistency Flag and `(0004,1202)`'s agreement with the end of the root chain are not checked, and under Deflated Explicit VR LE no offset resolves, so every record is listed in `records` and none is linked. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const dir = parseDicom(buf).directory; for (const patient of dir?.root ?? []) { for (const study of patient.lowerLevel) { for (const series of study.lowerLevel) { for (const image of series.lowerLevel) console.log(image.referencedFileId); } } } ``` ###### Returns [`DicomDirectory`](#dicomdirectory) \| `undefined` ###### Inherited from [`Dataset`](#dataset).[`directory`](#directory) ##### image ###### Get Signature > **get** **image**(): [`ImageView`](#imageview) Pixel-interpretation + geometry view. Surfaces exactly what a renderer needs without guessing: `rescaleSlope`/`signed`/ `photometricInterpretation` stay absent rather than defaulted, and the three pixel-spacing tags are distinct. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const img = parseDicom(buf).image; img.rescaleSlope; // undefined ⇒ MUST NOT assume 1 ``` ###### Returns [`ImageView`](#imageview) ###### Inherited from [`Dataset`](#dataset).[`image`](#image) ##### patient ###### Get Signature > **get** **patient**(): [`PatientView`](#patientview) Patient-identity view. Fail-safe typed-absent fields; `id` is **not** globally unique - match on the `{id, issuerOfId, ...}` tuple plus `otherIds`, never on a bare `(0010,0020)`. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const p = parseDicom(buf).patient; p.name?.alphabetic.familyName; // structured PN, never flattened ``` ###### Returns [`PatientView`](#patientview) ###### Inherited from [`Dataset`](#dataset).[`patient`](#patient) ##### series ###### Get Signature > **get** **series**(): [`SeriesView`](#seriesview) Series-identity & co-registration view. A shared `frameOfReferenceUid` means images are spatially co-registered. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const s = parseDicom(buf).series; s.modality; // "CT" ``` ###### Returns [`SeriesView`](#seriesview) ###### Inherited from [`Dataset`](#dataset).[`series`](#series) ##### study ###### Get Signature > **get** **study**(): [`StudyView`](#studyview) Study-identity view. `instanceUid` is the cross-system study key; `accessionNumber` ties it to the order. Memoised on first read. ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const s = parseDicom(buf).study; s.instanceUid; // "1.2.840.113619..." ``` ###### Returns [`StudyView`](#studyview) ###### Inherited from [`Dataset`](#dataset).[`study`](#study) #### Methods ##### elements() > **elements**(): readonly [`Element`](#element)[] All elements in this dataset, in parse (insertion) order. ###### Returns readonly [`Element`](#element)[] ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); for (const el of ds.elements()) console.log(el.tag, el.vr); ``` ###### Inherited from [`Dataset`](#dataset).[`elements`](#elements) ##### get() > **get**(`tag`): [`Element`](#element) \| `undefined` Look up a single element by tag. Tags are normalised to 8-char uppercase hex, so `"7fe00010"` and `"7FE00010"` resolve to the same element. Returns `undefined` when the tag is absent. ###### Parameters ###### tag `string` ###### Returns [`Element`](#element) \| `undefined` ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); const rows = ds.get("00280010"); // Rows if (rows?.value.kind === "numbers") console.log(rows.value.values[0]); ``` ###### Inherited from [`Dataset`](#dataset).[`get`](#get) ##### getAll() > **getAll**(`tag`): readonly [`Element`](#element)[] All elements matching a tag as an array (never `undefined`). A dataset holds at most one element per tag, so this returns a 0- or 1-length array - the convenience complement of [Dataset.get](#get) for callers that prefer an always-array shape. ###### Parameters ###### tag `string` ###### Returns readonly [`Element`](#element)[] ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); for (const el of ds.getAll("00080060")) console.log(el.value); ``` ###### Inherited from [`Dataset`](#dataset).[`getAll`](#getall) ##### has() > **has**(`tag`): `boolean` `true` when an element with the given tag is present (case-insensitive). ###### Parameters ###### tag `string` ###### Returns `boolean` ###### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); if (ds.has("00100010")) console.log("has Patient's Name"); ``` ###### Inherited from [`Dataset`](#dataset).[`has`](#has) *** ### ProfileDefinitionError Thrown by `defineProfile()` when the supplied options are invalid. #### Example ```ts import { defineProfile, ProfileDefinitionError } from "@cosyte/dicom"; try { defineProfile({ name: "" }); } catch (err) { if (err instanceof ProfileDefinitionError) { console.error(err.message); // "Profile name must be a non-empty string." } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new ProfileDefinitionError**(`message`, `profileName?`): [`ProfileDefinitionError`](#profiledefinitionerror) ###### Parameters ###### message `string` Actionable description of what made the options invalid. ###### profileName? `string` The offending profile's `name`, when known. ###### Returns [`ProfileDefinitionError`](#profiledefinitionerror) ###### Overrides `Error.constructor` #### Properties ##### profileName? > `readonly` `optional` **profileName?**: `string` The offending profile's `name` when known (`undefined` when the failure is the name itself). Lets callers attribute a composed-lineage failure to the specific profile that introduced it. *** ### Sequence One SQ (Sequence) element's structural body. `length` is the on-wire length: a real byte count for defined-length SQ, or `0xFFFFFFFF` (4_294_967_295) when undefined-length. #### Example ```ts import { Sequence } from "@cosyte/dicom"; // The parser constructs sequences as follows: // const sq = new Sequence([item0, item1], 0xFFFFFFFF); ``` #### Constructors ##### Constructor > **new Sequence**(`items`, `length`): [`Sequence`](#sequence) **`Internal`** Construct a new structural `Sequence`. The `items` array is frozen at the constructor boundary so downstream mutation cannot escape. ###### Parameters ###### items readonly [`Item`](#item)[] ###### length `number` ###### Returns [`Sequence`](#sequence) #### Properties ##### items > `readonly` **items**: readonly [`Item`](#item)[] ##### length > `readonly` **length**: `number` ## Interfaces ### CodedConcept A coded triplet (PS3.16 §8, Table 8-1): `Code Value (0008,0100)`, `Coding Scheme Designator (0008,0102)`, `Code Meaning (0008,0104)`. The canonical scheme OID is resolved only for the four standard designators; legacy SNOMED designators (`SRT`/`SNM3`/`99SDM`) deliberately resolve to `undefined` because their code values differ from `SCT` (CP-730). #### Example ```ts import type { CodedConcept } from "@cosyte/dicom"; const c: CodedConcept = { codeValue: "C-B1003", codingSchemeDesignator: "SCT", codeMeaning: "Hounsfield unit", schemeUid: "2.16.840.1.113883.6.96", }; ``` #### Properties ##### codeMeaning? > `readonly` `optional` **codeMeaning?**: `string` ##### codeValue? > `readonly` `optional` **codeValue?**: `string` ##### codingSchemeDesignator? > `readonly` `optional` **codingSchemeDesignator?**: `string` ##### schemeUid? > `readonly` `optional` **schemeUid?**: `string` *** ### DateParts The calendar components a temporal value actually stated, and nothing else. Every value is a number, `month` is 1 to 12 (never the JS `Date` 0 to 11), and a component the value did not state is ABSENT rather than present and `undefined`: there is no `precision` key because the key set is the precision. No `raw`, no `valid` and no parse bookkeeping reaches it. The shape is the one `Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject` accept, which is why the names are singular where [DicomTime](#dicomtime) and [DicomDateTime](#dicomdatetime) spell them plurally. Delete `offsetMinutes` and either constructor takes the rest with no key rename and no value adjustment. That is a documented property rather than a tested one: proving it would mean taking a dependency, and this package takes none for it. #### Example ```ts import { parseDateTime, toObject } from "@cosyte/dicom"; import type { DateParts } from "@cosyte/dicom"; const parts: DateParts | undefined = toObject(parseDateTime("202401151330").value); Object.keys(parts ?? {}); // ["year", "month", "day", "hour", "minute"] ``` #### Properties ##### day? > `readonly` `optional` **day?**: `number` Day of month, 1 to 31. ##### hour? > `readonly` `optional` **hour?**: `number` Hour of day, 0 to 23. ##### millisecond? > `readonly` `optional` **millisecond?**: `number` The first three digits of the stated fraction, right-padded with zeroes. ##### minute? > `readonly` `optional` **minute?**: `number` Minute, 0 to 59. ##### month? > `readonly` `optional` **month?**: `number` Calendar month, 1 to 12. ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` Signed minutes east of UTC, present only when the value carried an offset. ##### second? > `readonly` `optional` **second?**: `number` Second, 0 to 59. `TM` and `DT` permit `60` for a leap second and the decoders keep it, but a value stating it is not projected: there is no ISO rendering of it a reader does not move, and no instant to build. See the module doc block. ##### year? > `readonly` `optional` **year?**: `number` Calendar year as written, so `0050` is the year 50 and never 1950. *** ### DefineProfileOptions Options accepted by [defineProfile](#defineprofile). Only `name` is required; every other field defaults to empty. With `exactOptionalPropertyTypes`, omit an unset key rather than passing `undefined`. #### Example ```ts import { defineProfile, WARNING_CODES } from "@cosyte/dicom"; const lenientCd = defineProfile({ name: "lenient-cd", description: "Tolerant of conformance-loose archive CDs", suppress: [WARNING_CODES.DICOM_ODD_LENGTH_VALUE_PADDED], }); ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` ##### escalate? > `readonly` `optional` **escalate?**: readonly [`WarningCode`](#warningcode)[] ##### extends? > `readonly` `optional` **extends?**: [`Profile`](#profile-2) \| readonly [`Profile`](#profile-2)[] ##### name > `readonly` **name**: `string` ##### privateTags? > `readonly` `optional` **privateTags?**: `Readonly`\<`Record`\<`string`, `Readonly`\<`Record`\<`string`, [`PrivateTagDefinition`](#privatetagdefinition)\>\>\>\> ##### suppress? > `readonly` `optional` **suppress?**: readonly [`WarningCode`](#warningcode)[] *** ### DeidentificationMethodCode One row of PS3.16 CID 7050 "De-identification Method": the three Basic Coded Entry Attributes (PS3.3 2026d Table 8.8-1a) of one code, exactly as the context group spells them. `deidentify()` writes each code it records as one Item of `(0012,0064)` De-identification Method Code Sequence carrying Code Value `(0008,0100)`, Coding Scheme Designator `(0008,0102)` and Code Meaning `(0008,0104)`, and nothing else. Coding Scheme Version is omitted because PS3.3 requires it only where the designator is not sufficient to identify the code, and `DCM` is. #### Example ```ts import { DEIDENTIFICATION_METHOD_CODES, type DeidentificationMethodCode } from "@cosyte/dicom"; const profile: DeidentificationMethodCode = DEIDENTIFICATION_METHOD_CODES.profile; profile.codeValue; // "113100" ``` #### Properties ##### codeMeaning > `readonly` **codeMeaning**: `string` Code Meaning `(0008,0104)`, a VR `LO` value, spelled as CID 7050 spells it. ##### codeValue > `readonly` **codeValue**: `string` Code Value `(0008,0100)`, a VR `SH` value. ##### codingSchemeDesignator > `readonly` **codingSchemeDesignator**: `"DCM"` Coding Scheme Designator `(0008,0102)`. Every CID 7050 row is `DCM`. *** ### DeidentifiedAttribute One audited attribute outcome. `tag`, `keyword`, `action`, `applied` and `repeatingGroup` carry **never** a decoded value: `keyword`, `action` and `repeatingGroup` come from the Part 6 and Annex E tables, and `tag` is bound to a tag those tables carry a row for, which is membership in a closed table rather than a shape test. **🩺 `contextPath` is NOT in that class and this docstring used to say it was.** It is a chain of tags read off the wire, bound by nothing - see [DeidentifiedAttribute.contextPath](#contextpath-1) and the note on [DeidentifyReport](#deidentifyreport). #### Example ```ts import { deidentify, parseDicom, type DeidentifiedAttribute } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); report.attributes.forEach((a: DeidentifiedAttribute) => { console.log(a.keyword, a.action, a.applied); // composed from tables - safe to log }); ``` #### Properties ##### action > `readonly` **action**: `"D"` \| `"Z"` \| `"X"` \| `"K"` \| `"C"` \| `"U"` The resolved single action after collapsing any conditional code. ##### applied > `readonly` **applied**: [`AppliedAction`](#appliedaction) ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag/index chain for an attribute inside a sequence; omitted at the root. **🩺 Each segment is `TAG[index]`, and the `TAG` half is read off the wire with no table behind it.** It is whatever tag the descent walked, so it is bound by neither a shape test nor membership in a closed one - which is what separates it from `tag`, `keyword`, `action` and `repeatingGroup`. On a file where an under-declared Value Length desynchronized the reader onto four bytes sitting **inside** somebody's value, those four bytes become a segment here. It is not the only identifier on this report read off the wire - [DeidentifyReport.removedPrivateTags](#removedprivatetags) and [UnauditableSequenceFinding.tag](#tag-6) are too - but those two are disclosed as such, and this one was documented as structural. Measured on a synthetic `LO` carrier holding `"MRS BRAIN SMITHSON"` that under-declares by four: the reader resynchronizes onto a fabricated `SQ` header, descends it, and the report reads `contextPath: ["53484E4F[0]"]` - `"HSON"` in wire order, recovered by writing the two halves back with `writeUInt16LE`. **No warning is raised and every finding array on the report is empty.** Change the surname and the published segment changes with it. `PRE-EXISTING`, on every release that has shipped the field. **🛑 IT IS NOT THE ONLY PLACE THOSE BYTES SURFACE, AND AN EARLIER DRAFT OF THIS NOTE SAID IT WAS. A GRADED PASS REFUTED THAT AND IT MUST NOT COME BACK.** On the same file the de-identified `Dataset` still carries the fabricated `(5348,4E4F)`, so `serializeDicom` writes its header back out in full - `"HSON"` included - inside an object stamped `(0012,0062) Patient Identity Removed = YES`. That re-emission belongs to the disclosed under-declare carrier class, not to this field, and neither is a bound on the other: **redacting `contextPath` from a log does not make the object safe to share.** **That fixture no longer reproduces either half.** `(5348,4E4F)` has no row in this build's PS3.6 registry and none in Table E.1-1, so `deidentify()` now removes the fabricated Sequence whole before it reads the VR, and neither this field nor the object carries `"HSON"` (see [UnregisteredElementRemoval](#unregisteredelementremoval)); that is what `test/integration/phi-diagnostic-surface.test.ts` pins now. It is a fact about that shape and **not a bound on this field**: a Sequence the run does descend still contributes whatever tag the wire gave it. It is published anyway, on the same footing as [DeidentifyReport.removedPrivateTags](#removedprivatetags): **where** an attribute sat is the whole audit value of the field, and withholding it would destroy that on every well-formed file in order to bound a malformed one. **Treat it as PHI when the source is untrusted.** ##### keyword > `readonly` **keyword**: `string` ##### repeatingGroup? > `readonly` `optional` **repeatingGroup?**: `string` Present when the action came from a Table E.1-1 row that names a repeating-group family rather than this single tag: the mask that matched, e.g. `"60xx4000"` for Overlay Comments in any overlay plane. `tag` is always the concrete tag that was in the file. Absent for every exact-tag row. ##### tag > `readonly` **tag**: `string` *** ### DeidentifyOptions Options controlling a de-identification run. All optional - the default is the Basic Application Level Confidentiality Profile with no Retain options. #### Example ```ts import { deidentify, parseDicom, type DeidentifyOptions } from "@cosyte/dicom"; const opts: DeidentifyOptions = { retain: ["RetainLongitudinalTemporal", "CleanDescriptors"] }; const { dataset } = deidentify(parseDicom(buf), opts); ``` #### Properties ##### deidentificationMethod? > `readonly` `optional` **deidentificationMethod?**: `string` Text **added to** `(0012,0063)` De-identification Method. Default names the Basic Profile and the active options. **It never changes `(0012,0064)`.** The CID 7050 codes the run writes there follow the options that ran, and free text cannot alter what ran; see `DEIDENTIFICATION_METHOD_CODES`. **The default is multi-valued, one Value per name, and that is a conformance fix rather than a style choice.** PS3.5 2026c Table 6.2-1 caps an `LO` at **64 characters per Value**, and `(0012,0063)` is `1-n`; the single-value default this replaced measured 76 characters with no options and 272 with every option name published at that time, so every run this library ever made wrote a value no `LO` may legally carry. The Profile name is now one Value of 61 characters and each active option is its own, so no option subset can exceed the maximum. **Your string is not bounded here, but it is no longer silent.** A value of your own longer than 64 characters is written through as given - it is yours, and splitting or truncating it would invent a record you did not write - and `report.warnings` carries `DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH` to say so. Split it on `` yourself if a strict receiver is in your path. The same code covers an over-long value the **source file** wrote and this run kept, which in the common case is this library's own 76-character text from a release before the default became multi-valued. The measurement is over **bytes**: a Value of 64 bytes or fewer can never carry more than 64 characters, so it cannot miss one that is genuinely over, but PS3.5 §6.2 specifies the bound in characters rather than bytes and excludes Code Extension escape sequences from the count, so it raises on any conformant Value whose bytes outnumber its counted characters. PS3.15 E.1.1 says this string is "inserted in or added to" the attribute, so a value the incoming Data Set already carried is kept and this one is appended after a `\` as a further value of the `1-n` attribute - the provenance chain, not a replacement. **This string is itself a `1-n` value**: it is split on `` and only the values not already recorded are added. **Trailing SPACE and NUL are padding, not content** (PS3.5 Table 6.2-1's `LO` row, which describes a **Value** - and `LO` is `1-n`). They are ignored when a value here is matched against one already recorded, **per value, on both sides**, and they are trimmed from the value written. That makes repeated de-identification a fixed point **from the first pass**, for every string: with or without a delimiter, and with a pad byte on any value, last or not. A string that is padding only records nothing. Leading spaces are yours and are written through untouched. One bound, and it is over the value that would be **written**, not over the join: when that value would exceed the largest Value Length an `LO` can encode, the prior value is **replaced** rather than added to - an element the serializer cannot encode would take the whole de-identified object down - and `report.warnings` carries `DICOM_DEIDENT_METHOD_NOT_ADDED`. That includes a prior value already at the ceiling which records this method already, where there is no join to exceed anything. A string longer than that ceiling on its own is not bounded here and will fail to serialize, exactly as it did before this option grew a join. `DICOM_DEIDENT_METHOD_NOT_ADDED` means "the length ceiling was reached", never "every fallback is disclosed": a `(0012,0063)` a file encoded under a VR other than `LO` is also replaced, and that one raises `DICOM_DEIDENT_METHOD_NOT_LO`. Two codes rather than one because the causes are unrelated - the chain outgrew the VR, or the bytes were never in that VR at all - and a prior value that is empty or padding only raises neither, because nothing was lost. When the prior value **is** kept, `report.warnings` carries `DICOM_DEIDENT_METHOD_PRIOR_RETAINED`: `(0012,0063)` is not in Table E.1-1, so nothing in the run inspected or redacted those bytes, and a name a sender wrote there is in output stamped `(0012,0062) = YES`. That code discloses the **retention** only; the **length** of what is written, from whatever source, is disclosed by `DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH` and by nothing else. ##### profile? > `readonly` `optional` **profile?**: [`Profile`](#profile-2) A [Profile](#profile-2) whose private-dictionary overlay names the known-safe private attributes to keep when `RetainSafePrivate` is active. **Not the only way an attribute is known safe, and no longer required for `RetainSafePrivate` to keep anything.** PS3.15 2026c §E.3.10 lists four ways that knowledge may be established and a profile is two of them (Conformance Statement documentation, and "some other means"); the first is the **file's own** Private Data Element Characteristics Sequence (0008,0300), which `deidentify()` reads when this Option is active whether or not a profile is passed. So a file from a vendor you hold no profile for keeps the private attributes **that file declares** non-identifying, and passing a profile only ever adds to that: a profile retention survives a block the declaration calls `UNSAFE` or says nothing about. 🩺 **What the declaration route retains rests on the SENDER's assertion.** The system that wrote the file said those blocks carry no identifying information; nothing here re-derives it, and an opaque value in a block declared `SAFE` reaches de-identified output unexamined. If you do not trust the sender, leave `RetainSafePrivate` off - §E.3.10's own alternative, and the only mitigation the standard offers. With neither this nor a declaration in the file, `RetainSafePrivate` keeps nothing (fail-safe). ##### retain? > `readonly` `optional` **retain?**: readonly [`DeidentifyOption`](#deidentifyoption)[] Annex E option sets to activate (Retain* / Clean*). Default: none. ##### uidMap? > `readonly` `optional` **uidMap?**: `Map`\<`string`, `string`\> A caller-owned source→replacement UID cache. Pass one shared map across a whole study/archive to make UID remapping consistent by construction even across separate calls (it is consistent anyway - the mapping is content- derived - but a shared map also makes repeats O(1)). ##### uidRoot? > `readonly` `optional` **uidRoot?**: `string` Root for generated UIDs (action `U`). Default `"2.25"`. *** ### DeidentifyReport The audit trail returned alongside the de-identified dataset. Most fields are composed from static tables: Part 6 keywords, Annex E action codes, the active option names. **Several fields are not, and they are named here rather than in a footnote.** 🛑 **Do not quote a COUNT of them, here or anywhere else.** The count read "one" and then "two" and then "three", and was wrong every time it was read, because each correction bumped the numeral without re-deriving the list. **The list below carried its own numerals until one had to be added, which is the same disease one step removed, so they are gone.** Treat it as the current reading of a surface that has grown, not as a proof of exhaustiveness. - **`uidMap`** - its keys are the source UIDs read out of the file, kept so a caller can make UID replacement consistent across a study or an archive. A Study or SOP Instance UID is a unique identifying number, so treat it as PHI. - **`removedPrivateTags`** - see the field's own note. On a *well-formed* file these are the sender's own private tag numbers and carry nothing; on a malformed one a tag can be four bytes of a value, and it is measured, not theoretical. - **`unauditableSequences[].tag`** - see [UnauditableSequenceFinding](#unauditablesequencefinding). Same shape as `removedPrivateTags` by a narrower route: a private carrier a [Profile](#profile-2) vouched for and this run emptied is named there, and an under-declared length upstream can resynchronize the reader onto four bytes that spell such a block. The package's usual answer to a fabricated header, `undefinedVrElements`, carries a byte offset and no tag, and still answers it whenever the fabricated VR is outside the 34 PS3.5 §6.2 defines. It cannot when the fabricated VR is one of them, because those two files are byte-identical. - **`unenumerablePrivateRemovals[].tag`** - see [UnenumerablePrivateRemoval](#unenumerableprivateremoval). The same four bytes by the same route, on the record of the removal rather than of an emptying, and this one is **uncapped** because it is the record of an action rather than a diagnostic. - **`undefinedVrElements[].byteLength` and `unauditableSequences[].byteLength`** - the declared Value Length read off the element header, so on a fabricated header it is four document bytes wearing a decimal: `"SO\0\0"` publishes `20307`, two letters of a surname, put back with one `readUInt32LE`. **They JOINED this list rather than always having been on it.** Through `0.0.14` the two `DICOM_DEIDENT_*_NOT_AUDITABLE` **messages** rendered the same number; binding it out of those factory signatures left these model fields as its only publisher, which is a smaller surface and not a closed one. Narrowing them is a product call rather than a defect fix: a bound empties the field on every well-formed file, where the number is exactly the audit information it exists to carry. - **`group0004Removals[].tag`** - see [Group0004Removal](#group0004removal). The same four bytes by the same route as `removedPrivateTags`, on the record of a removal PS3.15 §E.1.1 requires. On a well-formed file these are PS3.6's registered DICOMDIR directory-structuring tags and carry nothing. - **`fileMetaElementsDropped[].tag` and `.vr`** - see [FileMetaDroppedElement](#filemetadroppedelement). Two bytes rather than four: the File Meta pre-pass stops at the first non-`0002` group, so the group half of the tag is fixed and only the element number is free, and the VR is two more bytes the same desynchronized read would supply. `.byteLength` joins `undefinedVrElements[].byteLength` in the entry above by the same route. - **`contextPath`, on every finding that carries one** - see [DeidentifiedAttribute.contextPath](#contextpath-1), which holds the measurement. The segment tags come off the wire with no table behind them, so a fabricated `SQ` header the reader descended is named there, `PRE-EXISTING`, with **no warning and no finding array to correlate it with**. This is the field the rest of this docstring, the tolerance table and the troubleshooting guide all called structural. **It is a logging hazard and nothing more: on that same file the de-identified object itself re-emits the fabricated header, so redacting this field does not make the object safe.** `embeddedAttributes[].hidden` **left that list** in `DICOM-DIAGNOSTIC-PHI-RESIDUALS` - an entry is now a literal PS3.15 Table E.1-1 row, so it is not value-bearing. Its own disclosure had been reworded twice by then, and this repo deletes a disclosure at that point rather than writing a third; what is true of the field is stated once, on [EmbeddedAttributeFinding](#embeddedattributefinding). The field is **still uncapped**. So "the report is safe to log apart from `uidMap`" is **not** an accurate description of this type, and was corrected rather than kept convenient. **This is the only copy of that list.** Two others lived in module docstrings, still naming `hidden` after it left and never naming `contextPath` at all; a graded pass found them and they were deleted rather than resynced. #### Example ```ts import { deidentify, parseDicom, type DeidentifyReport } from "@cosyte/dicom"; const { report }: { report: DeidentifyReport } = deidentify(parseDicom(buf)); console.log(report.attributes.length, "attributes acted on"); console.log(report.warnings.map((w) => w.code)); // e.g. burned-in annotation ``` #### Properties ##### attributes > `readonly` **attributes**: readonly [`DeidentifiedAttribute`](#deidentifiedattribute)[] Per-attribute outcomes for every attribute Annex E acted on. ##### embeddedAttributes > `readonly` **embeddedAttributes**: readonly [`EmbeddedAttributeFinding`](#embeddedattributefinding)[] Values emptied because whole Data Elements were encoded inside them by an over-declared Value Length. Empty on a well-formed file; a non-empty array means the *source* was malformed in a way that hid attributes from the action table, so treat it as a data-quality alarm on the sender as well as an audit line. See [EmbeddedAttributeFinding](#embeddedattributefinding). ##### fileMetaElementsDropped > `readonly` **fileMetaElementsDropped**: readonly [`FileMetaDroppedElement`](#filemetadroppedelement)[] Non-modeled `(0002,xxxx)` File Meta elements this run **dropped** instead of re-emitting, because the de-identified File Meta group describes this de-identifying application rather than the source (PS3.15 §E.1.1). Empty when the source group held only modeled elements, and empty when the source parsed with no File Meta group at all. **This is the record of a deliberate fidelity loss, not of a defect.** The byte-for-byte File Meta round trip `FileMeta.extraElements` documents is scoped to parse-then-serialize and does not hold here. **Capped; the count beside it is not.** See [FileMetaDroppedElement](#filemetadroppedelement) and [DeidentifyReport.fileMetaElementsDroppedCount](#filemetaelementsdroppedcount). ##### fileMetaElementsDroppedCount > `readonly` **fileMetaElementsDroppedCount**: `number` How many non-modeled `(0002,xxxx)` elements this run dropped, **complete at any input size**. The field exists because its sibling array is capped: a caller that needs to *count* the fidelity loss cannot get the number from an array whose length saturates, and re-parsing the output does not give it either - the elements are gone from the output by design. One number cannot be inflated by an attacker-chosen element count, so bounding it would buy nothing. ##### group0004RemovalCount > `readonly` **group0004RemovalCount**: `number` How many `(0004,xxxx)` elements this run removed, **complete at any input size**, for the reason [DeidentifyReport.fileMetaElementsDroppedCount](#filemetaelementsdroppedcount) states. It is also the field that tells the two §E.1.1 rules apart at a glance: this one counts group-0004 removals, the other counts File Meta drops, and neither can be read off the other. ##### group0004Removals > `readonly` **group0004Removals**: readonly [`Group0004Removal`](#group0004removal)[] Data Elements whose Group Number is `0004`, **removed** at every depth this run reached, as PS3.15 §E.1.1 requires "from any SOP Instance or DICOM File other than a DICOMDIR File". Empty when the object carried none, and empty when it declared Media Storage SOP Class UID `1.2.840.10008.1.3.10` - the carve-out, which is disclosed on [DeidentifyReport.warnings](#warnings-2) as `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED` rather than left to be inferred from an empty array. **Capped; the count beside it is not.** See [Group0004Removal](#group0004removal) and [DeidentifyReport.group0004RemovalCount](#group0004removalcount). ##### removedPrivateTags > `readonly` **removedPrivateTags**: readonly `string`[] Private tags removed (kept ones are omitted) - **every one of them, whichever rule removed it**: the Basic Profile's default removal, a reservation the file did not settle, and, since the unenumerable class became a removal, a private attribute this run did not enumerate. Its meaning is unchanged and it is still uncapped; what it does not carry is a **reason**, which is why the unenumerable removals are also recorded on [DeidentifyReport.unenumerablePrivateRemovals](#unenumerableprivateremovals), where they can be told apart from the rest. **This is the second field that is not value-free, and the qualification is measured.** A tag here is composed from four bytes of the source, and on a file whose Value Lengths disagree with its bytes those four bytes can be document content rather than a tag the sender wrote: an `OB` carrier holding `"SECRET-NOTE-"` followed by a well-formed odd-group header reports `["41534342"]`, whose wire-order bytes read `"SABC"`. That reproduces identically on every release that has shipped this field. It is reported anyway, because *which* private tags were removed is the whole audit value of the field and withholding them would destroy it on every well-formed file to bound a malformed one. Narrowing it - to blocks a creator in the same Data Set actually reserved, say - is a product decision about audit value versus a four-byte echo, not a defect fix, and it has not been made. Treat this array as PHI when the source is untrusted. ##### retained > `readonly` **retained**: readonly [`DeidentifyOption`](#deidentifyoption)[] The Retain/Clean options that were active for this run. **Not a list of what survived.** Registered attributes Table E.1-1 does not list are kept without appearing anywhere in this field - `(0012,0063)` De-identification Method and the prior Items of `(0012,0064)` De-identification Method Code Sequence are the ones whose retention is disclosed, as `DICOM_DEIDENT_METHOD_PRIOR_RETAINED` and `DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED` on [DeidentifyReport.warnings](#warnings-2), because neither is an option set. ##### uidMap > `readonly` **uidMap**: `ReadonlyMap`\<`string`, `string`\> Source UID → replacement UID, for cross-file consistency. The **keys are document values**, not composed identifiers: this is the one field of the report that carries PHI. ##### unauditableSequences > `readonly` **unauditableSequences**: readonly [`UnauditableSequenceFinding`](#unauditablesequencefinding)[] Carriers whose nested Data Sets this run could not reach and **emptied** for it: an `SQ` whose items the parser did not materialize, so the run had no Data Sets to walk and could not discharge PS3.15 §E.1.1's obligation inside them, and a private carrier a [Profile](#profile-2) declares `SQ` that the parse tree resolved otherwise. Content was dropped from the de-identified output, and for the first producer the matching `DICOM_SQ_NOT_DESCENDED` entry on `Dataset.warnings` says why the parse refused. 🛑 **THIS ARRAY NO LONGER PRODUCES `applied: "kept"`, AND THAT IS AN AUDIT-CONTRACT CHANGE.** A private attribute retained under `RetainSafePrivate` whose value this run never enumerated used to be listed here, kept verbatim, with the array saying so. That instance is **removed** now and recorded on [DeidentifyReport.unenumerablePrivateRemovals](#unenumerableprivateremovals). An entry here has one meaning again: this content is **not** in your output. **Empty on a well-formed file**, including one whose private attributes a profile vouches for. It stopped being populated by ordinary conformant files when the retained class left it. **Capped, and the cap is on the record only.** A crafted input can carry tens of thousands of un-auditable elements, so this array (and its matching warnings) stops at `MAX_UNAUDITABLE_SEQUENCE_FINDINGS`. Every un-auditable carrier is still emptied whether or not it is listed; an array exactly that long means "at least this many", so read it as truncated rather than as a total. The unenumerable **removals** are budgeted apart from it and their record is not capped at all. **It covers private sequences too, since `DICOM-PRIVATE-SQ-CARVE-OUT`.** A private `SQ` a [Profile](#profile-2) vouches for under `RetainSafePrivate` used to be kept verbatim and never appear here; it is now emptied and listed on the same terms as any other, because the profile's licence under PS3.15 §E.3.10 is over a private attribute and not over an item stream nothing could read. ##### undefinedVrElements > `readonly` **undefinedVrElements**: readonly [`UndefinedVrFinding`](#undefinedvrfinding)[] Elements emptied because their on-wire VR is not one of the 34 PS3.5 §6.2 defines, so their bytes are not a Value Field this library decoded and PS3.15 §E.1.1's obligation over what is inside them could not be discharged. Empty on a file conformant to **PS3.5 2026c**: a sender that writes one of the 34 VRs that edition defines never produces one, and an Implicit VR LE file **cannot** - there the VR comes from the dictionary. The edition is not pedantry: §6.2 exists precisely to say how a *future* VR will be encoded, so a file conformant to a later edition using a newly defined VR is the population that sentence exists for. (What such a file does on this library's parse path is not summarized here - it was measured and the shapes disagree.) A non-empty array means the source desynchronized the reader, usually by under-declaring a Value Length somewhere earlier. See [UndefinedVrFinding](#undefinedvrfinding). **Capped, and the cap is on the record only**, exactly as [DeidentifyReport.unauditableSequences](#unauditablesequences) is: a crafted 1 MiB input can carry over a hundred thousand such elements, so this array and its matching warnings stop at `MAX_UNDEFINED_VR_FINDINGS`. Every one of them is still emptied; an array exactly that long means "at least this many". **A finding here names a byte offset, not a tag** - uniquely among the report's findings, and for a reason worth reading in [UndefinedVrFinding](#undefinedvrfinding): the tag of a fabricated header is itself part of some element's value. This list has **no carve-out**, and the reason is structural rather than a promise: `keepOrEmpty` is the **only** path that writes a source value into de-identified output unchanged, and the test sits at the top of it. Every other outcome - `X` remove, `Z`/`C` empty, `D` dummy, `U` remap, and a private tag the Basic Profile drops - already replaces the value. So a `RetainSafePrivate` element a [Profile](#profile-2) vouches for still reaches this test and is still emptied. Its sibling [DeidentifyReport.unauditableSequences](#unauditablesequences) had a real one until `DICOM-PRIVATE-SQ-CARVE-OUT`; neither has one now, and they arrive at that by different routes - this one because nothing bypasses `keepOrEmpty`, that one because a vouched-for private `SQ` is routed into the ordinary `SQ` branches. ##### unenumerablePrivateRemovals > `readonly` **unenumerablePrivateRemovals**: readonly [`UnenumerablePrivateRemoval`](#unenumerableprivateremoval)[] Private attributes **removed because this run did not enumerate their value** - one entry per instance, naming the tag, the outcome (`"removed"`), the reason (`"unenumerable"`) and the Data Set it lived in. **This is the surface the fail-safe's audit half is built on, and it is the one array on this report that is never capped or truncated.** A caller can separate, from this record alone and for every such removal at any input size, the attributes removed for being unenumerable from the attributes removed by the Annex E action table (`report.attributes`) and from the ones whose value was emptied. The matching warnings ARE bounded, so past their cap this record is the only complete account and the diagnostics are the bounded one. **Empty unless `RetainSafePrivate` plus a `Profile` is active**, because without both nothing private is retained far enough to be judged: the Basic Profile removes every private attribute and names it in [DeidentifyReport.removedPrivateTags](#removedprivatetags) instead. A profile lookup that **misses** is that case too, and never appears here. **The file's own (0008,0300) declaration does not populate it either**, and that is not an omission: a value that declaration covers is one §E.3.10 says the run knows about, so it is retained rather than reaching this rule at all. See [UnenumerablePrivateRemoval](#unenumerableprivateremoval) for what counts as enumeration, for the over-redaction this costs, and for why it names an instance rather than a tag. ##### unregisteredElementRemovalCount > `readonly` **unregisteredElementRemovalCount**: `number` How many elements this run removed under the rule [DeidentifyReport.unregisteredElementRemovals](#unregisteredelementremovals) records, **complete at any input size**, for the reason [DeidentifyReport.fileMetaElementsDroppedCount](#filemetaelementsdroppedcount) states. `0` on a run that removed none. ##### unregisteredElementRemovals > `readonly` **unregisteredElementRemovals**: readonly [`UnregisteredElementRemoval`](#unregisteredelementremoval)[] Non-private Data Elements **removed** at every depth this run reached because this build's PS3.6 2026d registry does not carry their tag and Table E.1-1 does not list it. See [UnregisteredElementRemoval](#unregisteredelementremoval) for the rule, for what it costs on a newer-edition file, and for why an entry names a byte offset and no tag. Empty on a run that removed none. A separate record from [DeidentifyReport.attributes](#attributes), [DeidentifyReport.removedPrivateTags](#removedprivatetags), [DeidentifyReport.group0004Removals](#group0004removals) and [DeidentifyReport.undefinedVrElements](#undefinedvrelements): an element removed here is on none of those. **Capped; the count beside it is not.** See [DeidentifyReport.unregisteredElementRemovalCount](#unregisteredelementremovalcount). ##### warnings > `readonly` **warnings**: readonly [`DicomParseWarning`](#dicomparsewarning)[] Safety warnings - notably burned-in-pixel annotation that cannot be cleaned. *** ### DeidentifyResult The result of [deidentify](#deidentify): a new dataset plus its audit report. #### Example ```ts import { deidentify, parseDicom, serializeDicom, type DeidentifyResult } from "@cosyte/dicom"; const { dataset, report }: DeidentifyResult> = deidentify(parseDicom(buf)); const safe = serializeDicom(dataset); // input dataset is never mutated void report; ``` #### Type Parameters ##### TDataset `TDataset` #### Properties ##### dataset > `readonly` **dataset**: `TDataset` ##### report > `readonly` **report**: [`DeidentifyReport`](#deidentifyreport) *** ### DicomDate A tolerantly-decoded `DA` (Date) value. `raw` always carries the on-wire string (PHI-safe - a date is not an identifier on its own, but is preserved verbatim regardless). `valid` is `true` only when the string parsed cleanly to a `YYYYMMDD` calendar date; otherwise the numeric fields are omitted and `raw` is the source of truth. #### Example ```ts import type { DicomDate } from "@cosyte/dicom"; const d: DicomDate = { raw: "20240115", valid: true, year: 2024, month: 1, day: 15 }; ``` #### Properties ##### day? > `readonly` `optional` **day?**: `number` ##### month? > `readonly` `optional` **month?**: `number` ##### raw > `readonly` **raw**: `string` ##### valid > `readonly` **valid**: `boolean` ##### year? > `readonly` `optional` **year?**: `number` *** ### DicomDateTime A tolerantly-decoded `DT` (DateTime) value (PS3.5 §6.2). `offsetMinutes` is the signed UTC offset (`&ZZXX` suffix) in minutes when present. #### Example ```ts import type { DicomDateTime } from "@cosyte/dicom"; const dt: DicomDateTime = { raw: "20240115133015", valid: true, year: 2024, month: 1, day: 15, hours: 13, minutes: 30, seconds: 15 }; ``` #### Properties ##### day? > `readonly` `optional` **day?**: `number` ##### fractionalSeconds? > `readonly` `optional` **fractionalSeconds?**: `number` ##### hours? > `readonly` `optional` **hours?**: `number` ##### minutes? > `readonly` `optional` **minutes?**: `number` ##### month? > `readonly` `optional` **month?**: `number` ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` ##### raw > `readonly` **raw**: `string` ##### seconds? > `readonly` `optional` **seconds?**: `number` ##### valid > `readonly` **valid**: `boolean` ##### year? > `readonly` `optional` **year?**: `number` *** ### DicomDirectory The Directory Record tree of a DICOMDIR, read from its offsets. `records` is every Item of the Directory Record Sequence, in order, whether or not an offset reaches it. `root` is the root directory entity: the record `(0004,1200)` names, then each `(0004,1400)` successor in order. A record is reached at most once: an offset that would reach one again is not followed. #### Example ```ts import { parseDicom, type DirectoryRecord } from "@cosyte/dicom"; const dir = parseDicom(buf).directory; // undefined unless the object is a DICOMDIR const walk = (records: readonly DirectoryRecord[], depth = 0): void => { for (const r of records) { console.log(" ".repeat(depth), r.type, r.referencedFileId?.join("/")); walk(r.lowerLevel, depth + 1); } }; if (dir !== undefined) walk(dir.root); ``` #### Properties ##### records > `readonly` **records**: readonly [`DirectoryRecord`](#directoryrecord)[] Every Directory Record, in Directory Record Sequence order. ##### root > `readonly` **root**: readonly [`DirectoryRecord`](#directoryrecord)[] The root directory entity, in `(0004,1400)` order. *** ### DicomParseWarning Data shape for every Tier-2 warning emitted by the parser. Warnings are plain data (distinct from `DicomParseError`, which is a thrown `Error` subclass) so they can be safely accumulated into `Dataset.warnings` and passed to `onWarning` callbacks. There is intentionally NO `snippet` field on warnings: real-world files routinely produce 50+ warnings and a per-warning snippet would balloon retained memory. Snippets appear only on `DicomParseError` (the strict-mode escalation path). #### Example ```ts import type { DicomParseWarning } from "@cosyte/dicom"; const w: DicomParseWarning = { code: "DICOM_MISSING_PREAMBLE", message: "No DICM magic at offset 128.", position: { byteOffset: 0 }, }; ``` #### Properties ##### code > `readonly` **code**: [`WarningCode`](#warningcode) ##### message > `readonly` **message**: `string` ##### position > `readonly` **position**: [`DicomPosition`](#dicomposition) *** ### DicomPosition Positional context for a `DicomParseWarning` or `DicomParseError`. **🛑 `byteOffset` IS NOT ALWAYS RELATIVE TO THE SOURCE BUFFER, AND THIS JSDOC SAID IT WAS.** For the Deflated Explicit VR LE transfer syntax, `deflated: true` says the offset indexes the inflated dataset buffer rather than the on-disk source. That flag is the only frame this type carries, and it is not the only frame this parser has: a defined-length Sequence Item is parsed from a **slice**, so a warning raised inside one carries an item-relative offset with nothing on the position to say so. The same is true of `Element.byteOffset`, and has been since the parser was written. **The residual is `PRE-EXISTING` and is not closed here.** What is closed is the thrown side: `DicomParseError.offsetFrame` names the coordinate system from a closed set (see `OFFSET_FRAMES`), and that covers a Tier-3 fatal and the `{ strict: true }` escalation of a Tier-2 warning. It does not reach a warning on the lenient path, which is this type. Do not read the fatal's frame contract as one this type has. With `exactOptionalPropertyTypes: true`, callers should omit unset keys rather than passing `undefined` (mirrors `@cosyte/hl7` sibling discipline). #### Example ```ts import type { DicomPosition } from "@cosyte/dicom"; const p: DicomPosition = { byteOffset: 132, fileMeta: true }; ``` #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag chain for nested SQ items, e.g. `["0040A730", "0", "00080100"]`. Omit when at root. ##### deflated? > `readonly` `optional` **deflated?**: `boolean` True when offset is into the inflated dataset buffer (Deflated TS only). Omit when not applicable. ##### fileMeta? > `readonly` `optional` **fileMeta?**: `boolean` True when offset is inside the File Meta group. Omit (do not pass `undefined`) when not applicable. *** ### DicomTime A tolerantly-decoded `TM` (Time) value (PS3.5 §6.2; max length 14 bytes). `fractionalSeconds` is the value after the decimal point as a number in `[0,1)` when present. #### Example ```ts import type { DicomTime } from "@cosyte/dicom"; const t: DicomTime = { raw: "133015.250000", valid: true, hours: 13, minutes: 30, seconds: 15, fractionalSeconds: 0.25 }; ``` #### Properties ##### fractionalSeconds? > `readonly` `optional` **fractionalSeconds?**: `number` ##### hours? > `readonly` `optional` **hours?**: `number` ##### minutes? > `readonly` `optional` **minutes?**: `number` ##### raw > `readonly` **raw**: `string` ##### seconds? > `readonly` `optional` **seconds?**: `number` ##### valid > `readonly` **valid**: `boolean` *** ### DirectoryRecord One Directory Record of a DICOMDIR: an Item of the Directory Record Sequence `(0004,1220)`, with the records its offsets name. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const dir = parseDicom(buf).directory; for (const patient of dir?.root ?? []) { patient.type; // "PATIENT" for (const study of patient.lowerLevel) study.item.get("0020000D"); // Study Instance UID } ``` #### Properties ##### index > `readonly` **index**: `number` The record's 0-based index in the Directory Record Sequence. ##### item > `readonly` **item**: [`Item`](#item) The record's own Data Set, for its record keys. ##### lowerLevel > `readonly` **lowerLevel**: readonly [`DirectoryRecord`](#directoryrecord)[] The records of this record's lower-level directory entity: the one its `(0004,1420)` names, then each `(0004,1400)` successor in order. Empty when the offset is zero or names no record. ##### referencedFileId > `readonly` **referencedFileId**: readonly `string`[] \| `undefined` The Referenced File ID `(0004,1500)`: its components in order (a path relative to the File-set root, one component per value), as the file wrote them less only the padding PS3.5 makes insignificant in `CS` (the trailing pad, and each value's leading and trailing spaces). Kept verbatim: never de-identified, rewritten or joined, and the element's own bytes on `item` are untouched. `undefined` when the record carries none. ##### type > `readonly` **type**: `string` \| `undefined` The Directory Record Type `(0004,1430)` value (`"PATIENT"`, `"STUDY"`, `"SERIES"`, `"IMAGE"` and the rest PS3.3 F.5 defines), or `undefined` when the record carries none. *** ### EmbeddedAttributeFinding One value that was emptied because a Data Element was found **inside** it. PS3.5 defines Value Length as the length of that element's own Value Field. A sender that over-declares it produces a file whose reading is self-consistent and whose next element has been absorbed into the previous one's value - and Table E.1-1 is keyed by tag, so an absorbed `(0010,0020)` is not an attribute any longer and no action fires on it. `deidentify` therefore refuses to keep a value whose tail decodes as whole Data Elements it would have acted on (PS3.15 §E.1 "all instances"; §E.3.5 is the standard's own precedent for removing identifying information embedded inside a string attribute). `tag` and `vr` are the carrier's own and are structural. **`hidden` was not, and it is bound now.** Every tag in an embedded run is composed from four bytes that were sitting *inside* the carrier's value - that is the whole reason this type exists - and a run needs only ONE actionable attribute to be reported, so through `0.0.13` the rest of the run was listed beside it. Measured: a `CS` carrier over-declaring across a fabricated `"SMIT"` header beside a genuine `(0010,0020)` reported `hidden: ["4D535449", "00100020"]`, and `4D535449` is `"SMIT"` in wire order. **An entry is now one of the 653 literal rows of PS3.15 Table E.1-1 that this run's options left actionable.** That is a **membership** bound rather than a shape one - the posture this package already takes for a VR and for a Private Creator - so what survives names a published table entry rather than a document byte, the same trade rendering a VR makes with the 34. **A repeating-group mask hit is NOT in that set and is excluded**: `(50xx,xxxx)` Curve Data leaves the whole 16-bit element number free, so a mask match proves a rule exists without making the membership finite. A graded pass caught a draft of this filter that admitted it. **🛑 THAT IS NOT AN ALL-CLEAR OVER THIS TYPE.** `contextPath` below is unbound and unchanged, `hidden` is uncapped, and [DeidentifyReport](#deidentifyreport) names the report's other value-bearing fields. A `DeidentifyReport` is still not safe to log whole. #### Example ```ts import { deidentify, parseDicom } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); for (const e of report.embeddedAttributes) { console.warn(`${e.tag} hid ${e.hidden.join(", ")} in its value`); } ``` #### Properties ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag/index chain when the carrier is inside a sequence item; omitted at the root. **Built by the same descent as [DeidentifiedAttribute.contextPath](#contextpath-1) and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value.** Read that field's note before logging this one. ##### hidden > `readonly` **hidden**: readonly `string`[] The tags found inside the carrier's value **that this run acts on and that a published table names**, in wire order. **Not every tag in the run** - see this type's own remarks. **🩺 IT MAY BE EMPTY, AND EMPTY DOES NOT MEAN "NOTHING WAS HIDDEN HERE".** A run whose only actionable members are private attributes, Curve Data or Overlay elements reports a finding with no tags: the carrier was still emptied, and the accompanying `DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED` warning still counts the whole run. The presence of the finding is the fact; this list is the part of it that can be named. ##### tag > `readonly` **tag**: `string` The carrier - the element whose over-declared value held the others. ##### vr > `readonly` **vr**: [`VR`](https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md#vr-1) The carrier's VR. Always one of the string VRs; binary VRs are not scanned. *** ### FileMeta The Part-10 File Meta Information group, projected as a typed view over `(0002,xxxx)` elements parsed during the File Meta pre-pass. Only `transferSyntaxUID` is required because it is the dispatch input for the transfer-syntax parsers; everything else is optional because real-world clinical files routinely omit one or more Type-1 elements. `validate()` adds opinion-bearing checks for those missing elements. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); if (ds.fileMeta !== undefined) { const ts = ds.fileMeta.transferSyntaxUID; // always present when fileMeta is defined } ``` #### Properties ##### extraElements? > `readonly` `optional` **extraElements?**: readonly [`FileMetaRawElement`](#filemetarawelement)[] Any `(0002,xxxx)` elements the source carried that the typed fields above do not model, preserved in tag order so a round-trip re-emits the File Meta group byte-for-byte. Omitted when the group held only modeled elements. ## 🛑 The byte-for-byte round trip does NOT hold for `deidentify()` output That promise is about **parse then serialize**, and it is scoped to it deliberately rather than by omission. `deidentify()` rebuilds this group to describe the de-identifying application instead of the source (PS3.15 §E.1.1: the File Meta Information "shall be replaced with a description of the de-identifying application", because otherwise "identity information may leak through unmodified File Meta Information ... includ[ing] information regarding Application Entity Titles, Presentation Addresses, implementation information, and private information"). So on the de-identify path this array is **dropped in full** - not filtered, not sampled - and `sourceApplicationEntityTitle` goes with it, while `implementationClassUID` and `implementationVersionName` are replaced with this library's own. The elements that survive are the ones that identify the **object** rather than its sender: `fileMetaInformationVersion`, `mediaStorageSOPClassUID`, `transferSyntaxUID`, and `mediaStorageSOPInstanceUID` (remapped unless `RetainUIDs` is active). What was dropped is counted on the report `deidentify()` returns, so a caller learns the fidelity loss from the run rather than by diffing the bytes. A caller that needs the source group verbatim must read it off the **parsed** dataset; it is not recoverable from de-identified output, and it is not meant to be. ##### fileMetaInformationVersion? > `readonly` `optional` **fileMetaInformationVersion?**: `Buffer`\<`ArrayBufferLike`\> ##### implementationClassUID? > `readonly` `optional` **implementationClassUID?**: `string` ##### implementationVersionName? > `readonly` `optional` **implementationVersionName?**: `string` ##### mediaStorageSOPClassUID? > `readonly` `optional` **mediaStorageSOPClassUID?**: `string` ##### mediaStorageSOPInstanceUID? > `readonly` `optional` **mediaStorageSOPInstanceUID?**: `string` ##### sourceApplicationEntityTitle? > `readonly` `optional` **sourceApplicationEntityTitle?**: `string` ##### transferSyntaxUID > `readonly` **transferSyntaxUID**: `string` *** ### FileMetaDroppedElement One non-modeled `(0002,xxxx)` File Meta element `deidentify()` **dropped** rather than re-emitting into de-identified output. ## Why the drop is unconditional PS3.15 2026c §E.1.1 requires the File Meta Information of a de-identified DICOM File to "be replaced with a description of the de-identifying application", because otherwise "identity information may leak through unmodified File Meta Information ... [t]his includes information regarding Application Entity Titles, Presentation Addresses, implementation information, and private information". A non-modeled `(0002,xxxx)` element is by definition one this library cannot describe: `(0002,0017)` Sending AE Title, `(0002,0018)` Receiving AE Title, `(0002,0100)` Private Information Creator UID and `(0002,0102)` Private Information are all in that class, and so is anything a vendor invented. The fail-safe direction is to drop what cannot be described, so no Annex E Option and no parse quality changes the outcome. ## Capped record, complete count The array is bounded per run (`MAX_FILE_META_DROP_FINDINGS`) because the number of elements a crafted File Meta group can carry is chosen by the input. The **action** is not bounded: every such element is dropped whether or not it is listed, and [DeidentifyReport.fileMetaElementsDroppedCount](#filemetaelementsdroppedcount) is the complete total at any input size. An array exactly the cap's length means "at least this many"; read the count, not the length. ## What it carries `tag` and `vr` come off the wire. The File Meta pre-pass stops at the first element whose group is not `0002`, so a tag reaching this record always has `0002` as its group, but its element number is two bytes the file supplied and a File Meta element that over-declared its length can desynchronize the pre-pass onto two bytes of somebody's value. Same standing exception as [DeidentifyReport.removedPrivateTags](#removedprivatetags), by a narrower route and with two bytes rather than four. `byteLength` is the length of the value that was dropped, read off that same header. It is published because *what* was dropped is the whole audit value of a record of a deliberate fidelity loss. #### Example ```ts import { deidentify, parseDicom, type FileMetaDroppedElement } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); report.fileMetaElementsDropped.forEach((d: FileMetaDroppedElement) => { console.warn(`${d.tag} ${d.vr}: ${String(d.byteLength)} bytes not re-emitted`); }); ``` #### Properties ##### byteLength > `readonly` **byteLength**: `number` Byte length of the value that is not in the de-identified output. ##### tag > `readonly` **tag**: `string` The dropped element's `(0002,xxxx)` tag. ##### vr > `readonly` **vr**: [`VR`](https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md#vr-1) The VR the source file wrote for it (the File Meta group is Explicit VR LE). *** ### FileMetaRawElement A non-modeled `(0002,xxxx)` File Meta element, preserved verbatim so the serializer can re-emit an exotic File Meta group byte-for-byte. The typed [FileMeta](#filemeta-3) fields cover the common Type-1/Type-3 elements; anything else a source file carried - e.g. `(0002,0017)`/`(0002,0018)` Sending/Receiving AE Title, `(0002,0100)` Private Information Creator UID, `(0002,0102)` Private Information - is captured here as raw bytes (the on-wire value, even-length per PS3.5 §6.2) rather than dropped. `value` is a defensive copy, so the view never aliases the parsed input buffer. 🛑 **THE ROUND TRIP IS PARSE-THEN-SERIALIZE ONLY. `deidentify()` DROPS EVERY ELEMENT THIS TYPE DESCRIBES**, and the exact examples above are why: a Sending AE Title and a Private Information pair are the identity information PS3.15 §E.1.1 names. See [FileMeta.extraElements](#extraelements) for the whole carve-out. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); // Anything the typed FileMeta fields do not model, in ascending tag order. for (const raw of ds.fileMeta?.extraElements ?? []) { raw.tag; // e.g. "00020017" Sending Application Entity Title raw.vr; // the VR the source wrote (File Meta is always Explicit VR LE) raw.value.length; // even, per PS3.5 2026c section 7.1.1 } ``` `(0002,0016)` Source Application Entity Title is deliberately NOT the example: it is one of the typed fields, so it never reaches this array. #### Properties ##### tag > `readonly` **tag**: `string` 8-char uppercase hex tag, e.g. `"00020100"`. ##### value > `readonly` **value**: `Buffer` The raw on-wire value bytes (even-length), copied out of the input. ##### vr > `readonly` **vr**: [`VR`](https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md#vr-1) The element's Value Representation as read under Explicit VR LE. *** ### FrameFunctionalGroups The five Enhanced multi-frame functional-group macros, resolved for a single frame Per-Frame-else-Shared (PS3.3 §C.7.6.16). Each macro is typed-absent when present in neither the per-frame nor the shared group (the three *geometry* macros are treated as required for an enhanced object - see [ImageView.frame](#frame)). #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const f = parseDicom(enhancedBuf).image.frame(0); f.planePosition?.imagePositionPatient; // this frame's [x,y,z] f.pixelMeasures?.pixelSpacing; // this frame's [row,col] mm ``` #### Properties ##### frameVoiLut? > `readonly` `optional` **frameVoiLut?**: `object` ###### windowCenter? > `readonly` `optional` **windowCenter?**: readonly (`number` \| `null`)[] ###### windowWidth? > `readonly` `optional` **windowWidth?**: readonly (`number` \| `null`)[] ##### index > `readonly` **index**: `number` ##### pixelMeasures? > `readonly` `optional` **pixelMeasures?**: `object` ###### pixelSpacing? > `readonly` `optional` **pixelSpacing?**: readonly (`number` \| `null`)[] ###### sliceThickness? > `readonly` `optional` **sliceThickness?**: `number` ###### spacingBetweenSlices? > `readonly` `optional` **spacingBetweenSlices?**: `number` ##### pixelValueTransformation? > `readonly` `optional` **pixelValueTransformation?**: `object` ###### rescaleIntercept? > `readonly` `optional` **rescaleIntercept?**: `number` ###### rescaleSlope? > `readonly` `optional` **rescaleSlope?**: `number` ###### rescaleType? > `readonly` `optional` **rescaleType?**: `string` ##### planeOrientation? > `readonly` `optional` **planeOrientation?**: `object` ###### imageOrientationPatient? > `readonly` `optional` **imageOrientationPatient?**: readonly (`number` \| `null`)[] ##### planePosition? > `readonly` `optional` **planePosition?**: `object` ###### imagePositionPatient? > `readonly` `optional` **imagePositionPatient?**: readonly (`number` \| `null`)[] *** ### Group0004Removal One Data Element whose Group Number is `0004`, **removed** from de-identified output. ## The rule, and the one object it does not apply to PS3.15 2026c §E.1.1: "All Data Elements with a Group Number of 0004 shall be removed from any SOP Instance or DICOM File other than a DICOMDIR File." The rule is unconditional over the Annex E Options - it sits in the list §E.1.1 imposes on any de-identifier claim and no Option qualifies it - so no `retain` argument brings a `(0004,xxxx)` element back. The DICOMDIR carve-out is decided once per run from the source File Meta's `(0002,0002)` Media Storage SOP Class UID, and only the value `1.2.840.10008.1.3.10` (Media Storage Directory Storage) selects it. An object that declares no Media Storage SOP Class UID at all is therefore **not** a DICOMDIR and the removal applies to it. When the carve-out does fire, this array is empty and `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED` says what else the run did not do. ## Every depth, and the removal decides nothing below it The test runs on each element of each Data Set the run reaches, root and Sequence Item alike, so a `(0004,xxxx)` planted inside an item is removed there too. It is a test on **one element's tag**: it never stands in for a decision about the Data Sets nested inside some other element, which is the shape `DICOM-PRIVATE-SQ-CARVE-OUT` was opened for. Removing a `(0004,xxxx)` that is itself a Sequence removes its items with it, exactly as a Table E.1-1 `X` action does. ## Capped record, complete count Bounded per run at `MAX_GROUP_0004_FINDINGS`, on its own counter so a flood of one diagnostic class cannot spend another's budget. The removal itself is not bounded, and [DeidentifyReport.group0004RemovalCount](#group0004removalcount) is complete at any input size. ## What it carries `tag` is composed from four bytes of the source and `contextPath` from four more per segment - the standing exception [DeidentifyReport.removedPrivateTags](#removedprivatetags) and [UnauditableSequenceFinding](#unauditablesequencefinding) already carry, by the same route. On a well-formed file these are the DICOMDIR directory-structuring tags PS3.6 registers and carry nothing. #### Example ```ts import { deidentify, parseDicom, type Group0004Removal } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); report.group0004Removals.forEach((r: Group0004Removal) => { console.warn(`${r.tag} removed`, r.contextPath ?? "root"); }); ``` #### Properties ##### applied > `readonly` **applied**: `"removed"` The outcome, always `"removed"`: no element bearing that tag is in the Data Set that held it, in the returned dataset or in the serialized bytes. ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag/index chain when the element was inside a sequence item; omitted at the root. **Built by the same descent as [DeidentifiedAttribute.contextPath](#contextpath-1) and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value.** Read that field's note before logging this one. ##### tag > `readonly` **tag**: `string` The removed element's tag. Its group is always `0004`. *** ### ImageView Pixel-interpretation + geometry metadata - the "wrong pixels look fine" and "looks fine, measures wrong" classes. v1 does not decode pixels; this view surfaces exactly what a renderer needs so it does not have to guess. Safety-critical omissions are intentional: `rescaleSlope` is absent (not `1`) when the tag is absent; `signed` is absent (not a guess) unless `(0028,0103)` was present; `photometricInterpretation` is absent (not `MONOCHROME2`) when absent; the three pixel-spacing fields are distinct. When `modalityLutSequence` / `voiLutSequence` are present they are authoritative over the linear `rescale*` / `window*` pairs. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const img = parseDicom(buf).image; img.rescaleSlope; // undefined ⇒ caller MUST NOT assume 1 img.signed; // undefined ⇒ signedness unknown, never guess img.pixelSpacing; // patient-plane mm - distinct from imagerPixelSpacing ``` #### Properties ##### bitsAllocated? > `readonly` `optional` **bitsAllocated?**: `number` ##### bitsStored? > `readonly` `optional` **bitsStored?**: `number` ##### columns? > `readonly` `optional` **columns?**: `number` ##### frameOfReferenceUid? > `readonly` `optional` **frameOfReferenceUid?**: `string` ##### highBit? > `readonly` `optional` **highBit?**: `number` ##### imageOrientationPatient? > `readonly` `optional` **imageOrientationPatient?**: readonly (`number` \| `null`)[] ##### imagePositionPatient? > `readonly` `optional` **imagePositionPatient?**: readonly (`number` \| `null`)[] ##### imagerPixelSpacing? > `readonly` `optional` **imagerPixelSpacing?**: readonly (`number` \| `null`)[] ##### isEnhancedMultiFrame > `readonly` **isEnhancedMultiFrame**: `boolean` `true` when this object carries Per-Frame/Shared Functional Groups. ##### modalityLutSequence? > `readonly` `optional` **modalityLutSequence?**: readonly [`Item`](#item)[] ##### nominalScannedPixelSpacing? > `readonly` `optional` **nominalScannedPixelSpacing?**: readonly (`number` \| `null`)[] ##### numberOfFrames? > `readonly` `optional` **numberOfFrames?**: `number` ##### photometricInterpretation? > `readonly` `optional` **photometricInterpretation?**: `string` ##### pixelRepresentation? > `readonly` `optional` **pixelRepresentation?**: `number` Raw `(0028,0103)` value: 0 = unsigned, 1 = signed. Absent ⇒ unknown. ##### pixelSpacing? > `readonly` `optional` **pixelSpacing?**: readonly (`number` \| `null`)[] ##### planarConfiguration? > `readonly` `optional` **planarConfiguration?**: `number` ##### realWorldValueMaps? > `readonly` `optional` **realWorldValueMaps?**: readonly [`RealWorldValueMap`](#realworldvaluemap)[] ##### rescaleIntercept? > `readonly` `optional` **rescaleIntercept?**: `number` ##### rescaleSlope? > `readonly` `optional` **rescaleSlope?**: `number` ##### rescaleType? > `readonly` `optional` **rescaleType?**: `string` ##### rows? > `readonly` `optional` **rows?**: `number` ##### samplesPerPixel? > `readonly` `optional` **samplesPerPixel?**: `number` ##### signed? > `readonly` `optional` **signed?**: `boolean` `true`/`false` only when `(0028,0103)` was 1/0; absent ⇒ never guessed. ##### sliceThickness? > `readonly` `optional` **sliceThickness?**: `number` ##### sopInstanceUid? > `readonly` `optional` **sopInstanceUid?**: `string` ##### spacingBetweenSlices? > `readonly` `optional` **spacingBetweenSlices?**: `number` ##### units? > `readonly` `optional` **units?**: `string` ##### voiLutSequence? > `readonly` `optional` **voiLutSequence?**: readonly [`Item`](#item)[] ##### windowCenter? > `readonly` `optional` **windowCenter?**: readonly (`number` \| `null`)[] ##### windowWidth? > `readonly` `optional` **windowWidth?**: readonly (`number` \| `null`)[] #### Methods ##### frame() > **frame**(`index`): [`FrameFunctionalGroups`](#framefunctionalgroups) Resolve the functional-group macros for frame `index` Per-Frame-else-Shared. Throws [DicomValueError](#dicomvalueerror) `FRAME_INDEX_OUT_OF_RANGE` for an index outside `[0, numberOfFrames)` and `MISSING_REQUIRED_FUNCTIONAL_GROUP` when an enhanced object lacks a required geometry macro in both groups. ###### Parameters ###### index `number` ###### Returns [`FrameFunctionalGroups`](#framefunctionalgroups) *** ### OtherPatientId One entry of Other Patient IDs Sequence `(0010,1002)` - a `{id, issuer, typeCode}` triple (PS3.3 §10.15, the DICOM analogue of an HL7 v2 CX repetition). Surfaced so a caller never matches on a bare `(0010,0020)`. #### Example ```ts import type { OtherPatientId } from "@cosyte/dicom"; const o: OtherPatientId = { id: "MRN-42", issuer: "HOSP_A", typeCode: "TEXT" }; ``` #### Properties ##### id? > `readonly` `optional` **id?**: `string` ##### issuer? > `readonly` `optional` **issuer?**: `string` ##### typeCode? > `readonly` `optional` **typeCode?**: `string` *** ### ParseOptions Options accepted by `parseDicom`. With `exactOptionalPropertyTypes: true`, callers omit unset keys rather than passing `undefined` for any field below. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf, { strict: false, stripPreamble: "tolerate", copyValues: false, onWarning: (w) => console.warn(w.code), }); ``` #### Properties ##### copyValues? > `readonly` `optional` **copyValues?**: `boolean` When `true`, every `Element.rawBytes` is `Buffer.from(slice)` - copying each value out so the source buffer can be released. When `false` (the default), `Element.rawBytes` is `Buffer.subarray(slice)` - a zero-copy view that pins the source ArrayBuffer until every Element is GC'd. Omit to use the default. ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) Synchronous callback invoked once per Tier-2 warning, after the warning has been pushed to `Dataset.warnings`. Throwing handlers are silently swallowed, for parser-state safety. Omit to skip the callback entirely. ##### profile? > `readonly` `optional` **profile?**: [`Profile`](#profile-2) Source/vendor tolerance preset. Applies the profile's `escalations` / `suppressions` to warning emission and its `privateDictionary` to Implicit-VR resolution of private data elements. A profile only tightens or annotates - it never makes the default lenient parse throw outside the four Tier-3 fatals, and a private creator the profile does not recognize degrades to generic UN handling plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning, never a wrong decode. Omit (do not pass `undefined`) for the unprofiled default behaviour. ##### strict? > `readonly` `optional` **strict?**: `boolean` When `true`, every Tier-2 warning is escalated to a thrown `DicomParseError` carrying the warning code. Default `false`. 🛑 **THE ESCALATED DIAGNOSTIC CARRIES SOURCE BYTES THAT THE WARNING DOES NOT.** A `DicomParseWarning.message` is a frozen registry string with only structural tokens filled in: `{tag}` renders only a tag PS3.6's element registry carries a literal row for, `{vr}` only one of the 34 VRs PS3.5 section 6.2 defines, and a raw length or byte value a header carries is bound out of the factory signature rather than rendered. That now holds on the `deidentify()` codes too: `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` and `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` rendered `Element.rawBytes.length` through `0.0.14`, which equals the declared Value Length and was reachable from a fabricated header, and both slots are gone. So is `DICOM_ITEM_CROSSES_SEQUENCE_END`'s remaining-bytes count, because **a raw number shifted by a constant the reader can compute is that raw number**: it was the enclosing sequence's declared Value Length less the bytes of that sequence already consumed. **The exceptions are named in one place that is not a record of a past change, and are deliberately not restated here** - the `WARNING_MESSAGES` docblock in `./warnings.ts`, which this JSDoc used to carry a copy of. **This is a statement about `w.message` and about nothing else**: the two byte counts still exist on `report.undefinedVrElements[].byteLength` and `report.unauditableSequences[].byteLength`, model fields on a type whose own docs say it is not a value-free surface. **No safe-to-log verdict is stated here** - that sentence was corrected twice and is deleted rather than tried a third time; the mechanism is above and the treatment is in the package's troubleshooting docs. The `DicomParseError` this option raises in its place is a different and larger surface: it also carries `snippet`, **16 raw bytes, unredacted**, read at the warning's own `byteOffset`. A message-only PHI review of the lenient path therefore does not transfer to the strict one. Log `err.code`, `err.byteOffset`, `err.offsetFrame` and `err.message`; treat `err.snippet` as PHI. **The snippet is cut in the SAME FRAME the `byteOffset` is counted in**, so it is the bytes at the offset the diagnostic names: file-absolute at the root, relative to the enclosing slice inside a defined-length Sequence or Item, and into the inflated stream under Deflated Explicit VR LE. It was not always: until `DICOM-FATAL-MESSAGE-REGISTRY` the offset moved with the frame while the cut was always taken from the whole file, so inside a defined-length Item the 16 bytes were **an unrelated element's** - a diagnostic disclosing data from a part of the document the reader was never asked about. That is closed. **What is NOT closed, and never was a defect: the bytes are still raw source bytes.** Reading them as safe because the message beside them is registry-bound is the mistake this whole paragraph exists to prevent. **`byteOffset` NOW CARRIES A FRAME-OF-REFERENCE CONTRACT, AND IT IS A NAME AND NOT AN ORIGIN.** `err.offsetFrame` says which of three coordinate systems the number is counted in (`OFFSET_FRAMES`), so a consumer can tell a root offset from an Item-relative one instead of guessing. **A nested offset is still not a key you can look up against the root**, and it is not made into one here: where a slice begins is deliberately unpublished, because the distance between two frames is a declared Value Length off the wire. The escalated warning's own `position` is unchanged and still carries no frame beyond `deflated` - see `DicomPosition`. Omit (do not pass `undefined`) to use the default. ##### stripPreamble? > `readonly` `optional` **stripPreamble?**: `"tolerate"` \| `"require"` Preamble policy: - `"tolerate"` (default): attempt to start at offset 0 if `DICM` magic is missing at offset 128; emit `DICOM_MISSING_PREAMBLE`. - `"require"`: throw `DicomParseError(NOT_DICOM_PART_10)` when no `DICM` magic is present. Omit to use the default. *** ### PatientView Patient & study identity (the wrong-patient failure class). `id` is **not globally unique**; correct cross-system matching needs the `{id, issuerOfId, issuerQualifiers}` tuple plus `otherIds`. `name` keeps its full `PN` component structure, never flattened to a string. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const p = parseDicom(buf).patient; p.id; // "MRN-42" - meaningless without the issuer p.issuerOfId; // "HOSP_A" p.name?.alphabetic.familyName; // "Doe" ``` #### Properties ##### birthDate? > `readonly` `optional` **birthDate?**: [`DicomDate`](#dicomdate) ##### id? > `readonly` `optional` **id?**: `string` ##### issuerOfId? > `readonly` `optional` **issuerOfId?**: `string` ##### issuerQualifiers? > `readonly` `optional` **issuerQualifiers?**: readonly [`Item`](#item)[] ##### name? > `readonly` `optional` **name?**: [`PersonName`](#personname) ##### otherIds > `readonly` **otherIds**: readonly [`OtherPatientId`](#otherpatientid)[] ##### sex? > `readonly` `optional` **sex?**: `string` *** ### PersonName A decoded `PN` value - up to three component groups separated on-wire by `=` (PS3.5 §6.2.1.1). `alphabetic` is always present; `ideographic` and `phonetic` are present only when the value supplied them. #### Example ```ts import type { PersonName } from "@cosyte/dicom"; // "Yamada^Tarou=山田^太郎=やまだ^たろう" declare const pn: PersonName; pn.alphabetic.familyName; // "Yamada" pn.ideographic?.familyName; // "山田" ``` #### Properties ##### alphabetic > `readonly` **alphabetic**: [`PersonNameGroup`](#personnamegroup) ##### ideographic? > `readonly` `optional` **ideographic?**: [`PersonNameGroup`](#personnamegroup) ##### phonetic? > `readonly` `optional` **phonetic?**: [`PersonNameGroup`](#personnamegroup) *** ### PersonNameGroup One of the three component groups of a `PN` value (PS3.5 §6.2.1.1): alphabetic, ideographic, or phonetic. Each holds the five `^`-delimited components; missing components are the empty string (never `undefined`). #### Example ```ts import type { PersonNameGroup } from "@cosyte/dicom"; const g: PersonNameGroup = { familyName: "Doe", givenName: "Jane", middleName: "", namePrefix: "", nameSuffix: "", }; ``` #### Properties ##### familyName > `readonly` **familyName**: `string` ##### givenName > `readonly` **givenName**: `string` ##### middleName > `readonly` **middleName**: `string` ##### namePrefix > `readonly` **namePrefix**: `string` ##### nameSuffix > `readonly` **nameSuffix**: `string` *** ### PixelDataFragments The Basic Offset Table and the fragments of one encapsulated Pixel Data element, each as the raw bytes of its Item Value. Every `Buffer` here is a view over the element's own `Element.rawBytes`, with no Item header and no Sequence Delimitation Item in it, so it follows that element's retention: a zero-copy view over your input by default, and a copy that outlives it under `parseDicom(bytes, { copyValues: true })`. #### Example ```ts import { parseDicom, readPixelDataFragments } from "@cosyte/dicom"; const pixels = readPixelDataFragments(parseDicom(buf)); if (pixels !== undefined) { pixels.basicOffsetTable?.length; // 0 when the table is empty pixels.fragments.length; // how many fragment Items the file carries } ``` #### Properties ##### basicOffsetTable > `readonly` **basicOffsetTable**: `Buffer`\<`ArrayBufferLike`\> \| `undefined` The Basic Offset Table's Item Value, uninterpreted. **Zero bytes when the table is empty**, which PS3.5 2026c section A.4 permits and decoders must accept. `undefined` only when the Item stream holds no Item at all, which A.4 does not permit; nothing is invented in its place. ##### fragments > `readonly` **fragments**: readonly `Buffer`\<`ArrayBufferLike`\>[] The Item Value of every Item after the Basic Offset Table, in file order. Each is returned whole: its content is never read, so bytes inside a fragment that happen to look like a delimiter are fragment bytes (A.4: a decoder "may not scan for a Sequence Delimitation Item"). *** ### PrivateTagDefinition One private-data attribute definition supplied by a [Profile](#profile-2)'s private-dictionary overlay. The `vr` resolves the Implicit-VR of a private data element whose on-wire encoding carries no VR; `keyword` / `name` carry the vendor-documented identity for tooling and docs. #### Example ```ts import type { PrivateTagDefinition } from "@cosyte/dicom"; const def: PrivateTagDefinition = { vr: "OB", keyword: "CSAImageHeaderInfo", name: "CSA Image Header Info", }; ``` #### Properties ##### keyword > `readonly` **keyword**: `string` ##### name > `readonly` **name**: `string` ##### vr > `readonly` **vr**: [`VR`](https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md#vr-1) *** ### Profile A source/vendor tolerance preset. A `Profile` bundles three things that only ever **tighten or annotate** a parse - never loosen it past the Postel's-Law default: - `escalations` - Tier-2 warning codes promoted to a thrown `DicomParseError` (a stricter posture for known-unsafe deviations). - `suppressions` - Tier-2 warning codes silenced because they are a documented, benign quirk of the named source (annotation, not loss). - `privateDictionary` - a private-creator-keyed overlay resolving the Implicit-VR of vendor private data elements via the file's **live** private-creator string (never a hard-coded block number). Build one with `defineProfile()`; never hand-author the frozen shape. Profiles are immutable and composable via `extends`. #### Example ```ts import { parseDicom, profiles } from "@cosyte/dicom"; const ds = parseDicom(buf, { profile: profiles.siemens }); console.log(ds.fileMeta?.transferSyntaxUID); ``` #### Properties ##### describe? > `readonly` `optional` **describe?**: () => `string` Render a human-readable, deterministic one-line summary of the profile. ###### Returns `string` ##### description? > `readonly` `optional` **description?**: `string` ##### escalations > `readonly` **escalations**: `ReadonlySet`\<[`WarningCode`](#warningcode)\> ##### lineage > `readonly` **lineage**: readonly `string`[] ##### name > `readonly` **name**: `string` ##### privateDictionary > `readonly` **privateDictionary**: `ReadonlyMap`\<`string`, `ReadonlyMap`\<`string`, [`PrivateTagDefinition`](#privatetagdefinition)\>\> Creator string → canonical private-tag key (`"GGGGxxEE"`, e.g. `"0029xx10"`) → definition. The `xx` placeholder stands for the file-assigned private block byte, mirroring the published DICOM private-dictionary notation; resolution is therefore by creator string, never by a fixed block number. ##### suppressions > `readonly` **suppressions**: `ReadonlySet`\<[`WarningCode`](#warningcode)\> *** ### RealWorldValueMap A Real World Value Mapping - slope/intercept bound atomically to its UCUM measurement-units code, so a number is never detached from its units. From Real World Value Mapping Sequence `(0040,9096)`. #### Example ```ts import type { RealWorldValueMap } from "@cosyte/dicom"; const m: RealWorldValueMap = { slope: 1, intercept: 0, unitsCode: { codeValue: "[hnsf'U]" } }; ``` #### Properties ##### intercept? > `readonly` `optional` **intercept?**: `number` ##### slope? > `readonly` `optional` **slope?**: `number` ##### unitsCode? > `readonly` `optional` **unitsCode?**: [`CodedConcept`](#codedconcept) *** ### SeriesView Series-level identity & co-registration. Images sharing a `frameOfReferenceUid` are spatially co-registered. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const s = parseDicom(buf).series; s.modality; // "CT" s.frameOfReferenceUid; // shared ⇒ co-registered ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` ##### frameOfReferenceUid? > `readonly` `optional` **frameOfReferenceUid?**: `string` ##### instanceUid? > `readonly` `optional` **instanceUid?**: `string` ##### modality? > `readonly` `optional` **modality?**: `string` ##### number? > `readonly` `optional` **number?**: `number` *** ### StudyView Study-level identity. `instanceUid` is the cross-system study key; `accessionNumber` ties the study to the order. #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const s = parseDicom(buf).study; s.instanceUid; // "1.2.840.113619..." s.accessionNumber; // "ACC123" ``` #### Properties ##### accessionNumber? > `readonly` `optional` **accessionNumber?**: `string` ##### date? > `readonly` `optional` **date?**: [`DicomDate`](#dicomdate) ##### description? > `readonly` `optional` **description?**: `string` ##### id? > `readonly` `optional` **id?**: `string` ##### instanceUid? > `readonly` `optional` **instanceUid?**: `string` ##### time? > `readonly` `optional` **time?**: [`DicomTime`](#dicomtime) *** ### ToDateOptions The one option [toDate](#todate) takes, and the only key it carries. `assumeOffsetMinutes` is the caller's declaration of the zone a value that carries none was written in. An explicit `0` means "read this naive value as UTC", which is a decision this library will not make on a caller's behalf. #### Example ```ts import { parseDate, toDate } from "@cosyte/dicom"; import type { ToDateOptions } from "@cosyte/dicom"; const options: ToDateOptions = { assumeOffsetMinutes: -300 }; toDate(parseDate("20240115").value, options)?.toISOString(); // "2024-01-15T05:00:00.000Z" ``` #### Properties ##### assumeOffsetMinutes? > `readonly` `optional` **assumeOffsetMinutes?**: `number` Minutes east of UTC to read a value that states no offset in. *** ### UidRemapper A UID remapper: a stable `map(src)` plus the backing cache it fills. #### Example ```ts import { makeUidRemapper, type UidRemapper } from "@cosyte/dicom"; const remap: UidRemapper = makeUidRemapper(); const replaced = remap.map("1.2.840.10008.5.1.4.1.1.2"); remap.cache.get("1.2.840.10008.5.1.4.1.1.2") === replaced; // true ``` #### Properties ##### cache > `readonly` **cache**: `Map`\<`string`, `string`\> The source→replacement cache, exposed for reporting / reuse. ##### map > `readonly` **map**: (`sourceUid`) => `string` Map one source UID to its deterministic replacement (cached). ###### Parameters ###### sourceUid `string` ###### Returns `string` *** ### UnauditableSequenceFinding One carrier this run could not reach the Data Sets inside, and **emptied** for it. [UnauditableSequenceFinding.applied](#applied-2) names that outcome and is the field to read first; it is the only outcome this finding has. **Two producers, and neither ships bytes.** The ordinary one is an `SQ` element whose `items` the parser never materialized. The second is a private data element retained under `RetainSafePrivate` whose `Profile` entry declares it `SQ` while the parse tree says otherwise - `UN` under Implicit VR LE when the profile was passed to `deidentify()` but not to `parseDicom`, or whatever binary VR the sender wrote under Explicit VR, which wins in the parser. The profile is the authority that retained the element, and it has said the value is a Sequence of Items; with no items on the tree, the §E.1.1 obligation below falls on the carrier just the same. Such an element keeps its parsed VR in the output and is emptied rather than re-typed to `SQ` (`DICOM-PRIVATE-SQ-PARSE-VR`). **🛑 THERE WAS A THIRD PRODUCER AND IT IS RETIRED: `applied: "kept"` IS GONE FROM THIS TYPE AND FROM EVERY RUN.** Through `0.0.19` a private data element the profile vouched for whose value this run never enumerated was **retained verbatim** and named here with `applied: "kept"` - the package reporting that it had shipped a value it did not read. Such an instance is now **removed** instead, and its record moved to [DeidentifyReport.unenumerablePrivateRemovals](#unenumerableprivateremovals), which is a different surface with a different guarantee: uncapped, complete, and stating a reason. This array never carries a retained private value again, so an entry here always means content is **not** in your output. That is an audit-contract change for anyone who switched on `applied`; see [UnenumerablePrivateRemoval](#unenumerableprivateremoval). PS3.5 2026c §7.5.1 "Item Encoding Rules" states that "Each Item Value shall contain a DICOM Data Set composed of Data Elements", so an `SQ` element's value is never opaque bytes - it is Data Elements this run is obliged to reach. PS3.15 2026c §E.1.1 "De-identifier" states that obligation directly: an implementation claiming the Basic Application Level Confidentiality Profile "shall protect or retain all instances of the Attributes listed in [Table E.1-1], whether contained in the top level Data Set or embedded in an Item of a Sequence of Items". When the item stream cannot be enumerated the obligation cannot be discharged element by element, so it falls on the enclosing attribute - the escalation §E.1.1 itself uses for a SOP Instance UID inside a Sequence, where "the enclosing Attribute in the top-level Data Set must be encrypted in its entirety". (That sentence is written about the encrypt-and-replace mechanism for SOP Instance UIDs, not about Table E.1-1 generally; it is cited here as the standard's own precedent for escalating to the carrier, not as a rule about this case.) Both fields are structural: `tag` is the carrier's and `byteLength` is the recorded span of the value that was dropped. **No _decoded_ value appears here, and that is not the same as "safe to log".** On the second producer `tag` can be four bytes of another element's value: a length under-declared upstream resynchronizes the reader mid-value, and if the bytes it lands on spell a private block this caller's profile declares `SQ`, followed by a VR that is one of the 34, the fabricated header is what you get here. The package's answer to that class is normally `report.undefinedVrElements`, which names a byte offset and **no tag**, and it still answers the case where the fabricated VR is outside the 34 - but it cannot answer this one, because a fabricated `OB` header and a genuine one are byte-identical. So this shares the standing exception `report.removedPrivateTags` and `uidMap` already have: a `DeidentifyReport` is **not** a value-free surface. Treat it as document content, at the sensitivity of the file it came from. For the first producer the parser announces the underlying refusal on `Dataset.warnings`: `DICOM_SQ_NOT_DESCENDED` for a defined-length Implicit VR LE value whose dictionary-resolved `SQ` was not a valid item stream. **Do not generalise that to the second.** There the file may be entirely conformant - an honest defined-length `OB` carrier raises nothing at all - so this report field, not `Dataset.warnings`, is where that drop is visible. #### Example ```ts import { deidentify, parseDicom } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); for (const s of report.unauditableSequences) { console.warn(`${s.tag}: ${String(s.byteLength)} bytes dropped, item stream unreadable`); } ``` #### Properties ##### applied > `readonly` **applied**: `"emptied"` What happened to that carrier's value: `"emptied"`, always. The value is not in the de-identified output. **🛑 `"kept"` WAS A MEMBER OF THIS UNION AND IS NOT ANY MORE.** It meant "the value IS in the output, byte for byte, unexamined", and a report field that can say so is exactly what this package no longer does: that instance is removed now and recorded on [DeidentifyReport.unenumerablePrivateRemovals](#unenumerableprivateremovals). The field itself is kept, so `applied === "emptied"` keeps compiling and keeps meaning what it always meant; a comparison against `"kept"` does not compile any more, which is the intended way to find out. ##### byteLength > `readonly` **byteLength**: `number` Byte length of the carrier's value field that was **dropped**. Structural, never a decoded value. ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag/index chain when the carrier is inside a sequence item; omitted at the root. **Built by the same descent as [DeidentifiedAttribute.contextPath](#contextpath-1) and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value.** Read that field's note before logging this one. ##### tag > `readonly` **tag**: `string` The carrier element. *** ### UndefinedVrFinding One element that was emptied because its **on-wire VR is not one of the 34 PS3.5 section 6.2 defines**, so nothing this library did to its bytes counts as decoding a Value Field. ## Why such an element exists at all Under an Explicit VR Transfer Syntax the VR is two bytes the sender wrote, and this parser trusts them (Postel's Law on the read path). The routine way two arbitrary bytes end up in a VR field is an **under**-declared Value Length upstream: the reader finishes the short value, and the leftover bytes of the value that was actually encoded are read as the next Data Element header. Tag, VR and length are then all fragments of somebody's value, and the element that genuinely followed is consumed as this fabricated element's "value". Measured on `scripts/measure-sq-bound-grid.ts`: a carrier under-declaring by 6 produces `(4156,554C)` with the VR bytes `"E "`, whose value holds the source `(0010,0020)` Patient ID in full. It reaches **string** carriers exactly as it reaches binary ones, because the carrier's own VR is not what decides it. ## Why emptying, and why it is not a guess PS3.5 2026c section 6.2 requires every VR not yet defined to use the long-form Data Element Structure - "with reserved bytes after the VR and a 32-bit unsigned integer VL" - so an unrecognized VR read short-form, which is what this parser does, is by the standard's own structure rule not a reading of a Value Field. There is nothing to prove about the content: the test is a membership check against the closed 34-VR set on a field the parser already recorded, so there is no scan, no per-offset loop, and no cost that follows an attacker-chosen value length. PS3.15 2026c section E.1.1 obliges an implementation claiming the Basic Application Level Confidentiality Profile to "protect or retain all instances of the Attributes listed in [Table E.1-1]". Those instances cannot be reached inside bytes that were never a value, so the obligation falls on the carrier. **`UN` is not this.** `UN` is one of the 34, so an ordinary unknown-VR element - the Implicit VR fallback for a tag this build's dictionary does not publish, and the CP-246 shape - never reaches here. That is the line the sibling `SQ`-with-no-items rule could not draw. ## Why this finding names no tag, when every sibling finding does Because the tag **may be content**, and nothing here can tell. The paragraph above is the whole argument: when an under-declare desynchronized the reader, the four tag bytes and the two VR bytes were read out of the middle of some element's Value Field, so reporting the "tag" would republish four bytes of the document. An unrecognized VR written honestly, at a correct length, raises this same code and has an ordinary tag - **and the two are indistinguishable here**, so the tag is withheld on both routes rather than on a guess. Measured on a synthetic `ST` carrier holding `"MR BRAIN SMITHSON"`, the fabricated tag is `48544F53` - four bytes of the surname. [EmbeddedAttributeFinding](#embeddedattributefinding) and [UnauditableSequenceFinding](#unauditablesequencefinding) may carry a tag because theirs came from a header the sender really wrote; this one may not, and the asymmetry is the finding rather than an inconsistency. `byteOffset` locates the element instead - a position this parser counted. **🩺 THAT WITHHOLDING IS NOT WHOLE, AND THIS PARAGRAPH USED TO CLAIM IT WAS.** It said "nothing here renders a document byte ... and the structural `contextPath`. Safe to log." `contextPath` is not structural: its segments are tags read off the wire by the same descent, so the header this type refuses to name by `tag` can be named by the `contextPath` of a finding one level down. See [DeidentifiedAttribute.contextPath](#contextpath-1) for the measurement. `byteOffset` and `byteLength` are unaffected and the reasoning above them still stands. #### Example ```ts import { deidentify, parseDicom } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); for (const u of report.undefinedVrElements) { console.warn(`offset ${String(u.byteOffset)}: ${String(u.byteLength)} bytes dropped`); } ``` #### Properties ##### byteLength > `readonly` **byteLength**: `number` Byte length of the value field that was dropped. **An input-derived count, and on this finding specifically that is not a formality.** For the sibling findings the length came from a header the sender wrote; here the four length bytes can themselves be value bytes, like the tag bytes. What is published is the *number* they decode to, and one `readUInt32LE` puts the bytes back: a fabricated header reading `"SO\0\0"` publishes `20307`, two letters of a surname. Do not describe this field as "structural, never a value" the way its siblings are described. **🩺 IT IS NO LONGER "the same footing as every `{n}` in the warning registry", AND THAT SENTENCE WAS THE ONE THIS FIELD SHIPPED WITH.** `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` rendered exactly this number and does not any more - it is bound out of `undefinedVrNotAuditable`'s signature. So this field is now a **model field**, on the same standing exception as `removedPrivateTags`, `unauditableSequences[].tag`, `uidMap` and `contextPath`: narrowing it is a product call, because a bound empties it on every well-formed file, where it is exactly the audit number the field exists to carry. Deliberately unchanged; see this type's own summary. ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of the emptied element's header. **This is how the element is identified, and there is deliberately no `tag` field** - see the note above: a fabricated header's tag bytes are part of some element's value. ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag/index chain when the carrier is inside a sequence item; omitted at the root. **Built by the same descent as [DeidentifiedAttribute.contextPath](#contextpath-1) and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value.** Read that field's note before logging this one. *** ### UnenumerablePrivateRemoval One private attribute **removed** because this run did not enumerate its value - the record that discharges the audit half of the fail-safe, and the one report surface that is complete at any input size. ## What "enumerated" means here, and what it deliberately does not A run has enumerated a private value when it **walked it as DICOM Data Elements** and put each of them through the Annex E action table (a private `SQ` the parser materialized items for), when the whole value **was matched as a member of a closed table the caller supplied** (a Private Creator `(gggg,00EE)` whose decoded value is in the profile's private dictionary), or when the value is **zero-length** and so encodes no Data Set. Nothing else. **🛑 DECODING A VALUE UNDER THE VR THE PROFILE RESOLVED FOR IT IS NOT ENUMERATION, AND NEITHER IS THE EMBEDDED-ATTRIBUTE SCANNER'S SILENCE.** A decoded `LO`, `ST`, `OB`, `OW` or `UN` value is a byte run that can carry a Data Set written in a transfer syntax this run never tested for; two cells of the matrix in `test/integration/deident-private-reservation.test.ts` are perfectly scannable Implicit VR LE string carriers whose values the scanner DID read and found nothing in, and which carried a nested `(0010,0010)` anyway, because the nested tiles are Explicit VR and the file is Implicit. So the predicate is **what the run did with the value**, never the VR and never the scanner's reach. ## The cost, stated at its real size With `RetainSafePrivate` plus a `Profile`, three classes of private value reach the output on something this run enumerated: an instance the run walked as Data Elements, a Private Creator the profile's dictionary vouches for, and a zero-length value. **An ordinary vendor scalar under an ordinary string VR that the file's own declaration does not name safe is removed**, because this package enumerates nothing inside a retained private value that is not one of those three. That is over-redaction traded for a closed identity leak: PS3.15 2026c §E.3.10 retains Private Attributes "known by the de-identifier to be safe from identity leakage" and sends "all other Private Attributes" to removal or to the `(0008,0307)` element-specific action, which this library does not implement - and a value nothing enumerated is not known to be safe however ordinary it looks. Through `0.0.19` such a value was **kept** and disclosed instead; the disclosure said outright that it was not a fix. ## 🩺 The fourth class, which this record never names A private value reaches the output on one more route, and it is the one that rests on no enumeration at all: the **file's own** Private Data Element Characteristics Sequence (0008,0300), which §E.3.10 names first among the ways an Attribute is known safe and which needs no `Profile`. A value in a block whose Block Identifying Information Status (0008,0303) reads `SAFE`, or listed in a `MIXED` block's Nonidentifying Private Elements (0008,0304), is retained unexamined on the **sender's** written assertion, so it is never removed and never appears here. That is the opposite trade from the one above, made in the open: the caller who does not trust the sender leaves `RetainSafePrivate` off, which is the one mitigation §E.3.10 offers. ## Per INSTANCE, never per tag An entry names one **occurrence**: a tag together with the Data Set it lived in, which `contextPath` identifies (absent means the root Data Set). Private blocks are reserved per Data Set (PS3.5 §7.8.1), so the same private tag can occur several times in one object with different enumerability, and removing one occurrence never removes a sibling one. ## Uncapped, and that is a decision rather than an oversight Every consumer-controlled **diagnostic** in this package is capped, because a finding emitted per element is amplified by an element count the input chooses. This is not a diagnostic: it is the record of an **action**, on the same footing as [DeidentifyReport.removedPrivateTags](#removedprivatetags), which is uncapped for the same reason. A caller's whole guarantee is that they can separate the unenumerable removals from the Annex E ones for **every** removal, so a cap here would silently take the guarantee away exactly on the files that need it most. The matching warnings stay bounded; this array does not. ## What it carries, and what that is worth logging `tag` is composed from four bytes of the source. On a well-formed file it is the sender's own private tag number and carries nothing, but on a file whose Value Lengths disagree with its bytes those four bytes can be document content - the standing exception [DeidentifyReport.removedPrivateTags](#removedprivatetags) and [UnauditableSequenceFinding](#unauditablesequencefinding) already carry, by the same route, and `contextPath` is unbound in the same way. It is published anyway, because *which* attribute was removed and *where* is the whole audit value of the record. Treat this array as PHI when the source is untrusted. #### Example ```ts import { deidentify, parseDicom, type UnenumerablePrivateRemoval } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf), { retain: ["RetainSafePrivate"], profile }); report.unenumerablePrivateRemovals.forEach((r: UnenumerablePrivateRemoval) => { console.warn(`${r.tag} ${r.applied} (${r.reason})`, r.contextPath ?? "root"); }); ``` #### Properties ##### applied > `readonly` **applied**: `"removed"` The outcome, always `"removed"`: the Data Set that held this instance carries no element bearing that tag, in the returned dataset and in the serialized bytes. Distinct from **emptied**, where an element with that tag is present carrying a zero-length value. ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag/index chain naming the Data Set this instance lived in; **omitted at the root**, which is what identifies the removal as per-instance rather than per-tag. **Built by the same descent as [DeidentifiedAttribute.contextPath](#contextpath-1) and carrying the same caveat: each segment's tag is read off the wire, bound by nothing, so on a desynchronized read it can be four bytes of a value.** Read that field's note before logging this one. ##### reason > `readonly` **reason**: `"unenumerable"` Why, always `"unenumerable"`: this run did not enumerate the value, so PS3.15 §E.3.10's "known ... to be safe" was never established for it. This is what separates an entry here from an attribute the Annex E action table removed (`report.attributes`, `applied: "removed"`) and from one whose value was emptied. ##### tag > `readonly` **tag**: `string` The removed instance's tag. *** ### UnregisteredElementRemoval One non-private Data Element **removed** because this build's PS3.6 2026d registry does not carry its tag and PS3.15 2026d Table E.1-1 does not list it. ## The rule A Table E.1-1 miss used to mean "keep": an attribute the table does not list went into de-identified output verbatim with a report that said nothing about it. That is right for an attribute PS3.6 registers, which the Profile has judged, and wrong for one it does not: the notes to PS3.15 2026d Table E.1-1 name "new Standard Attributes" among the places identifying information may be, and say removing only the known risks "may fail when the Standard is extended, or when a vendor adds unanticipated Standard Attributes". So an even-group tag with **no registry row and no Table E.1-1 row** is removed, at every depth `deidentify()` walks, whatever its VR (`UN`, an on-wire VR outside the 34 PS3.5 section 6.2 defines, and `SQ` included; a Sequence goes whole and nothing inside it is walked or recorded). "Registered" means a literal PS3.6 row, or a masked row the tag matches, with the `50xx` / `60xx` groups bounded by PS3.5 2026c section 7.6 (even groups `5000`-`501E` and `6000`-`601E`). Private tags, group `0004`, group `0002`, group lengths `(gggg,0000)` and every tag Table E.1-1 lists are not decided by this rule. ## What it costs **A conformant attribute from a PS3.6 edition newer than this build's pin is removed too**, because nothing separates it from an invented one. That is over-redaction and it is deliberate; a caller who needs such an attribute back has no switch for it in this release. ## What it carries, and what it deliberately does not **No tag and no VR.** The trigger is "no registry row carries this tag", and four bytes an under-declared length upstream made the reader take as a header satisfy it by construction: the fixture in `test/integration/deident-undefined-vr.test.ts` fabricates `(4854,4F53)`, `"THSO"` in wire order, four letters of a surname. Publishing the tag would republish them, so the element is identified by `byteOffset`, a position this parser counted, exactly as [UndefinedVrFinding](#undefinedvrfinding) does. `contextPath` names the Sequences this run **descended** to reach the element, never the removed element itself, and carries the caveat every finding's `contextPath` carries: see [DeidentifiedAttribute.contextPath](#contextpath-1). ## Capped record, complete count Bounded per run at `MAX_UNREGISTERED_ELEMENT_FINDINGS`, on its own counter so a flood of one diagnostic class cannot spend another's budget. The removal itself is not bounded, and [DeidentifyReport.unregisteredElementRemovalCount](#unregisteredelementremovalcount) is complete at any input size. #### Example ```ts import { deidentify, parseDicom, type UnregisteredElementRemoval } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); report.unregisteredElementRemovals.forEach((r: UnregisteredElementRemoval) => { console.warn(`removed at offset ${String(r.byteOffset)}`, r.contextPath ?? "root"); }); ``` #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of the removed element's header, as the parser recorded it on `Element.byteOffset`. **This is how the element is identified, and there is deliberately no `tag` field**: see the note above. ##### contextPath? > `readonly` `optional` **contextPath?**: readonly `string`[] Tag/index chain when the element was inside a sequence item; omitted at the root. **Built by the same descent as [DeidentifiedAttribute.contextPath](#contextpath-1) and carrying the same caveat: each segment's tag is read off the wire.** Read that field's note before logging this one. ## Type Aliases ### AppliedAction > **AppliedAction** = `"removed"` \| `"emptied"` \| `"dummied"` \| `"uid-remapped"` \| `"cleaned"` \| `"kept"` What `deidentify` actually did to one attribute - the concrete outcome of the resolved Annex E action. - `removed` - the element was deleted (`X`). - `emptied` - replaced with a zero-length value (`Z`). - `dummied` - replaced with a non-identifying dummy of compatible VR (`D`). - `uid-remapped` - UID(s) replaced with internally-consistent UIDs (`U`). - `cleaned` - conservatively blanked because a safe similar-meaning value cannot be synthesised at the metadata layer (`C`; see known limitations). - `kept` - retained, either by an active Retain option or because the SQ was kept and its items cleaned recursively. #### Example ```ts import { deidentify, parseDicom, type AppliedAction } from "@cosyte/dicom"; const { report } = deidentify(parseDicom(buf)); const removed = report.attributes.filter((a) => a.applied === ("removed" satisfies AppliedAction)); ``` *** ### DeidentifyErrorCode > **DeidentifyErrorCode** = *typeof* [`DEIDENTIFY_ERROR_CODES`](#deidentify_error_codes)\[keyof *typeof* [`DEIDENTIFY_ERROR_CODES`](#deidentify_error_codes)\] One of the [DEIDENTIFY\_ERROR\_CODES](#deidentify_error_codes) values. #### Example ```ts import { DeidentifyError, type DeidentifyErrorCode } from "@cosyte/dicom"; const code: DeidentifyErrorCode = "INVALID_OPTIONS"; throw new DeidentifyError("unknown retain option", code); ``` *** ### DeidentifyOption > **DeidentifyOption** = `Exclude`\<`AnnexEOption`, `"CleanPixelData"` \| `"CleanRecognizableVisual"`\> The PS3.15 Annex E option sets `deidentify` honours - the *metadata-affecting* columns of Table E.1-1. The two pixel-level options (`CleanPixelData` §E.3.1, `CleanRecognizableVisual` §E.3.2) are deliberately excluded: this is a metadata-only de-identifier and cannot inspect pixels (deferred to `@cosyte/dicom-pixel`). When pixel data is present it always warns rather than claiming the image is clean. **§E.3.6 is two Options, and each has its own name here.** Table E.1-1 gives them separate columns: `Rtn. Long. Full Dates` (keep dates and times as they are) and `Rtn. Long. Modif. Dates` (keep them only as modified values). - `RetainLongitudinalTemporal` carries the **full-dates** column, the *less* protective branch: where the two columns differ, it says `K` (keep the real value). Activate it only when real dates are genuinely required. - `RetainLongitudinalTemporalModifiedDates` carries the **modified-dates** column, which says `C` (clean it) on every row where the two differ, and makes the run write `(0028,0303) = MODIFIED`. The two are **mutually exclusive**: §E.3.6 defines them as alternatives, and a call naming both is rejected with [DeidentifyError](#deidentifyerror). Leave both off and the Basic Profile action applies, which removes or empties dates. 🩺 **Date *modification* is the caller's, on both branches.** This library applies the column and writes the declaration; it shifts, aggregates and transforms nothing, so a `MODIFIED` it writes is true only if you performed the transformation §E.3.6 describes. `report.warnings` carries `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on every run that activates the modified-dates Option, saying exactly that. #### Example ```ts const retain: DeidentifyOption[] = ["RetainLongitudinalTemporal", "RetainSafePrivate"]; ``` *** ### DicomValue > **DicomValue** = \{ `kind`: `"empty"`; \} \| \{ `kind`: `"text"`; `value`: `string`; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `kind`: `"strings"`; `values`: readonly `string`[]; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `kind`: `"personName"`; `values`: readonly [`PersonName`](#personname)[]; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `kind`: `"numbers"`; `values`: readonly `number`[]; \} \| \{ `kind`: `"bigints"`; `values`: readonly `bigint`[]; \} \| \{ `kind`: `"attributeTags"`; `values`: readonly [`Tag`](https://docs.cosyte.com/dicom/api/@cosyte/namespaces/Dictionary.md#tag-1)[]; \} \| \{ `kind`: `"decimalString"`; `values`: readonly (`number` \| `null`)[]; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `kind`: `"integerString"`; `values`: readonly (`number` \| `null`)[]; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `kind`: `"dates"`; `values`: readonly [`DicomDate`](#dicomdate)[]; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `kind`: `"times"`; `values`: readonly [`DicomTime`](#dicomtime)[]; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `kind`: `"dateTimes"`; `values`: readonly [`DicomDateTime`](#dicomdatetime)[]; `warnings?`: readonly [`DicomParseWarning`](#dicomparsewarning)[]; \} \| \{ `bytes`: `Buffer`; `kind`: `"binary"`; \} \| \{ `items`: readonly [`Item`](#item)[]; `kind`: `"sequence"`; \} The lazily-decoded value of an [Element](#element), as a discriminated union on `kind`. See the module doc for the VR → kind mapping. Narrow on `kind` (the `switch-exhaustiveness-check` lint rule keeps consumers honest). #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); const v = ds.get("00100010")?.value; // Patient's Name (PN) if (v?.kind === "personName") { console.log(v.values[0]?.alphabetic.familyName); } ``` *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] Discriminant type for `DicomParseError.code`. Narrowing a caught error by this code lets consumers write exhaustive `switch` blocks (enabled by the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free comparison against the `FATAL_CODES` registry. #### Example ```ts import type { FatalCode } from "@cosyte/dicom"; function describe(code: FatalCode): string { switch (code) { case "EMPTY_INPUT": return "input was empty"; case "NOT_DICOM_PART_10": return "input is not a DICOM Part 10 file"; case "INVALID_FILE_META": return "File Meta group is missing or malformed"; case "UNSUPPORTED_TRANSFER_SYNTAX": return "Transfer Syntax UID is not supported by v1"; } } ``` *** ### OffsetFrame > **OffsetFrame** = *typeof* [`OFFSET_FRAMES`](#offset_frames)\[keyof *typeof* [`OFFSET_FRAMES`](#offset_frames)\] The frame a `DicomParseError.byteOffset` is counted in. See [OFFSET\_FRAMES](#offset_frames). #### Example ```ts import type { OffsetFrame } from "@cosyte/dicom"; function indexable(frame: OffsetFrame): boolean { // Only the root frame's offsets index the buffer the caller passed in. switch (frame) { case "input": return true; case "inflated-dataset": case "value-slice": return false; } } ``` *** ### OnWarningCallback > **OnWarningCallback** = (`warning`) => `void` Synchronous callback invoked once per Tier-2 warning emitted during parse. The callback fires AFTER the warning has been pushed to `ctx.warnings`; if the callback throws, the parser silently swallows the exception and continues (mirrors `@cosyte/hl7` sibling). #### Parameters ##### warning [`DicomParseWarning`](#dicomparsewarning) #### Returns `void` #### Example ```ts import type { OnWarningCallback } from "@cosyte/dicom"; const onWarning: OnWarningCallback = (w) => { if (w.code === "DICOM_MISSING_PREAMBLE") { // ... } }; ``` *** ### ProfilePrivateTags > **ProfilePrivateTags** = `Readonly`\<`Record`\<`string`, [`PrivateTagDefinition`](#privatetagdefinition)\>\> One vendor's private-dictionary overlay as authored: canonical `"GGGGXXLL"` key → definition. Case-insensitive on input; normalized to uppercase on store. #### Example ```ts import type { ProfilePrivateTags } from "@cosyte/dicom"; const csa: ProfilePrivateTags = { "0029XX10": { vr: "OB", keyword: "CSAImageHeaderInfo", name: "CSA Image Header Info" }, }; ``` *** ### SerializeErrorCode > **SerializeErrorCode** = *typeof* [`SERIALIZE_ERROR_CODES`](#serialize_error_codes)\[keyof *typeof* [`SERIALIZE_ERROR_CODES`](#serialize_error_codes)\] Discriminant for [DicomSerializeError.code](#code-2), enabling exhaustive `switch` narrowing (the `switch-exhaustiveness-check` lint rule). #### Example ```ts import type { SerializeErrorCode } from "@cosyte/dicom"; function describe(code: SerializeErrorCode): string { switch (code) { case "MISSING_TRANSFER_SYNTAX": return "dataset has no Transfer Syntax UID to serialize under"; case "UNSUPPORTED_TRANSFER_SYNTAX": return "Transfer Syntax UID is neither native nor a section A.4 one"; case "INVALID_ENCAPSULATED_PIXEL_DATA": return "top-level Pixel Data is not a section A.4 fragment stream"; case "DIRECTORY_OFFSET_UNRESOLVED": return "a DICOMDIR offset names no Directory Record the dataset holds"; case "DIRECTORY_OFFSET_DEFLATED": return "a Deflated DICOMDIR carries a non-zero Directory Record offset"; } } ``` *** ### ValueErrorCode > **ValueErrorCode** = *typeof* [`VALUE_ERROR_CODES`](#value_error_codes)\[keyof *typeof* [`VALUE_ERROR_CODES`](#value_error_codes)\] Discriminant for [DicomValueError.code](#code-3), enabling exhaustive `switch` narrowing (the `switch-exhaustiveness-check` lint rule). #### Example ```ts import type { ValueErrorCode } from "@cosyte/dicom"; function describe(code: ValueErrorCode): string { switch (code) { case "FRAME_INDEX_OUT_OF_RANGE": return "frame index outside [0, numberOfFrames)"; case "MISSING_REQUIRED_FUNCTIONAL_GROUP": return "enhanced object lacks a required geometry macro"; } } ``` *** ### WarningCode > **WarningCode** = *typeof* [`WARNING_CODES`](#warning_codes)\[keyof *typeof* [`WARNING_CODES`](#warning_codes)\] Discriminant type for `DicomParseWarning.code`. Narrowing a warning by this code lets consumers write exhaustive `switch` blocks (enabled by the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free comparison against the `WARNING_CODES` registry. #### Example ```ts import type { DicomParseWarning, WarningCode } from "@cosyte/dicom"; function describe(w: DicomParseWarning): string { const code: WarningCode = w.code; if (code === "DICOM_MISSING_PREAMBLE") return "preamble missing"; return `warning: ${code}`; } ``` ## Variables ### CODING\_SCHEME\_OIDS > `const` **CODING\_SCHEME\_OIDS**: `object` Canonical coding-scheme OIDs (PS3.16 §8). **Only** the four standard designators are mapped. Legacy SNOMED designators (`SRT` / `SNM3` / `99SDM`) are intentionally absent: their code values differ from `SCT` (CP-730), so resolving them to the SNOMED OID would imply a false equality. #### Type Declaration ##### DCM > `readonly` **DCM**: `"1.2.840.10008.2.16.4"` = `"1.2.840.10008.2.16.4"` ##### LN > `readonly` **LN**: `"2.16.840.1.113883.6.1"` = `"2.16.840.1.113883.6.1"` ##### SCT > `readonly` **SCT**: `"2.16.840.1.113883.6.96"` = `"2.16.840.1.113883.6.96"` ##### UCUM > `readonly` **UCUM**: `"2.16.840.1.113883.6.8"` = `"2.16.840.1.113883.6.8"` #### Example ```ts import { CODING_SCHEME_OIDS } from "@cosyte/dicom"; CODING_SCHEME_OIDS.SCT; // "2.16.840.1.113883.6.96" ``` *** ### DEFAULT\_UID\_ROOT > `const` **DEFAULT\_UID\_ROOT**: `"2.25"` = `"2.25"` DICOM `2.25` UUID-derived root (PS3.5 §B.2) - no registration required. #### Example ```ts import { makeUidRemapper, DEFAULT_UID_ROOT } from "@cosyte/dicom"; const remap = makeUidRemapper(DEFAULT_UID_ROOT); remap.map("1.2.840.113619.2.55.3").startsWith("2.25."); // true ``` *** ### DEIDENTIFICATION\_METHOD\_CODES > `const` **DEIDENTIFICATION\_METHOD\_CODES**: `object` PS3.16 2026d CID 7050 "De-identification Method" (context group version `20170914`, UID `1.2.840.10008.6.1.925`), with the code `deidentify()` records for the Profile and for each [DeidentifyOption](#deidentifyoption). - `rows` - all thirteen rows, in the context group's own order. - `profile` - `113100` Basic Application Confidentiality Profile, written by every run. - `options` - the code written when that Option was active for the run. An Option "ran" when it was active: the Annex E column it selects is resolved off the option set, whether or not the object carried an attribute it acts on. `113107` therefore says the modified-dates column was resolved, **not** that any date was shifted; this library transforms no date, and every such run carries `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on `report.warnings`. `113101` and `113102` are in `rows` and are never written: they name the two pixel-level Options, which this metadata-only layer does not perform. #### Type Declaration ##### contextGroupUid > `readonly` **contextGroupUid**: `"1.2.840.10008.6.1.925"` ##### contextGroupVersion > `readonly` **contextGroupVersion**: `"20170914"` ##### options > `readonly` **options**: `Readonly`\<`Record`\<[`DeidentifyOption`](#deidentifyoption), [`DeidentificationMethodCode`](#deidentificationmethodcode)\>\> ##### profile > `readonly` **profile**: [`DeidentificationMethodCode`](#deidentificationmethodcode) ##### rows > `readonly` **rows**: readonly [`DeidentificationMethodCode`](#deidentificationmethodcode)[] #### Example ```ts import { DEIDENTIFICATION_METHOD_CODES } from "@cosyte/dicom"; DEIDENTIFICATION_METHOD_CODES.contextGroupUid; // "1.2.840.10008.6.1.925" DEIDENTIFICATION_METHOD_CODES.options.RetainUIDs.codeValue; // "113110" DEIDENTIFICATION_METHOD_CODES.rows.length; // 13 ``` *** ### DEIDENTIFY\_ERROR\_CODES > `const` **DEIDENTIFY\_ERROR\_CODES**: `Readonly`\<\{ `INVALID_OPTIONS`: `"INVALID_OPTIONS"`; `UNSUPPORTED_TRANSFER_SYNTAX`: `"UNSUPPORTED_TRANSFER_SYNTAX"`; \}\> Stable codes for [DeidentifyError](#deidentifyerror). - `INVALID_OPTIONS`: an author-time misconfiguration of the call itself (an unknown Retain option, both PS3.15 §E.3.6 temporal Options at once, a malformed `uidRoot`). - `UNSUPPORTED_TRANSFER_SYNTAX`: the Dataset's File Meta Transfer Syntax UID is one this de-identifier refuses to act on, whatever the options. Today that is exactly the four JPIP Referenced syntaxes of PS3.5 2026c sections A.6, A.7, A.11 and A.12: such an object references its pixels through Pixel Data Provider URL `(0028,7FE0)`, which has no PS3.15 Table E.1-1 row, so a de-identified copy would keep the URL by omission. Nothing is de-identified and no dataset or report is returned. #### Example ```ts import { DEIDENTIFY_ERROR_CODES } from "@cosyte/dicom"; DEIDENTIFY_ERROR_CODES.INVALID_OPTIONS; // "INVALID_OPTIONS" DEIDENTIFY_ERROR_CODES.UNSUPPORTED_TRANSFER_SYNTAX; // "UNSUPPORTED_TRANSFER_SYNTAX" ``` *** ### DEIDENTIFY\_OPTIONS > `const` **DEIDENTIFY\_OPTIONS**: readonly [`DeidentifyOption`](#deidentifyoption)[] The metadata option-set names, frozen for runtime validation. #### Example ```ts import { DEIDENTIFY_OPTIONS } from "@cosyte/dicom"; DEIDENTIFY_OPTIONS.includes("RetainUIDs"); // true ``` *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable string codes for every Tier-3 fatal the parser may throw. Locked at four codes per `PROJECT.md` "Fatal errors only for unrecoverable structural corruption": anything less severe MUST be a Tier-2 warning (see `./warnings.ts`). Consumers narrow on `err.code` to react to specific structural failures. #### Type Declaration ##### EMPTY\_INPUT > `readonly` **EMPTY\_INPUT**: `"EMPTY_INPUT"` = `"EMPTY_INPUT"` ##### INVALID\_FILE\_META > `readonly` **INVALID\_FILE\_META**: `"INVALID_FILE_META"` = `"INVALID_FILE_META"` ##### NOT\_DICOM\_PART\_10 > `readonly` **NOT\_DICOM\_PART\_10**: `"NOT_DICOM_PART_10"` = `"NOT_DICOM_PART_10"` ##### UNSUPPORTED\_TRANSFER\_SYNTAX > `readonly` **UNSUPPORTED\_TRANSFER\_SYNTAX**: `"UNSUPPORTED_TRANSFER_SYNTAX"` = `"UNSUPPORTED_TRANSFER_SYNTAX"` #### Example ```ts import { parseDicom, FATAL_CODES, DicomParseError } from "@cosyte/dicom"; try { parseDicom(Buffer.alloc(0)); } catch (err) { if (err instanceof DicomParseError && err.code === FATAL_CODES.EMPTY_INPUT) { // handle empty input } } ``` *** ### OFFSET\_FRAMES > `const` **OFFSET\_FRAMES**: `object` The coordinate systems a `byteOffset` this parser publishes can be counted in. A byte offset is a number and a number alone says nothing about where its zero is. This parser reads a Data Set out of three different buffers over one `parseDicom` call, so the same small integer means three different things depending on which one is being read - and until `DICOM-DIAGNOSTIC-PHI- RESIDUALS` closed it, nothing on the thrown error said which. A consumer cutting `input.subarray(err.byteOffset, err.byteOffset + 16)` to see what upset the parser was, inside a Sequence Item, cutting an unrelated element - the exact defect the `{ strict: true }` snippet itself was once fixed for. **A frame NAME is published; a frame ORIGIN is not, and that asymmetry is deliberate.** The name is drawn from the closed set below, which the parser chooses and no sender can influence, and that membership is the whole of its bound. An origin has no such table: it is a position reached by summing the declared lengths that led to it, so two of them differ by a wire field, and a message that already publishes `byteOffset` would be one number short of one. **That is a weaker argument than an impossibility and is stated as weaker**, since the library publishes positions freely in the `"input"` frame; a graded pass said so. The item asked for the frame to be NAMED, and nothing here needs the origin, so the cheap side of the trade is taken. #### Type Declaration ##### INFLATED\_DATASET > `readonly` **INFLATED\_DATASET**: `"inflated-dataset"` = `"inflated-dataset"` Byte 0 is byte 0 of the inflated Data Set of a Deflated Explicit VR LE object (`1.2.840.10008.1.2.1.99`). The compressed input holds no such byte, so the offset does not index it at any scale. ##### INPUT > `readonly` **INPUT**: `"input"` = `"input"` Byte 0 is byte 0 of the buffer handed to `parseDicom`. The only frame in which indexing the caller's own input by `byteOffset` is meaningful. ##### VALUE\_SLICE > `readonly` **VALUE\_SLICE**: `"value-slice"` = `"value-slice"` Byte 0 is byte 0 of a slice this parser cut from inside a Value Field: a defined-length Sequence Item's value, or an `SQ`/`UN` value handed to a descent. **Where that slice begins is deliberately not published** - see this table's own note. #### Example ```ts import { parseDicom, DicomParseError, OFFSET_FRAMES } from "@cosyte/dicom"; try { parseDicom(buffer); } catch (err) { if (err instanceof DicomParseError && err.offsetFrame === OFFSET_FRAMES.INPUT) { // Only here is `err.byteOffset` an index into the buffer you passed in. console.error(buffer.subarray(err.byteOffset, err.byteOffset + 16)); } } ``` *** ### profiles > `const` **profiles**: `Readonly`\<\{ `ge`: [`Profile`](#profile-2); `lenient`: [`Profile`](#profile-2); `philips`: [`Profile`](#profile-2); `siemens`: [`Profile`](#profile-2); `strict`: [`Profile`](#profile-2); \}\> Frozen namespace of every built-in profile: three vendor private-dictionary overlays (`ge`, `siemens`, `philips`) and two posture presets (`strict`, `lenient`). Pass one straight to `parseDicom`. #### Example ```ts import { parseDicom, profiles } from "@cosyte/dicom"; const ds = parseDicom(buf, { profile: profiles.siemens }); console.log(profiles.siemens.describe?.()); ``` *** ### SERIALIZE\_ERROR\_CODES > `const` **SERIALIZE\_ERROR\_CODES**: `object` Stable string codes the serializer may throw. Narrow on [DicomSerializeError.code](#code-2) to react to a specific failure. #### Type Declaration ##### DIRECTORY\_OFFSET\_DEFLATED > `readonly` **DIRECTORY\_OFFSET\_DEFLATED**: `"DIRECTORY_OFFSET_DEFLATED"` = `"DIRECTORY_OFFSET_DEFLATED"` ##### DIRECTORY\_OFFSET\_UNRESOLVED > `readonly` **DIRECTORY\_OFFSET\_UNRESOLVED**: `"DIRECTORY_OFFSET_UNRESOLVED"` = `"DIRECTORY_OFFSET_UNRESOLVED"` ##### INVALID\_ENCAPSULATED\_PIXEL\_DATA > `readonly` **INVALID\_ENCAPSULATED\_PIXEL\_DATA**: `"INVALID_ENCAPSULATED_PIXEL_DATA"` = `"INVALID_ENCAPSULATED_PIXEL_DATA"` ##### MISSING\_TRANSFER\_SYNTAX > `readonly` **MISSING\_TRANSFER\_SYNTAX**: `"MISSING_TRANSFER_SYNTAX"` = `"MISSING_TRANSFER_SYNTAX"` ##### UNSUPPORTED\_TRANSFER\_SYNTAX > `readonly` **UNSUPPORTED\_TRANSFER\_SYNTAX**: `"UNSUPPORTED_TRANSFER_SYNTAX"` = `"UNSUPPORTED_TRANSFER_SYNTAX"` #### Example ```ts import { SERIALIZE_ERROR_CODES } from "@cosyte/dicom"; SERIALIZE_ERROR_CODES.MISSING_TRANSFER_SYNTAX; // "MISSING_TRANSFER_SYNTAX" ``` *** ### VALUE\_ERROR\_CODES > `const` **VALUE\_ERROR\_CODES**: `object` Stable string codes the domain helpers may throw. Narrow on [DicomValueError.code](#code-3) to react to a specific contract violation. #### Type Declaration ##### FRAME\_INDEX\_OUT\_OF\_RANGE > `readonly` **FRAME\_INDEX\_OUT\_OF\_RANGE**: `"FRAME_INDEX_OUT_OF_RANGE"` = `"FRAME_INDEX_OUT_OF_RANGE"` ##### MISSING\_REQUIRED\_FUNCTIONAL\_GROUP > `readonly` **MISSING\_REQUIRED\_FUNCTIONAL\_GROUP**: `"MISSING_REQUIRED_FUNCTIONAL_GROUP"` = `"MISSING_REQUIRED_FUNCTIONAL_GROUP"` #### Example ```ts import { VALUE_ERROR_CODES } from "@cosyte/dicom"; VALUE_ERROR_CODES.FRAME_INDEX_OUT_OF_RANGE; // "FRAME_INDEX_OUT_OF_RANGE" ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Package version string for `@cosyte/dicom`. Synchronized with `package.json#version` by `scripts/sync-version.mjs`, which the `version` script runs after `changeset version` so the bump and this constant land in the same "Version Packages" commit. Stays on the uniform `0.0.x`-until-first-alpha ladder (locked across the `@cosyte/*` suite): patch bumps via Changesets through pre-alpha, with no `0.1.0` milestone bump. #### Example ```ts import { VERSION } from "@cosyte/dicom"; console.log(`@cosyte/dicom v${VERSION}`); ``` *** ### WARNING\_CODES > `const` **WARNING\_CODES**: `object` Stable string codes for every Tier-2 warning the parser may emit. The registry is frozen via `as const` so TypeScript infers the exact string-literal union for `WarningCode` - there is zero runtime cost and no magic-string comparisons for consumers. Reserved-but-not-emitted codes carry inline comments documenting which stage activates them; the union is declared whole so the schema is stable for consumers. #### Type Declaration ##### DICOM\_BOM\_IN\_TEXT\_VR > `readonly` **DICOM\_BOM\_IN\_TEXT\_VR**: `"DICOM_BOM_IN_TEXT_VR"` = `"DICOM_BOM_IN_TEXT_VR"` ##### DICOM\_BURNED\_IN\_ANNOTATION\_NOT\_REMOVED > `readonly` **DICOM\_BURNED\_IN\_ANNOTATION\_NOT\_REMOVED**: `"DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED"` = `"DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED"` ##### DICOM\_CHARSET\_AMBIGUOUS\_SEPARATOR > `readonly` **DICOM\_CHARSET\_AMBIGUOUS\_SEPARATOR**: `"DICOM_CHARSET_AMBIGUOUS_SEPARATOR"` = `"DICOM_CHARSET_AMBIGUOUS_SEPARATOR"` ##### DICOM\_CHARSET\_BYTES\_UNDECODABLE > `readonly` **DICOM\_CHARSET\_BYTES\_UNDECODABLE**: `"DICOM_CHARSET_BYTES_UNDECODABLE"` = `"DICOM_CHARSET_BYTES_UNDECODABLE"` ##### DICOM\_CHARSET\_ESCAPE\_UNDECLARED > `readonly` **DICOM\_CHARSET\_ESCAPE\_UNDECLARED**: `"DICOM_CHARSET_ESCAPE_UNDECLARED"` = `"DICOM_CHARSET_ESCAPE_UNDECLARED"` ##### DICOM\_CHARSET\_EXTENSION\_NOT\_RESET > `readonly` **DICOM\_CHARSET\_EXTENSION\_NOT\_RESET**: `"DICOM_CHARSET_EXTENSION_NOT_RESET"` = `"DICOM_CHARSET_EXTENSION_NOT_RESET"` ##### DICOM\_DA\_LEGACY\_FORMAT > `readonly` **DICOM\_DA\_LEGACY\_FORMAT**: `"DICOM_DA_LEGACY_FORMAT"` = `"DICOM_DA_LEGACY_FORMAT"` ##### DICOM\_DEIDENT\_DATES\_NOT\_TRANSFORMED > `readonly` **DICOM\_DEIDENT\_DATES\_NOT\_TRANSFORMED**: `"DICOM_DEIDENT_DATES_NOT_TRANSFORMED"` = `"DICOM_DEIDENT_DATES_NOT_TRANSFORMED"` ##### DICOM\_DEIDENT\_DICOMDIR\_FILE\_SET\_NOT\_DISCHARGED > `readonly` **DICOM\_DEIDENT\_DICOMDIR\_FILE\_SET\_NOT\_DISCHARGED**: `"DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED"` = `"DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED"` ##### DICOM\_DEIDENT\_EMBEDDED\_ATTRIBUTE\_REMOVED > `readonly` **DICOM\_DEIDENT\_EMBEDDED\_ATTRIBUTE\_REMOVED**: `"DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED"` = `"DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED"` ##### DICOM\_DEIDENT\_FILE\_META\_REPLACED > `readonly` **DICOM\_DEIDENT\_FILE\_META\_REPLACED**: `"DICOM_DEIDENT_FILE_META_REPLACED"` = `"DICOM_DEIDENT_FILE_META_REPLACED"` ##### DICOM\_DEIDENT\_GROUP\_0004\_REMOVED > `readonly` **DICOM\_DEIDENT\_GROUP\_0004\_REMOVED**: `"DICOM_DEIDENT_GROUP_0004_REMOVED"` = `"DICOM_DEIDENT_GROUP_0004_REMOVED"` ##### DICOM\_DEIDENT\_METHOD\_CODES\_PRIOR\_REPLACED > `readonly` **DICOM\_DEIDENT\_METHOD\_CODES\_PRIOR\_REPLACED**: `"DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED"` = `"DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED"` ##### DICOM\_DEIDENT\_METHOD\_CODES\_PRIOR\_RETAINED > `readonly` **DICOM\_DEIDENT\_METHOD\_CODES\_PRIOR\_RETAINED**: `"DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED"` = `"DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED"` ##### DICOM\_DEIDENT\_METHOD\_NOT\_ADDED > `readonly` **DICOM\_DEIDENT\_METHOD\_NOT\_ADDED**: `"DICOM_DEIDENT_METHOD_NOT_ADDED"` = `"DICOM_DEIDENT_METHOD_NOT_ADDED"` ##### DICOM\_DEIDENT\_METHOD\_NOT\_LO > `readonly` **DICOM\_DEIDENT\_METHOD\_NOT\_LO**: `"DICOM_DEIDENT_METHOD_NOT_LO"` = `"DICOM_DEIDENT_METHOD_NOT_LO"` ##### DICOM\_DEIDENT\_METHOD\_PRIOR\_RETAINED > `readonly` **DICOM\_DEIDENT\_METHOD\_PRIOR\_RETAINED**: `"DICOM_DEIDENT_METHOD_PRIOR_RETAINED"` = `"DICOM_DEIDENT_METHOD_PRIOR_RETAINED"` ##### DICOM\_DEIDENT\_METHOD\_VALUE\_OVER\_LENGTH > `readonly` **DICOM\_DEIDENT\_METHOD\_VALUE\_OVER\_LENGTH**: `"DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH"` = `"DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH"` ##### DICOM\_DEIDENT\_PRIVATE\_CARRIER\_NOT\_AUDITABLE > `readonly` **DICOM\_DEIDENT\_PRIVATE\_CARRIER\_NOT\_AUDITABLE**: `"DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE"` = `"DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE"` ##### DICOM\_DEIDENT\_PRIVATE\_DECLARATION\_NOT\_RESOLVED > `readonly` **DICOM\_DEIDENT\_PRIVATE\_DECLARATION\_NOT\_RESOLVED**: `"DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED"` = `"DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED"` ##### DICOM\_DEIDENT\_SEQUENCE\_NOT\_AUDITABLE > `readonly` **DICOM\_DEIDENT\_SEQUENCE\_NOT\_AUDITABLE**: `"DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE"` = `"DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE"` ##### DICOM\_DEIDENT\_UNDEFINED\_VR\_NOT\_AUDITABLE > `readonly` **DICOM\_DEIDENT\_UNDEFINED\_VR\_NOT\_AUDITABLE**: `"DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE"` = `"DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE"` ##### DICOM\_DEIDENT\_UNREGISTERED\_ELEMENT\_REMOVED > `readonly` **DICOM\_DEIDENT\_UNREGISTERED\_ELEMENT\_REMOVED**: `"DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED"` = `"DICOM_DEIDENT_UNREGISTERED_ELEMENT_REMOVED"` ##### DICOM\_DIRECTORY\_OFFSET\_DEFLATED > `readonly` **DICOM\_DIRECTORY\_OFFSET\_DEFLATED**: `"DICOM_DIRECTORY_OFFSET_DEFLATED"` = `"DICOM_DIRECTORY_OFFSET_DEFLATED"` ##### DICOM\_DIRECTORY\_OFFSET\_MALFORMED > `readonly` **DICOM\_DIRECTORY\_OFFSET\_MALFORMED**: `"DICOM_DIRECTORY_OFFSET_MALFORMED"` = `"DICOM_DIRECTORY_OFFSET_MALFORMED"` ##### DICOM\_DIRECTORY\_OFFSET\_UNRESOLVED > `readonly` **DICOM\_DIRECTORY\_OFFSET\_UNRESOLVED**: `"DICOM_DIRECTORY_OFFSET_UNRESOLVED"` = `"DICOM_DIRECTORY_OFFSET_UNRESOLVED"` ##### DICOM\_DIRECTORY\_RECORD\_REVISITED > `readonly` **DICOM\_DIRECTORY\_RECORD\_REVISITED**: `"DICOM_DIRECTORY_RECORD_REVISITED"` = `"DICOM_DIRECTORY_RECORD_REVISITED"` ##### DICOM\_DT\_NONSTANDARD\_OFFSET > `readonly` **DICOM\_DT\_NONSTANDARD\_OFFSET**: `"DICOM_DT_NONSTANDARD_OFFSET"` = `"DICOM_DT_NONSTANDARD_OFFSET"` ##### DICOM\_DUPLICATE\_FILE\_META\_ELEMENT > `readonly` **DICOM\_DUPLICATE\_FILE\_META\_ELEMENT**: `"DICOM_DUPLICATE_FILE_META_ELEMENT"` = `"DICOM_DUPLICATE_FILE_META_ELEMENT"` ##### DICOM\_DUPLICATE\_TAG\_IN\_DATA\_SET > `readonly` **DICOM\_DUPLICATE\_TAG\_IN\_DATA\_SET**: `"DICOM_DUPLICATE_TAG_IN_DATA_SET"` = `"DICOM_DUPLICATE_TAG_IN_DATA_SET"` ##### DICOM\_EMPTY\_ITEM\_IN\_SEQUENCE > `readonly` **DICOM\_EMPTY\_ITEM\_IN\_SEQUENCE**: `"DICOM_EMPTY_ITEM_IN_SEQUENCE"` = `"DICOM_EMPTY_ITEM_IN_SEQUENCE"` ##### DICOM\_FILE\_META\_GROUP\_LENGTH\_MISMATCH > `readonly` **DICOM\_FILE\_META\_GROUP\_LENGTH\_MISMATCH**: `"DICOM_FILE_META_GROUP_LENGTH_MISMATCH"` = `"DICOM_FILE_META_GROUP_LENGTH_MISMATCH"` ##### DICOM\_FILE\_META\_GROUP\_LENGTH\_MISSING > `readonly` **DICOM\_FILE\_META\_GROUP\_LENGTH\_MISSING**: `"DICOM_FILE_META_GROUP_LENGTH_MISSING"` = `"DICOM_FILE_META_GROUP_LENGTH_MISSING"` ##### DICOM\_GROUP\_LENGTH\_IN\_DATASET > `readonly` **DICOM\_GROUP\_LENGTH\_IN\_DATASET**: `"DICOM_GROUP_LENGTH_IN_DATASET"` = `"DICOM_GROUP_LENGTH_IN_DATASET"` ##### DICOM\_IMPLICIT\_VR\_FOR\_PRIVATE\_TAG\_WITHOUT\_VR > `readonly` **DICOM\_IMPLICIT\_VR\_FOR\_PRIVATE\_TAG\_WITHOUT\_VR**: `"DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR"` = `"DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR"` ##### DICOM\_IS\_NONINTEGER\_VALUE > `readonly` **DICOM\_IS\_NONINTEGER\_VALUE**: `"DICOM_IS_NONINTEGER_VALUE"` = `"DICOM_IS_NONINTEGER_VALUE"` ##### DICOM\_ITEM\_CROSSES\_SEQUENCE\_END > `readonly` **DICOM\_ITEM\_CROSSES\_SEQUENCE\_END**: `"DICOM_ITEM_CROSSES_SEQUENCE_END"` = `"DICOM_ITEM_CROSSES_SEQUENCE_END"` ##### DICOM\_MISSING\_PREAMBLE > `readonly` **DICOM\_MISSING\_PREAMBLE**: `"DICOM_MISSING_PREAMBLE"` = `"DICOM_MISSING_PREAMBLE"` ##### DICOM\_NON\_ASCII\_IN\_ASCII\_VR > `readonly` **DICOM\_NON\_ASCII\_IN\_ASCII\_VR**: `"DICOM_NON_ASCII_IN_ASCII_VR"` = `"DICOM_NON_ASCII_IN_ASCII_VR"` ##### DICOM\_NONZERO\_RESERVED\_BYTES > `readonly` **DICOM\_NONZERO\_RESERVED\_BYTES**: `"DICOM_NONZERO_RESERVED_BYTES"` = `"DICOM_NONZERO_RESERVED_BYTES"` ##### DICOM\_ODD\_LENGTH\_VALUE\_PADDED > `readonly` **DICOM\_ODD\_LENGTH\_VALUE\_PADDED**: `"DICOM_ODD_LENGTH_VALUE_PADDED"` = `"DICOM_ODD_LENGTH_VALUE_PADDED"` ##### DICOM\_PIXEL\_DATA\_FRAGMENTS\_NOT\_DELIMITED > `readonly` **DICOM\_PIXEL\_DATA\_FRAGMENTS\_NOT\_DELIMITED**: `"DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED"` = `"DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED"` ##### DICOM\_PIXEL\_DATA\_LENGTH\_MISMATCH > `readonly` **DICOM\_PIXEL\_DATA\_LENGTH\_MISMATCH**: `"DICOM_PIXEL_DATA_LENGTH_MISMATCH"` = `"DICOM_PIXEL_DATA_LENGTH_MISMATCH"` ##### DICOM\_PRIVATE\_CREATOR\_UNKNOWN > `readonly` **DICOM\_PRIVATE\_CREATOR\_UNKNOWN**: `"DICOM_PRIVATE_CREATOR_UNKNOWN"` = `"DICOM_PRIVATE_CREATOR_UNKNOWN"` ##### DICOM\_PRIVATE\_TAG\_NO\_CREATOR > `readonly` **DICOM\_PRIVATE\_TAG\_NO\_CREATOR**: `"DICOM_PRIVATE_TAG_NO_CREATOR"` = `"DICOM_PRIVATE_TAG_NO_CREATOR"` ##### DICOM\_SQ\_NOT\_DESCENDED > `readonly` **DICOM\_SQ\_NOT\_DESCENDED**: `"DICOM_SQ_NOT_DESCENDED"` = `"DICOM_SQ_NOT_DESCENDED"` ##### DICOM\_TRAILING\_NULL\_IN\_TEXT\_VR > `readonly` **DICOM\_TRAILING\_NULL\_IN\_TEXT\_VR**: `"DICOM_TRAILING_NULL_IN_TEXT_VR"` = `"DICOM_TRAILING_NULL_IN_TEXT_VR"` ##### DICOM\_UI\_TRAILING\_SPACE > `readonly` **DICOM\_UI\_TRAILING\_SPACE**: `"DICOM_UI_TRAILING_SPACE"` = `"DICOM_UI_TRAILING_SPACE"` ##### DICOM\_UN\_PARSED\_AS\_SQ > `readonly` **DICOM\_UN\_PARSED\_AS\_SQ**: `"DICOM_UN_PARSED_AS_SQ"` = `"DICOM_UN_PARSED_AS_SQ"` ##### DICOM\_UNDEFINED\_LENGTH\_IN\_EXPLICIT\_VR > `readonly` **DICOM\_UNDEFINED\_LENGTH\_IN\_EXPLICIT\_VR**: `"DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR"` = `"DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR"` ##### DICOM\_UNSUPPORTED\_CHARSET > `readonly` **DICOM\_UNSUPPORTED\_CHARSET**: `"DICOM_UNSUPPORTED_CHARSET"` = `"DICOM_UNSUPPORTED_CHARSET"` ##### DICOM\_VR\_MISMATCH > `readonly` **DICOM\_VR\_MISMATCH**: `"DICOM_VR_MISMATCH"` = `"DICOM_VR_MISMATCH"` #### Example ```ts import { parseDicom, WARNING_CODES } from "@cosyte/dicom"; const ds = parseDicom(buf); if (ds.warnings.some((w) => w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE)) { // handle bare File Meta input } ``` ## Functions ### codingSchemeOid() > **codingSchemeOid**(`designator`): `string` \| `undefined` Resolve a coding-scheme designator to its canonical OID, or `undefined` for any non-standard / legacy designator (including `SRT`/`SNM3`/`99SDM`, which are NOT treated as `SCT`). #### Parameters ##### designator `string` \| `undefined` #### Returns `string` \| `undefined` #### Example ```ts import { codingSchemeOid } from "@cosyte/dicom"; codingSchemeOid("UCUM"); // "2.16.840.1.113883.6.8" codingSchemeOid("SRT"); // undefined - not SCT (CP-730) ``` *** ### decodeElementValue() > **decodeElementValue**(`element`): [`DicomValue`](#dicomvalue) Decode an [Element](#element)'s value to a typed [DicomValue](#dicomvalue). Pure + fail-safe: it never throws and never coerces a malformed value to a plausible-but-wrong one. Called lazily (and memoized) by `Element.value`. #### Parameters ##### element [`Element`](#element) #### Returns [`DicomValue`](#dicomvalue) #### Example ```ts import { parseDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); const v = ds.get("00280010")?.value; // Rows (US) if (v?.kind === "numbers") console.log(v.values[0]); ``` *** ### decodeText() > **decodeText**(`bytes`, `terms`): `string` Decode bytes to a string under the Specific Character Set terms, never throwing. With more than one term, and a Value 1 other than `ISO_IR 192`, `GB18030` or `GBK`, the bytes decode with ISO 2022 code extensions (PS3.5 section 6.1.2.5): every PS3.3 Table C.12-3 and C.12-4 escape sequence switches G0 or G1, no escape byte reaches the string, and the Value 1 designations come back after each CR, LF or FF, which are the reset points of a text VR. Bytes no designated set decodes read as U+FFFD. Only the string is returned here; `Element.value` carries the warnings for an element's value. Every other term list decodes under [resolveDecoderLabel](#resolvedecoderlabel)'s label: an unsupported label falls back to UTF-8, then to Latin-1 (1:1, cannot fail). #### Parameters ##### bytes `Buffer` ##### terms readonly `string`[] \| `undefined` #### Returns `string` #### Example ```ts import { decodeText } from "@cosyte/dicom"; const s = decodeText(buf, ["ISO_IR 192"]); // UTF-8 // ESC $ B, the JIS X 0208 bytes of one character, ESC ( B decodeText(Buffer.from([0x1b, 0x24, 0x42, 0x3b, 0x33, 0x1b, 0x28, 0x42]), ["", "ISO 2022 IR 87"]); ``` *** ### defineProfile() > **defineProfile**(`opts`): [`Profile`](#profile-2) Build a frozen [Profile](#profile-2) from a validated options object. #### Parameters ##### opts [`DefineProfileOptions`](#defineprofileoptions) #### Returns [`Profile`](#profile-2) #### Example ```ts import { defineProfile, parseDicom } from "@cosyte/dicom"; const acme = defineProfile({ name: "acme", description: "ACME PACS quirks", privateTags: { "ACME_PRIV_01": { "0019XX10": { vr: "DS", keyword: "AcmeDose", name: "ACME Dose" }, }, }, }); const ds = parseDicom(buf, { profile: acme }); console.log(acme.describe?.()); ``` *** ### deidentify() > **deidentify**(`ds`, `options?`): [`DeidentifyResult`](#deidentifyresult)\<[`Dataset`](#dataset)\> De-identify a [Dataset](#dataset) per PS3.15 Annex E - the Basic Application Level Confidentiality Profile, plus any Retain/Clean Options passed in `retain`. Pure: `ds` is never mutated. Returns a fresh dataset and a [DeidentifyReport](#deidentifyreport) of tags, keywords, action codes, warnings and the UID map. Everything in it is composed from static tables except `uidMap`, whose keys are the source UIDs the file carried: treat that field as PHI. The returned Data Set says what this run did to its **dates** as well as to its identity: `(0028,0303) Longitudinal Temporal Information Modified` carries `UNMODIFIED` when `RetainLongitudinalTemporal` was active, `MODIFIED` when `RetainLongitudinalTemporalModifiedDates` was, and `REMOVED` under neither, replacing any value the source carried at that tag. It is **not** on [DeidentifyReport](#deidentifyreport) - like `(0012,0062)`, it is a statement the object makes about itself, and the report's shape is unchanged by it. 🩺 A `MODIFIED` means the modified-dates column was resolved; **you** perform the date transformation §E.3.6 describes, and `report.warnings` carries `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` saying so. **A JPIP Referenced object is refused, not de-identified.** When `ds.fileMeta.transferSyntaxUID` is one of the four JPIP Referenced syntaxes (PS3.5 2026c sections A.6, A.7, A.11 and A.12), the call throws before it reads the options or the Data Set, because that object's Pixel Data Provider URL `(0028,7FE0)` has no PS3.15 Table E.1-1 action and would otherwise be kept by omission. The refusal keys on the File Meta alone: a non-JPIP object that carries `(0028,7FE0)` is de-identified like any other, and keeps it. #### Parameters ##### ds [`Dataset`](#dataset) ##### options? [`DeidentifyOptions`](#deidentifyoptions) = `{}` #### Returns [`DeidentifyResult`](#deidentifyresult)\<[`Dataset`](#dataset)\> #### Throws [DeidentifyError](#deidentifyerror) (`UNSUPPORTED_TRANSFER_SYNTAX`) for a `Dataset` whose File Meta Transfer Syntax UID is one of the four JPIP Referenced syntaxes, whatever the options; its message is a fixed string. Checked first, so it is the code such a call gets even when its options are invalid too. #### Throws [DeidentifyError](#deidentifyerror) (`INVALID_OPTIONS`) for an unknown Retain option, for both PS3.15 §E.3.6 temporal Options in one call, or for a malformed `uidRoot`. #### Example ```ts import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom"; const { dataset, report } = deidentify(parseDicom(buf)); const clean = serializeDicom(dataset); // safe to share console.log(report.attributes.length, "attributes acted on"); ``` *** ### isKnownCharsetTerm() > **isKnownCharsetTerm**(`term`): `boolean` `true` when `term` is a Specific Character Set defined term this build can map to a decoder (the empty default-repertoire term counts as known). #### Parameters ##### term `string` #### Returns `boolean` #### Example ```ts import { isKnownCharsetTerm } from "@cosyte/dicom"; isKnownCharsetTerm("ISO_IR 192"); // true isKnownCharsetTerm("ISO_IR 14"); // false - does not exist ``` *** ### makeUidRemapper() > **makeUidRemapper**(`root?`, `cache?`): [`UidRemapper`](#uidremapper) Build a [UidRemapper](#uidremapper) rooted at `root`, optionally seeded with (and writing through to) a caller-owned `cache` for cross-call sharing. #### Parameters ##### root? `string` = `DEFAULT_UID_ROOT` Dotted-decimal UID root (default [DEFAULT\_UID\_ROOT](#default_uid_root)). ##### cache? `Map`\<`string`, `string`\> = `...` Source→replacement map to fill (default a fresh `Map`). #### Returns [`UidRemapper`](#uidremapper) #### Throws when `root` is not a valid dotted-decimal OID prefix or leaves no room for a value component within the 64-char UID limit. #### Example ```ts import { makeUidRemapper } from "@cosyte/dicom"; const remap = makeUidRemapper(); remap.map("1.2.840.113619.2.55.3") === remap.map("1.2.840.113619.2.55.3"); // true ``` *** ### parseDate() > **parseDate**(`raw`): `object` Decode a single `DA` value. `legacy` is `true` for any tolerated non-canonical form (retired dotted `YYYY.MM.DD`, or an unparseable value such as `"ANONYMIZED"`), signalling the caller to emit `DICOM_DA_LEGACY_FORMAT`. #### Parameters ##### raw `string` #### Returns `object` ##### legacy > **legacy**: `boolean` ##### value > **value**: [`DicomDate`](#dicomdate) #### Example ```ts import { parseDate } from "@cosyte/dicom"; parseDate("20240115").value.valid; // true parseDate("2024.01.15").legacy; // true ``` *** ### parseDateTime() > **parseDateTime**(`raw`): `object` Decode a single `DT` value. `nonstandardOffset` is `true` when a UTC offset suffix is present but malformed / out of range, signalling the caller to emit `DICOM_DT_NONSTANDARD_OFFSET`. #### Parameters ##### raw `string` #### Returns `object` ##### nonstandardOffset > **nonstandardOffset**: `boolean` ##### value > **value**: [`DicomDateTime`](#dicomdatetime) #### Example ```ts import { parseDateTime } from "@cosyte/dicom"; parseDateTime("20240115133015+0100").value.offsetMinutes; // 60 ``` *** ### parseDicom() **`Internal`** - implementation signature. Public JSDoc lives on the overloads above. #### Call Signature > **parseDicom**(`input`): [`Dataset`](#dataset) Parse a DICOM Part 10 buffer into a structural [Dataset](#dataset). Lenient by default - recoverable deviations (missing preamble, File Meta group-length mismatch, odd-length value, etc.) are pushed into `ds.warnings` with stable codes from `WARNING_CODES`. Four unrecoverable structural failures throw `DicomParseError`: - `EMPTY_INPUT` - empty `Buffer | Uint8Array | ArrayBuffer`. - `NOT_DICOM_PART_10` - input lacks both `DICM` magic at offset 128 and a recognizable `(0002,0000)` File Meta Group Length at offset 0. - `INVALID_FILE_META` - File Meta is truncated or `(0002,0010)` Transfer Syntax UID is missing. - `UNSUPPORTED_TRANSFER_SYNTAX` - Transfer Syntax UID is none of the supported ones: the four native syntaxes (`1.2.840.10008.1.2`, `…1.2.1`, `…1.2.2`, `…1.2.1.99`), every encapsulated Pixel Data syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE, MPEG, HEVC, JPEG XL, Deflated Image Frame, Encapsulated Uncompressed), and the four JPIP Referenced syntaxes of sections A.6, A.7, A.11 and A.12. The SMPTE ST 2110 syntaxes and every retired UID stay refused. An object under a section A.4 syntax is read under Explicit VR Little Endian rules, as A.4 requires: its metadata is fully readable, and its Pixel Data fragments are handed back as opaque, undecoded bytes by [readPixelDataFragments](#readpixeldatafragments). **No pixel is ever decoded.** An object under a JPIP Referenced syntax is read the same way for metadata: sections A.6 and A.11 make the Data Set Explicit VR Little Endian, and sections A.7 and A.12 deflate that Data Set per RFC 1951, so it is inflated first under the Deflated reader's decompression cap. Its Pixel Data Provider URL (0028,7FE0) is returned as the `UR` element the file carries and is **never fetched, resolved or validated**. The object is read-only here: `serializeDicom` refuses to write the four JPIP syntaxes, and `deidentify` refuses to de-identify an object under one of them. Pass `{ strict: true }` to escalate every Tier-2 warning to a thrown `DicomParseError` carrying the warning code. ##### Parameters ###### input `ArrayBuffer` \| `Buffer`\<`ArrayBufferLike`\> \| `Uint8Array`\<`ArrayBufferLike`\> ##### Returns [`Dataset`](#dataset) ##### Example ```ts import { parseDicom, WARNING_CODES, DicomParseError } from "@cosyte/dicom"; import { readFileSync } from "node:fs"; // Three input shapes: Buffer, Uint8Array, ArrayBuffer. const bytes = readFileSync("study.dcm"); const ds1 = parseDicom(bytes); const ds2 = parseDicom(new Uint8Array(bytes)); const ds3 = parseDicom(bytes.buffer); // Inspect File Meta + warnings. console.log(ds1.fileMeta?.transferSyntaxUID); for (const w of ds1.warnings) { if (w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE) { console.warn("bare File Meta input at offset", w.position.byteOffset); } } // Strict mode + onWarning callback. try { parseDicom(bytes, { strict: true, onWarning: (w) => console.error(w.code, "at offset", w.position.byteOffset), }); } catch (err) { if (err instanceof DicomParseError) { console.error(err.code, err.byteOffset, err.snippet); } } ``` #### Call Signature > **parseDicom**(`input`, `options`): [`Dataset`](#dataset) Parse a DICOM Part 10 buffer into a structural [Dataset](#dataset). Lenient by default - recoverable deviations (missing preamble, File Meta group-length mismatch, odd-length value, etc.) are pushed into `ds.warnings` with stable codes from `WARNING_CODES`. Four unrecoverable structural failures throw `DicomParseError`: - `EMPTY_INPUT` - empty `Buffer | Uint8Array | ArrayBuffer`. - `NOT_DICOM_PART_10` - input lacks both `DICM` magic at offset 128 and a recognizable `(0002,0000)` File Meta Group Length at offset 0. - `INVALID_FILE_META` - File Meta is truncated or `(0002,0010)` Transfer Syntax UID is missing. - `UNSUPPORTED_TRANSFER_SYNTAX` - Transfer Syntax UID is none of the supported ones: the four native syntaxes (`1.2.840.10008.1.2`, `…1.2.1`, `…1.2.2`, `…1.2.1.99`), every encapsulated Pixel Data syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE, MPEG, HEVC, JPEG XL, Deflated Image Frame, Encapsulated Uncompressed), and the four JPIP Referenced syntaxes of sections A.6, A.7, A.11 and A.12. The SMPTE ST 2110 syntaxes and every retired UID stay refused. An object under a section A.4 syntax is read under Explicit VR Little Endian rules, as A.4 requires: its metadata is fully readable, and its Pixel Data fragments are handed back as opaque, undecoded bytes by [readPixelDataFragments](#readpixeldatafragments). **No pixel is ever decoded.** An object under a JPIP Referenced syntax is read the same way for metadata: sections A.6 and A.11 make the Data Set Explicit VR Little Endian, and sections A.7 and A.12 deflate that Data Set per RFC 1951, so it is inflated first under the Deflated reader's decompression cap. Its Pixel Data Provider URL (0028,7FE0) is returned as the `UR` element the file carries and is **never fetched, resolved or validated**. The object is read-only here: `serializeDicom` refuses to write the four JPIP syntaxes, and `deidentify` refuses to de-identify an object under one of them. Pass `{ strict: true }` to escalate every Tier-2 warning to a thrown `DicomParseError` carrying the warning code. ##### Parameters ###### input `ArrayBuffer` \| `Buffer`\<`ArrayBufferLike`\> \| `Uint8Array`\<`ArrayBufferLike`\> ###### options [`ParseOptions`](#parseoptions) ##### Returns [`Dataset`](#dataset) ##### Example ```ts import { parseDicom, WARNING_CODES, DicomParseError } from "@cosyte/dicom"; import { readFileSync } from "node:fs"; // Three input shapes: Buffer, Uint8Array, ArrayBuffer. const bytes = readFileSync("study.dcm"); const ds1 = parseDicom(bytes); const ds2 = parseDicom(new Uint8Array(bytes)); const ds3 = parseDicom(bytes.buffer); // Inspect File Meta + warnings. console.log(ds1.fileMeta?.transferSyntaxUID); for (const w of ds1.warnings) { if (w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE) { console.warn("bare File Meta input at offset", w.position.byteOffset); } } // Strict mode + onWarning callback. try { parseDicom(bytes, { strict: true, onWarning: (w) => console.error(w.code, "at offset", w.position.byteOffset), }); } catch (err) { if (err instanceof DicomParseError) { console.error(err.code, err.byteOffset, err.snippet); } } ``` *** ### parsePersonName() > **parsePersonName**(`value`): [`PersonName`](#personname) Parse a single (already charset-decoded, pad-trimmed) PN value string into its structured [PersonName](#personname) form. #### Parameters ##### value `string` #### Returns [`PersonName`](#personname) #### Example ```ts import { parsePersonName } from "@cosyte/dicom"; const pn = parsePersonName("Doe^Jane^^Dr^"); pn.alphabetic.familyName; // "Doe" pn.alphabetic.namePrefix; // "Dr" ``` *** ### parseSpecificCharacterSet() > **parseSpecificCharacterSet**(`bytes`): readonly `string`[] Parse a `(0008,0005)` Specific Character Set value (a `CS`, possibly multi-valued via ``) into its trimmed **defined terms**. The first value may be empty (the ISO-2022 "G0 starts as ISO_IR 6" rule); it is preserved as `""` rather than dropped so [resolveDecoderLabel](#resolvedecoderlabel) can apply the default-repertoire fallback. A component this closed table does not name is not a defined term, and it is **withheld** rather than returned. This value is multi-valued on the backslash, every component is a string a sender authored, and the result lands on `Element.specificCharacterSet`, where it presents itself as an identifier a downstream package may interpolate: that is the field, and the delimiter, this package's measured PHI leak ran through. Decoding is unaffected, because [resolveDecoderLabel](#resolvedecoderlabel) already skipped any term it could not map and skips the withheld marker for the same reason. #### Parameters ##### bytes `Buffer` #### Returns readonly `string`[] #### Example ```ts import { parseSpecificCharacterSet } from "@cosyte/dicom"; // bytes for "ISO 2022 IR 6\\ISO 2022 IR 87" const terms = parseSpecificCharacterSet(buf); ``` *** ### parseTime() > **parseTime**(`raw`): `object` Decode a single `TM` value (max 14 bytes). Precision may be truncated from the right (`HH`, `HHMM`, `HHMMSS`, `HHMMSS.FFFFFF`). #### Parameters ##### raw `string` #### Returns `object` ##### value > **value**: [`DicomTime`](#dicomtime) #### Example ```ts import { parseTime } from "@cosyte/dicom"; parseTime("133015.5").value.fractionalSeconds; // 0.5 ``` *** ### readCode() > **readCode**(`item`): [`CodedConcept`](#codedconcept) Read the coded triplet off a code-item dataset (e.g. one item of a Code Sequence). Every part is independently fail-safe; `schemeUid` is the resolved OID for the designator when standard, else `undefined`. #### Parameters ##### item [`Dataset`](#dataset) #### Returns [`CodedConcept`](#codedconcept) #### Example ```ts import { parseDicom, readCode } from "@cosyte/dicom"; const units = parseDicom(buf).image.realWorldValueMaps?.[0]?.unitsCode; // units?.codeValue / units?.codingSchemeDesignator / units?.codeMeaning ``` *** ### readPixelDataFragments() > **readPixelDataFragments**(`ds`): [`PixelDataFragments`](#pixeldatafragments) \| `undefined` Read the Basic Offset Table and every fragment of a Data Set's encapsulated Pixel Data `(7FE0,0010)` as raw bytes, without decoding anything. Pass the root [Dataset](#dataset) `parseDicom` returned: its Pixel Data is the top-level element PS3.5 2026c section A.4 encapsulates. An `Item` is a `Dataset` too, and is read the same way over its own elements. **`undefined` means this Data Set has no encapsulated fragments**: Pixel Data is absent, or it is present in native format with an explicit Value Length. That is never an empty list and never a Basic Offset Table, so it cannot be mistaken for an encapsulated result. **The list can be short, and the parse says so.** A stream that ends with the input before its Sequence Delimitation Item still parses, and `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` on `ds.warnings` says that the fragments read may not be all the sender wrote. Read the warnings before treating the list as complete. #### Parameters ##### ds [`Dataset`](#dataset) #### Returns [`PixelDataFragments`](#pixeldatafragments) \| `undefined` #### Throws `DicomParseError` with code `INVALID_FILE_META` when the element's bytes are not an Item stream (an Item header cut short, an Item reaching past the bytes, or a tag other than `(FFFE,E000)` / `(FFFE,E0DD)`). `parseDicom` refuses such bytes itself, so only a hand-built `Element` reaches this; the error carries no fragment byte and no length outside its 16-byte `snippet`. #### Example ```ts import { parseDicom, readPixelDataFragments } from "@cosyte/dicom"; const ds = parseDicom(buf); // e.g. a JPEG 2000 object const pixels = readPixelDataFragments(ds); if (pixels === undefined) { // no encapsulated Pixel Data: absent, or native } else { for (const fragment of pixels.fragments) { // hand `fragment` to a codec of your choice; it is the bytes as written } } ``` *** ### resolveDecoderLabel() > **resolveDecoderLabel**(`terms`): `string` Resolve the `TextDecoder` label to use for a value, given the dataset's Specific Character Set terms. Prefers a multibyte/extended decoder when the term list mixes single-byte and code-extension sets; falls back to the default repertoire (Latin-1, lenient) when no term resolves. [decodeText](#decodetext) decodes under this label when the list has one term, or when Value 1 is `ISO_IR 192`, `GB18030` or `GBK`. Any other multi-term list decodes with ISO 2022 code extensions instead, whatever label this returns. #### Parameters ##### terms readonly `string`[] \| `undefined` #### Returns `string` #### Example ```ts import { resolveDecoderLabel } from "@cosyte/dicom"; resolveDecoderLabel(["ISO 2022 IR 6", "ISO 2022 IR 87"]); // "iso-2022-jp" resolveDecoderLabel(undefined); // "latin1" ``` *** ### serializeDicom() > **serializeDicom**(`ds`): `Buffer` Serialize a [Dataset](#dataset) to a spec-clean DICOM Part 10 `Buffer`. The dataset's transfer syntax is preserved (no transcoding): pixel-data fragments are written back byte-for-byte, nested sequences carry the same bytes with each Item's Data Elements in ascending tag order, and scalar values are re-emitted with correct even-length padding and File Meta group length. Pure function - the input `Dataset` is never mutated. **Element order.** Every Data Set is written in ascending tag order (PS3.5 2026c §7.1, §7.5.1): the root, and the Data Set of every Item in every Sequence the writer can walk on the wire, at every depth up to `NESTING_DEPTH_LIMIT`. Items stay in their order (PS3.5 2026c §7.5) and no value changes; only whole element spans move, and only where the parsed `items` match the bytes, so the output reads back as the source did. Limits: a tag repeated inside an Item is kept twice, in source order, so that output still breaks PS3.5 2026c §7.1's "at most once"; a Sequence whose Item stream cannot be walked to exactly its end, one nested past the bound, one whose `items` do not match its bytes (a Sequence the parser did not descend, for one), and any `UN`-carried Sequence (under Implicit VR LE that includes a private Sequence inside an Item, even one a `Profile` resolved to `SQ`, since a default read resolves its tag to `UN`) are written as read, unordered; an element whose own bytes do not show where a reader ends it (an undefined-length `UN` the parser could not read as a Sequence, or a value missing its Sequence Delimitation Item) is written after the ascending rest of its Data Set, because a reader takes what follows it into its value, and so is a Sequence nested past the bound (other than a defined-length one under Implicit VR LE), whose end the writer would have to walk past the bound to see; and an element the parser relocated because a length lied is ordered where it was placed, since ordering cannot recover an order the source destroyed. **Encapsulated objects.** Under every Transfer Syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest: the list `parseDicom` reads), the File Meta Transfer Syntax UID is kept, the Data Set is written as Explicit VR Little Endian, and the top-level Pixel Data `(7FE0,0010)` is written as `OB` of undefined length: the input's Basic Offset Table Item and fragment Items byte for byte and in order, then a zero-length Sequence Delimitation Item. Nothing is decoded, transcoded or re-framed, and no offset table is interpreted or rebuilt. The limit sits with it: a top-level Data Set that section A.4 does not allow is refused with `INVALID_ENCAPSULATED_PIXEL_DATA` rather than repaired, which includes a fragment stream `parseDicom` read with `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED`, an odd Item Length, an empty fragment, native or absent top-level Pixel Data, and Float or Double Float Pixel Data. Pixel Data nested in a Sequence Item is written as read. **DICOMDIR offsets.** For a `Dataset` whose File Meta Media Storage SOP Class UID is `1.2.840.10008.1.3.10`, `(0004,1200)`, `(0004,1202)` and each Directory Record's `(0004,1400)` and `(0004,1420)` are written as the byte offset, from the first preamble byte of the output, of the Directory Record each named when the file was read (PS3.3 2026d Table F.3-3), whatever moved it: the rebuilt File Meta group, the ascending order above, or `deidentify()`. A record is the Item of the Directory Record Sequence `(0004,1220)` whose `Item.fileOffset` the offset equals; nothing is found by scanning for an Item tag. A zero offset is written as zero. Limits: the retired MRDR offset `(0004,1504)` is written as read, and nothing checks record keys or `(0004,1202)` against the root chain. **Input contract.** The writer is designed for a [Dataset](#dataset) produced by `parseDicom`: it relies on the parser's `Element.rawBytes` representation (value-only for scalars and Implicit-LE defined-length `SQ`; full on-wire span for Explicit `SQ`, undefined-length spans, encapsulated Pixel Data, and the `UN`/CP-246 fallbacks). A hand-built `Dataset` must follow the same convention for its bytes to be encoded correctly. **Round-trip scope.** `parseDicom(out)` re-reads to a dataset that is equal over the *modeled* surface (every dataset element + the typed "../dataset/file-meta".FileMeta fields plus any non-modeled File Meta elements preserved on `extraElements`), not a byte-exact copy of the original file: the 128-byte preamble is normalized to zeros, the File Meta group is rebuilt in ascending tag order (modeled fields + `extraElements` - see encodeFileMeta), the Data Sets are ordered as above, odd-length values are padded even, and retired `(gggg,0000)` group lengths are dropped. #### Parameters ##### ds [`Dataset`](#dataset) #### Returns `Buffer` #### Throws [DicomSerializeError](#dicomserializeerror) with code `MISSING_TRANSFER_SYNTAX` when the dataset has no File Meta Transfer Syntax UID, `UNSUPPORTED_TRANSFER_SYNTAX` when that UID is neither one of the four native syntaxes nor a section A.4 one, `INVALID_ENCAPSULATED_PIXEL_DATA` when it is a section A.4 one and the top-level Pixel Data is not a fragment stream section A.4 allows, `DIRECTORY_OFFSET_UNRESOLVED` when a DICOMDIR carries an offset that is not zero and names no Directory Record the `Dataset` holds, or is not one 32-bit unsigned integer (never written stale or as zero), or `DIRECTORY_OFFSET_DEFLATED` when a DICOMDIR under Deflated Explicit VR LE carries a non-zero offset. Nothing is returned on a throw. #### Example ```ts import { parseDicom, serializeDicom } from "@cosyte/dicom"; const ds = parseDicom(buf); const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax // parseDicom(out) re-reads to a structurally-equal dataset. ``` *** ### toDate() > **toDate**(`value`, `options?`): `Date` \| `undefined` The value as an absolute instant, ONLY where the zone is determinate. A stated `&ZZXX` offset wins and `options.assumeOffsetMinutes` is ignored. With no stated offset the caller's `assumeOffsetMinutes` is applied, an explicit `0` meaning "treat this naive value as UTC". With neither, the answer is `undefined`: the host machine's zone is never read and UTC is never assumed. A value with no year is never an instant, so a `TM` always answers `undefined` however the call is made. A non-finite `assumeOffsetMinutes` (`NaN`, `Infinity`, `-Infinity`) names no zone either, so it answers `undefined` rather than an `Invalid Date` whose `getTime()` is `NaN`, and so does a finite offset so large that applying it leaves the range a `Date` can represent. A value naming no real calendar point is refused before any of that, so an impossible day is never rolled into the next month. Components below the stated precision fill to their lowest legal value for the instant alone; the value's own precision is untouched, and a later [toObject](#toobject) or [toISO](#toiso) on it returns what it returned before. A four-digit year below 100 stays that year, so `0050` is the year 50 and the two-digit remapping of `Date.UTC` never reaches the result. #### Parameters ##### value `DicomTemporal` \| `null` \| `undefined` ##### options? [`ToDateOptions`](#todateoptions) #### Returns `Date` \| `undefined` #### Example ```ts import { parseDate, parseDateTime, toDate } from "@cosyte/dicom"; toDate(parseDate("20240115").value); // undefined: no zone was stated toDate(parseDate("20240115").value, { assumeOffsetMinutes: 0 })?.toISOString(); // "2024-01-15T00:00:00.000Z" toDate(parseDateTime("20240115133015+0100").value)?.toISOString(); // "2024-01-15T12:30:15.000Z" toDate(parseDate("18700230").value, { assumeOffsetMinutes: 0 }); // undefined: never 1 March for a value the sender wrote as February ``` *** ### toISO() > **toISO**(`value`): `string` \| `undefined` The value as an ISO-8601 string, TRUNCATED to the precision it stated. Nothing is padded out: a `DA` renders `2024-01-15`, a `DT` that stated only an hour renders `2024-01-15T13`, and a `TM` renders the bare time. Fractional digits are rendered exactly as written. A stated offset is appended, `Z` when it is zero; a value that stated NO offset gets nothing appended, because a fabricated `Z` would claim UTC the sender never wrote. Returns `undefined` for an invalid value, for `null` / `undefined`, for a value that stated no component at all, and for one naming no real calendar point: a string such as `"2024-02-30"` is worse than no answer, because every ISO-8601 reader moves it silently to 1 March. It never throws. This is not a byte round-trip of the wire value and is not meant to be: `serializeDicom` remains the route that reproduces the original bytes. #### Parameters ##### value `DicomTemporal` \| `null` \| `undefined` #### Returns `string` \| `undefined` #### Example ```ts import { parseDate, parseDateTime, parseTime, toISO } from "@cosyte/dicom"; toISO(parseDateTime("20240115133015-0500").value); // "2024-01-15T13:30:15-05:00" toISO(parseTime("133015").value); // "13:30:15" toISO(parseDate("18700431").value); // undefined: April has no 31st ``` *** ### toObject() > **toObject**(`value`): [`DateParts`](#dateparts) \| `undefined` The calendar components a `DA`, `TM` or `DT` value stated, as a frozen object. Returns `undefined` for a value the decoders marked `valid: false`, for `null` / `undefined`, for a value that stated no component at all, and for one whose components name no real calendar point (30 February, 29 February outside a leap year, 31 April, second 60). It never throws. `hours` / `minutes` / `seconds` are renamed to the singular `hour` / `minute` / `second`; `raw`, `valid` and the `legacy` and `nonstandardOffset` flags the decoders report beside the value never appear. #### Parameters ##### value `DicomTemporal` \| `null` \| `undefined` #### Returns [`DateParts`](#dateparts) \| `undefined` #### Example ```ts import { parseDate, parseDateTime, parseTime, toObject } from "@cosyte/dicom"; toObject(parseDateTime("20240115133015").value); // { year: 2024, month: 1, day: 15, hour: 13, minute: 30, second: 15 } toObject(parseTime("133015.123456").value).millisecond; // 123, from the digits toObject(parseDate("18700230").value); // undefined: February has no 30th ``` --- URL: https://docs.cosyte.com/dicom/troubleshooting Markdown: https://docs.cosyte.com/dicom/troubleshooting.md # Troubleshooting: symptoms and causes `@cosyte/dicom` is built to be **correct and honest about its edges** rather than to claim more than it delivers. This page answers the question you arrive with: *my object did something I did not expect, what is going on and what do I do about it.* It is symptom-to-cause: the error model, a symptom table, what a diagnostic may and may not carry, and the structural boundaries that explain the symptoms above. Everything here is a documented boundary, not a bug: the lenient parser never silently drops or garbles data; where a boundary applies, the raw bytes are preserved (often with a warning), they are simply not further interpreted. **The "do not over-trust" list lives on one page and it is not this one.** [Known limitations](https://docs.cosyte.com/dicom/limitations.md) is the index of what is out of scope, which PHI residuals are measured and still open, and what is never defaulted. Read it before you point this package at real data. This page explains the symptoms; that page states the boundaries. ## When does it throw vs warn? Only an unrecoverable Tier-3 condition throws a `DicomParseError`; everything else is a warning on `ds.warnings`. The whole Tier-3 set is the table below, and `FATAL_CODES` is its registry. ```ts runnable throws import { parseDicom } from "@cosyte/dicom"; // Bytes that are not a Part 10 object: a structural fatal, not a tolerated quirk. parseDicom(Buffer.from("plainly not a DICOM object, just ASCII bytes", "ascii")); // throws DicomParseError (NOT_DICOM_PART_10) ``` | Fatal code (throws) | Meaning | | ----------------------------- | ---------------------------------------------------------------------- | | `NOT_DICOM_PART_10` | No preamble/`DICM` and no recoverable File Meta: not a Part 10 object. | | `INVALID_FILE_META` | The File Meta group is present but structurally unreadable. | | `UNSUPPORTED_TRANSFER_SYNTAX` | Not a native, PS3.5 2026c A.4 or JPIP Referenced one (SMPTE, retired). | | `EMPTY_INPUT` | Zero-length input. | Narrow on the caught error via `err instanceof DicomParseError` and `err.code === FATAL_CODES.*` (see [Tolerance & the warning model](https://docs.cosyte.com/dicom/spec-notes-tolerance.md)). Everything a real-world archive does short of that (a missing preamble, an odd-length value, an off-spec VR, a group-length mismatch) is a warning you triage, not an exception you catch. ## Common symptoms | Symptom | Likely cause | What to do | | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ds.get("PatientName")` is `undefined` | `get` takes the **tag** form, not a keyword | Use the tag (`ds.get("00100010")`), or resolve a keyword with `Dictionary.byKeyword("PatientName")?.tag`. | | `ds.image.rescaleSlope` is `undefined` | Rescale Slope was absent | This is by design. It is **not** defaulted to `1`. Apply a fallback deliberately in your own code if the modality warrants it. | | `ds.image.signed` is `undefined` | Pixel Representation `(0028,0103)` was absent | Signedness is unknown, never guessed. Do not assume unsigned. | | A `DICOM_VR_MISMATCH` warning | The on-wire Explicit VR disagreed with the dictionary | The **on-wire** VR is used (Postel's Law: on the read path the sender's own declaration wins) and the warning recorded. | | A `DICOM_PRIVATE_CREATOR_UNKNOWN` warning | A private tag's creator is not in the active profile | The element degrades to `UN`; add the creator via a [profile](https://docs.cosyte.com/dicom/spec-notes-profiles.md) to resolve it. | | `ds.get(tag)?.value` is `{ kind: "binary" }` for Pixel Data | Pixel data is exposed raw, never decoded | Expected. Decoding pixels is out of scope (see below). | | A `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` warning | Encapsulated Pixel Data `(7FE0,0010)` ran to the end of the input after a whole fragment, with no `(FFFE,E0DD)` Sequence Delimitation Item | The metadata is intact and every whole fragment read is on `readPixelDataFragments(ds)`, but **the fragment list may be short**: nothing in the stream says where the sender meant it to end, so do not treat the pixels as complete, and re-fetch the object. PS3.5 2026c section A.4 requires the delimiter, so a conformant file cannot raise this and `{ strict: true }` refuses it. The message carries no fragment byte, length or count; `position.byteOffset` is the Pixel Data element's own header. A fragment cut short mid-value is not this code: it is the fatal `INVALID_FILE_META`. | | A `DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` warning after `deidentify` | The object may carry burned-in PHI in the pixels | Metadata de-id cannot clean pixels; route to a pixel-cleaning step before sharing. | | A `DICOM_SQ_NOT_DESCENDED` warning | A defined-length Implicit VR LE element resolved to `SQ` from the dictionary, but its value is not an `(FFFE,E000)` item stream | The bytes are kept intact on `Element.rawBytes` and the rest of the object parses, but `Element.items` is absent, so nothing can navigate inside it. `deidentify()` therefore **empties** that element rather than shipping bytes it cannot audit. `RetainSafePrivate` plus a profile exempted it in earlier releases and no longer does - see the next row. | | A `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` warning after `deidentify`, and an attribute you expected is now empty | An element reached `deidentify()` whose on-wire VR is not one of the 34 PS3.5 §6.2 defines, so no Table E.1-1 row can say what its bytes mean | Fail-safe by design. Two very different files reach it: one **conformant to a future edition of PS3.5**, carrying a VR this release does not know (the header is read correctly per §6.2 and the value is emptied anyway, because nothing can classify it); and one **malformed upstream**, where an under-declared Value Length left the reader mid-value and leftover bytes tiled into a Data Element header. `report.undefinedVrElements` names the **byte offset** and the byte length dropped - deliberately not a tag, because on the second route a fabricated header's tag bytes are part of some element's value. Capped at 64 entries; the emptying is not. An Implicit VR LE file cannot trip it. | | A `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` warning after `deidentify`, and a sequence you expected is now empty | That `SQ` element reached `deidentify()` with no items, so its item stream could not be walked | Fail-safe by design: PS3.15 §E.1.1 obliges a de-identifier to reach listed attributes inside a Sequence of Items, and a run that cannot enumerate them must not pass them through. `report.unauditableSequences` names the tag and the byte length dropped (this class capped at 64 on its own counter, so count the `applied: "emptied"` entries and not the array; the emptying is not capped). Usually a sender-side encoding defect, but not always: a conformant file nested past this library's own `NESTING_DEPTH_LIMIT` of 64 is refused identically. | | A `DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE` warning after `deidentify`, and a private attribute you expected is GONE | A private attribute your `Profile` vouched for under `RetainSafePrivate` held a value this run did not enumerate, so it was removed rather than retained | Expected, and it is a removal rather than an emptying: the Data Set holds no element bearing that tag at all. PS3.15 §E.3.10 licenses retention for a Private _Attribute_ "known by the de-identifier to be safe from identity leakage"; it cannot vouch for a Data Set the sender nested in that attribute's value, which §E.1.1 still covers, so a value nothing enumerated is not known to be safe and the clause's other branch (remove) applies. `report.unenumerablePrivateRemovals` names the instance - the tag, the Data Set it lived in, `applied: "removed"` and `reason: "unenumerable"` - and that record is complete and never capped, while these warnings stop at 64. **This fires on ordinary vendor values too**, because the package enumerates nothing inside a retained private value beyond three cases: a value it **walked as Data Elements** (a private `SQ` whose items the parser materialized), a Private Creator `(gggg,00EE)` whose whole decoded value is a member of your profile's private dictionary, and a zero-length value. Those three still reach your output; so does a vouched value the file's own `(0008,0300)` declaration names safe (its block's status is `SAFE`, or a `MIXED` block lists it individually), which is retained on the sender's word rather than on anything this run enumerated and is never recorded on `report.unenumerablePrivateRemovals`. Outside that declaration, everything else a profile vouched for is removed, an ordinary vendor scalar under an ordinary string VR included. **Pass your profile to `parseDicom` as well** and a vendor _sequence_ is walked and retained instead, which is the one route that genuinely audits the inside. 🛑 **The code is the pin's and its meaning is not**: in earlier releases it said the value had been SHIPPED unexamined. | | A `DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED` warning after `deidentify`, and a value you expected is now empty | The sender over-declared that element's Value Length, so the element that followed it was absorbed into its value | The carrier is emptied rather than kept, because an attribute encoded inside a value is invisible to the PS3.15 action table. `report.embeddedAttributes` names the carrier, and `hidden` names the attributes hiding in it **that this run acts on and that PS3.15 Table E.1-1 gives a literal row** - a run's other members are not listed, and the list can be empty while the finding is real. The warning's own count covers the whole run. The file is malformed at source - raise it with the sender. | | A `DICOM_ITEM_CROSSES_SEQUENCE_END` warning | An `(FFFE,E000)` Item declared a length reaching past the end its enclosing `SQ` declared, so the item's value read takes bytes that belong to the enclosing Data Set | The sender's two length fields contradict each other: PS3.5 section 7.5.2 makes the `SQ`'s Value Length the exact extent of the item stream, section 7.5.1 governs the Item's own length, and this file gets one of them wrong. **The reading is unchanged and this is disclosure only** - the parser follows the Item's length, as every release has, so nothing about how your file is read changed in this release. **Where the disputed element lands is shape-specific, and this warning cannot tell you which shape you have** - the two readings are byte-identical. Both are pinned as measurements rather than summarised: on one, the element that follows the sequence becomes a per-item attribute with a `deidentify()` `contextPath` naming that item; on the other, it stays at the root with no `contextPath` at all. **Neither of those is the mis-report and this guide no longer calls one of them that** - calling the first a `contextPath` "naming an item it was never in" says you know the sender meant the element to be at the root, and that is precisely the thing the byte-identity denies you. **Treat a de-identified object built from such a file as unverified, and do not trust `(0012,0062) Patient Identity Removed = YES` on it.** The private-attribute routes this used to fire beside are closed (`DICOM-PRIVATE-CREATOR-RESERVATION-LEAK` for the absorb direction, `DICOM-ITEM-EJECT-ROUTE` for the eject direction, and `DICOM-PRIVATE-SQ-CARVE-OUT` for the vouched-for private `SQ` that was kept verbatim ahead of all of them), but **that is a statement about private attributes under `RetainSafePrivate`, not about the object** - the structural relocation this warning is about is unchanged. Which way an element goes depends on where the sender put it relative to the disputed sequence, and this warning does not report that - so read it as "the structure is in dispute", never as an all-clear about what was retained. **The parser deliberately does not prefer the sequence's extent**, even though PS3.5 §7.5.2 reads like it should: a file whose item over-declares and a file whose sequence under-declares are byte-identical, so no reader can honour that section on the first without imposing it on the second. Check `Element.items` against what you expected before trusting a `contextPath`, and raise the file with the sender. | | A `DICOM_DUPLICATE_TAG_IN_DATA_SET` warning | The same Data Element Tag was read twice in one Data Set, so the second element REPLACED the first | **This one reports a loss, not a tolerated deviation.** A parsed Data Set is a `Map`, so the earlier element's value is not in the object and cannot be recovered from it; the survivor is indistinguishable from an element the sender wrote once, which is why the parse says so explicitly. The warning's `position.byteOffset` is the offset of the header that replaced, which is the surviving `Element.byteOffset`. **That makes it a lookup for a collision at the ROOT and nowhere else**: inside a defined-length Sequence Item, `Element.byteOffset` is relative to that item's own slice, so the same number can also name an untouched root element, and nothing on the warning says which Data Set it came from (`position.contextPath` is not populated by any parser warning). Read it as "an element was destroyed somewhere in this object" unless you have established the frame. PS3.5 section 7.1 requires a tag to occur at most once in a Data Set and section 7.5.1 requires the same inside an Item, so a conformant file cannot raise it. The usual cause is not a sender writing a tag twice: it is a length field that lies, so bytes inside some element's value are read as a Data Element header, or an Item that under-declares and ejects its trailing elements into the enclosing Data Set (see the row above). **The reading is unchanged and this is disclosure only** - the last element read still wins, exactly as in every previous release, and no value is invented for the one it replaced. The message names no tag, deliberately: on the length-lie route those four bytes are document content. Treat the object as incomplete, do not trust a `deidentify()` report built from it, and raise the file with the sender. | | A `DICOM_DUPLICATE_FILE_META_ELEMENT` warning | The same `(0002,xxxx)` tag was read twice in the File Meta group, and it is one of the eight this library projects into typed `FileMeta` fields | **The same loss as the row above, in the group that decides how every following byte is read.** The File Meta group is an array, not a map, so nothing is overwritten - but a modeled tag is answered by a **first-match** search and is excluded from `FileMeta.extraElements`, so a second copy is in neither the typed field nor the verbatim residue. It left the object. **This code and its Data Set sibling resolve a repeat the opposite way round, deliberately, because the readings do: the FIRST copy wins here, the LAST read wins in a Data Set.** Neither reading changed in this release, and no value is guessed for the copy that lost. Two copies of `(0002,0010)` Transfer Syntax UID carrying different UIDs are two readings of the same file and the order alone picks one - a file like that may parse cleanly, parse into a different object, or throw `INVALID_FILE_META` from the dataset parser, depending only on which copy came first. Unlike the Data Set code, `position.byteOffset` is unambiguously **file-absolute** (the File Meta group is never nested) and locates the copy that was **dropped**, not the survivor. A repeated `(0002,xxxx)` this library does not model is silent, because every copy of one is kept in `extraElements` - and `serializeDicom` then re-emits both copies, `PRE-EXISTING` and unchanged here. **It covers the group as the parser delimits it, not the group**: a copy an intermediary appended past an honest `(0002,0000)` group length is never a File Meta element to this parser and is relocated into the main Data Set, silently, on this release and every earlier one. PS3.5 2026c section 7.1 requires a Data Element to occur at most once in a Data Set; **PS3.10, which governs this group, is not vendored in this repo, so no conformance verdict is claimed here** - the code's trigger is narrower and does not need one, firing exactly when a value the file carried does not reach the parsed object. The message names no tag. Raise the file with the sender, and do not trust a `deidentify()` report built from it. | | A `DICOM_DEIDENT_METHOD_NOT_ADDED` warning after `deidentify`, and the prior `(0012,0063)` text is gone | The `(0012,0063)` value `deidentify()` would have had to write - the one the file already carried, with or without this run's method text appended to it - would not fit the largest Value Length an `LO` can encode | PS3.15 section E.1.1 says a de-identifier's method text is "inserted in or added to" that attribute, so `deidentify()` adds. This code is raised when the value it would have to write is longer than that VR can encode: `LO` is a short-form VR with a 16-bit Value Length, and an element the serializer cannot encode would take the whole de-identified object down rather than lose one attribute. The prior value is **replaced** instead, which is what every release before this one did on every file, and this warning exists so that fallback is disclosed. **Read it as "the length ceiling was reached", never as "every fallback is disclosed"**: a `(0012,0063)` a file encoded under a VR other than `LO` is also replaced, and that one is disclosed by its own code, `DICOM_DEIDENT_METHOD_NOT_LO`, and is a different fallback with a different cause. Truncating your earlier de-identification records instead was refused deliberately: choosing which to drop is a policy the standard does not state. Reachable only from a `(0012,0063)` already at or within a few bytes of 65,534 - including one that needs no append at all, because it already records this run's method. The message carries no value and no length. If you need the full chain, read `(0012,0063)` off the SOURCE dataset before de-identifying. | | A `DICOM_DEIDENT_METHOD_NOT_LO` warning after `deidentify`, and the prior `(0012,0063)` text is gone | The source file encoded `(0012,0063)` under a Value Representation other than `LO`, so `deidentify()` replaced it rather than adding to it | The join that makes `(0012,0063)` a provenance chain concatenates `LO` values with the `5CH` delimiter, which is a text operation defined only for the VR the Data Dictionary gives that tag. Bytes under any other VR are not values it can join into (an `OB` or `UN` value is arbitrary octets), so guessing an encoding for them was refused: this library reports rather than invents. **This is the same loss as `DICOM_DEIDENT_METHOD_NOT_ADDED` for an unrelated reason**, which is why it is a separate code: one says the chain outgrew the VR, this one says the bytes were never in that VR to begin with. Every release before this one made the same replacement **silently**. A prior value that is empty or padding only raises nothing, because nothing was lost. The warning carries no value, no length and **no VR**: two bytes read out of a fabricated header are document content, not structure. | | A `DICOM_DEIDENT_METHOD_PRIOR_RETAINED` warning after `deidentify` | The source file already carried a `(0012,0063)` De-identification Method value, and it is in the de-identified output beside the one this run recorded | PS3.15 section E.1.1 says a de-identifier's method text is "inserted in or added to" that attribute, so keeping the sender's earlier record is the conformant act and this warning is **not** a defect report - it is a disclosure. `(0012,0063)` is **not in Table E.1-1**, so no rule in the run inspected, audited or redacted those bytes: if the sender wrote identifying text there, that text is in output stamped `(0012,0062) Patient Identity Removed = YES`. The warning carries no value, no length and no VR; `position.byteOffset` locates the element. It is **not** recorded on `report.retained`, which lists the Annex E option sets active for the run. **Read it as "bytes from the input file are in `(0012,0063)`", never as "the sender wrote something identifying"**: de-identifying an object this library already de-identified raises it too, because the prior value is then this library's own earlier record and nothing on the wire tells the two apart. If your source may put identifying text in `(0012,0063)`, read the attribute off the de-identified dataset and decide for yourself - this library will not remove an attribute no profile lists. | | A `DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH` warning after `deidentify` | The `(0012,0063)` De-identification Method this run wrote carries a Value longer than 64 bytes | PS3.5 Table 6.2-1 caps an `LO` **Value** at 64 characters, and `(0012,0063)` is `1-n`, so the bound is per Value and never on the whole Value Field: a field of 619 bytes made of ten Values of 61 is conformant. The text this library composes for itself is inside the maximum on all 512 option subsets, so a Value that is over came from one of the two places this run does not compose: **your own `deidentificationMethod`**, or a value **the source file already carried** and PS3.15 section E.1.1 obliges this to keep. Neither is shortened, split or truncated, because either would invent a de-identification record nobody wrote. **The likeliest writer of the second one is this library**: every object any published release de-identified without a caller-supplied method carries a 76-character Value, and re-de-identifying one keeps it, so expect this warning on anything de-identified before the default became multi-valued. Split your own method on `\` if a strict receiver is in your path, and read `(0012,0063)` off the output to decide what to do about a prior one. **The measurement is over bytes**, and a Value of 64 bytes or fewer can never hold more than 64 characters, so it cannot miss one that is genuinely over. It does over-report, on any conformant Value whose bytes outnumber its counted characters: PS3.5 section 6.2 specifies those lengths in characters rather than bytes, and excludes Code Extension escape sequences from the count. A multi-byte `(0008,0005)` repertoire and an ISO/IEC 2022 escape sequence both reach that condition. The message carries no value, no length, no count of how many Values are over and no origin, and `position.byteOffset` locates the prior element where the file carried one and is `0` when it did not, because on that route the over-long bytes are your argument rather than the file's. | | A `DICOM_DEIDENT_METHOD_CODES_PRIOR_RETAINED` warning after `deidentify` | The source file already carried Items in `(0012,0064)` De-identification Method Code Sequence, and they are in the de-identified output ahead of the codes this run added | PS3.15 section E.1.1 says codes from CID 7050 for the Profile and Options used "shall be added to" that attribute, so keeping a prior de-identifier's Items is the conformant act and this warning is a **disclosure**, not a defect report. Neither `(0012,0064)` nor the Code Value, Coding Scheme Designator and Code Meaning inside its Items has a Table E.1-1 row, so the bytes the sender wrote there are in output stamped `(0012,0062) Patient Identity Removed = YES` exactly as written, and **no prior code is checked against CID 7050 or any terminology**. This run appends only the codes no prior Item already records (the same Coding Scheme Designator and Code Value), so de-identifying this library's own output again with the same options raises this warning and adds nothing. A prior `(0012,0064)` with zero Items raises nothing. It is the `(0012,0064)` sibling of `DICOM_DEIDENT_METHOD_PRIOR_RETAINED`, which still means `(0012,0063)` only. The warning carries no value, no count and no VR; `position.byteOffset` locates the element. If your source may put identifying text there, read `(0012,0064)` off the de-identified dataset and decide for yourself: this library will not remove an attribute no profile lists. | | A `DICOM_DEIDENT_METHOD_CODES_PRIOR_REPLACED` warning after `deidentify`, and the prior `(0012,0064)` content is gone | The source file's `(0012,0064)` was not a Sequence of Items the parse produced: it was encoded under a Value Representation other than `SQ`, or it was an `SQ` whose items were not parsed | There were no Items to add this run's codes to, so `deidentify()` wrote `(0012,0064)` holding this run's codes alone; inventing Items out of bytes that are not Items was refused. This is the posture `DICOM_DEIDENT_METHOD_NOT_LO` takes for `(0012,0063)`, under its own code because the attribute differs. An `SQ` whose items were not parsed also raises `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE`, because `deidentify()` empties that carrier before this runs. A prior value that is empty or padding only raises nothing, because nothing was lost. The warning carries no value, no length and **no VR**: two bytes read out of a fabricated header are document content, not structure. If you need the replaced bytes, read `(0012,0064)` off the SOURCE dataset before de-identifying, and raise the file with the sender. | ## Keeping PHI out of logs **This section states a mechanism, not a verdict.** The verdict form of it was written two ways in two releases - "safe to log whole on any well-formed file", then "safe on a well-formed file and not unconditionally safe" - so it is deleted rather than given a third wording. What follows is what the code does, and you decide. A Tier-2 warning's `message` comes from a frozen registry keyed by the code, and the only things substituted into it are structural. No warning factory accepts a string read out of the document. Each substitution is bounded a different way, and the three ways are not interchangeable: - **`{tag}` is a MEMBERSHIP test.** It renders a tag only when PS3.6's element registry carries a **literal row** for it, and `` otherwise. A repeating-group family row does not count: `(50xx,xxxx)` Curve Data leaves the whole 16-bit element number free, so admitting a family match would admit 16 x 65,536 tags whose free bits are raw document bytes. - **`{vr}` is a membership test** against the 34 VRs PS3.5 2026c §6.2 defines. - **A raw number a header carries has neither a shape nor a membership to test, so where it is bound at all the bound is the absence of the slot.** There is no `renderLength` and there must not be one. `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_NONZERO_RESERVED_BYTES` and `DICOM_PIXEL_DATA_LENGTH_MISMATCH` cannot be handed one. **And a raw number SHIFTED by a constant the reader can compute is that raw number**, because an addition puts it back, so a bound on a rendering's magnitude bounds nothing about its content: `DICOM_ITEM_CROSSES_SEQUENCE_END` printed the bytes that remained inside the sequence, which is the sequence's own declared Value Length less the bytes of that sequence already consumed. It cannot be handed either number now. `w.code` and `w.position` carry nothing from the document. **The exceptions are named in ONE place that is not a record of a past change, and are deliberately not restated here** - the `WARNING_MESSAGES` docblock in `src/parser/warnings.ts`. No count of the copies is quoted, here or there: this package deletes a count it has corrected twice rather than incrementing it. **What that closed, measured rather than argued.** An `ST` carrying `"MR BRAIN SMITHSON "` that under-declares by 12 desynchronizes the **Explicit VR LE** reader onto a fabricated header whose declared length is odd. In an earlier release `DICOM_ODD_LENGTH_VALUE_PADDED` rendered four bytes of the name as its tag (`4E495320`, `"IN S"` in wire order) **and four more as its decimal length** - eight payload bytes in one message, each reversible with one typed read. On six other under-declare deltas `DICOM_NONZERO_RESERVED_BYTES` printed two more bytes of the same name as two decimals, a shape no detector in this repo had ever looked for. Both are closed. **What it costs you, stated rather than minimised.** A message about a **private** element, a **Group Length** `(gggg,0000)`, or a **repeating-group member** such as `(6000,3000)` Overlay Data no longer names its tag, on any file. The element is still in the object under that tag, and `position.byteOffset` locates the header. **Which channel carries a message is still part of the answer.** The fixtures that produce a desynchronized read mostly die before a `Dataset` exists, so their messages reach you through `onWarning` or through the `{ strict: true }` `DicomParseError` rather than a surviving `ds.warnings`. That no measured fixture put one on a surviving `ds.warnings` is a fact about those fixtures, not a promise about the parser - review all three channels the same way. **Several codes take no tag parameter at all, which is a stronger bound than any renderer. No count is written here; the factories that take one say so in their own JSDoc.** `DICOM_PRIVATE_TAG_NO_CREATOR`, `DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR` and `DICOM_PRIVATE_CREATOR_UNKNOWN` fire only on an odd group, which no closed table this library holds can vouch for. `DICOM_NONZERO_RESERVED_BYTES` and `DICOM_DUPLICATE_TAG_IN_DATA_SET` fire where the header may not be a header at all. `DICOM_GROUP_LENGTH_IN_DATASET` joined them for a third reason: PS3.6 carries exactly one literal row ending `0000`, and it is File Meta, so its slot could never render anything but ``. A slot that cannot render is still a slot a future call site can be handed. The element is still in the object under its tag, and `position.byteOffset` locates the header. Two things that array does **not** cover: - **A `DicomParseError` carries a `snippet`**: up to 16 bytes of the source as hex. Those are raw input bytes and the library does not redact them. Log `err.code`, `err.byteOffset` and `err.message`; treat `err.snippet` as PHI. **`{ strict: true }` turns every Tier-2 warning into one of these**, so the frozen-registry guarantee above covers the `message` and nothing else on the strict path. The snippet is cut **in the frame its `byteOffset` is counted in**, so it is the bytes at the offset the error names: file-absolute at the root, relative to the enclosing slice inside a defined-length Sequence or Item, into the inflated stream under Deflated Explicit VR LE. It was not always, and the difference matters if you have logs from an earlier release: the cut used to be taken from the whole file at an offset that had already moved with the frame, so an escalation raised inside a defined-length Item returned **an unrelated element's** bytes. Review the two paths separately; a message-only review of the lenient path does not transfer. - **A Tier-3 fatal's `message` is registry-composed too.** It is looked up in a second frozen registry, and the factories that build one **take no tag and no wire-length parameter**, so neither can be interpolated: on the desynchronized reading those are four bytes out of somebody's value. **They are withheld on the other reading too, and the reason is not that the fatal fires only when a length field is lying.** It does not: a spec-clean object cut short by two bytes raises `ELEMENT_LENGTH_EXCEEDS_BUFFER` with every declared length in the file honest, and a file cut short inside its File Meta group raises `FILE_META_GROUP_LENGTH_OVERRUNS` with `(0002,0000)` untouched. The number is four bytes a sender wrote in either case, which is bound enough. `err.byteOffset` locates the element instead. A VR still reads, when it names one of the 34; anything else renders ``. - **⚠ Two of those messages lost their remaining-bytes count, so a string match on them stops matching.** `ELEMENT_LENGTH_EXCEEDS_BUFFER` and `FILE_META_GROUP_LENGTH_OVERRUNS` printed how many bytes remained in the buffer; they do not now, and the factories take no parameter for it. A defined-length Sequence Item is parsed from a **slice**, so inside one that count is the Item's own declared length less an offset the message publishes, and an addition puts it back. `err.code` is unchanged and which files throw is unchanged. - **⚠ And every Tier-3 message's suffix changed, from `(offset=N)` to `(offset=N frame=F)`.** An offset is not a locator until you know where its zero is, and `err.byteOffset` had become the sole locator on the two messages above. `err.offsetFrame` names the coordinate system from a closed three-entry set (`OFFSET_FRAMES`: `"input"`, `"inflated-dataset"`, `"value-slice"`), and **only `"input"` means the number indexes the buffer you passed in.** The same defect in the same file reports `0`, `24` or `40` depending on where inside a Sequence Item the offending element sits. Where a slice **begins** is deliberately not published: the distance between two frames is a declared Value Length off the wire. If you have code that cuts your own copy of a file at `err.byteOffset`, gate it on `err.offsetFrame === "input"`; before this existed it was reading an unrelated element whenever the fatal came from inside an Item. **`DicomParseWarning.position` is NOT framed this way** - it carries only the `deflated` flag - and `Element.byteOffset` is not framed at all. Both are unchanged. - **Value-decode deviations do not appear on `ds.warnings`.** Decode is lazy, so a `DA` in a legacy format or a `UI` with the wrong pad surfaces on the decoded value's own `warnings` (`el.value.warnings`), never folded into the frozen dataset array. Those messages are built from the same registry and are equally safe, but a logger that only reads `ds.warnings` will not see them at all. A `DeidentifyReport` is **not** safe to log whole, and the exceptions are listed rather than counted: the count read one, then two, then three, and was wrong each time. `uidMap`'s keys are the source UIDs read out of the file: they are there so UID replacement stays consistent across a study, and a study UID is a unique identifier. `removedPrivateTags` is composed from tag numbers, which are structural on every conformant file but are read out of the document, so a **fabricated** odd-group header makes four bytes of somebody's value into a tag string there. And `unauditableSequences[].tag` joins them, for the same reason by a narrower route: a private carrier a `Profile` vouched for and this run emptied is named there, and a length under-declared upstream can resynchronize the reader onto four bytes that spell such a block. **`unenumerablePrivateRemovals[].tag` joins them too**, by the identical route on the removal record rather than the emptied one, and it is **uncapped**, because it records an action rather than a diagnostic. Each entry there carries `tag`, `applied`, `reason` and an unbounded `contextPath` when the instance was inside a sequence item (omitted at the root). Sending a report to a log that leaves the box is a decision to make per field, and this list grew. The package normally answers a fabricated header with `undefinedVrElements`, which carries a byte offset and **no tag**, and it still does whenever the fabricated VR is not one of the 34 PS3.5 §6.2 defines. It cannot when the fabricated VR is one of them, because a fabricated `OB` header and a genuine one are byte-identical, and no bound can tell them apart. The alternative was keeping that carrier verbatim, which is what earlier releases did and what shipped the nested value itself, so this is the better of the two. **`undefinedVrElements[].byteLength` and `unauditableSequences[].byteLength` joined the list** when the `DICOM_DEIDENT_*_NOT_AUDITABLE` messages stopped rendering it: the number is `Element.rawBytes.length`, which equals the declared Value Length off the element header, so on a fabricated header `"SO\0\0"` publishes `20307`. Moving it off the message left these fields its only publisher, which is a smaller surface and not a closed one - a bound here would empty the field on every well-formed file, where it is the audit number the field exists to carry. `embeddedAttributes[].hidden` **used to join them and no longer does**: an entry is now a tag this run's own resolved Annex E action fired on that has a **literal row** in PS3.15 Table E.1-1, of which there are 653 - not any four bytes the scanner tiled over. **A repeating-group mask hit is excluded on purpose**, because `(50xx,xxxx)` Curve Data leaves the whole element number free and a mask match proves a rule exists without making the membership finite. Two consequences to read with it - the list can be **empty on a real finding** (a run whose only actionable members are private attributes, Curve Data or Overlay elements names none of them, and the carrier was still emptied), and it is **still uncapped**. And `contextPath` does still join them, **on every finding that carries one** - a segment is `TAG[index]` and the tag half is read off the wire with nothing behind it, so a fabricated `SQ` header the reader was desynchronized onto is named there. Measured on a synthetic `LO` carrier holding `MRS BRAIN SMITHSON` that under-declares by four: the report reads `contextPath: ["53484E4F[0]"]`, which is `HSON` in wire order, with **no warning at all and every finding array empty**. Both are `PRE-EXISTING` on every release that has shipped the field, and both are named here so nobody logs them in the meantime. **But redacting `contextPath` is a logging fix and not an object fix** - on that same file the de-identified object still carries the fabricated `(5348,4E4F)`, so the serializer writes `HSON` back out under a `Patient Identity Removed = YES` stamp. That re-emission is the under-declared-carrier class in the row above, not this field; neither one bounds the other. **That fixture no longer reproduces either half**: `(5348,4E4F)` has no row in this build's PS3.6 registry, so `deidentify()` now removes the fabricated Sequence whole instead of descending it (see [Attributes neither table carries](https://docs.cosyte.com/dicom/deidentification.md#attributes-neither-table-carries)). That is a fact about that shape and not a bound on the field: a Sequence the run does descend still contributes whatever tag the wire gave it. What is left over - keywords, action codes, applied outcomes, repeating-group masks, and the attribute tags, which are bound to tags Annex E carries a row for - is composed from static tables and carries nothing. The field-by-field split between identifiers and values is in [Tolerance](https://docs.cosyte.com/dicom/spec-notes-tolerance.md#the-model-fields-that-are-bounded-and-the-ones-that-are-values). Keep the same discipline in your own code: log `w.code` and `w.position`, not the element value. ## The boundaries behind these symptoms Depth tracks the code and never leads it. Each subsection below explains why a symptom above is a boundary rather than a bug, in enough detail to act on. **The permanent non-goals are not restated here.** Pixel decode, DIMSE networking, DICOMweb, pixel scrubbing and the rest are enumerated once, with the companion package each is deferred to, on [Known limitations](https://docs.cosyte.com/dicom/limitations.md). What follows is the narrower question a symptom raises: given that this package reads and writes metadata, where inside the metadata surface does it stop. ### Boundaries within the metadata surface - **Four native transfer syntaxes and the section A.4 ones read and written.** Implicit VR LE, Explicit VR LE, Explicit VR BE, and Deflated Explicit VR LE are read and written. Every encapsulation syntax PS3.5 2026c section A.4 names (JPEG, JPEG-LS, JPEG 2000, HTJ2K, RLE and the rest) is read and written under Explicit VR LE rules; the writer refuses with `INVALID_ENCAPSULATED_PIXEL_DATA`, rather than repairing, a top-level Pixel Data section A.4 does not allow, a stream read with `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` included. A compressed pixel stream is never decompressed: the writer passes its fragments through byte-for-byte, and `readPixelDataFragments` hands them back as raw bytes. - **The four JPIP Referenced syntaxes are read, never written or de-identified.** PS3.5 2026c sections A.6 and A.11 are read under Explicit VR LE rules, and sections A.7 and A.12 are inflated first; the Pixel Data Provider URL `(0028,7FE0)` comes back as the `UR` element the file carries and is never fetched. `serializeDicom` refuses all four with `UNSUPPORTED_TRANSFER_SYNTAX`, and `deidentify()` refuses them too, throwing a `DeidentifyError` that carries the `UNSUPPORTED_TRANSFER_SYNTAX` code. See [Known limitations](https://docs.cosyte.com/dicom/limitations.md). - **A DICOMDIR's offsets are rewritten, or the write is refused.** `serializeDicom` writes each Directory Record offset as the byte offset of the record it named when read, and refuses with `DIRECTORY_OFFSET_UNRESOLVED` an offset it cannot tie to a record (one that raised `DICOM_DIRECTORY_OFFSET_UNRESOLVED` or `DICOM_DIRECTORY_OFFSET_MALFORMED` on the parse, for one) and with `DIRECTORY_OFFSET_DEFLATED` a Deflated DICOMDIR carrying a non-zero offset. See [Known limitations](https://docs.cosyte.com/dicom/limitations.md). - **Only typed `FileMeta` fields round-trip.** `serializeDicom` recomputes a spec-clean File Meta group; File Meta elements outside the typed model are not preserved verbatim through the model. - **De-identification is metadata-only and fail-safe toward removal.** Conditional Annex E codes collapse to their most-protective branch (no IOD Type-1 analysis); private attributes are removed by default unless a profile marks a creator's tags safe. - **An element that over-declares its own Value Length hides the next element inside its value, and `deidentify()` empties rather than keeps it.** PS3.5 defines Value Length as the length of _that_ element's Value Field; a sender that writes a larger number produces a file whose reading is self-consistent and in which the following element has been absorbed, header and all. Nothing on the wire says which length lied, so `parseDicom` reads it exactly as written and this is **not** recoverable at parse time. What `deidentify()` does is narrower and fail-safe: before keeping a value, it checks whether the value's tail decodes - in the file's own transfer syntax - as whole Data Elements ending exactly at the end of the value, at least one of which this run would have acted on, and containing a byte the carrier's VR cannot legally hold (PS3.5 §6.1.3 and Table 6.1-1 permit five control characters in DICOM text; Table 6.2-1 decides which of them each VR may hold - all five in `LT`/`ST`/`UT`, ESC only in `LO`/`SH`/`UC`/`PN`, none elsewhere). If so the value is emptied, `report.embeddedAttributes` records the carrier and the tags found inside it, and `DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED` is raised. **Carriers are string VRs only, and the gap is live rather than theoretical**: the identical over-declare into an `OB`, `OW`, `UN` or `US` carrier still writes the identifier into de-identified output with no warning and no report entry. That is measured and still exactly true for this route, which is every default `deidentify()` run. The only carrier that gets a diagnostic is one that is itself a private attribute reached through `RetainSafePrivate` plus a `Profile`, and what such a diagnostic says is "this value was not enumerated, so the attribute was removed" or "this value was dropped" (emptied, where the profile declares it `SQ`), never "a swallow was detected here". Arbitrary bytes are what those VRs are for, so no content test can tell a swallow from a legitimate value there. Treat a file that raised this warning as a file whose _sender_ is malformed: other attributes in it may be carrying the same defect where it cannot be seen. - **An unrecognized Explicit VR is read long-form, and `deidentify()` still empties the element.** Two separate rules, one clause. PS3.5 2026c §6.2 says every VR defined in a future edition "shall be of the same Data Element Structure as defined in [§7.1.2] with reserved bytes after the VR and a 32-bit unsigned integer VL". So when the two on-wire VR bytes are not one of the 34 this release knows, `parseDicom` reads a **12-byte header**, exactly as it does for `OB` or `UT` - and `serializeDicom` writes one. An earlier release read an 8-byte header, which took the length from the two bytes §6.2 reserves. What such a file did then depended on its own payload bytes - sometimes a whole-object refusal, sometimes a clean parse into a tree the sender did not write - so there is deliberately no one-sentence account of it here; `scripts/measure-unrecognized-vr.ts` in the repository prints the per-shape table for both readings. The value is otherwise treated like any other: a declared length past the end of the buffer, or an undefined length, is refused the same way it is for every non-`SQ` VR. **The de-identify rule is unchanged and still fires**, because reading the header is not the same as knowing what the value means: Table E.1-1 acts per attribute, and nothing can say whether a VR from a later edition holds a name, a date or an opaque blob. So `deidentify()` empties the element, `report.undefinedVrElements` records it, and `DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` is raised. The record is capped at 64 entries; the emptying is not. **On a conformant future-VR file that is a real cost** - a legitimate value is destroyed because this release cannot classify it - and it is the same over-redaction trade the sequence rule below makes. **The finding names a byte offset and no tag, uniquely among the report's findings, and that is deliberate.** The header may still have been fabricated out of the middle of some element's value: bytes that happen to form a complete long-form header tile just as readily as short-form ones did, and on a carrier whose payload is `"MR BRAIN SMITHSO"` the fabricated tag renders as `48544F53`, four letters of the surname. An unrecognized VR written honestly raises the same code with an ordinary tag, and **nothing here can tell the two apart**, so the tag is withheld on both routes rather than on a guess. The byte offset locates the element instead and is a position the parser counted. An Implicit VR LE file cannot reach any of this: there the VR comes from the dictionary. `UN` is one of the 34 and is unaffected. **There is no exemption**: unlike the sequence rule below, `RetainSafePrivate` plus a `Profile` does not keep such an element. If the element previously appeared in `report.embeddedAttributes` (an undefined-VR carrier whose bytes happened to tile as Data Elements), it now appears in `report.undefinedVrElements` instead. - **A sequence `deidentify()` could not open is emptied, not kept, so expect data loss on a malformed file.** Recursion is driven by `Element.items`. An `SQ` element that has none is un-auditable: PS3.5 §7.5.1 says its value is "a DICOM Data Set composed of Data Elements", and PS3.15 §E.1.1 obliges an implementation claiming the Basic Profile to protect the listed attributes "whether contained in the top level Data Set or embedded in an Item of a Sequence of Items". A run that cannot enumerate them cannot discharge that, so it discharges it on the carrier: the element is replaced with a zero-item sequence, `report.unauditableSequences` records the tag and the byte length dropped, and `DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE` is raised. That record is **capped at 64 entries** so a crafted file cannot amplify a per-element diagnostic. The unenumerable-removal warnings below are budgeted on their own counter against the same cap, so neither class can spend the other's. The emptying itself is never capped, so 64 of them means "at least 64". Before this, those bytes were written into de-identified output verbatim, identifiers and all, with a clean report. The accompanying `DICOM_SQ_NOT_DESCENDED` on `ds.warnings` says why the parse refused, and that is usually a sender-side encoding defect worth raising with them. **It is not always the sender**: a conformant file whose sequences nest deeper than this library's own `NESTING_DEPTH_LIMIT` of 64 is refused the same way and loses that sequence too. **A private `SQ` kept under `RetainSafePrivate` plus a `Profile` was exempt in earlier releases and is not any more**. It was kept verbatim and unaudited because the profile vouched for the element and this rule ran after that decision. The profile's licence under PS3.15 §E.3.10 is over a **Private Attribute**, not over a Data Set nested in its value, so a vouched-for private `SQ` now takes the same two branches every other `SQ` takes: emptied and recorded when its items were never materialized, walked when they were. **A private carrier the profile declares `SQ` is emptied even when the parse tree calls it something else** (`DICOM-PRIVATE-SQ-PARSE-VR`, closed since). The parse tree and the profile disagree about the same bytes in two ordinary situations: under Implicit VR LE a private tag carries **no VR on the wire**, so `SQ` there is an inference made **at parse time** and a profile passed only to `deidentify()` leaves the element `UN`; and under Explicit VR the wire's VR wins, so a sender who writes the attribute `OB` or `UN` yields that even on a fully conformant file with an honest Value Length. In both cases the profile that retained the element has told us its value is a Sequence of Items, there is no item stream on the tree to walk, and the carrier is emptied and named in `report.unauditableSequences`. It keeps the VR the file carried; nothing is re-typed to `SQ`. **On the Explicit VR shape `ds.warnings` is empty**, so the report is the only channel. **That naming is one of the report fields that is not value-free**, listed above, and this is why: the tag it records can itself be four bytes of another element's value. **Pass the same profile to `parseDicom` as well as to `deidentify()`** and the sequence is walked and its non-PHI content retained, rather than dropped. **One shape is still exempt and still leaks.** An undefined-length `UN` value the CP-246 descent could not read as a sequence keeps `vr === "UN"` and raises nothing beyond a possible `DICOM_VR_MISMATCH`. The rule cannot be extended to `UN` in general, because every ordinary `UN` element also has `items === undefined` and applying it there would empty every unknown-VR element in every file. **Where a `Profile` does declare that private attribute `SQ`, the rule above reaches it** and empties it: that test does not look at the length field, so this residual is about elements no profile named. **And the limit on that closure is not what an earlier wording of this paragraph said - the two sets are incomparable, not nested - so that wording is deleted rather than adjusted again**: the rule empties what a `Profile` declares `SQ`, and **every other declaration falls through to the removal rule in the next bullet**. This is measured as a matrix rather than listed here, because the list has been wrong twice. **Pass your profile to `parseDicom` as well** and a vendor sequence is walked instead of guessed at. So the reliable test is still `el.items === undefined`, and a report is a record of what was reached, not a proof that everything was. - **A private attribute your profile vouched for is REMOVED when this run did not enumerate its value, and `report.unenumerablePrivateRemovals` says so.** PS3.15 §E.3.10 retains Private Attributes "known by the de-identifier to be safe from identity leakage" and requires that "all other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**"; `(0008,0307)` is not implemented here, so removal is the branch available. Enumeration is one of exactly four things: the run **walked the value as Data Elements** and put each through the action table, the whole value **was matched as a member of your profile's private dictionary** (a Private Creator), the value is **zero-length**, or the **file itself declared it safe** - a block whose Block Identifying Information Status `(0008,0303)` reads `SAFE`, or an element a `MIXED` block lists in Nonidentifying Private Elements `(0008,0304)`, both inside Private Data Element Characteristics Sequence `(0008,0300)`. 🩺 **That fourth one is the sender's assertion and not this run's finding**, and it needs no profile: the value is retained unexamined because the system that wrote the file said the block carries no identifying information. If you do not trust the sender, do not pass `RetainSafePrivate`. **Decoding a value under the VR your profile declares for it is not enumeration**, and neither is the embedded-attribute scanner's silence: that scanner reads string carriers only and decodes tiles in the file's own encoding, so a nested Data Set written in another transfer syntax passed it untouched on a perfectly scannable `LO` carrier - measured. **So this is over-redaction and it is most of the option**: an ordinary vendor scalar under an ordinary string VR that the file's own declaration does not name safe is removed. Each removal is recorded per instance (tag plus the Data Set it lived in, `applied: "removed"`, `reason: "unenumerable"`), named in `report.removedPrivateTags`, and warned under `DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE`. **The record is complete and never capped; only the warnings are bounded**, so past 64 of them the record is still the whole account. In earlier releases the value was kept verbatim under `(0012,0062) = YES` and only disclosed, and `report.unauditableSequences` carried it under its retired `kept` outcome, which no longer exists on that field. - **A private value vanishes under `RetainSafePrivate`, and `report.removedPrivateTags` names it.** The file's own length fields contradict each other about where a Sequence Item ends: its `(FFFE,E000)` Item declares more bytes than the enclosing `SQ` element's Value Length allows, so the Item reads past the end of its sequence and absorbs the element that followed it. PS3.5 2026c §7.8.1 scopes a Private Creator's block reservation to the Data Set it appears in and an Item is its own Data Set ("The scope of the reservation is just within the Item. Items do not inherit the Private Data Element reservations made by Private Creator Data Elements in the Data Set in which the Item is nested"), so on such a file **which Data Set a private element is in is not determined by the file**. PS3.15 2026c §E.3.10 retains only Private Attributes "known by the de-identifier to be safe from identity leakage" and requires that "all other Private Attributes shall be removed **or processed in the element-specific manner recommended by Deidentification Action (0008,0307), if present within Private Data Element Characteristics Sequence (0008,0300)**". That clause licenses two dispositions; this library does not implement `(0008,0307)`, so `deidentify()` removes every private element **it reaches** in that Item and at every depth below it, and records each in `report.removedPrivateTags`. Since `DICOM-PRIVATE-SQ-CARVE-OUT` that reach includes the items of a private `SQ` the profile vouches for, which used to be settled before the descent and kept verbatim with nothing inside it examined. **This is the fix for a leak measured on a published tarball** and needing the opt-in `RetainSafePrivate` plus a vendor profile: a private value the sender wrote **outside** the sequence was retained on the Item's reservation, `report.removedPrivateTags` read `[]`, and the object was still stamped `(0012,0062) Patient Identity Removed = YES`. **The mirror direction is also refused, since `DICOM-ITEM-EJECT-ROUTE`.** An Item that _under_-declares **ejects** its trailing elements out into the enclosing Data Set, where a Private Creator that lands there would otherwise vouch for elements the sender never reserved. So a Data Set retains nothing private that it holds **after** a sequence whose own contents contradict the extent it declared, at every depth and not only at the root. A reservation the sender wrote **ahead of** that sequence is untouched. Two shapes of the same contradiction are covered, because the parser records them differently: an item stream that over-runs the sequence under Explicit VR, and a sequence the parser could not walk at all (`DICOM_SQ_NOT_DESCENDED`) under Implicit VR LE. **A Data Set is a `Map`, so an ejected element whose tag it already holds overwrites in place**; the rule checks `Element.byteOffset` as well as position for that reason. **That overwrite destroys the Data Set's own element** before `deidentify()` ever runs, silently and with no report entry, and this release does not change that: if you need the original, the fix is at the sender. **One shape is still not covered and still leaks**, `PRE-EXISTING` and pinned by a test: a **private `SQ`** a profile vouches for is kept verbatim before the descent runs, so nothing inside it is examined. For it, `report.removedPrivateTags` reading `[]` is not proof that nothing was retained. **`ds.warnings` is not the audit channel for any of these shapes.** Where a `DICOM_ITEM_CROSSES_SEQUENCE_END` warning does fire it means the file's structure is in dispute, so it never says the retention was audited and is never an all-clear about what was retained. If you expected those private attributes and need them, the fix is at the sender: an Item's declared length and its sequence's declared length have to agree. **It costs content on files that lie**: where the creator and the data element were both genuine Item content the reservation was real, and it is removed anyway, because nothing on the wire distinguishes that case. A file whose length fields agree is unaffected, whatever they say. - **Repeating-group rows are matched by mask, within the range the standard bounds them to.** `(50xx,xxxx)` Curve Data, `(60xx,3000)` Overlay Data and `(60xx,4000)` Overlay Comments are stated by the standard as a group mask rather than a single tag. `deidentify()` matches them in the sixteen even groups PS3.5 defines (`6000`-`601E` for overlays, `5000`-`501E` for curves), removes them, and records them in the report with a `repeatingGroup` field naming the mask that matched. Even groups above the bound and odd groups are not overlay or curve groups and are left alone; odd groups are private and go through the private-attribute path instead. - **`RetainLongitudinalTemporal` means the standard's full-dates option, the less protective one; `RetainLongitudinalTemporalModifiedDates` is the other.** PS3.15 §E.3.6 defines two longitudinal-temporal Options and Table E.1-1 gives them separate columns. The first name carries the **full-dates** column, so wherever the two columns disagree you keep the real value where modified-dates would have cleaned it; the second carries the modified-dates column and cleans it. They are **mutually exclusive** and a call naming both is rejected with a `DeidentifyError`. Activate full dates only when real dates are genuinely required. **Neither option shifts a date for you: this library performs no date transformation at all**, so the modification §E.3.6 asks for on the modified-dates branch is yours to do. - **The de-identified object records which of those states it is in, and you read it off the object rather than asking the sender.** `deidentify()` writes `(0028,0303) Longitudinal Temporal Information Modified` on every run: **`UNMODIFIED`** when `RetainLongitudinalTemporal` was active, **`MODIFIED`** when `RetainLongitudinalTemporalModifiedDates` was, **`REMOVED`** when neither was, per PS3.15 §E.3.6 and §E.2. A `REMOVED` and an `UNMODIFIED` on an object with no dates in it mean different things and both are useful: the first says this run was not permitted to keep dates, the second says any dates present are real. **It is replaced rather than added to** (the attribute is `VM 1`), so a value the source file carried is gone from the output and the state you read is this run's. **A `MODIFIED` says the modified-dates column was resolved, not that this library transformed a date** - every such run raises `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on `report.warnings`, and [Known limitations](https://docs.cosyte.com/dicom/limitations.md) has the whole residual - so if you shift dates yourself after a `RetainLongitudinalTemporal` call, the `UNMODIFIED` in your output is wrong for your object and overwriting it is your job. **Read the top-level Data Set's `(0028,0303)`, never one nested in a Sequence Item**: the attribute has no row in Table E.1-1, so a nested copy is retained by omission and still says whatever the sender wrote. ## Where to go next - A symptom you could not find above, or one you want the boundary for rather than the fix: [Known limitations](https://docs.cosyte.com/dicom/limitations.md) owns the scope non-goals, the open PHI residuals, the structural facts no reader can resolve, and the list of what is never defaulted. - What a diagnostic may and may not carry, field by field: [Tolerance & warnings](https://docs.cosyte.com/dicom/spec-notes-tolerance.md). - Install prerequisites and the release ladder: [Installation](https://docs.cosyte.com/dicom/installation.md). - The full public surface and the exact fields each view decodes: the package's `README.md` and [The object model](https://docs.cosyte.com/dicom/spec-notes-model.md). --- URL: https://docs.cosyte.com/dicom/limitations Markdown: https://docs.cosyte.com/dicom/limitations.md # Known limitations: the "do not over-trust" list Read this page before you put `@cosyte/dicom` in front of real data. Mis-reading a patient identifier, an image's signedness, or a rescale slope can cause real clinical harm, and a de-identifier that reports a clean run it did not perform is worse than one that refuses. This package is engineered to prevent over-trust, so the boundary is a **deliverable**, not a footnote: it is a page of its own, held to the same bar as the rest of these docs, and the introduction and the cookbook link into it from every route that depends on it. **This page is an index, not a census.** Each entry is one line plus a link to the section that owns it, and the owning section is where the measurements, the fixtures and the residuals live. Read the section before concluding a class is closed. Nothing here is a count: counts in this repository have been corrected twice and then deleted. --- ## 1. Scope: what v1 does not do at all These are non-goals, not gaps. Each is a companion package or another tool's job. | Not in v1 | Where it goes | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | **Pixel decode or decompression** in any transfer syntax, frame assembly from fragments, offset-table interpretation, rendering, windowing, or any measurement computed from pixels | `@cosyte/dicom-pixel` | | **Repairing a fragment stream on write.** `serializeDicom` writes each PS3.5 2026c section A.4 syntax and refuses a malformed top-level Pixel Data with `INVALID_ENCAPSULATED_PIXEL_DATA` | out of scope | | **Burned-in annotation removal / Clean Pixel Data.** A de-identified output from this package is **metadata-de-identified only**, and it warns rather than claiming otherwise | `@cosyte/dicom-pixel` | | **Networking.** No DIMSE: no C-STORE, C-FIND, C-MOVE, MWL or MPPS | `@cosyte/dicom-net` | | **Web services.** No DICOMweb: no QIDO, WADO or STOW | `@cosyte/dicomweb` | | **Transcoding.** The serializer re-emits in the dataset's source transfer syntax only | out of scope | | **Terminology resolution.** Coded values are surfaced with their designator and canonical source but are never validated, looked up, or cross-mapped; `SRT` is not normalized to `SCT` | `@cosyte/terminology` | | **Typed SR and RT models.** SR content trees and RT objects are navigable as raw structure, with no clinical model over them. Deliberately flagged and deferred rather than half-built | out of scope for v1 | | **Patient matching.** Patient ID is surfaced with its issuer; the library never decides that two identifiers are the same person | out of scope | | **A File-set view.** A DICOMDIR's Directory Record tree is read and its offsets rewritten on write (below), but no DICOMDIR is built from files and nothing is removed from a File-set | out of scope for v1 | Supported transfer syntaxes: the four native ones, Implicit VR LE `1.2.840.10008.1.2`, Explicit VR LE `...1.2.1`, Deflated Explicit VR LE `...1.2.1.99` and Explicit VR BE `...1.2.2` (retired, legacy-only), plus **every encapsulation syntax PS3.5 2026c section A.4 names**: JPEG Baseline, Extended, Lossless and Lossless First-Order Prediction; RLE Lossless; JPEG-LS Lossless and Near-Lossless; JPEG 2000 and HTJ2K; MPEG2; MPEG-4 AVC/H.264; HEVC/H.265 Main and Main 10; JPEG XL; Deflated Image Frame Compression; and Encapsulated Uncompressed Explicit VR LE. `Dictionary.uid()` names each UID. An encapsulated object is read under Explicit VR LE rules, as section A.4 requires, so its metadata parses in full and `readPixelDataFragments` hands back its Basic Offset Table and fragments as raw bytes. **Its pixels are never decoded.** `serializeDicom` writes each of those syntaxes back under the object's own UID, a de-identified object included: the Data Set as Explicit VR LE, and the top-level Pixel Data as `OB` of undefined length, its Basic Offset Table and fragment Items copied byte for byte and in order, then a Sequence Delimitation Item. Nothing is transcoded, re-framed or re-encoded, and no offset table is rebuilt. The writer's limit sits with it: a top-level Data Set section A.4 does not allow is refused with `INVALID_ENCAPSULATED_PIXEL_DATA`, never repaired (see below). The four JPIP Referenced syntaxes are read too, for metadata only (next paragraph). Every other registered UID stays the fatal `UNSUPPORTED_TRANSFER_SYNTAX`, named by its PS3.6 registry name: the SMPTE ST 2110 syntaxes and every retired UID, the retired JPEG processes included; `serializeDicom` refuses the same UIDs with its own `UNSUPPORTED_TRANSFER_SYNTAX`. Deflated Explicit VR LE deflates the whole dataset stream rather than the pixels, and it is inflated on parse. **The four JPIP Referenced syntaxes are read for metadata, and only read.** JPIP Referenced `...1.2.4.94` and JPIP HTJ2K Referenced `...1.2.4.204` (PS3.5 2026c sections A.6 and A.11) are read under Explicit VR LE rules, as those sections require. JPIP Referenced Deflate `...1.2.4.95` and JPIP HTJ2K Referenced Deflate `...1.2.4.205` (sections A.7 and A.12) are that same Data Set compressed per RFC 1951, so they are inflated first, under the decompression cap and the inflate fatals Deflated Explicit VR LE has. Such an object carries no Pixel Data: it references its pixels through Pixel Data Provider URL `(0028,7FE0)`, which `ds.get("00287FE0")` returns as the `UR` element the file carries, its decoded `value` the URL text with only its trailing padding removed. **The URL is never fetched, resolved or validated**, and nothing in this package opens a connection to the host it names. The limits sit with that capability: - **`serializeDicom` refuses all four** with `UNSUPPORTED_TRANSFER_SYNTAX` and returns no bytes. A JPIP object is read, never written. - **`deidentify()` refuses all four**: it throws a `DeidentifyError` carrying the `UNSUPPORTED_TRANSFER_SYNTAX` code, whatever options you pass, and returns no dataset and no report. `(0028,7FE0)` has no PS3.15 Table E.1-1 row, so a de-identified copy would keep the URL by omission, and whether that URL can carry identity is an open question this package has not ruled on. The refusal keys on the File Meta Transfer Syntax alone: a non-JPIP object that carries `(0028,7FE0)` is de-identified like any other and keeps it (see [De-identification](https://docs.cosyte.com/dicom/deidentification.md#scope-limits-on-this-pages-subject)). - **Three misuses section A.6 forbids parse without a code**, as the section A.4 ones above do: an object with no `(0028,7FE0)` (then `ds.get("00287FE0")` is `undefined`, never an empty string or a placeholder), one that carries a top-level Pixel Data anyway, and one whose Photometric Interpretation is outside the four Values section A.6 allows. No IOD or module validation is done. Three limits sit with that capability. A fragment stream that ends before its Sequence Delimitation Item still parses, with `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` saying the fragment list may be short. Two misuses of an encapsulation syntax that PS3.5 2026c section A.4 forbids parse **without** a code: an object with no top-level Pixel Data, and one whose top-level Pixel Data is native, for which `readPixelDataFragments` returns `undefined`, as it does under Explicit VR LE. And **the writer is stricter than the reader**: `serializeDicom` throws a `DicomSerializeError` with code `INVALID_ENCAPSULATED_PIXEL_DATA`, and returns no bytes, for every top-level Data Set under a section A.4 syntax that the section does not allow, rather than repairing it. That is no top-level Pixel Data, native top-level Pixel Data, Float or Double Float Pixel Data `(7FE0,0008)` / `(7FE0,0009)`, a fragment stream not ended by its Sequence Delimitation Item (so an object read with `DICOM_PIXEL_DATA_FRAGMENTS_NOT_DELIMITED` is read and not written, since a delimiter appended to a stream that may be short would be a well-formed file missing frames), anything other than an Item of defined length in the stream, an Item of odd length, a fragment Item of length zero after the Basic Offset Table (an empty Basic Offset Table is fine), and a stream with no Basic Offset Table Item or no fragment Item after it. Its message is a fixed string. Pixel Data inside a Sequence Item, such as an Icon Image Sequence, is written as read and not checked. **DICOMDIR: the record tree is read, and its offsets are rewritten on write.** For an object whose `(0002,0002)` is `1.2.840.10008.1.3.10` (Media Storage Directory Storage), `ds.directory` is a `DicomDirectory`: every Item of the Directory Record Sequence `(0004,1220)` as a `DirectoryRecord` with its `(0004,1430)` type, its Referenced File ID `(0004,1500)` components and the records its `(0004,1420)` offset names as lower-level, and the root entity `(0004,1200)` names. An offset resolves only when it is exactly where a record's `(FFFE,E000)` Item tag sits in the file, counted from the first byte of the File Preamble, which is how PS3.3's Basic Directory IOD defines these offsets (PS3.3 is not vendored here, so no clause is claimed for it); one that is not, a Value Length other than 4, and a record reached twice each raise a warning (`DICOM_DIRECTORY_OFFSET_UNRESOLVED`, `DICOM_DIRECTORY_OFFSET_MALFORMED`, `DICOM_DIRECTORY_RECORD_REVISITED`) and resolve nothing. `serializeDicom` writes `(0004,1200)`, `(0004,1202)`, `(0004,1400)` and `(0004,1420)` as the byte offsets of the records they named when the file was read, including after `deidentify()` has moved every record. The limits sit with it: - **Referenced File IDs are kept verbatim**: never de-identified, rewritten or re-pointed, by the reader, `deidentify()` or the writer. A path that carries identifying text carries it into the output. - **The two File-set clauses of PS3.15 §E.1.1 are not discharged.** No DICOMDIR is created from the de-identified files, and the non-de-identified DICOMDIR is not removed from its File-set; `deidentify()` raises `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED` on every DICOMDIR run. - **A DICOMDIR carrying an offset that names no record is refused on write**, with `DicomSerializeError` code `DIRECTORY_OFFSET_UNRESOLVED` and no bytes returned: an offset that did not resolve on read, a value that is not one 32-bit unsigned integer, and a non-zero offset on a `Dataset` whose Directory Record Sequence was removed or emptied, or was never there. It is never written stale (it would name the wrong bytes) and never written as zero (that prunes the tree). - **A Deflated DICOMDIR with records is refused on write**, with `DIRECTORY_OFFSET_DEFLATED`: a position inside a deflated stream names no Item a reader can seek to, so any non-zero offset is refused, and `parseDicom` resolves no offset of such a file and warns `DICOM_DIRECTORY_OFFSET_DEFLATED`, because PS3.10 requires a DICOMDIR File to use Explicit VR Little Endian. One whose offsets are all zero is written. - **Not checked:** record keys against PS3.3's Directory Record definitions, `(0004,1202)` against the end of the root chain, the File-set Consistency Flag, and Private Record UIDs. The retired MRDR offset `(0004,1504)` is written as read and not rewritten. An Implicit VR LE or Explicit VR BE DICOMDIR is read and written without a warning, although PS3.10 makes Explicit VR LE the only conformant syntax for a DICOMDIR File. --- ## 2. Open PHI residuals: measured, disclosed, and NOT closed **None of these is an all-clear, and a `DeidentifyReport` that reads clean does not by itself close any of them.** Each is either a product decision this package has deliberately not made, or a structural fact about DICOM that no reader can resolve from the wire. - **`RetainSafePrivate` now keeps only what this run could account for, and everything else private is REMOVED.** Two routes in the package write a private value into de-identified output: a `Profile` you pass, and the **file's own** Private Data Element Characteristics Sequence `(0008,0300)`, which needs no profile. Four classes of value take them: one the run **walked as Data Elements** and put through the Annex E action table (a private `SQ` whose items the parser materialized), a **Private Creator `(gggg,00EE)` whose whole decoded value is a member of your profile's private dictionary**, a **zero-length** value, which encodes no Data Set, and one **the file itself declares non-identifying** - a block whose Block Identifying Information Status `(0008,0303)` reads `SAFE`, or an element a `MIXED` block lists in Nonidentifying Private Elements `(0008,0304)`. 🩺 **The fourth class is the sender's word and not this run's finding, and that is the limitation rather than a feature note.** The value is retained unexamined because the system that wrote the file asserted the block is safe; an Item that does not resolve keeps nothing and says so under `DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED`, but a well-formed declaration is taken at its word. PS3.15 §E.3.10 offers exactly one mitigation and it is yours: if you do not trust the sender, do not pass `RetainSafePrivate`, and every private attribute is removed. Anything else a profile vouches for is removed, recorded per instance in `report.unenumerablePrivateRemovals` with `applied: "removed"` and `reason: "unenumerable"`, named in `report.removedPrivateTags`, and warned under `DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE`. **The removal record is complete and never capped** at any input size; only the warnings are bounded. 🛑 **The cost is over-redaction and it is nearly all of the profile route: an ordinary vendor scalar under an ordinary string VR that the file's own declaration does not name safe is removed.** Decoding a value under the VR your profile declares for it is not an enumeration of what the value encodes, and neither is the embedded-attribute scanner's silence: that scanner reads string carriers only and decodes tiles in the file's own encoding, so a nested Data Set written in another transfer syntax passed it untouched on a perfectly scannable `LO` carrier - measured, and the reason the predicate is what the run DID with the value rather than the VR or the scanner's reach. If you need those values back, that needs a content test separating a nested Data Set from a legitimate binary blob, which is an open product question and not a flag. Earlier releases **kept** this class verbatim under `(0012,0062) = YES` and merely disclosed it, which was a disclosure and not a fix. The surface it shipped through is **pinned as a measured matrix** in `test/integration/deident-private-reservation.test.ts` rather than described in prose, because the prose has been wrong twice. Detail: [Known limitations in the README](https://github.com/cosyte/dicom#known-limitations--non-goals). - **Two audit-contract changes came with that, and a consumer switching on either has to act.** `report.unauditableSequences` no longer produces its retired `kept` outcome for a retained private value - its `applied` field was `"emptied" | "kept"` and is `"emptied"` alone now, so an entry there means content is **not** in your output, which is what it meant before that second outcome joined it - and `DICOM_DEIDENT_PRIVATE_CARRIER_NOT_AUDITABLE` changes meaning from "this value was shipped unexamined" to "this attribute was removed unexamined". **No published warning code is renamed or retired**, so narrowing on that code name keeps compiling: re-read what it now means rather than re-typing it. **`DICOM_DEIDENT_PRIVATE_DECLARATION_NOT_RESOLVED` is added**, raised per Item of `(0008,0300)` that does not resolve to a block of private Data Elements reserved in that Data Set, so a declaration you expected to keep something and that kept nothing is never silent. The removal record (`report.unenumerablePrivateRemovals`) is a **new** report surface, and it is the only one that can tell you an attribute went for being unenumerable rather than by the action table. - **An over-declared Value Length into a binary carrier still leaks.** A length that swallows the following element into an `OB`, `OW`, `US` or `UN` value is not detected, and a `(0010,0020)` Patient ID inside it reaches de-identified output with **no warning and no report entry**. That is measured and still exactly true for this route, which is every default `deidentify()` run. The only carrier that gets a diagnostic is one that is itself a private attribute reached through `RetainSafePrivate` plus a `Profile` (the bullet above). A carrier reached through the file's own `(0008,0300)` declaration gets **none**: the declaration is what makes that value known safe, so the run keeps it rather than judging it, which is the sender-trust cost stated in that bullet. What the profile-route diagnostic says is "this value was not enumerated, so the attribute was removed" or "this value was dropped" (emptied, where the profile declares it `SQ`), never "a swallow was detected here". Arbitrary bytes are exactly what those VRs are for, so no content test can decide it. String carriers **are** covered, because there the same bytes are provably outside the VR's repertoire. If you accept files from a sender you do not control, treat a binary attribute's declared length as untrusted. - **`(0012,0063)` De-identification Method can carry a value longer than the VR permits, and this library is the likeliest writer of it.** PS3.5 2026c Table 6.2-1 caps an `LO` at 64 characters **per Value**, and `(0012,0063)` is `1-n`. Every object this package de-identified without a caller-supplied method string, in every release up to and including this one, carried a single Value longer than that, in the one attribute a strict receiver reads to decide whether the object was de-identified at all. The method text is multi-valued now, one Value for the Profile and one per active Option. **Re-de-identifying an object written by an earlier release keeps the over-long Value**, because a prior record is added to rather than rewritten, which is the conformant act (PS3.15 2026c §E.1.1: the method text is "inserted in or added to" the attribute). Your own `deidentificationMethod` is **not** bounded for you either: split it on `\` yourself if a strict receiver is in your path. - **`contextPath` on a `DeidentifyReport` finding is inert and is not structural.** It is not a key you can look anything up with, it is corroborated by nothing else in the report, and each segment's tag half is read off the wire with nothing behind it, so a fabricated `SQ` header the reader was desynchronized onto is named there. **It is not safe to log.** - **What a warning message may contain, as a mechanism rather than as a verdict.** The verdict form of this bullet ("safe on a well-formed file, not unconditionally safe") was corrected twice, so it is deleted rather than given a third wording. The registry's `{tag}` slot renders **only a tag PS3.6's element registry carries a literal row for**, and `` otherwise; `{vr}` renders only one of the 34 VRs PS3.5 2026c §6.2 defines; and **a raw number a header carries is bound out of the factory signature where it is bound at all**, because a declared Value Length has neither a shape nor a membership a renderer could test. `w.code` and `w.position` carry nothing from the document. **And a raw number SHIFTED by a constant the reader can compute is that raw number**, which is the eighth instance of this class: `DICOM_ITEM_CROSSES_SEQUENCE_END` printed the bytes that remained inside the sequence, and that count is the sequence's own declared Value Length less the bytes of that sequence already consumed. **The exceptions are named in ONE place that is not a record of a past change, and are deliberately not restated here** - the `WARNING_MESSAGES` docblock in `src/parser/warnings.ts`. No count of the copies is quoted, here or there: this package deletes a count it has corrected twice rather than incrementing it. **What that closed, measured on an `ST` carrying `"MR BRAIN SMITHSON "` whose Value Length under-declares:** under Explicit VR LE the reader desynchronizes onto a fabricated header whose declared length is odd, and in an earlier release `DICOM_ODD_LENGTH_VALUE_PADDED` rendered **four bytes of the name as the tag and four more as the decimal length** - eight payload bytes in one message. `DICOM_NONZERO_RESERVED_BYTES` printed two more as decimals on six other deltas. Both are closed. **What it costs, stated rather than minimised:** a message about a **private** element, a **Group Length** `(gggg,0000)` or a **repeating-group member** such as `(6000,3000)` Overlay Data no longer names its tag on any file, well-formed or not. The element is still in the Data Set under that tag, and `position.byteOffset` locates the header. **The channel still matters as much as the field.** The fixtures that produce a desynchronized read mostly die before a `Dataset` exists, so what carries their messages is `onWarning` and the `{ strict: true }` `DicomParseError` rather than a surviving `ds.warnings` - a fact about those fixtures and not a promise about the parser. Treat both channels the same way. Full treatment: [Keeping PHI out of logs](https://docs.cosyte.com/dicom/troubleshooting.md#keeping-phi-out-of-logs). - **Two carriers this list once named as open are now bound, and neither bound is an all-clear.** `DICOM_PRIVATE_TAG_NO_CREATOR`, `DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR` and `DICOM_PRIVATE_CREATOR_UNKNOWN` take no tag at all any more, because an odd group is the one class of tag no closed table this library holds can vouch for; the element is still in the object and `position.byteOffset` still locates it. `report.embeddedAttributes[].hidden` now lists only tags this run acted on that have a **literal row** in PS3.15 Table E.1-1 - 653 of them - rather than any four bytes a run tiled over. **A repeating-group mask hit is excluded and that exclusion is the whole bound**: `(50xx,xxxx)` Curve Data leaves the entire 16-bit element number free, so a mask match proves a rule exists without making the membership finite, and a draft that stopped at "an even group" was measured admitting `500C5241`. Two consequences: `hidden` can be **empty on a real finding**, which does not mean nothing was hidden, and it is still **uncapped**. `report.removedPrivateTags` is deliberately unchanged: narrowing it would empty the field on every well-formed file, which is what it exists to record. - **A standard attribute from a PS3.6 edition newer than this build is REMOVED, conformant or not.** `deidentify()` removes a non-private attribute that neither this build's PS3.6 2026d registry nor Table E.1-1 carries, because nothing on the wire separates a later edition's attribute from one a sender invented. The cost is content on a conformant newer-edition object, with no switch to keep it in this release; the removal is recorded by byte offset and never by tag. **It narrows the older gap and does not close it**: an attribute PS3.6 registers and the pinned Table E.1-1 does not list is still kept as the source wrote it. Detail: [Attributes neither table carries](https://docs.cosyte.com/dicom/deidentification.md#attributes-neither-table-carries). - **A de-identified DICOMDIR is NOT File-set conformant, and the run says so rather than implying otherwise.** PS3.15 §E.1.1's group-0004 bullet removes `(0004,xxxx)` from everything that is not a DICOMDIR, and this package does that; for an object whose `(0002,0002)` is `1.2.840.10008.1.3.10` it honours the carve-out and keeps them. Its Directory Records are de-identified as Data Sets, like any other Sequence Item, so a PATIENT record's Patient's Name and Patient ID are handled by Table E.1-1, and `serializeDicom` rewrites each record offset to where its record lands. The two File-set clauses are **not** discharged: no DICOMDIR is created from the de-identified files it references, and no non-de-identified DICOMDIR is removed from the File-set, because this library has no view of a File-set. Every such run raises `DICOM_DEIDENT_DICOMDIR_FILE_SET_NOT_DISCHARGED`, **including one where the object carried no `(0004,xxxx)` element at all**, because what was not discharged has nothing to do with what the object happened to carry. **Referenced File IDs `(0004,1500)` are kept verbatim**, so a File-set whose paths carry identifying text keeps it; the record keys are only what Table E.1-1 names. Build a de-identified File-set from the de-identified files, not from this output. - **`(0028,0303) = MODIFIED` is written, and this library performs no date transformation.** `deidentify()` writes `(0028,0303) Longitudinal Temporal Information Modified` on every run, in the three states PS3.15 2026c defines: **`REMOVED`** when no Retain Longitudinal Temporal Information Option was active, **`UNMODIFIED`** when `RetainLongitudinalTemporal` was, and **`MODIFIED`** when `RetainLongitudinalTemporalModifiedDates` was. §E.3.6 defines those last two as **mutually exclusive** Options, so a call naming both is rejected with a `DeidentifyError`. **The limitation is what `MODIFIED` does not mean here.** §E.3.6 has two halves: the object's dates "shall be modified", and "the manner of date modification shall be described in the Conformance Statement". This library delivers the half a library can: it resolves Table E.1-1's modified-dates column, which is the more protective of the two temporal columns wherever they differ, and it writes the declaration. It **performs no date transformation** at all, on either branch, and it states no Conformance Statement, because PS3.2 Annex N scopes one to a named product and version and a library is neither. **So you perform the date transformation, and a `MODIFIED` on an object whose dates nobody shifted is a caller defect this package cannot detect.** It is never silent about it: every run under that Option carries `DICOM_DEIDENT_DATES_NOT_TRANSFORMED` on `report.warnings`, naming the half of §E.3.6 the run did not discharge. **Nothing here claims PS3.15 Annex E conformance for the Retain Longitudinal Temporal Information With Modified Dates Option**, which §E.1.1 makes all-or-nothing. **If you shift dates yourself after the call under `RetainLongitudinalTemporal`, the `UNMODIFIED` in your output is wrong for your object**: use `RetainLongitudinalTemporalModifiedDates` instead or overwrite the attribute, and describe the manner of modification in your Conformance Statement, which §E.3.6 requires of anyone claiming that Option. **The declaration is the top-level Data Set's own**, which is where §E.2 and §E.3.6 put it: `(0028,0303)` has no row in Table E.1-1, so a copy the sender nested inside a Sequence Item is retained by omission like every other registered attribute the table does not list, and still says whatever that sender wrote. Read the Data Set's own `(0028,0303)`, never a nested one. - **A `DeidentifyReport` is not safe to log whole.** The value-bearing fields are named on the `DeidentifyReport` type. **Read the list on the type, never a count quoted anywhere** (including here): the count read one, then two, then three, and was wrong each time. `(0028,0303)` is **not** on it either: like `(0012,0062)`, it is a statement the object makes about itself, and the report's shape is unchanged by it. - **A `DicomParseError` is not safe to log whole.** It carries `snippet`, up to 16 raw source bytes as hex, and the library does not redact them. Log `err.code`, `err.byteOffset`, `err.offsetFrame` and `err.message`. `{ strict: true }` turns **every** Tier-2 warning into one of these, so a PHI review of the lenient path does not transfer to the strict one. --- ## 3. Structural facts that no reader can resolve These are not defects and they will not be fixed, because the information needed to fix them is not on the wire. - **An over-declaring element and a well-formed one are byte-identical.** Intent is not encoded. Any remedy therefore lives at the de-identify boundary or is a warning, never a parser bound. - **A Data Set is a `Map`, so a repeated tag loses a value.** The last read wins and nothing is guessed for the one it replaced; `DICOM_DUPLICATE_TAG_IN_DATA_SET` reports the loss. The File Meta group loses a repeat the **opposite** way round, first-match wins, reported by `DICOM_DUPLICATE_FILE_META_ELEMENT`. Neither can fire on a conformant file. - **The writer orders what it can walk, and only that.** A tag repeated inside an Item is written twice, a Sequence that cannot be walked, is nested past `NESTING_DEPTH_LIMIT` or whose parsed `items` do not match its bytes and any `UN`-carried Sequence are written as read, an element whose own bytes do not show where a reader ends it is written after the ascending rest of its Data Set whatever its tag, and an element a lying length relocated stays where it was placed. See [Serialization](https://docs.cosyte.com/dicom/serialization.md#what-the-writer-guarantees). - **`Element.byteOffset` inside a sequence item disagrees with itself and always has**: `0` inside a defined-length item (its own frame), file-absolute inside an undefined-length one. The same is true of a warning's `position.byteOffset`. There is no frame-of-reference contract on either, and **neither is covered by `DicomParseError.offsetFrame`** - that names the frame for a thrown fatal and for the `{ strict: true }` escalation of a warning, and for nothing on the lenient path. Measure it rather than assuming one. - **A failed CP-246 `UN` descent emits nothing.** The honest test for a consumer is `el.items === undefined`, **not** `ds.warnings`. - **The byte-for-byte File Meta round trip is scoped to parse-then-serialize, and `deidentify()` is outside it on purpose.** A de-identified File Meta group describes this de-identifying application (PS3.15 §E.1.1), so every non-modeled `(0002,xxxx)` element the source carried is dropped and the Source AE Title and the source's implementation identity go with it. This is a deliberate fidelity loss, recorded on `report.fileMetaElementsDropped` and counted on `report.fileMetaElementsDroppedCount`, and it is **not** recoverable from the output. If you need the source group verbatim, read it off the parsed dataset before the call. - **`NESTING_DEPTH_LIMIT` is this library's bound, not the standard's.** A fully conformant file nested deeper than it is refused, and a sequence refused that way is emptied by `deidentify()` rather than shipped, so expect data loss on such a file. --- ## 4. What is deliberately never defaulted The dangerous DICOM failure is the confident, wrong image, so the typed views answer "absent" rather than substituting a plausible value. `undefined` means the object did not carry the attribute. - `image.rescaleSlope` / `image.rescaleIntercept`: **`undefined` must not be read as 1 and 0.** - `image.signed`: `undefined` when `(0028,0103)` Pixel Representation was absent, never `false`. - `image.photometricInterpretation`: never defaulted to `MONOCHROME2`. - `image.pixelSpacing` and `image.imagerPixelSpacing`: different measurements, never substituted for each other. - `patient.id`: a string the sending system chose. Not globally unique, and never matched for you. - A malformed `DS` or `IS` token decodes to `null`, never to `NaN` coerced to `0`. --- ## 5. Where the detail lives | Topic | Page | | --------------------------------------------------------------------------- | -------------------------------------------------------------------- | | The error model and symptom-by-symptom triage | [Troubleshooting](https://docs.cosyte.com/dicom/troubleshooting.md) | | What a diagnostic carries and what it does not | [Tolerance](https://docs.cosyte.com/dicom/spec-notes-tolerance.md) | | Keeping PHI out of logs | [Keeping PHI out of logs](https://docs.cosyte.com/dicom/troubleshooting.md#keeping-phi-out-of-logs) | | The safety-critical views and their fail-safe rules | [Safety](https://docs.cosyte.com/dicom/spec-notes-safety.md) | | Writing bytes back out, and what the writer will not do | [Serialization](https://docs.cosyte.com/dicom/serialization.md) | | The de-identification surface and the scope limits on it | [De-identification](https://docs.cosyte.com/dicom/deidentification.md) | | Working recipes, each citing the PS3 clause it reads | [Cookbook](https://docs.cosyte.com/dicom/cookbook.md) | Everything on this page is a documented boundary rather than a bug. Where a limitation applies, the raw bytes are preserved (usually with a warning); they are simply not interpreted further. --- # @cosyte/x12 > ASC X12 EDI parser for HIPAA 005010 healthcare transactions. These are the 12 pages of the v0.1.2 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/x12/intro Markdown: https://docs.cosyte.com/x12/intro.md # @cosyte/x12 A developer-focused ASC X12 EDI parser for Node.js and TypeScript. Parse a real-world, vendor-quirky healthcare interchange and read fields out of it without first reading the X12 standard or a TR3 implementation guide. `@cosyte/x12` is the payer-side sibling of [`@cosyte/hl7`](https://github.com/cosyte/hl7): the API shape, profile system, and lenient-parser philosophy are deliberately mirrored. > **Status:** `0.1`, published on npm and public. The exported readers, builders, types and warning > codes are the surface we keep stable, and below 1.0 a breaking change bumps the minor version and > is called out in the changelog with its migration. `npm view @cosyte/x12 version` is the source of > truth for the current version. The **shipped** surface is the HIPAA 005010 read side for 270, 271, 276, > 277/277CA, 278, 820, 834, 835, 837P/I/D, 999, and TA1, the emit > side (`serializeX12` + `buildInterchange` and a domain builder for each of those sets), > and the descriptive trading-partner profile system. This documentation is gated to that surface: > where the parser does not yet do a thing, this site says so rather than promising it. Non-healthcare > sets, EDIFACT, transport (AS2/SFTP/MLLP), and pre-005010 field maps are explicit v1 non-goals; see > [Troubleshooting & known limitations](https://docs.cosyte.com/x12/troubleshooting.md). ## The transaction sets it covers The HIPAA **005010** healthcare transaction sets. Every set listed below ships both a lenient reader and a domain builder: - **270**: eligibility inquiry - **271**: eligibility response - **276**: claim status request - **277**: claim status response (incl. **277CA** claim acknowledgment) - **278**: services review (request and response) - **820**: premium payment - **834**: benefit enrollment and maintenance - **835**: healthcare claim payment / advice (remittance / ERA) - **837P / 837I / 837D**: professional, institutional, and dental claims - **999 / TA1**: implementation and interchange acknowledgments The two **inquiry** directions used to be absent from that list and no longer are. The 270 eligibility inquiry has a reader (`get270Inquiry` / `parse270Inquiries`) and a builder (`build270`); the 276 claim status request has a reader (`get276StatusInquiry` / `parse276StatusInquiries`) and a builder (`build276`). Write "270" and "276" separately when you describe scope: one paired label over two halves is how this page was wrong about them once already. See [The 80/20 transaction sets](https://docs.cosyte.com/x12/spec-notes-transaction-sets.md) for the exact reader/builder pair each one ships and the safety-critical field each preserves verbatim. ## Install and smoke-test ```bash npm install @cosyte/x12 ``` Confirm the package resolves and a real entry point is callable. Decode the smallest useful interchange (an ISA/IEA envelope) and read the version symbol back: ```ts runnable import { parseX12, VERSION } from "@cosyte/x12"; typeof VERSION; // => "string" const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "IEA*0*000000001~"; const ix = parseX12(raw); ix.isa.elements[6].trim(); // => "SENDER" ix.delimiters.element; // => "*" Array.isArray(ix.warnings); // => true ``` If that resolves and returns, the install is good. Head to the [Installation](https://docs.cosyte.com/x12/installation.md) page for prerequisites and module-system notes, then the [Quickstart](https://docs.cosyte.com/x12/quickstart.md) for a first useful result. ## The archetype in one line The parser is **lenient by default** (vendor deviations become Tier-2 `warnings` carrying a stable code and positional context, not failures) while the builders **refuse** (never silently corrupt) a structurally impossible spec (Postel's Law). Only four unrecoverable Tier-3 structural errors ever throw. Money is decimal-exact end to end (`X12Decimal`, BigInt-backed, **never `parseFloat` an EDI amount**). See [Core Concepts](https://docs.cosyte.com/x12/spec-notes-envelope.md) for the mental model. ## Next - [Installation](https://docs.cosyte.com/x12/installation.md): prerequisites, module systems, and the PHI discipline. - [Quickstart](https://docs.cosyte.com/x12/quickstart.md): parse an 835 remittance and post the cash in a few lines. - [Core Concepts](https://docs.cosyte.com/x12/spec-notes-envelope.md): the envelope/loop model, the transaction sets, the tolerance tiers, and decimal-exact money. - [Building and serializing X12](https://docs.cosyte.com/x12/emit-and-serialize.md): the emit half, `serializeX12` and `buildInterchange`, and what a round trip does and does not reproduce. - [Trading-partner profiles](https://docs.cosyte.com/x12/profiles.md): describing what a partner actually sends, as fixture-grounded data rather than silent leniency. - [Cookbook](https://docs.cosyte.com/x12/cookbook.md): task-oriented recipes for the transactions you actually get handed. - [Troubleshooting & known limitations](https://docs.cosyte.com/x12/troubleshooting.md): fatal codes, the fail-safe rules, and what v1 does not do. - **API reference**: every export, generated from source. --- URL: https://docs.cosyte.com/x12/installation Markdown: https://docs.cosyte.com/x12/installation.md # Installation `@cosyte/x12` is a TypeScript ASC X12 toolkit for Node.js. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration, and it has **zero runtime dependencies**: Node stdlib only. > **Status:** `0.1`, published on npm and public. The `npm install` below is live. Below 1.0 a > breaking change bumps the minor version and is called out in the changelog with its migration. > Run `npm view @cosyte/x12 version` for the current version: it is the source of truth. ## Prerequisites - **Node.js >= 22.** The whole `@cosyte/*` suite targets ES2023 / Node 22+. - A package manager: `pnpm`, `npm`, or `yarn`. - **No runtime dependencies.** The parser, serializer, builders, and bundled HIPAA code-list snapshots are all Node stdlib only. The zero-dep rule is a release gate, not a preference. A new runtime dep needs an ADR and a changelog entry. ## Install ```bash npm install @cosyte/x12 ``` ## Smoke test Confirm the package resolves and a real entry point is callable. Decode the smallest valid interchange envelope and read its detected delimiters back: ```ts runnable import { parseX12 } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "IEA*0*000000001~"; const ix = parseX12(raw); ix.delimiters.element; // => "*" ix.delimiters.segment; // => "~" ix.isa.elements[16]; // => ":" Array.isArray(ix.warnings); // => true ``` If that resolves and returns, the install is good. Head to the [Quickstart](https://docs.cosyte.com/x12/quickstart.md). ## Module systems `@cosyte/x12` is `"type": "module"` and exposes both conditions, so both of these resolve to the right build without extra configuration: ```ts // ESM / TypeScript import { parseX12, get835 } from "@cosyte/x12"; ``` ```js // CommonJS const { parseX12, get835 } = require("@cosyte/x12"); ``` The single top-level entry point (`@cosyte/x12`) publishes per-condition types (`.d.ts` for `import`, `.d.cts` for `require`), gated by `attw` on every release, and resolves under both `node16` and legacy `node10` module resolution. Editor IntelliSense matches the build you actually load. ## PHI discipline Every example in this documentation uses **synthetic** fixtures: fabricated names, obviously-fake IDs, pre-2024 control numbers. Do the same in your own tests: X12 healthcare interchanges carry PHI, and a real interchange committed to a repository is a leak the moment it publishes. See [Troubleshooting](https://docs.cosyte.com/x12/troubleshooting.md) for how the parser keeps field content out of its warnings and logs. --- URL: https://docs.cosyte.com/x12/quickstart Markdown: https://docs.cosyte.com/x12/quickstart.md # Quickstart This page gives you a first useful result: read an **835** remittance advice (the electronic EOB) and pull out the money (the payment header, each claim's charge/paid/patient-responsibility split, and the CARC adjustment reasons) in a few lines. The 835 is the #1 thing an integration consultant is handed, so it is the fastest way to see the library earn its keep. The reader is **lenient** (vendor quirks become stable-coded `warnings`, never silent failures) and money is decimal-exact throughout (`X12Decimal`, BigInt-backed; **never `parseFloat` an EDI amount**). > Every interchange below is **synthetic**: fabricated names, obviously-fake IDs, pre-2024 control > numbers. X12 healthcare data is PHI; a fixture must never hold a real one. ## Parse an 835 and read the money `parseX12` decodes the interchange; `get835(delimiters, tx)` walks an 835 transaction set into a typed `X12Remittance` (or returns `undefined` if the transaction set is not an 835). ```ts runnable import { parseX12, get835 } from "@cosyte/x12"; const raw = `ISA*00* *00* *ZZ*MEDICARE *ZZ*SUBMITTER *260601*1200*^*00501*000000001*0*P*:~ GS*HP*MEDICARE*SUBMITTER*20260601*1200*1*X*005010X221A1~ ST*835*0001~ BPR*I*450.00*C*ACH*CCP*01*123456789*DA*987654321*1512345678**01*111111111*DA*222222222*20260601~ TRN*1*0012345*1512345678~ DTM*405*20260601~ N1*PR*MEDICARE PART A~ N3*123 PAYER WAY~ N4*BALTIMORE*MD*21244~ PER*BL*JANE COORDINATOR*TE*5551234567~ N1*PE*SAMPLE CLINIC INC~ N3*456 PROVIDER LN~ N4*CLEVELAND*OH*44113~ REF*TJ*123456789~ LX*1~ CLP*PT-ACCT-001*1*500.00*450.00*50.00*MC*PAYER-CLAIM-001*11*1~ NM1*QC*1*PATIENT*TEST*A***MI*MEMBER001~ NM1*82*2*RENDERING PROVIDER INC*****XX*1234567890~ DTM*232*20260501~ DTM*233*20260501~ SVC*HC:99213*500.00*450.00**1~ DTM*472*20260501~ CAS*PR*1*50.00~ REF*6R*LINE-CTRL-001~ SE*23*0001~ GE*1*1~ IEA*1*000000001~`; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835"); const remit = tx ? get835(ix.delimiters, tx) : undefined; if (remit === undefined) throw new Error("not an 835"); // Payment header: the money-movement primitive. remit.payment.totalActualPayment?.toString(); // => "450.00" remit.payment.creditDebitFlag; // => "C" remit.payment.method; // => "ACH" remit.traces[0]?.referenceId; // => "0012345" // Per claim: your account number echoed back, and the charge/paid/responsibility split. const claim = remit.claims[0]; claim?.patientControlNumber; // => "PT-ACCT-001" claim?.totalChargeAmount?.toString(); // => "500.00" claim?.totalPaymentAmount?.toString(); // => "450.00" claim?.patientResponsibilityAmount?.toString(); // => "50.00" // Per service line: the CARC adjustment. Group code is the safety-critical field. const line = claim?.serviceLines[0]; line?.productServiceId; // => "99213" const adj = line?.adjustments[0]; adj?.groupCode; // => "PR" adj?.reasonCode; // => "1" adj?.amount?.toString(); // => "50.00" ``` The `groupCode` (`PR` = patient responsibility, `CO` = contractual obligation, …) is what tells you who owes the money. Never infer it, read it. The `reasonDescription` on each adjustment is prefilled from the bundled CARC snapshot when the code is recognized; an unrecognized code is preserved verbatim and raises `X12_UNKNOWN_CARC` (the value is never dropped). ## Respect the balance warning The walker runs the TR3 X221A1 §1.10.2 balance invariants and emits `X12_835_REMIT_BALANCE_MISMATCH` on a mismatch. It **never silently rebalances**. **Gate on two codes, not one:** where a term of an equation is `undefined` the equation cannot be run at all and you get `X12_835_BALANCE_NOT_EVALUABLE` instead, which is a document you must not auto-post either. Through `0.0.12` that case collapsed to zero and raised the mismatch, so a gate written against the mismatch alone stops firing on it when you upgrade. ```ts runnable import { parseX12, get835, WARNING_CODES } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*MEDPAY *ZZ*CLINIC001 " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HP*MEDPAY*CLINIC001*20260601*1200*1*X*005010X221A1~" + "ST*835*0001~" + "BPR*I*450.00*C*ACH*CCP*01*021000021*DA*1234567*1512345678**01*021000021*DA*98765*20260601~" + "TRN*1*0012345*1512345678~" + "N1*PR*MEDICARE PART A~" + "N1*PE*SAMPLE CLINIC INC*XX*1234567890~" + "LX*1~" + "CLP*PT-ACCT-001*1*500.00*450.00*50.00*MB*CLAIMREF001*11~" + "NM1*QC*1*PATIENT*TEST****MI*MEMBER001~" + "SVC*HC:99213*500.00*450.00~" + "CAS*PR*1*50.00~" + "SE*11*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835"); const remit = get835(ix.delimiters, tx!)!; const doNotPost = remit.warnings.some( (w) => w.code === WARNING_CODES.X12_835_REMIT_BALANCE_MISMATCH || w.code === WARNING_CODES.X12_835_BALANCE_NOT_EVALUABLE, ); doNotPost; // => false if (doNotPost) { // Do NOT auto-post. Route to a human: either the payer's numbers don't add // up, or an amount one of the equations needs did not decode at all. } ``` ## Unrecoverable input throws: everything else is a warning Only four structurally unrecoverable conditions throw a typed `X12ParseError` (empty input, no ISA header, a truncated ISA, or unrecoverable delimiters). Vendor quirks never throw; they collect on `.warnings`: ```ts runnable throws import { parseX12 } from "@cosyte/x12"; // Not an X12 interchange at all: a structural fatal, not a tolerated quirk. parseX12("this is not an X12 interchange"); // throws X12ParseError (X12_NO_ISA_HEADER) ``` ## Next - [Cookbook](https://docs.cosyte.com/x12/cookbook.md): recipes for the 277CA and 277 status answers, the 271 TRN-echo round-trip, the 270 and 276 inquiry side, the 837 claim walk, the 278 certification decision, the 834 roster, the 999 and TA1 acknowledgments, and warning triage. - [Core Concepts](https://docs.cosyte.com/x12/spec-notes-envelope.md): the envelope/loop model, the transaction sets, the tolerance tiers, and decimal-exact money. - [Building and serializing X12](https://docs.cosyte.com/x12/emit-and-serialize.md): the emit half, and what a round trip does and does not reproduce. - [Trading-partner profiles](https://docs.cosyte.com/x12/profiles.md): documenting a partner's conventions without changing the parse. - [Troubleshooting & known limitations](https://docs.cosyte.com/x12/troubleshooting.md): fatal codes, the fail-safe rules, and what v1 does not do. > **About runnable examples.** The blocks tagged ` ```ts runnable ` above are extracted by the test > suite, executed against the built package, and their `// =>` results asserted, so a documented > example can never silently drift from the code (`docSnippetSuite()`, the documentation analog of the > parser conformance runners). Blocks shown as plain ` ```ts ` are illustrative. --- URL: https://docs.cosyte.com/x12/spec-notes-envelope Markdown: https://docs.cosyte.com/x12/spec-notes-envelope.md # The envelope & loop model Every X12 interchange is a set of **nested envelopes**. Understanding the four levels (and the loop structure inside a transaction) is the whole mental model; once you have it, every reader in this library reads the same way. ## The four envelope levels ``` ISA ─ interchange header (the outermost envelope; fixed-width, 106 bytes) │ GS ─ functional group header (groups same-type transactions) │ │ ST ─ transaction set header (one business document: an 835, an 837, …) │ │ │ … body segments (BPR, CLP, SVC, NM1, …) │ │ SE ─ transaction set trailer (segment count + control number) │ GE ─ functional group trailer (transaction count + control number) IEA ─ interchange trailer (group count + control number) ``` `parseX12` decodes this whole tree into an immutable `X12Interchange`: - `ix.isa` / `ix.iea`: the interchange envelope (`IsaSegment` / `IeaSegment`). - `ix.groups[]`: one `X12FunctionalGroup` per GS..GE, each with `.gs` / `.ge`. - `ix.groups[i].transactions[]`: one `X12TransactionSet` per ST..SE, each with `.st` / `.se` and the decoded body `.segments`. ```ts runnable import { parseX12 } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*MEDPAY *ZZ*CLINIC001 " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HP*MEDPAY*CLINIC001*20260601*1200*1*X*005010X221A1~" + "ST*835*0001~" + "BPR*I*450.00*C*ACH*CCP*01*021000021*DA*1234567*1512345678**01*021000021*DA*98765*20260601~" + "SE*3*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); ix.isa.elements[6].trim(); // => "MEDPAY" ix.groups[0]?.gs.elements[1]; // => "HP" ix.groups[0]?.transactions[0]?.st.elements[1]; // => "835" ``` ## Delimiters are detected, never assumed X12 does not fix its delimiters. The sender declares them, and clearinghouses vary. `@cosyte/x12` detects all four from fixed byte positions in the ISA, so you never configure them: - **Element separator**: ISA byte 3 (classically `*`). - **Repetition separator**: ISA-11 (position 82; classically `^` in 005010). - **Component (sub-element) separator**: ISA-16 (the byte before the segment terminator; classically `:`, but Medicare and some BCBS plans use others). - **Segment terminator**: the byte immediately after ISA-16 (classically `~`). `ix.delimiters` carries the detected set; every reader and the `getSegmentValue` dot-path resolver use it, so a partner who ships `|` elements and `\` components parses with no special handling. ### When an ISA element carries the element separator Detection verifies the element separator at all 16 fixed ISA positions, which makes 17 the **floor** on how many parts the header splits into, not the count. An ISA element value carrying that same byte splits again: that element comes back a prefix and everything after it is displaced, so the control number, the usage indicator and the version can each answer some other element's value. **How far is not derivable from `isa.elements`** - more than one element can carry an extra separator, and one sitting between two of them is displaced less than one sitting after both - so `isa.raw` plus the fixed widths is the only route back. `@cosyte/x12` reports this as `X12_ISA_EXTRA_ELEMENT_SEPARATOR` and **re-frames nothing** - the byte is both content under the ISA's fixed widths and the separator the segment declares in-band, the interchange is not 005010-conformant either way, and nothing settles which reading is right. All 106 bytes stay on `isa.raw`, so the transmitted span of any element is recoverable if you decide it should be. `parseX12` raises this ahead of the warnings it raises after it, so when you see it in `ix.warnings`, treat those as provisional. **That is a statement about `ix.warnings` and `onWarning` only.** `serializeX12(ix, { specClean: true })` reconciles ISA-13 against IEA-02 off `isa.elements[13]` with no arity awareness and never raises this code, so on that channel a lone `X12_CONTROL_NUMBER_MISMATCH` can be a displaced read, and the absence of this warning is not evidence the header framed. ### When a delimiter is `?` `?` is the conventional X12 release character, and this library treats it as one. It is also admissible at any of the four delimiter positions, because nothing in the ISA layout reserves it, so a sender can declare it as structure. **One byte cannot both separate and escape**, so wherever a role is declared as `?`, the splitter for that role stops treating `?` as an escape and splits literally: the segment terminator, an envelope segment's elements, and a body segment's elements. Bounds worth knowing if you meet such a set, and this is not a closed account of what one can do to a document. A `?` **repetition or component** separator still does not split, so `getSegmentValue(seg, "01-2", ix.delimiters)` will not resolve against it. With `?` as the **element** separator, a segment ending in an empty last element puts a `?` immediately before the terminator, which the terminator scan still reads as an escape, so that segment merges with the one after it and you get `X12_MISSING_SE`. **🩺 On the emit side a `?` in ANY of the four roles is REFUSED, and that is the asymmetry: lenient on parse, strict on emit.** Every builder - `buildInterchange`, the seven per-TR3 domain builders, `build999` and `buildTA1` - refuses such a set with its own existing typed error, naming the role. The reason is that one byte cannot both separate and escape on the way out either, and there is nothing a caller can do about it at the value level: - An escape is written by **prefixing** `?` to the byte it protects, so where `?` is a delimiter the protection is emitted as structure. - A **composite or a repetition** is joined with the separator you declared, so where that separator is `?` the library's own structural join is emitted as an escape. **This one needs no value of yours at all** - an 837 with a `?` component separator emitted `SV1-01-2` (the procedure code) and `HI-01-2` (the diagnosis code) fused into the preceding component on every document, silently. If you must exchange with a partner who declares `?` as structure, you can still **read** their traffic: `parseX12` accepts every such set, and `serializeX12` re-emits one byte for byte. The read-side bounds above are unchanged by the refusal. **What the refusal is, exactly: an equality test on the delimiter you declare.** Read it as a property of the SET you declare, never as a guarantee about the documents this library can compose. **A second refusal sits beside it: a delimiter must be SHAPED like one.** Each of the four roles must be a string of exactly one visible character, and the four must be mutually distinct - the same predicate `parseX12` applies to an inbound ISA, where failing it is the fatal `X12_INVALID_DELIMITERS`. So `segmentTerminator: "~~"`, an empty or whitespace delimiter, a numeric one, and a set that uses one character in two roles are all refused on emit. Some of those used to build with `warnings: []`, including `segmentTerminator: "~\r\n"` - if you were declaring that to get line-broken output, it never produced any: line breaks between segments are tolerated on READ, so the model recorded `~` and `serializeX12` emitted none. **It counts UTF-16 code units, not bytes**, so a single-code-unit character that is several bytes on the wire (`"\u00a7"`, or the smart quote `"\u2019"` a companion-guide PDF gives you instead of `'`) still builds and still displaces every ISA position after it. Declare delimiters from the basic single-byte set. All of it is recorded in `KNOWN-LIMITATIONS.md`. ## Line endings between segments X12 is a single-line format, but most senders write a line break after every segment terminator so the file is readable. `parseX12` absorbs it: **any run of CR / LF bytes** between segments, so `~\r\n`, `~\r`, `~\n`, a double-spaced `~\n\n`, and a doubled-in-transit `~\r\r\n` all parse to the same interchange. All 15 CR/LF sequences of length 0 to 3 frame identically. You never configure this, and it never warns. A run of CR / LF between segments can never be structural, which is what makes absorbing an unbounded one safe: `parseX12` refuses any ASCII control character (CR and LF among them) at **all four** delimiter positions, the segment terminator included, as the Tier-3 fatal `X12_INVALID_DELIMITERS`. So a line break is never itself a delimiter. Note the terminator is the byte immediately **after** ISA-16, as stated above; ISA-16 is the component separator. (This describes what this library does. It takes no position on what ASC X12 permits between segments.) The break is discarded rather than recorded, and that has one consequence worth knowing before you diff an emit against its input: ```ts runnable import { parseX12, serializeX12 } from "@cosyte/x12"; const prettyPrinted = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~\n" + "GS*HC*SENDER*RECEIVER*20260601*1200*1*X*005010X222A2~\n" + "ST*837*0001~\nSE*1*0001~\nGE*1*1~\nIEA*1*000000001~\n"; const emitted = serializeX12(parseX12(prettyPrinted)); // The line breaks are gone, so the emit is the compact form: emitted === prettyPrinted; // => false emitted.includes("\n"); // => false // What DOES hold, and is what the round trip is for. The emit is a fixed // point, so serializing it again is a byte-level no-op: emitted === serializeX12(parseX12(emitted)); // => true ``` `serializeX12` is **byte-faithful for the segments on the model**: each of those comes back verbatim, including element padding, composite structure, and `?`-release escapes. Anything the parser did not record does not come back, and line breaks are only the most common of several such things. **`serialize(parse(s)) === s` is not guaranteed in general**, and having no line breaks is not enough to make it hold. Six constructs are known not to survive: 1. **Line breaks between segments**, as above. Silent. 2. **A doubled segment terminator** outside a transaction. It delimits a zero-length segment carrying no elements, so there is nothing to retain. Silent. 3. **A missing final terminator**, which the emit supplies. Silent. 4. **Post-IEA `trailingBytes`**, re-joined from segment slices rather than preserved verbatim. 5. **TA1 position.** A TA1 that appeared **after** a functional group is collected onto `ix.ta1Segments` and emitted immediately after the ISA, so the emit **reorders** it. Silent, and unlike the others nothing is lost: the model and the warning stream both round-trip identically. It is also the only construct that moves something _else_ - a segment outside a transaction is placed correctly relative to the groups but not relative to a TA1 hoisted past it. This library takes no position on where ASC X12 requires a TA1 to sit. 6. **A segment whose first element is empty (`*A*B~`), outside a transaction.** It has no id for the envelope walker to dispatch on, so it is skipped entirely: absent from the model, absent from the emit, and it does not even raise `X12_UNEXPECTED_SEGMENT`. Silent, and the only case here that loses a value with no diagnostic at all. Inside an open transaction the same segment is kept and re-emitted normally. **A segment outside a transaction is not on that list.** It is kept on `ix.orphanSegments` and re-emitted at its structural anchor, so the segment, its value and its `X12_UNEXPECTED_SEGMENT` warning all survive the round trip. See [Segments outside a transaction](#segments-outside-a-transaction) below. Cases 2 to 6 all break the round trip on inputs containing no line breaks at all, and **five of the six (1, 2, 3, 5, 6) produce no warning**, so a clean `ix.warnings` does not tell you a round trip will be byte-exact; only case 4 warns. What is **measured**, across every fixture committed to this repository: each emit is a fixed point and re-parses to an identical model with an identical warning stream; the fixtures carrying no line breaks return byte-identical; and the pretty-printed ones differ from their source by **line breaks and nothing else** (no element value lost, altered, reordered, or re-escaped). No corpus size is quoted: it moves with every fixture added, and the suite re-derives the sweep from the tree on every run. Two caveats bound how far that sweep can be pushed: the corpus contains **no instance of cases 2 to 6** and no orphan at all, and **all but one of the byte-identical fixtures are `golden/*.edi`**, which are serializer output by construction, so `envelope/no-trailing-crlf.edi` is the only independent witness. That is why the six cases are enumerated here rather than left to the sweep. So for a file whose only irregularity is pretty-printing, the round trip is safe to build on for data and not for a byte-level diff, and diffing your emit against `serializeX12(parseX12(source))` is the way to ignore the line-break noise. [Building and serializing X12](https://docs.cosyte.com/x12/emit-and-serialize.md) covers the emit side as a whole: composing an interchange from nothing, what a builder refuses, and the `{ specClean: true }` reconciliation. ## Segments outside a transaction The envelope grammar binds body segments to an open `ST..SE` transaction set. A segment that arrives outside one has nowhere to go in `ix.groups`. The positions that do this are a stray segment between `GE` and `IEA`, a body segment between an `SE` and its group's `GE`, a body segment between `GS` and the first `ST`, an `ST` with no open group, an `SE` that closes nothing, a `GE` that closes nothing, and a `TA1` inside an open group. That last one is the exception to this section's title. `TA1` is envelope-level by spec, so a `TA1` inside an open group lands on `ix.orphanSegments` even when it arrived **between an `ST` and its `SE`** - and it is lifted out of that transaction's `segments` and `rawSegments`. For a document containing such a `TA1`, `ix.groups` is not the whole typed model. That is long-standing behaviour; what changed is that the segment is retained and re-emitted rather than discarded. It is also the one orphan that is anchored _inside_ a transaction, so it is the case any re-emission design has to get right - see the second example below. Each raises `X12_UNEXPECTED_SEGMENT`, is retained verbatim on `ix.orphanSegments`, and is re-emitted at the structural `anchor` recorded alongside it: ```ts runnable import { parseX12, serializeX12 } from "@cosyte/x12"; const withStray = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HC*SENDER*RECEIVER*20260601*1200*1*X*005010X222A2~" + "ST*837*0001~SE*2*0001~GE*1*1~" + "REF*ZZ*VENDORTAG~" + // outside every transaction "IEA*1*000000001~"; const ix = parseX12(withStray); // Reported, and kept. The value is still reachable: ix.orphanSegments.length; // => 1 ix.orphanSegments[0]?.segment.id; // => "REF" ix.orphanSegments[0]?.context; // => "body-outside-transaction" // `segmentIndex` is the join key back to the warning that describes it: const warning = ix.warnings.find((w) => w.code === "X12_UNEXPECTED_SEGMENT"); warning?.position.segmentIndex === ix.orphanSegments[0]?.segmentIndex; // => true // And it comes back in the emit, byte for byte, with its warning: const round = serializeX12(ix); round === withStray; // => true parseX12(round).orphanSegments.length; // => 1 // `anchor` is the structural slot it goes back into - after the one group // that closed, before the IEA. ix.orphanSegments[0]?.anchor.kind; // => "interchange" ``` Two things this deliberately does **not** do. An orphan is not decoded by any `get*` reader. And a `TA1` inside an open group is **not** added to `ix.ta1Segments` (that surface means "envelope-level TA1", and `parseTA1` reads it). Retention and re-emission are not promotion: the segment is kept and put back where it was, not moved into a position the grammar says it does not occupy. **Placement is by `anchor`, never by `segmentIndex`, and the difference is the whole correctness argument.** `segmentIndex` is an index into the **input** stream, and the emit is not in input order: it hoists `ta1Segments` ahead of the groups (case 5 above), and a doubled terminator's zero-length segment occupies an input index that is never emitted. Replaying an orphan by input index therefore splices it into whatever occupies that slot in the output. Measured on a two-group interchange with a `TA1` after the first group, that put a stray segment **inside** an 835's `ST..SE` body between `CLP` and `SE`, where re-parsing raised no warning at all and `get835` would have walked it as claim content; a stray `SE` closed the transaction early and corrupted SE-01. An `anchor` names a slot in the typed tree instead - which group, which transaction, which offset inside it - and a slot in the tree is invariant under both reorderings, because it does not mention bytes. Use `segmentIndex` to join an orphan to its warning; never use it to place one. ```ts runnable import { parseX12, serializeX12 } from "@cosyte/x12"; // A TA1 *between* an ST and its SE: the one orphan that is anchored inside an // open transaction rather than beside one. const inTx = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HP*SENDER*RECEIVER*20260601*1200*1*X*005010X221A1~" + // SE-01 is 4: four physical segments sit between the ST and the SE. "ST*835*0001~CLP*ACCT1*1*100*0~TA1*000000001*260601*1200*A*000~SE*4*0001~" + "GE*1*1~IEA*1*000000001~"; const lifted = parseX12(inTx); lifted.orphanSegments[0]?.anchor.kind; // => "transaction" // It is lifted OUT of the transaction body, and stays out on the round trip: lifted.groups[0]?.transactions[0]?.rawSegments.length; // => 3 serializeX12(lifted) === inTx; // => true ``` Those last two numbers are both right, and the gap between them is the point. `rawSegments` is **3** because the walker lifted the `TA1` off the transaction, while SE-01 is **4** because four segments are physically present between the `ST` and the `SE`, which is what X12.6 counts. `{ specClean: true }` reconciles against the emitted bytes, so it counts the re-emitted orphan and agrees with the `4`; reconciling against `rawSegments` alone would "correct" a right count into a wrong one. `orphanSegments` is empty for a well-formed interchange, so a non-empty one is itself the signal that the sender's framing did not match the envelope grammar. Check it alongside `ix.warnings`. **Treat an orphan as PHI when you log it.** A warning `message` is a lookup into a frozen registry and its metadata is positional, so the whole `ix.warnings` array is safe to log. An orphan is not the same kind of thing: it carries the sender's bytes verbatim, exactly as `tx.rawSegments` and `isa.raw` do, and nothing guarantees a segment outside a transaction is free of patient data. Log `context` and `segmentIndex`, which name the rule that broke and where to look, rather than the whole entry. ## Segments, elements, composites, repetitions Inside a transaction, every body segment is an immutable `X12Segment`: a segment `id` (`BPR`, `CLP`, `NM1`, …) plus **1-indexed** `elements`. The `?`-release-character escape is honored losslessly (`?~` → a literal `~`, `?*` → `*`, `??` → `?`). The `getSegmentValue(seg, path, delimiters)` dot-path resolver walks the three axes: - **Elements**: `"03"` is the third element. - **Composites**: `"03-1"` is the first sub-element of element 3 (`-N` is 1-indexed). - **Repetitions**: `"03[0]"` is the first repetition of element 3 (`[N]` is 0-indexed). ```ts runnable import { parseX12, getSegmentValue } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HC*SENDER*RECEIVER*20260601*1200*1*X*005010X222A2~" + "ST*837*0001~" + "HI*ABK:J45.50*ABF:E11.9~" + "SE*2*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const hi = ix.groups[0]?.transactions[0]?.segments.find((s) => s.id === "HI"); getSegmentValue(hi!, "01-1", ix.delimiters); // => "ABK" getSegmentValue(hi!, "01-2", ix.delimiters); // => "J45.50" getSegmentValue(hi!, "02-1", ix.delimiters); // => "ABF" ``` ## Binary segments: BDS and BIN Two segments carry binary data, such as an attachment: `BDS` (BDS-01 filter, BDS-02 length, BDS-03 data) and `BIN` (BIN-01 length, BIN-02 data). Their data element can hold any octet from `0x00` to `0xFF`, delimiters included, so `parseX12` frames it by the **octet count its length element declares**, not by scanning for delimiters. The segment stays one segment, the data stays one element, and every segment after it frames as sent. A `?` inside the declared span is data, never an escape. `getSegmentValue`, `getAllSegmentValues` and `elementValue` return the data element verbatim: no release unescape and no split on the repetition or component separator. ```ts runnable import { parseX12, getSegmentValue, serializeX12 } from "@cosyte/x12"; // BIN-01 declares 12 octets, and the data carries a `~` and a `*`. const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*PI*SENDER*RECEIVER*20260601*1200*1*X*005010X210~" + "ST*275*0001~" + "BIN*12*PART~ONE*TWO~" + "SE*3*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const bin = ix.groups[0]?.transactions[0]?.segments[1]; bin?.id; // => "BIN" getSegmentValue(bin!, "02", ix.delimiters); // => "PART~ONE*TWO" ix.warnings.length; // => 0 serializeX12(ix) === raw; // => true ``` Limits worth knowing before you rely on it: - **The count is in octets.** A `Buffer` is read one character per octet, so its count is exact. A string is counted one UTF-16 code unit per octet, exact at or below U+00FF; a span holding anything above that raises `X12_BINARY_LENGTH_UNVERIFIABLE` and is carried verbatim. Pass an interchange that carries attachments as a `Buffer`. - **Nothing is decoded.** BDS-01's filter (`B64`, for example) is not applied, and nothing inside the data is parsed. - **A count that cannot be honoured is reported, never corrected**, anchored at the segment: `X12_BINARY_LENGTH_INVALID` where the length element is absent, empty or not 1 to 15 ASCII digits, `X12_BINARY_DATA_TRUNCATED` where the input ends inside the declared span, and `X12_BINARY_LENGTH_MISMATCH` where the byte after the span is not the segment terminator. On an invalid length nothing is inferred: the segment is framed by its delimiters like any other, so a delimiter in its data splits it. On a truncated span the data element holds only the octets present. On a mismatch the data element is exactly the declared span and the bytes after it stay on the segment's `raw`, so the default `serializeX12` emit still reproduces the input byte for byte. - **The length element is never rewritten on emit**, in either mode, even where it disagrees with the data. For a segment that parsed with none of those warnings it already equals the data's octet count, so the round trip above holds in `{ specClean: true, recomputeCounts: true }` too. `KNOWN-LIMITATIONS.md` carries the full account. ## Loops: the repeating sub-structures Above the segment sits the **loop**: a repeating group of segments that models a business entity (a claim, a service line, a subscriber). The TR3 implementation guides define each transaction's loop hierarchy; the per-transaction readers (`get835`, `get837Claims`, …) walk those loops for you and hand back a typed tree, so you rarely touch raw segments. When you _do_ need to describe a loop yourself, or understand how the built-ins are authored, the public `defineLoopSpec()` API is the same one the library uses internally (a dogfooding gate: the built-in specs like `REMIT_835_LOOP_2100` are authored through it): ```ts runnable import { defineLoopSpec } from "@cosyte/x12"; const Loop2300 = defineLoopSpec({ id: "2300", description: "837 Claim Information", trigger: "CLM", segments: [ { id: "CLM", usage: "required", max: 1 }, { id: "DTP", usage: "situational", max: ">1" }, { id: "HI", usage: "situational", max: ">1" }, ], }); Loop2300.trigger; // => "CLM" Loop2300.id; // => "2300" ``` Many claims transactions (837, 271, 277, 278) also nest an **HL hierarchy**: an explicit parent-pointer tree (`HL` segments) layered on top of the loops. The readers validate those pointers for integrity and **never silently re-number** a broken one; see [The tolerance tiers](https://docs.cosyte.com/x12/spec-notes-tolerance.md). --- URL: https://docs.cosyte.com/x12/spec-notes-transaction-sets Markdown: https://docs.cosyte.com/x12/spec-notes-transaction-sets.md # The 80/20 transaction sets X12 defines hundreds of transaction sets; HIPAA mandates a small handful for healthcare, and a smaller handful again carry the overwhelming majority of real integration traffic. `@cosyte/x12` covers the HIPAA **005010** healthcare sets and nothing else. Every set in the map below ships a lenient **reader** and a domain **builder**. This page is the map: what each set is, which function reads it, which builds it, and the one field each one preserves _verbatim_ because getting it wrong causes harm. > **Both inquiry directions are in the map now.** The 270 eligibility inquiry and the 276 claim > status request each have a reader and a builder of their own; this note used to say the 276 had > neither. Keep the two written separately: the 276 and the 277 are two halves of ONE implementation > guide (`005010X212`) and one paired label over two halves is how this page was wrong once already. > **Depth tracks the code.** Every function named below is a shipped export. Where a set has a > read-side limitation (e.g. 837 claim-/line-level provider addresses), it is called out in > [Troubleshooting & known limitations](https://docs.cosyte.com/x12/troubleshooting.md), not glossed over. ## The map | Set | What it is | Read | Build | Preserved verbatim | | ---------------------- | -------------------------------------------- | ------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **270** | Eligibility inquiry | `get270Inquiry` / `parse270Inquiries` | `build270` | the declared HL parent pointers, never re-numbered and never re-parented | | **271** | Eligibility response | `get271Eligibility` | `build271` | the 270's `TRN-02` trace, echoed onto the 271 (reassociation) | | **276** | Claim status request | `get276StatusInquiry` / `parse276StatusInquiries` | `build276` | the declared HL parent pointers, never re-numbered and never re-parented; the `TRN-02` trace the 277 must echo | | **277** | Claim status response | `get277Status` | `build277` | the 276's trace; the STC category/status/entity triple | | **277CA** | Claim acknowledgment | `get277CADisposition` | `build277CA` | per-claim accept/reject disposition + your submitted trace | | **278** | Services review request / response | `get278Request` / `get278Response` | `build278Request` / `build278Response` | the `HCR-01` certification action (response), never inferred | | **820** | Premium payment | `get820Payments` | `build820` | monetary amounts (emitted as-is; no balance equation) | | **834** | Benefit enrollment & maintenance | `get834Header` / `get834Enrollments` | `build834` | the `INS-03` / `HD-01` maintenance-type code (X12 0875) | | **835** | Claim payment / advice (ERA) | `get835` | `build835` | every monetary field; the balance is checked, never rebalanced | | **837P / 837I / 837D** | Professional / institutional / dental claims | `get837Claims` | `build837P` / `build837I` / `build837D` | the HL hierarchy; HI diagnosis qualifier → code system | | **999** | Implementation acknowledgment | `parse999` | `build999` | per-segment / per-element syntax error notes | | **TA1** | Interchange acknowledgment | `parseTA1` | `buildTA1` | the interchange acknowledgment + note codes | ## Routing: which set is this? An interchange can carry multiple functional groups and transaction sets. Route on **GS-01** (the functional identifier) and **ST-01** (the transaction set ID), then hand the transaction to the matching reader: ```ts runnable import { parseX12, get835 } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*MEDPAY *ZZ*CLINIC001 " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HP*MEDPAY*CLINIC001*20260601*1200*1*X*005010X221A1~" + "ST*835*0001~" + "BPR*I*450.00*C*ACH*CCP*01*021000021*DA*1234567*1512345678**01*021000021*DA*98765*20260601~" + "TRN*1*0012345*1512345678~" + "SE*3*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const group = ix.groups[0]; group?.gs.elements[1]; // => "HP" // Find the 835 transaction and decode it. A reader returns `undefined` for a // mis-routed transaction, so this pattern is safe across mixed interchanges. const tx = group?.transactions.find((t) => t.st.elements[1] === "835"); const remit = tx ? get835(ix.delimiters, tx) : undefined; remit?.traces[0]?.referenceId; // => "0012345" ``` Every set in the map has at least one worked example on this site, and the [Cookbook](https://docs.cosyte.com/x12/cookbook.md) is where they live. The general emit surface those domain builders sit on top of is [Building and serializing X12](https://docs.cosyte.com/x12/emit-and-serialize.md). ## The reader/builder symmetry Every builder round-trips through its reader: `get835(parseX12(serializeX12(build835(spec))))` reproduces the spec field-for-field. That symmetry is the correctness contract: a builder that emitted something its own reader could not read back would be caught by the round-trip property tests. The builders are **pure functions**: they never auto-send, open a socket, or touch the filesystem, and they **refuse** a structurally impossible spec with a typed error rather than emitting corruption. **One place that symmetry is enforced by a value check rather than by construction: the 278's review level.** Every HL-03 in the library is a library constant chosen by tree position, with a single exception - `Build278ReviewSpec.levelCode`, which you supply and which the TR3s limit to `EV` (patient event) and `SS` (service). A level outside those two emits a perfectly well-formed document, but the review loop it opens is one no reader opens, so `get278Response` returns the review **and its `HCR-01` certification decision** as absent, with no warning. Nothing is mis-read - no decision comes back as a different decision, and the bytes stay on `tx.segments` - but the certification action is the field this library places verbatim and never infers, so `build278Request` / `build278Response` **refuse** an out-of-enum level (`X12_278_BUILD_INVALID_SPEC`) rather than emit a document that loses it. An omitted `levelCode` still defaults to `EV`. ## What is out of scope (v1) - **Non-healthcare sets**: 850 (purchase order), 856 (ASN), 810 (invoice), 204 (load tender), etc. - **The EDIFACT syntax family**: a different standard entirely. - **Transport**: AS2, SFTP, MLLP-style delivery. This is a parser/serializer, not a comms stack. - **Pre-005010 field maps.** Pre-005010 input is _tolerated and flagged_ (`X12_PRE_005010`), not decoded against those older guides. See [Troubleshooting & known limitations](https://docs.cosyte.com/x12/troubleshooting.md) for the full non-goals list. --- URL: https://docs.cosyte.com/x12/spec-notes-tolerance Markdown: https://docs.cosyte.com/x12/spec-notes-tolerance.md # Tolerance tiers & warning codes `@cosyte/x12` follows Postel's Law: **liberal on input, conservative on output.** Real payer and clearinghouse traffic is full of deviations that a strict validator would reject and a production integration cannot afford to drop. The parser tolerates them, records each one as a stable-coded warning with positional context, and preserves the raw value, so you decide what to do, instead of the library throwing your pipeline off the rails on a vendor quirk. ## The three tiers - **Tier 1: no deviation flagged.** Parses with no warnings. - **Tier 2: tolerated deviation.** A miscount, a dangling release character, an unknown CARC/RARC/HI code, an HL parent mismatch, a balance mismatch, a pre-005010 version. The parser **keeps going**, preserves the verbatim value, and emits a warning. This is the overwhelming majority of real-world quirks. - **Tier 3: unrecoverable structural corruption.** The bytes are not X12 at all. There are exactly **four** of these, and they always throw (see below). Only Tier 3 throws by default. Everything in Tier 2 is a warning you triage, not an exception you catch. **One thing sits outside the tiers: line breaks between segments are silently normalized.** Any run of CR / LF bytes after a segment terminator is absorbed with no warning, so a pretty-printed file is Tier 1, and so is a double-spaced one. That is the footnote "the parser never silently drops anything" needs: it drops inter-segment whitespace, never a decoded element value. The practical consequence is that `serializeX12(parseX12(file))` returns the compact form rather than your original bytes. It is not, however, the only thing the round trip does not reproduce. A **segment outside a transaction** is a Tier-2 deviation (`X12_UNEXPECTED_SEGMENT`): it is warned about, kept verbatim on `ix.orphanSegments`, and re-emitted at the structural `anchor` recorded with it, so both the value and the warning do survive the round trip. What it is not is **decoded** - no `get*` reader will see it, because retention and placement are not promotion into the typed tree. See [Line endings between segments](https://docs.cosyte.com/x12/spec-notes-envelope.md) for the full list of what the emit does not reproduce, and [KNOWN-LIMITATIONS.md](https://github.com/cosyte/x12/blob/main/KNOWN-LIMITATIONS.md) for the canonical statement of the boundary. ## Warnings collect on the model, and stream Every warning lands on the returned model's `.warnings` array (`ix.warnings`, `remit.warnings`, `sub.warnings`, …), each carrying a stable `code` (from `WARNING_CODES`), a `message` that is a **lookup into a frozen registry** rather than anything built from your document, and a `position`. No warning factory takes a value parameter, so no element can reach a `message`; the values stay on the model. See [Keeping PHI out of logs](https://docs.cosyte.com/x12/troubleshooting.md). You can also stream them live via the `onWarning` callback: ```ts runnable import { parseX12, type X12ParseWarning } from "@cosyte/x12"; const seen: X12ParseWarning[] = []; // ISA-12 declares an old version family (004010): tolerated, flagged, not fatal. const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00401*000000001*0*P*:~" + "IEA*0*000000001~"; const ix = parseX12(raw, { onWarning: (w) => seen.push(w) }); seen.length > 0; // => true ix.warnings.length > 0; // => true ``` ## The four fatal codes These are the only conditions that throw an `X12ParseError`, regardless of `strict`. They are unrecoverable structural corruption, not tolerable quirks: ```ts runnable import { FATAL_CODES } from "@cosyte/x12"; FATAL_CODES.X12_EMPTY_INPUT; // => "X12_EMPTY_INPUT" FATAL_CODES.X12_NO_ISA_HEADER; // => "X12_NO_ISA_HEADER" FATAL_CODES.X12_ISA_TOO_SHORT; // => "X12_ISA_TOO_SHORT" FATAL_CODES.X12_INVALID_DELIMITERS; // => "X12_INVALID_DELIMITERS" ``` - **`X12_EMPTY_INPUT`**: nothing to parse. - **`X12_NO_ISA_HEADER`**: the input does not begin with an ISA; it is not an X12 interchange. - **`X12_ISA_TOO_SHORT`**: the ISA is truncated below its fixed 106 bytes, so delimiters can't be read. - **`X12_INVALID_DELIMITERS`**: the delimiters can't be recovered from the ISA. ```ts runnable throws import { parseX12 } from "@cosyte/x12"; parseX12(""); // throws X12ParseError (X12_EMPTY_INPUT) ``` ## Escalate to strict when you want a conformance gate For a trusted trading partner where any deviation should be a hard failure, pass `{ strict: true }`. Every Tier-2 deviation then throws an `X12ParseError` **carrying the same warning code** it would otherwise have recorded, so a strict run fails on every deviation the parser flags, and a lenient run is production tolerance, with one code vocabulary across both: ```ts runnable throws import { parseX12 } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00401*000000001*0*P*:~" + "IEA*0*000000001~"; // The pre-005010 version is a warning by default; strict turns it into a throw. parseX12(raw, { strict: true }); // throws X12ParseError (X12_PRE_005010) ``` ## Integrity checks warn: they never rebalance or renumber The 835 balance invariants, the 837 HL parent-pointer integrity, and the serializer's envelope-count reconciliation all follow the same rule: on a mismatch they **surface a warning and preserve the inbound values verbatim**. The library will not "fix" a payer artifact for you. Gate your own posting/adjudication on the warning. See [Decimal-exact money](https://docs.cosyte.com/x12/spec-notes-money.md) for the 835 balance equations, and [Troubleshooting](https://docs.cosyte.com/x12/troubleshooting.md) for the full symptom → cause table. --- URL: https://docs.cosyte.com/x12/spec-notes-money Markdown: https://docs.cosyte.com/x12/spec-notes-money.md # Decimal-exact money Every monetary, percentage, and quantity field in `@cosyte/x12` decodes as an **`X12Decimal`**, never a JavaScript `number`. This is not a nicety. It is a correctness boundary. An EDI remittance is a financial instrument; representing `0.10 + 0.20` as a binary float (`0.30000000000000004`) and posting the difference is the money-handling analog of mis-reading a dose. > **The rule:** `@cosyte/x12` **never** calls `parseFloat` on an EDI amount, and neither should you. > Read the `X12Decimal`, do exact arithmetic on it, and format it back to a string. > **On the BUILD side this is enforced, not just a convention.** The builder specs type their > monetary and quantity fields as `X12Decimal`, so a TypeScript caller cannot hand them anything > else. A JavaScript or JSON-driven caller can, and every one of those slots now type-checks before > emitting: a raw `number` is **refused** rather than rendered. It used to be rendered: `0.1 + 0.2` > reached an 835's CLP-05 as `0.30000000000000004` with `warnings.length === 0`. Build the > `X12Decimal` yourself at your boundary; the library will not round for you, because choosing > between `0.30` and `0.3` is a decision about your money, not ours. > > **One caveat on the error you get.** Most slots refuse with that builder's typed, code-tagged > error. `build835`'s balance-equation amounts do not: the balance guard runs first and calls > `X12Decimal` methods on your value, so any amount it reads as a term of one of the three TR3 > X221A1 §1.10.2 invariants throws a plain `TypeError` with no `code`: `payment.totalActualPayment`, > `claim.totalChargeAmount`, `claim.totalPaymentAmount`, every `adjustments[].amount`, > `serviceLine.chargeAmount`, `serviceLine.paymentAmount` and `providerAdjustments[].amount`. It > still refuses; it just refuses untyped. Every other `X12Decimal` field refuses typed. See > `KNOWN-LIMITATIONS.md`. ## What `X12Decimal` is `X12Decimal` is a string-backed, `BigInt`-exact fixed-point number: it holds the unscaled integer and a scale, so the value is preserved to the exact number of decimal places the wire carried. It exposes only methods (the internals are non-enumerable), and construction is total. `fromString` returns `undefined` for a non-numeric string rather than throwing or silently coercing: ```ts runnable import { X12Decimal } from "@cosyte/x12"; const a = X12Decimal.fromString("0.10")!; const b = X12Decimal.fromString("0.20")!; // Exact: no binary-float drift. a.add(b).toString(); // => "0.30" // The verbatim string is preserved, decimal places and all. X12Decimal.fromString("450.00")!.toString(); // => "450.00" // A non-numeric input is rejected, not coerced. X12Decimal.fromString("not a number"); // => undefined ``` ## An amount this library cannot read never reads as `0` `X12Decimal.fromString` returning `undefined` is only half the story: the readers have to put _something_ on the model. Through `0.0.12` every monetary and quantity slot was typed `X12Decimal`, which cannot express "did not decode", so a claim's `totalPaymentAmount`, a service line's `chargeAmount` and an 837's `totalCharge` all fell back to `X12Decimal.ZERO`. A consumer reading the model could not tell that `0` from a zero the sender did state. **As of `0.0.13` no reader substitutes `X12Decimal.ZERO` for a value it did not decode.** Every slot that used to get one is `X12Decimal | undefined` and reads `undefined` instead. That is a breaking type change on the read model, and it is the point of it: the two facts are spec-distinct and now the model says which one you have. Read it as a rule about the substitution rather than as a census of the model: a row whose amount does not decode is sometimes dropped whole instead, which has a report of its own (`X12_AMOUNT_ROW_DROPPED`, below). No total is published here. An element that is present and does not match the shape `X12Decimal` decodes also emits **`X12_UNPARSEABLE_DECIMAL`**, carrying the failing element in `position.elementIndex`, so the two routes to `undefined` stay distinguishable: ```ts runnable import { parseX12, get835, WARNING_CODES } from "@cosyte/x12"; // BPR-02 carries a thousands separator, which X12 forbids in an R-type element. const raw = "ISA*00* *00* *ZZ*PAYER *ZZ*PROVIDER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HP*PAYER*PROVIDER*20260601*1200*1*X*005010X221A1~" + "ST*835*0001~" + "BPR*I*1,234.56*C*ACH*CCP*01*1*DA*1*1*20260601~" + "TRN*1*T1*1~" + "SE*4*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const tx = ix.groups[0]!.transactions[0]!; const remit = get835(ix.delimiters, tx)!; // The slot decoded nothing, and says so. remit.payment.totalActualPayment; // => undefined // The warning says WHY: the sender put bytes there and they did not decode. remit.warnings.some((w) => w.code === WARNING_CODES.X12_UNPARSEABLE_DECIMAL); // => true ``` Three things to hold onto: - **`undefined` means "this library decoded no value", not "the sender sent nothing".** Both routes land there. Gate on the warning to tell them apart, and gate on `undefined` itself before you post an amount, exactly as you would on a balance mismatch. - **An _absent_ element is a different fact and does not raise this code.** It reads `undefined`, and `X12_UNPARSEABLE_DECIMAL` fires only when the sender put bytes there and this library could not read them. Do not widen that into "an absent element is silent": where the absent element takes a whole `AMT` or `ADX` row off the model, the drop is announced by `X12_AMOUNT_ROW_DROPPED` (below), which is raised on both routes and so separates neither. Read the guarantee in exactly that direction: an unwarned value **at an element a reader decoded** is what the sender sent. It is not a promise about every slot on the model, because a slot a reader never read cannot warn. One instance in this library, an 837 service line whose `SVx` never decoded because it does not match the variant the submission resolved to, leaves `charge` and `units` `undefined` and is announced by its own warning, **`X12_837_SERVICE_LINE_NOT_DECODED`**, anchored at the `LX` that opened the line. **Do not read that as the only one, and do not gate on that code alone**: a Loop 2400 carrying a SECOND `SV1` / `SV2` / `SV3` reaches the same `charge: undefined` by a different route - the repeat writes its own absent element over the amount the first one stated - and on that line a service segment DID decode, so `X12_837_SERVICE_LINE_NOT_DECODED` does not fire. **`X12_837_SERVICE_SEGMENT_REPEATED`**, anchored at the repeated segment, is what announces it. No census of never-read slots is published here, on purpose: the rule is what holds. - **The 835 balance invariants do not sum an absent term.** Where any term of a TR3 X221A1 §1.10.2 equation is `undefined`, the equation is reported as **`X12_835_BALANCE_NOT_EVALUABLE`** rather than as a mismatch: nothing was measured out of balance, and substituting `0` for the missing term would be this library asserting a total nobody sent. - **On an optional slot the same warning disambiguates `undefined`.** A `paidUnitsOfService` of `undefined` used to mean either "the payer omitted it" or "the payer sent something unreadable". With a warning at that element index, it means the second; without one, the first. The bytes themselves are never in the message (they are consumer-controlled, and a monetary element is exactly where a mis-mapped identifier lands). Read them off `tx.segments[…].raw` when you need them. If you are walking segments yourself rather than using a `get*` reader, `readElementDecimal` gives you the same distinction in-band, as `{ value, status }` with `status` one of `"decoded"` / `"absent"` / `"unparseable"`. ## An amount row that decodes nothing loses the whole row, and says so `AMT` and `ADX` are not slots on a bigger record: each one _is_ a record, carrying an amount plus the thing the amount is about. So when the amount does not decode there is no row to put on the model at all, and AMT-01's qualifier or ADX-02's adjustment reason code goes with it. Through `0.0.12` that happened in complete silence - `AMT*B6~` gave `claim.amounts: []` and `warnings: []`, which reads exactly like a document that never carried the segment. **`X12_AMOUNT_ROW_DROPPED` reports it**, anchored at the `AMT` / `ADX` itself and carrying **no** `position.elementIndex`, because one of the two routes into it is an absent element and an absent element has no index to name: ```ts runnable import { parseX12, get835, WARNING_CODES } from "@cosyte/x12"; // AMT-02 is absent. The B6 qualifier the payer did send is lost with the row. const raw = "ISA*00* *00* *ZZ*PAYER *ZZ*PROVIDER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HP*PAYER*PROVIDER*20260601*1200*1*X*005010X221A1~" + "ST*835*0001*005010X221A1~" + "BPR*I*450.00*C*ACH*CCP*01*1*DA*1*1512345678**01*1*DA*2*20260601~" + "TRN*1*0012345*1512345678~" + "LX*1~" + "CLP*PT-ACCT-001*1*500.00*450.00*50.00*MC*PAYER-CLAIM-001*11*1~" + "AMT*B6~" + "SVC*HC:99213*500.00*450.00**1~" + "CAS*PR*1*50.00~" + "SE*10*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const tx = ix.groups[0]!.transactions[0]!; const remit = get835(ix.delimiters, tx)!; // No row reached the model. remit.claims[0]!.amounts.length; // => 0 // And the loss is on the warning channel rather than left for you to notice. remit.warnings.map((w) => w.code); // => ["X12_AMOUNT_ROW_DROPPED"] // The absent route raises this code ALONE. An unparseable AMT-02 raises it // alongside X12_UNPARSEABLE_DECIMAL at the failing element, which is what // tells the two apart - this code is raised for both and separates neither. remit.warnings.some((w) => w.code === WARNING_CODES.X12_UNPARSEABLE_DECIMAL); // => false ``` Bounds worth reading literally: - **It is additive: nothing moved onto it.** An unparseable amount still raises `X12_UNPARSEABLE_DECIMAL` at its own element, now alongside this code rather than instead of it, so a gate you already wrote against that code still fires on exactly the documents it fired on. What that gate never saw, on any release, is the absent-amount row above. Gate on both. - **It reports a row whose AMOUNT was read and decoded no value, and nothing wider.** It is a property of the READ, not of the walker's control flow. A segment discarded before its amount is read is not on this channel, and neither is one whose amount decoded and then found nothing open to attach the row to. **Do not read that as "nothing open means silent":** the 835 and the 837 decode the amount first, so an `AMT` with an absent amount and no claim open does raise this code. - **On the 835 and the 837 the lost row may be a LINE-level one.** Those readers attach an `AMT` to the open service line first and to the claim only when there is none, so an unchanged `claim.amounts` is not evidence the warning is stale. - **An 820 `RMR` is not on this channel, and not because its row survives.** `decodeRmr` drops on open-item identity (RMR-01 and RMR-02 both empty) **before** RMR-04 is read: an `RMR` that states an open item and no amount keeps its row with `amountPaid` `undefined`, while one that states an amount and **no** open item is dropped whole. That second case is a separate loss with a report of its own, `X12_STATED_AMOUNT_DISCARDED`, in the next section. ## A row the sender DID state, discarded anyway, and the report that says so The section above is about an amount this library could not read. This one is the opposite case, and it is worth keeping distinct because the reason the row is gone is different: **the reader threw it away for a reason that has nothing to do with the amount, so the amount was never the problem and may never even have been looked at.** The bytes are preserved verbatim on `tx.segments[…].raw`; decode them yourself before you post from them. **This code does not promise they will decode** - see the last bound below. **`X12_STATED_AMOUNT_DISCARDED` reports it as of `0.0.13`**, anchored at the segment and carrying no `position.elementIndex`. Two routes reach it, and the code does not say which: - An **820 `RMR`** under an open remittance loop whose RMR-01 and RMR-02 are **both empty** while RMR-04 or RMR-05 is populated. The open item is refused on identity before either amount element is read, so a stated payment, a stated amount due and RMR-03's payment action code leave together. - An **837 `AMT`** arriving while a Loop 2430 line adjudication (`SVD`) is open. `AMT-02` decoded, and the v1 adjudication model has no amount row to carry it, so the row is skipped rather than attached to this submission's own service line, which is not whose amount it is. ```ts runnable import { parseX12, get820Payments, WARNING_CODES } from "@cosyte/x12"; // RMR-01 and RMR-02 are both empty, so the open item has no identity to key // on - but RMR-04 and RMR-05 state 150.00 apiece. const raw = "ISA*00* *00* *ZZ*EMPLOYER *ZZ*PLAN " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*RA*EMPLOYER*PLAN*20260601*1200*1*X*005010X218~" + "ST*820*0001*005010X218~" + "BPR*C*250.00*C*ACH*CCP*01*1*DA*1*1512345678**01*1*DA*2*20260601~" + "TRN*1*PREM-0001~" + "ENT*1*2J*FI*GRP-0001~" + "RMR****150.00*150.00~" + "SE*6*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const tx = ix.groups[0]!.transactions[0]!; const prem = get820Payments(ix.delimiters, tx)!; // The whole open item is gone, payment and amount due with it. prem.remittances[0]!.openItems.length; // => 0 // And it is on the warning channel rather than left for you to notice. prem.warnings.map((w) => w.code); // => ["X12_STATED_AMOUNT_DISCARDED"] // This route refuses the row BEFORE attempting the decode, so it raises no // unparseable report - true even where the amount bytes are unreadable. prem.warnings.some((w) => w.code === WARNING_CODES.X12_UNPARSEABLE_DECIMAL); // => false ``` Bounds worth reading literally: - **It is additive, like the code above.** `X12_AMOUNT_ROW_DROPPED` and `X12_UNPARSEABLE_DECIMAL` fire on exactly the documents they fired on before. Nothing moved. - **The two amount-row codes are disjoint and never name the same segment.** This one requires an amount element the sender populated; the other requires one that decoded no value. So the code you get is the discriminant: it tells you whether this library failed to read the amount or discarded the row for some other reason. Gate on both. - **It reports a segment that arrived while the loop that would carry its row was open.** An `AMT` or `ADX` that reaches a reader with no such loop open is a different loss and is still silent: the 834's `AMT` with no `HD` open, the 820's `ADX` with no remittance open, and the 835's and 837's `AMT` before any claim or service line. `KNOWN-LIMITATIONS.md` records those. - **🩺 It says nothing about whether the amount would have decoded, and this is the bound to read literally.** On the `RMR` route the row is refused before the decode is attempted, so the code is raised on `RMR****1,234.56~` exactly as on `RMR****150.00*150.00~`, with no `X12_UNPARSEABLE_DECIMAL` beside it in either case. **Do not read an unaccompanied instance as evidence the bytes are postable.** Only route 2 guarantees a decodable amount, because there `AMT-02` decoded before the row was skipped. ## The operations you need `X12Decimal` covers the arithmetic a posting or balance check requires, all exact, all returning new immutable instances: ```ts runnable import { X12Decimal } from "@cosyte/x12"; const charge = X12Decimal.fromString("500.00")!; const paid = X12Decimal.fromString("450.00")!; charge.subtract(paid).toString(); // => "50.00" paid.negate().toString(); // => "-450.00" charge.equals(paid); // => false paid.isZero(); // => false X12Decimal.ZERO.isZero(); // => true // compareTo returns -1 | 0 | 1 paid.compareTo(charge); // => -1 ``` `signum()` gives the sign as `-1 | 0 | 1`, `abs()` the magnitude, and `toNumber()` exists as a deliberate, lossy escape hatch. Use it only for display math you will never post from. ## Why the 835 balance is decimal-exact The 835 reader runs the TR3 X221A1 §1.10.2 balance invariants entirely in `X12Decimal`, so the check is exact rather than float-approximate: - **Service line:** `SVC-03` line paid + Σ(line `CAS` adjustments) = `SVC-02` line charge. - **Claim:** `CLP-04` claim paid + Σ(**all** `CAS` in the claim, both claim-level and every nested line) = `CLP-03` claim charge. Patient responsibility (`CLP-05`) is **not a separate term** in this equation. It is informational, and it equals the sum of the `PR`-group `CAS` adjustments, which are already inside that Σ. Adding it again would double-count. - **Remit total:** `BPR-02` = Σ(`CLP-04` claim payments) − Σ(`PLB` provider-level adjustments), where `PLB` amounts carry the raw EDI sign (a positive `PLB` is a take-back). On a mismatch the reader emits `X12_835_REMIT_BALANCE_MISMATCH` and **preserves the inbound values verbatim**. It never rebalances. The `build835` builder reuses those same authoritative validators and **refuses** to emit an out-of-balance remit (`Remit835BuildError`), so the read guard and the emit guard share one source of truth. See [Tolerance tiers](https://docs.cosyte.com/x12/spec-notes-tolerance.md) for the warn-never-fix rule, and the [Cookbook](https://docs.cosyte.com/x12/cookbook.md) for the full posting recipe. ## Sign discipline Amounts carry the sign the wire gave them, and the library never flips it for you. In the 835 top-line equation a positive `PLB` reduces the payment (a take-back); in an 837 line adjudication the CAS group codes tell you the direction. Read the sign, don't assume it: the same discipline that keeps the `groupCode` a value you read rather than infer. --- URL: https://docs.cosyte.com/x12/emit-and-serialize Markdown: https://docs.cosyte.com/x12/emit-and-serialize.md # Building and serializing X12 The read side is only half of `@cosyte/x12`. The emit side ships too, and it is two layers: - **`serializeX12(ix)`** turns any `X12Interchange` back into bytes. It is the exact inverse of `parseX12` for every segment the parser recorded, and it is what you use when you already have an interchange in hand: one you parsed, or one a builder composed. - **`buildInterchange(spec)`** composes an interchange from nothing. It owns the envelope mechanics (the ISA fixed-width layout, the GS / GE / ST / SE / IEA control segments, and the SE-01 / GE-01 / IEA-01 counts), so you never hand-count a segment total. The per-guide domain builders (`build835`, `build837P`, `build271`, …) all sit on top of it; this page is the floor they stand on. > Every interchange below is **synthetic**: fabricated names, obviously-fake ids, pre-2024 control > numbers. X12 healthcare data is PHI; a fixture must never hold a real one. ## The asymmetry: liberal on read, conservative on emit The parser is lenient. The builders are not, and that is deliberate (Postel's Law). Where the parser meets a structure it cannot decode it **preserves the bytes and warns**; where a builder meets one of the specs listed below it **refuses with a typed error rather than silently corrupting the document**. Nothing is ever quietly corrected on the way out. A wrong element position on a claim is a wrong payment. Concretely, a builder refuses rather than guesses when: - a **delimiter set is not shaped like one**: each of the four roles must be exactly one visible character and the four must be mutually distinct; - a delimiter is **`?`** in any of the four roles, because one byte cannot both separate and escape, so the library's own structural join would be emitted as an escape (and the escape as structure); - a **required control number is empty**, which used to be zero-padded into a control number nobody sent; - a slot typed `X12Decimal` is handed a raw `number`, which is how `0.1 + 0.2` used to reach a monetary element as `0.30000000000000004`. Each refusal is a typed, code-tagged error (`X12BuildError` from `buildInterchange`, and each domain builder's own class), so you branch on `err.code` rather than on a message. ## Compose an interchange ```ts runnable import { buildInterchange, parseX12, serializeX12 } from "@cosyte/x12"; const ix = buildInterchange({ senderId: "SENDER", receiverId: "RECEIVER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groups: [ { functionalIdCode: "HC", groupControlNumber: "1", versionRelease: "005010X222A2", transactions: [ { transactionSetIdCode: "837", transactionSetControlNumber: "0001", implementationConventionReference: "005010X222A2", segments: [["BHT", "0019", "00", "SUBMIT-0001", "20260601", "1200", "CH"]], }, ], }, ], }); const emitted = serializeX12(ix); // The counts are computed, never supplied: ST + BHT + SE is three segments. emitted.includes("ST*837*0001*005010X222A2~"); // => true emitted.includes("SE*3*0001~"); // => true emitted.includes("GE*1*1~"); // => true emitted.endsWith("IEA*1*000000001~"); // => true // The emit is a fixed point: re-parsing and re-emitting is a byte-level no-op. serializeX12(parseX12(emitted)) === emitted; // => true ``` Element values on a `SegmentSpec` are **logical**, not framed: the builder applies the `?`-release escape on emit, so a value carrying an active delimiter survives instead of splitting its own segment. The segment id (element 0) is emitted verbatim, because a segment id is the library's and not caller content. ## What a refusal looks like `?` is the conventional X12 release character. Declaring it as one of the four delimiters is admissible on the wire, and `parseX12` reads such an interchange happily. Emitting one is refused, because the library would have to write its own structural joins as escapes: ```ts runnable import { buildInterchange, X12BuildError, X12_BUILD_ERROR_CODES } from "@cosyte/x12"; let refusal: X12BuildError | undefined; try { buildInterchange({ senderId: "SENDER", receiverId: "RECEIVER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", componentSeparator: "?", groups: [], }); } catch (err) { if (err instanceof X12BuildError) refusal = err; } // Refused, with a code to branch on - not emitted with a warning attached. refusal !== undefined; // => true refusal?.code; // => X12_BUILD_ERROR_CODES.X12_BUILD_INVALID_SPEC ``` The refusal is an equality test on the delimiter set you declare. Read it as a property of that set and never as a guarantee about every document the library can compose: it says this set is unemittable, not that any other set is safe. ## Byte fidelity, and its two silent limits `serializeX12` is **byte-faithful for the segments on the model**. Each comes back verbatim, including element padding, composite structure and `?`-release escapes, in the order the model holds it. Anything the parser never recorded does not come back, so **`serialize(parse(s)) === s` is not guaranteed**, and a file with no line breaks is not thereby safe. Two of those are the ones to know, because both are **silent**: nothing warns, so a clean `ix.warnings` is not evidence that a round trip will be byte-exact. 1. **Line breaks between segments.** The parser absorbs any run of CR / LF bytes after a segment terminator and the model has nowhere to record it, so a pretty-printed or double-spaced source emits as its compact form. 2. **A doubled segment terminator** outside a transaction. It delimits a zero-length segment carrying no elements, so there is nothing to retain and nothing to put back. ```ts runnable import { parseX12, serializeX12 } from "@cosyte/x12"; // The `GE*1*1~~` is a doubled terminator: a zero-length segment between the // group trailer and the IEA. const doubled = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HC*SENDER*RECEIVER*20260601*1200*1*X*005010X222A2~" + "ST*837*0001~SE*2*0001~GE*1*1~~" + "IEA*1*000000001~"; const ix = parseX12(doubled); const emitted = serializeX12(ix); // Not reproduced, and nothing said so: emitted === doubled; // => false ix.warnings.length; // => 0 // What does hold is the fixed point, which is what the round trip is for. emitted === serializeX12(parseX12(emitted)); // => true ``` The full list is six constructs, enumerated with the rest of the emit contract in [Line endings between segments](https://docs.cosyte.com/x12/spec-notes-envelope.md), and the canonical statement of the boundary is the package's `KNOWN-LIMITATIONS.md`. ## Reconciling an envelope you did not write By default `serializeX12` reconciles nothing: it reproduces the model. Pass `{ specClean: true }` and it **checks** the envelope counts and the three control-number pairs (ISA-13 against IEA-02, GS-06 against GE-02, ST-02 against SE-02), surfacing any mismatch through `onWarning`. It still does not correct one. Substituting the recomputed SE-01 / GE-01 / IEA-01 counts is a second, separate opt-in (`{ recomputeCounts: true }`), which is **inert without `specClean`**, and control numbers are identity rather than derived, so they are **never** rewritten, only flagged. ```ts runnable import { parseX12, serializeX12, type X12ParseWarning } from "@cosyte/x12"; // SE-01 declares two segments; three are present. The sender's count is stale. const stale = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HC*SENDER*RECEIVER*20260601*1200*1*X*005010X222A2~" + "ST*837*0001~BHT*0019*00*SUBMIT-0001*20260601*1200*CH~SE*2*0001~" + "GE*1*1~IEA*1*000000001~"; const seen: X12ParseWarning[] = []; const emitted = serializeX12(parseX12(stale), { specClean: true, onWarning: (w) => seen.push(w), }); // Reported... seen.some((w) => w.code === "X12_SEGMENT_COUNT_MISMATCH"); // => true // ...and NOT corrected. The sender's SE-01 stands in the bytes. emitted.includes("SE*2*0001~"); // => true ``` ## Where to go next - [The 80/20 transaction sets](https://docs.cosyte.com/x12/spec-notes-transaction-sets.md): the builder each set is emitted through, and the field each one preserves verbatim. - [Cookbook](https://docs.cosyte.com/x12/cookbook.md): worked builder recipes, including the 271 TRN-echo round trip and the 837 guide-identifier envelope. - [Decimal-exact money](https://docs.cosyte.com/x12/spec-notes-money.md): why a monetary slot refuses a raw `number` rather than rounding it. - [Trading-partner profiles](https://docs.cosyte.com/x12/profiles.md): describing what a partner actually sends, on the read side. --- URL: https://docs.cosyte.com/x12/profiles Markdown: https://docs.cosyte.com/x12/profiles.md # Trading-partner profiles Two senders can both be conformant and still hand you different bytes. A clearinghouse adds a `REF` the base guide never mentions; a plan declares a backslash where everyone else declares a colon; a payer mandates an element the implementation guide calls situational. A **profile** is how `@cosyte/x12` writes that down: as typed, fixture-grounded data you can read, rather than as silent leniency buried in the parser. > Every interchange below is **synthetic**: fabricated names, obviously-fake ids, pre-2024 control > numbers. X12 healthcare data is PHI; a fixture must never hold a real one. ## Descriptive, not prescriptive This is the load-bearing sentence on the page, so it is the first one. **A profile describes what a partner sends. It does not change how the parser reads.** The lenient parser already absorbs the corpus deviations losslessly, most of them with no warning at all, so a profile has nothing to relax. Attaching one alters no decode, promotes no value, suppresses no warning and drops no byte. What it buys you is **attribution**: the deviation stops being an unexplained shape in someone's file and becomes a named, documented, fixture-cited convention. That is the opposite of a prescriptive profile, which would tell the parser to expect a shape and quietly discard whatever failed to match it. Discarding is exactly what making the deviation explicit exists to prevent. Two consequences worth being blunt about: - **A profile is not validation.** It never asserts that an interchange conforms to the partner's guide, and a document that contradicts every quirk in the profile parses identically with it and without it. - **A profile is not permission.** Each quirk optionally records a judgement of whether a partner may lawfully _require_ the deviation, under 45 CFR 162.915. That judgement is a human reading of the rule that somebody wrote down, never a check this library performed, and its fail-safe default is `"undetermined"`. Read that as "no claim was made", never as "permitted". ## What a profile declares `defineProfile()` takes a name, an optional description, an optional `extends` (parent profiles compose: lineage and quirks merge), and a list of **quirks**. Each quirk is: | Field | What it says | | ------------------ | ---------------------------------------------------------------------------------- | | `id` | Stable, kebab-case, unique within the profile. | | `effect` | Which bucket `describe()` renders it into: `relaxes`, `adds`, or `requires`. | | `summary` | One line, structural only. A summary never contains patient data. | | `fixture` | A real corpus file, relative to `test/fixtures/`, that demonstrates the deviation. | | `sourceCategory` | Where the deviation was observed (which companion guide, which corpus). | | `expectedWarnings` | The warning codes this deviation leads you to expect. Often empty. | | `conformance` | The optional 45 CFR 162.915 judgement above. Absent means `"undetermined"`. | **`fixture` is required at the type level, and that is the hard rule of the whole subsystem: there are no invented quirks.** A quirk with no file demonstrating it does not compile, and the repo's own accuracy suite parses each cited fixture and asserts the claimed deviation is actually present. A profile is therefore a record of something observed, not a guess about what a partner might do. Two profiles ship built in, `profiles.availity` and `profiles.bcbsCommon`, and both are authored through the same public `defineProfile()` a consumer uses. There is no private authoring path. ```ts runnable import { defineProfile, type X12ProfileSpec } from "@cosyte/x12"; const spec: X12ProfileSpec = { name: "anytown-regional", description: "Anytown Regional Health Plan - 835 remittance conventions", quirks: [ { id: "service-line-ref-f8", effect: "adds", summary: "Service line (Loop 2110) carries a REF*F8 original-reference identifier.", fixture: "remit/835-availity-quirk.edi", sourceCategory: "Anytown Regional 835 companion guide - service-line REF", }, ], }; const profile = defineProfile(spec); profile.name; // => "anytown-regional" profile.quirks.length; // => 1 // `describe()` returns DATA, not a formatted string, so tooling can consume it. const described = profile.describe(); described.adds.map((q) => q.id); // => ["service-line-ref-f8"] described.relaxes.length; // => 0 // No judgement was recorded, so none is reported. This is the fail-safe // default, and it is NOT a claim that a partner may require the deviation. described.adds[0]?.conformance; // => "undetermined" described.conformance.permitted.length; // => 0 described.conformance.undetermined.map((q) => q.id); // => ["service-line-ref-f8"] ``` `defineProfile()` refuses a malformed spec with a typed `X12ProfileError` rather than defining a half-valid profile, and the refusal message is bounded so a large value cannot grow it without limit. ## Applying one to a parse Pass the profile on the parse options. It attaches to the result as `ix.profile`, and it changes nothing else about the parse: ```ts runnable import { parseX12, profiles } from "@cosyte/x12"; // A BCBS-style envelope: ISA-16 declares a backslash component separator // rather than the colon most consumers assume. const raw = "ISA*00* *00* *ZZ*BCBSPLAN *ZZ*ANYTOWNCLINIC " + "*260601*1200*^*00501*000000001*0*P*\\~" + "GS*HP*BCBSPLAN*ANYTOWNCLINIC*20260601*1200*1*X*005010X221A1~" + "ST*835*0001~" + "SE*2*0001~GE*1*1~IEA*1*000000001~"; const withProfile = parseX12(raw, { profile: profiles.bcbsCommon }); const withoutProfile = parseX12(raw); // Attribution: the parse says which partner convention it was read under. withProfile.profile?.name; // => "bcbsCommon" withoutProfile.profile; // => undefined // And the decode is identical either way. The separator was detected from // ISA-16 by the parser, not supplied by the profile. withProfile.delimiters.component; // => "\\" withoutProfile.delimiters.component; // => "\\" withProfile.warnings.length === withoutProfile.warnings.length; // => true // What the profile adds is the documented reason that byte is what it is. withProfile.profile?.describe().relaxes.map((q) => q.id); // => ["backslash-component-separator"] ``` An explicit `profile` always wins. `parseX12(raw, { profile: null })` opts out for a single call. `setDefaultProfile()` registers a process-scoped default that `parseX12(raw)` consults when you pass none, and `getDefaultProfile()` reads it back. That default is the only mutable state in the library: it is not shared across worker threads and is not reset between test files, so prefer passing the profile explicitly and clean up in teardown if you do register one. ## Separating the warnings you expect from the ones you do not The one behavioural hook a profile carries is `partitionWarnings()`. It splits a parse's warnings against the union of every quirk's `expectedWarnings`, so an integration can alert on the genuinely unexpected instead of on a partner's known convention. It is a pure function: it never mutates the warnings, never removes one, and both halves together are always the whole input. ```ts runnable import { defineProfile, parseX12, partitionWarnings } from "@cosyte/x12"; // This partner is still on a pre-005010 version family. It is a known, // documented fact about them rather than a surprise about this file. const legacyPartner = defineProfile({ name: "legacy-version-partner", quirks: [ { id: "pre-005010-envelope", effect: "relaxes", summary: "ISA-12 declares a pre-005010 interchange control version.", fixture: "envelope/bcbs-subelement.edi", sourceCategory: "partner companion guide - interchange control version", expectedWarnings: ["X12_PRE_005010"], }, ], }); const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER " + "*260601*1200*^*00401*000000001*0*P*:~" + "IEA*0*000000001~"; const ix = parseX12(raw, { profile: legacyPartner }); const { expected, unexpected } = partitionWarnings(ix.warnings, legacyPartner); // The warning is still raised, still on the model, and still says the same // thing. The profile only sorts it. ix.warnings.some((w) => w.code === "X12_PRE_005010"); // => true expected.map((w) => w.code); // => ["X12_PRE_005010"] unexpected.length; // => 0 // Nothing was removed: the two halves are the whole warning list. expected.length + unexpected.length === ix.warnings.length; // => true ``` Alert on `unexpected`. An empty `expected` on a partner you wrote a profile for is worth looking at too: it usually means the convention you documented did not appear in this file. ## Where to go next - [Tolerance tiers & warning codes](https://docs.cosyte.com/x12/spec-notes-tolerance.md): what the parser tolerates before a profile is involved at all, and the codes `expectedWarnings` names. - [Building and serializing X12](https://docs.cosyte.com/x12/emit-and-serialize.md): the emit side, which profiles do not touch. - [Troubleshooting & known limitations](https://docs.cosyte.com/x12/troubleshooting.md): the bounds on the conformance judgement, and what a refusal message may and may not contain. --- URL: https://docs.cosyte.com/x12/cookbook Markdown: https://docs.cosyte.com/x12/cookbook.md # Cookbook Task-oriented recipes for the transactions you actually get handed. Each one is: here's the problem, here's the code, here's what you get back. Every symbol below is a real `@cosyte/x12` export, no pseudo-API. All sample EDI is **synthetic** (fabricated names, obviously-fake ids, pre-2024 control numbers); never paste a real interchange into a doc. The parser is **lenient by default**: vendor deviations become warnings with a stable code, not failures. Read [Getting started](https://docs.cosyte.com/x12/intro.md) first for the envelope model; the recipes here assume you can already get a parsed interchange. --- ## 1. Parse an 835 ERA and post payments **The problem:** you have a remittance advice (electronic EOB) and need to post the cash. Walk each claim and service line, read the CARC/RARC adjustments, and refuse to post an out-of-balance remit. `get835(delimiters, tx)` returns a typed `X12Remittance`, or `undefined` if the transaction set isn't an 835. Money is `X12Decimal` throughout (BigInt-exact, **never `parseFloat` an EDI amount**). ```ts import { parseX12, get835, lookupCarc, lookupRarc, WARNING_CODES } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*MEDPAY *ZZ*CLINIC001 " + "*260601*1200*^*00501*000000001*0*P*:~" + "GS*HP*MEDPAY*CLINIC001*20260601*1200*1*X*005010X221A1~" + "ST*835*0001~" + "BPR*I*450.00*C*ACH*CCP*01*021000021*DA*1234567*1512345678**01*021000021*DA*98765*20260601~" + "TRN*1*0012345*1512345678~" + "N1*PR*MEDICARE PART A~" + "N1*PE*SAMPLE CLINIC INC*XX*1234567890~" + "LX*1~" + "CLP*PT-ACCT-001*1*500.00*450.00*50.00*MB*CLAIMREF001*11~" + "NM1*QC*1*PATIENT*TEST****MI*MEMBER001~" + "SVC*HC:99213*500.00*450.00~" + "CAS*PR*1*50.00~" + "SE*11*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835"); const remit = tx ? get835(ix.delimiters, tx) : undefined; if (remit === undefined) throw new Error("not an 835"); // Payment header: the money movement primitive. remit.payment.totalActualPayment?.toString(); // "450.00" remit.payment.creditDebitFlag; // "C" remit.payment.method; // "ACH" remit.traces[0]?.referenceId; // "0012345": reassociation trace (EFT number) for (const claim of remit.claims) { claim.patientControlNumber; // "PT-ACCT-001": your account number, echoed back claim.totalChargeAmount?.toString(); // "500.00" claim.totalPaymentAmount?.toString(); // "450.00" claim.patientResponsibilityAmount?.toString(); // "50.00" for (const line of claim.serviceLines) { line.productServiceId; // "99213" line.paymentAmount?.toString(); // "450.00" // CARC: Claim Adjustment Reason Code. `reasonDescription` is prefilled // from the bundled snapshot; fall back to lookupCarc for the raw entry. for (const adj of line.adjustments) { adj.groupCode; // "PR": patient responsibility (the safety-critical field) adj.reasonCode; // "1" adj.reasonDescription ?? lookupCarc(adj.reasonCode)?.description; // "Deductible..." adj.amount?.toString(); // "50.00" } // RARC: Remittance Advice Remark Code (LQ*HE), if present. for (const remark of line.remarks) { remark.code; // "N4" remark.description ?? lookupRarc(remark.code)?.description; } } } ``` **Respect the balance warning.** The walker runs the TR3 X221A1 §1.10.2 balance invariants (the claim-level `CLP-04 + Σ(claim CAS + line CAS) == CLP-03`, the per-line `SVC-03 + Σ(line CAS) == SVC-02`, and the top-of-remit `BPR-02 == Σ(CLP-04) - Σ(PLB)`) and emits `X12_835_REMIT_BALANCE_MISMATCH` on a mismatch. It **never silently rebalances**. The inbound values stand. **Gate on two codes, not one:** where a term of an equation is `undefined` the equation cannot be run and you get `X12_835_BALANCE_NOT_EVALUABLE` instead, which is equally a document you must not auto-post. Through `0.0.12` that case collapsed to zero and raised the mismatch, so a gate written against the mismatch alone stops firing on it when you upgrade: ```ts const doNotPost = remit.warnings.some( (w) => w.code === WARNING_CODES.X12_835_REMIT_BALANCE_MISMATCH || w.code === WARNING_CODES.X12_835_BALANCE_NOT_EVALUABLE, ); if (doNotPost) { // Do NOT auto-post. Route to a human. Either the payer's numbers don't add // up, or an amount one of the equations needs did not decode at all. } ``` ### Check an adjustment against the CORE Code Combinations table you supply CAQH CORE Operating Rule 360 conveys why a claim was adjusted by the combined use of a group code, a reason code and optionally one or more remark codes, and its CORE Code Combinations table lists the combinations it expects for each of four CORE-defined business scenarios. `checkCoreCodeCombination` answers whether one adjustment's combination is in **your** copy of that table for a scenario you name. It answers that one question about that one adjustment, and nothing about the remit as a whole. **You supply the table and name its version.** No CORE table, excerpt or default ships with this package. CAQH CORE revises the table several times a year and publishes it under its own notice, so the version your posting system follows is yours to load, and the label you give it comes back beside every answer. ```ts runnable import { CORE_BUSINESS_SCENARIOS, checkCoreCodeCombination, type CoreCodeCombinationTable, } from "@cosyte/x12"; // YOUR table, loaded from the CORE Code Combinations version you follow. These // rows are invented for the example: no CORE table ships with this package. const table: CoreCodeCombinationTable = { version: "our-transcription-2026-02", rows: [ { scenario: "scenario-3", groupCode: "CO", reasonCode: "ZZ901" }, { scenario: "scenario-3", groupCode: "CO", reasonCode: "ZZ901", remarkCode: "ZZ-R1" }, ], }; // You name the scenario; nothing here infers one. const scenario = CORE_BUSINESS_SCENARIOS.SERVICE_NOT_COVERED; // In the loop above these are `adj` and the remarks you pair with it. const adjustment = { groupCode: "CO", reasonCode: "ZZ901" }; const remarks = [{ system: "HE", code: "ZZ-R1" }]; const listed = checkCoreCodeCombination({ table, scenario, adjustment, remarks }); listed.outcome; // => "in-table" listed.tableVersion; // => "our-transcription-2026-02" // A remark the table does not list with this pair. const unlisted = checkCoreCodeCombination({ table, scenario, adjustment, remarks: [{ system: "HE", code: "ZZ-R2" }], }); unlisted.outcome; // => "not-in-table" // No table at all: unevaluated, which is NOT permission to post. const noTable = checkCoreCodeCombination({ scenario, adjustment }); noTable; // => { outcome: "unevaluated", reason: "no-table", tableVersion: undefined } ``` **Three answers, and `unevaluated` is not permitted.** `in-table` means the table has a row for that scenario, group code and reason code, and lists every remark you passed with them; with no remarks, a row for the pair suffices. `not-in-table` means the table has rows for the scenario and this combination is not among them, including a combination the table lists only under another scenario. `unevaluated` means the check could not decide, and `reason` says why: no table, a scenario outside the four, no rows for that scenario, an empty group or reason code, or a remark that is not an `HE` remark code (an LQ `RX` reject reason, for instance). Gate on `in-table`; a gate written as "post unless `not-in-table`" posts every combination the check could not judge. **What it leaves to you.** You choose the scenario. The 835 carries remarks per claim and per service line rather than per adjustment, so you choose which remarks accompany the adjustment. Codes are compared exactly as written, with no case folding and no trimming. It judges one adjustment per call, does not say which remark was missing from the table, does not check a code's effective dates, and does not evaluate a scenario a health plan defines for itself or the retail pharmacy variant that pairs a reason code with an NCPDP reject code. A malformed table (no version label, a row with an empty code or an unknown scenario) throws `CoreCodeCombinationTableError` with the code `X12_CORE_COMBINATION_TABLE_INVALID`, and its message names the field and the row index, never a value from your table. --- ## 2. Parse a 277CA and route rejections **The problem:** a clearinghouse sent back a 277CA claim acknowledgment for a batch you submitted. You need to know, per claim, whether it was **accepted** into adjudication or **rejected** at the front door, and route the rejects for rework. `get277CADisposition(delimiters, tx)` admits only the X214 convention and returns an `X12ClaimStatusResponse`. The status lives in STC triples: **CSCC** (category, source 507) + **CSC** (status, source 508) + entity. Category `A1`/`A2` = acknowledged/accepted; `A7`/`A6` = rejected. ```ts import { parseX12, get277CADisposition } from "@cosyte/x12"; const ix = parseX12(raw277ca); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "277"); const ack = tx ? get277CADisposition(ix.delimiters, tx) : undefined; if (ack === undefined) throw new Error("not a 277CA (005010X214)"); const rejected: string[] = []; for (const claim of ack.claims) { claim.traces[0]?.referenceId; // echoes your submitted TRN, reassociate here const stc = claim.statuses[0]?.statuses[0]; if (stc === undefined) continue; stc.categoryCode; // "A2" accepted | "A7" rejected stc.statusCode; // e.g. "20" (accepted for processing) | "21" (missing/invalid) stc.statusDescription; // "Accepted for processing." | "Missing or invalid information." stc.entityCode; // e.g. "PR" | "85" (which entity the status is about) // A1/A2 = accepted into the adjudication system; anything else is a reject. const accepted = stc.categoryCode === "A1" || stc.categoryCode === "A2"; if (!accepted) rejected.push(claim.traces[0]?.referenceId ?? "(no trace)"); } // `rejected` now holds the traces to pull and rework. ``` `get277Status` decodes the plain 277 (X212) response the same way; it admits either convention, while `get277CADisposition` refuses a non-X214 transaction (returns `undefined`). Unknown category/status codes are preserved verbatim and raise `X12_UNKNOWN_CLAIM_STATUS_CATEGORY` / `X12_UNKNOWN_CLAIM_STATUS`. The code is never dropped. ### The plain 277 (X212): a status answer, with the charge it is about The X212 response carries the same STC triple plus the claim-level totals and the Loop 2220 service lines, and it echoes the requesting 276's `TRN-02` so you can reassociate: ```ts runnable import { parseX12, get277Status } from "@cosyte/x12"; const raw277 = [ "ISA*00* *00* *ZZ*MEDPAY *ZZ*ANYTOWNCLINIC *260601*1200*^*00501*000000010*0*P*:~", "GS*HN*MEDPAY*ANYTOWNCLINIC*20260601*1200*10*X*005010X212~", "ST*277*0001*005010X212~", "BHT*0010*08*STATUS-001*20260601*1200*DG~", "HL*1**20*1~", "NM1*PR*2*MEDPAY INSURANCE*****PI*PAYER01~", "HL*2*1*21*1~", "NM1*41*2*ANYTOWN CLINIC*****46*RECVR01~", "HL*3*2*19*1~", "NM1*1P*2*ANYTOWN CLINIC*****XX*1234567890~", "HL*4*3*22*0~", "NM1*IL*1*DOE*JANE****MI*MBR0001~", "TRN*2*ECHO-276-TRACE-001~", "STC*A2:20:PR*20260601*WQ*150~", "REF*1K*PCN0001~", "DTP*472*D8*20260520~", "SVC*HC:99213*150*0****1~", "STC*A2:20*20260601~", "REF*FJ*LINE001~", "DTP*472*D8*20260520~", "SE*19*0001~", "GE*1*10~", "IEA*1*000000010~", ].join("\n"); const ix277 = parseX12(raw277); const tx277 = ix277.groups[0]?.transactions.find((t) => t.st.elements[1] === "277"); const response = tx277 ? get277Status(ix277.delimiters, tx277) : undefined; response?.transactionType; // => "claim-status" // Reassociate on the trace you submitted, never on a name or a date. const statusClaim = response?.claims[0]; statusClaim?.traces[0]?.referenceId; // => "ECHO-276-TRACE-001" statusClaim?.references[0]?.value; // => "PCN0001" // The claim-level STC: the triple, plus what the status is ABOUT. const info = statusClaim?.statuses[0]; info?.actionCode; // => "WQ" info?.totalChargeAmount?.toString(); // => "150" info?.statuses[0]?.categoryCode; // => "A2" info?.statuses[0]?.statusCode; // => "20" info?.statuses[0]?.statusDescription; // => "Accepted for processing." // Loop 2220 carries the per-line answer. statusClaim?.serviceLines[0]?.procedureCode; // => "99213" statusClaim?.serviceLines[0]?.statuses[0]?.statuses[0]?.statusCode; // => "20" ``` --- ## 3. Build a 271, then parse it: the TRN-echo round-trip **The problem:** you're the payer side and need to emit an eligibility **response**, then prove the reassociation contract holds: the 271 echoes the requesting 270's TRN-02 **verbatim** so the provider can match your answer to their question. The inquiry side has its own pair, `build270` and `get270Inquiry`, so both ends of the exchange are typed; this recipe stays on the response half and demonstrates `build271` → `get271Eligibility`. `build271(spec)` computes the HL spine for you (source → receiver → subscriber → dependent) and **refuses** a structurally impossible hierarchy via `Eligibility271BuildError`. It returns a frozen `X12Interchange`. It never auto-sends, opens a socket, or touches the filesystem. ```ts import { parseX12, build271, get271Eligibility, X12Decimal, type Build271Spec } from "@cosyte/x12"; const traceFromThe270 = "ELIG20220627001"; // <- pulled from the inbound 270's TRN-02 const spec: Build271Spec = { envelope: { senderId: "MEDPAY", receiverId: "PROVIDER", interchangeDate: "220601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [ { entity: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123", }, receivers: [ { entity: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890", }, subscribers: [ { traces: [{ traceTypeCode: "2", referenceId: traceFromThe270 }], // echo it back name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001", }, benefits: [ { eligibilityCode: "1", // active coverage coverageLevelCode: "IND", serviceTypeCodes: [{ code: "30" }], monetaryAmount: X12Decimal.fromString("1000.00")!, }, ], }, ], }, ], }, ], }; const ix = build271(spec); // Round-trip it back through the reader: const tx = ix.groups[0]?.transactions[0]; const elig = tx ? get271Eligibility(ix.delimiters, tx) : undefined; // The safety-critical reassociation property holds byte-for-byte: elig?.subscribers[0]?.traces[0]?.referenceId === traceFromThe270; // true elig?.subscribers[0]?.name?.lastName; // "DOE" elig?.subscribers[0]?.benefits[0]?.eligibilityCode; // "1" ``` The same pattern (`build277` / `build277CA` echoing the 276's trace) covers claim-status responses. ### 🩺 Telling a rejected inquiry from a member with no benefits **The problem:** an empty benefit list has two completely different meanings. Either the payer answered and this member has no benefits of the kind you asked about, or the payer never processed your inquiry at all and said why, in an `AAA` request-validation segment. Reporting the second as the first tells a patient they have no coverage when nobody ever checked. `elig.aaaConditions` is the difference. It is **always present** and it is **empty** for a response that carries no `AAA`, so a present-and-empty collection is a stated zero rather than a reader that does not look. Each entry names the level the payer rejected at, the loop occurrence it belongs to, and the payer's own reject reason and follow-up action codes, echoed **verbatim**. The bundled description snapshots for those two code lists ship **empty**: their maintaining organisation requires permission for the use of its work products and none has been obtained, so this package ships the code and no description rather than a description nobody recorded terms for. The code is never lost, and every unrecognised one raises `X12_271_AAA_UNKNOWN_CODE` beside it. ```ts runnable import { parseX12, get271Eligibility } from "@cosyte/x12"; const raw271 = [ "ISA*00* *00* *ZZ*MEDPAY *ZZ*ANYTOWNCLINIC *260601*1200*^*00501*000000001*0*P*:~", "GS*HB*MEDPAY*ANYTOWNCLINIC*20260601*1200*1*X*005010X279A1~", "ST*271*0001*005010X279A1~", "BHT*0022*11*TXN-REF-001*20260601*1200~", "HL*1**20*1~", "NM1*PR*2*MEDPAY INSURANCE*****PI*PAYER01~", "HL*2*1*21*1~", "NM1*1P*2*ANYTOWN CLINIC*****XX*1234567890~", "HL*3*2*22*0~", "NM1*IL*1*DOE*JANE****MI*MBR0001~", "AAA*N**72*C~", "SE*10*0001~", "GE*1*1~", "IEA*1*000000001~", ].join("\n"); const ix271 = parseX12(raw271); const tx271 = ix271.groups[0]?.transactions.find((t) => t.st.elements[1] === "271"); const response = tx271 ? get271Eligibility(ix271.delimiters, tx271) : undefined; // No benefits came back. Do NOT read that as "no coverage" without this check: response?.subscribers[0]?.benefits.length; // => 0 response?.aaaConditions.length; // => 1 // The payer rejected, and here is where and why. Codes are verbatim. response?.aaaConditions[0]?.key.level; // => "subscriber" response?.aaaConditions[0]?.key.hierarchyId; // => "3" response?.aaaConditions[0]?.key.occurrenceIndex; // => 0 response?.aaaConditions[0]?.rejectReasonCode?.code; // => "72" response?.aaaConditions[0]?.followUpActionCode?.code; // => "C" // The bundled snapshot is empty, so the description is absent rather than guessed. response?.aaaConditions[0]?.rejectReasonCode?.description; // => undefined ``` A response with no `AAA` gives you `aaaConditions.length === 0`, and THAT is the empty benefit list you can report as "no benefits of that kind". The `AAA` segments stay on `tx.segments` verbatim as they always did; nothing moved off it to build this. --- ## 4. Read the inquiry side: a 270 and a 276 **The problem:** you are the payer or the clearinghouse, and what arrives is the **question** rather than the answer. A 270 asks whether a member is covered; a 276 asks what happened to a claim. Both are hierarchical documents, and the answer you send back has to reassociate to the trace the submitter chose. Write "270" and "276" separately when you describe this. They are two different implementation guides (`005010X279A1` and `005010X212`), each with a reader and a builder of its own, and one paired label over two halves is how this documentation was wrong about them once already. ### The 270 eligibility inquiry `get270Inquiry(delimiters, tx)` walks one transaction set; `parse270Inquiries(raw)` takes the raw bytes and gives you every 270 in the interchange, in transmitted order. The hierarchy is presented as the sender declared it: source, receiver, subscriber, and a dependent as **its own level** rather than flattened onto the subscriber it hangs under. ```ts runnable import { parse270Inquiries } from "@cosyte/x12"; const raw270 = [ "ISA*00* *00* *ZZ*ANYTOWNCLINIC *ZZ*MEDPAY *260601*1200*^*00501*000000001*0*P*:~", "GS*HS*ANYTOWNCLINIC*MEDPAY*20260601*1200*1*X*005010X279A1~", "ST*270*0001*005010X279A1~", "BHT*0022*13*REQ-0001*20260601*1200~", "HL*1**20*1~", "NM1*PR*2*MEDPAY INSURANCE*****PI*PAYER01~", "HL*2*1*21*1~", "NM1*1P*2*ANYTOWN CLINIC*****XX*1234567890~", "HL*3*2*22*0~", "TRN*1*ELIG20260601001*9SAMPLEORG~", "NM1*IL*1*DOE*JANE*A***MI*MBR0001~", "N3*100 MAIN ST~", "N4*COLUMBUS*OH*43215~", "DMG*D8*19850515*F~", "DTP*291*D8*20260601~", "EQ*30^35*HC:99213:25*IND~", "SE*15*0001~", "GE*1*1~", "IEA*1*000000001~", ].join("\n"); const inquiry = parse270Inquiries(raw270)[0]; inquiry?.header?.purposeCode; // => "13" // This interchange is pretty-printed, and the 270 reader SAYS SO rather than // absorbing it in silence: the shared parse consumes the CR / LF run before // the next segment opens, so it is part of no element, nothing on the model // records it, and `serializeX12` cannot put it back. The typed model is // otherwise identical to the compact form's. inquiry?.warnings.map((w) => w.code); // => ["X12_270_INTER_SEGMENT_LINE_BREAK"] const asked = inquiry?.informationSources[0]?.receivers[0]?.subscribers[0]; // The trace you must echo on the 271 you send back. asked?.traces[0]?.referenceId; // => "ELIG20260601001" asked?.name?.lastNameOrOrganizationName; // => "DOE" asked?.name?.idCode; // => "MBR0001" asked?.name?.dateOfBirth; // => "19850515" // EQ-01 repeats, so the service types come back as a list, and the bundled // snapshot fills a description in beside the code without ever replacing it. const request = asked?.inquiries[0]; request?.serviceTypeCodes.map((s) => s.code); // => ["30", "35"] request?.serviceTypeCodes[0]?.description; // => "Health Benefit Plan Coverage" // EQ-02 is a composite, and comes back as separated components rather than // one joined string: the separator is framing and appears in no value. request?.procedure?.qualifier; // => "HC" request?.procedure?.code; // => "99213" request?.procedure?.modifiers; // => ["25"] ``` **Read that line-break code's bound literally: CR and LF, and nothing else.** Whitespace that is not CR or LF, a space or a tab between segments, is not consumed by the shared interchange parse at all. It becomes part of the next segment's identifier, the functional group never frames, and `parse270Inquiries` answers the **empty list**, with the loss reported on the interchange's own warning channel rather than on a 270's. The code is raised once per transaction set however many runs the document carries, and it is raised on the 270 path only. **The declared HL parent pointers are preserved verbatim and never re-numbered.** A level whose declared parent does not resolve is left off the tree, together with everything beneath it, and the loss is reported (`X12_270_LEVEL_DETACHED`) rather than re-parented onto whichever level happened to be open. Check `inquiry.warnings` before you conclude a subscriber had no dependents. ### The 276 claim status request `get276StatusInquiry(delimiters, tx)` and `parse276StatusInquiries(raw)` are the same pair one guide over. The 276 adds a provider level (`19`) between the receiver and the subscriber, and each claim carries the `TRN-02` the 277 has to echo: ```ts runnable import { parse276StatusInquiries } from "@cosyte/x12"; const raw276 = [ "ISA*00* *00* *ZZ*ANYTOWNCLINIC *ZZ*MEDPAY *260601*1200*^*00501*000000011*0*P*:~", "GS*HR*ANYTOWNCLINIC*MEDPAY*20260601*1200*11*X*005010X212~", "ST*276*0001*005010X212~", "BHT*0010*13*STATUS-0001*20260601*1200~", "HL*1**20*1~", "NM1*PR*2*MEDPAY INSURANCE*****PI*PAYER01~", "HL*2*1*21*1~", "NM1*41*2*ANYTOWN CLINIC*****46*RECVR01~", "HL*3*2*19*1~", "NM1*1P*2*ANYTOWN CLINIC*****XX*1234567890~", "HL*4*3*22*0~", "NM1*IL*1*DOE*JANE*A***MI*MBR0001~", "DMG*D8*19850515*F~", "TRN*1*STATUS20260601001*9SAMPLEORG~", "REF*1K*PCN0001~", "AMT*T3*150~", "DTP*472*D8*20260520~", "SVC*HC:99213:25*150*****1~", "REF*FJ*LINE001~", "DTP*472*D8*20260520~", "SE*19*0001~", "GE*1*11~", "IEA*1*000000011~", ].join("\n"); const statusRequest = parse276StatusInquiries(raw276)[0]; statusRequest?.warnings.map((w) => w.code); // => [] statusRequest?.hierarchies.map((h) => h.levelCode); // => ["20", "21", "19", "22"] const subscriber = statusRequest?.informationSources[0]?.receivers[0]?.providers[0]?.subscribers[0]; subscriber?.name?.idCode; // => "MBR0001" const askedAbout = subscriber?.claims[0]; // The trace the 277 must echo back, verbatim. askedAbout?.trace?.referenceId; // => "STATUS20260601001" askedAbout?.references.map((r) => [r.qualifier, r.value]); // => [["1K", "PCN0001"]] // Money is X12Decimal here too, on the request side as much as the response. askedAbout?.amounts[0]?.amount.toString(); // => "150" const askedLine = askedAbout?.serviceLines[0]; askedLine?.procedure?.code; // => "99213" askedLine?.lineChargeAmount?.toString(); // => "150" askedLine?.unitsOfService?.toString(); // => "1" ``` The 276 reader attaches a level by its **own** HL-02 and by nothing else, exactly as the 270 does, so a dangling pointer, a pointer naming a level of the wrong kind, and a parent chain that returns to itself each leave that level and its subtree off the model, reported and never re-parented. The two readers' warning codes are **siblings, never one widened set** (`X12_270_*` and `X12_276_*`): gate on the one belonging to the document you are reading, and do not assume a code one of them raises has a counterpart on the other. The line-break report above is exactly that case: it exists on the 270 path and on no other, which is why the same pretty-printed framing leaves this 276 with an empty warning list. A 276 short of what a row is built from has reports of its own instead (`X12_276_REFERENCE_ROW_DROPPED` for a `REF` missing either element, `X12_276_DATE_ROW_DROPPED` for a `DTP` missing its qualifier or its value), because each of those is a record rather than a slot and there is no half a row to keep. --- ## 5. Parse an 837 claim: variant, hierarchy, diagnoses **The problem:** you received a claim and need to know which flavor it is (Professional / Institutional / Dental), walk the HL hierarchy (billing provider → subscriber → claim), and read the diagnosis codes with their code-system provenance. `get837Claims(delimiters, tx)` returns an `X12_837Submission`. Variant is resolved from the ST-03 implementation-convention reference (an `X222` guide → P, `X223` → I, `X224` → D), with an SVx fallback; an unresolvable one raises `X12_837_UNKNOWN_VARIANT`. **🩺 Which references resolve changed in this release, and it changes how some already-published files decode.** Through `0.0.13` the reader recognised exactly three: `005010X222A2`, `005010X223A3` and `005010X224A2`. That set contained **none** of the identifiers HIPAA adopts at 45 CFR 162.1102, and it was missing `005010X222A1` and `005010X223A2`, which are what CMS and state Medicaid companion guides require in ST-03 on production professional and institutional claims. So a conformant 837P declaring `005010X222A1` resolved to nothing and fell through to the SVx fallback, where one stray `SV2` re-typed the whole submission. The reader now recognises each base guide and each of its published errata. **If you read 837 files on `0.0.13` or earlier, re-check any routing you drove off `submission.variant`, and any predicate you wrote on `X12_837_UNKNOWN_VARIANT` or `X12_837_AMBIGUOUS_VARIANT`:** on a file whose ST-03 is now recognised, the variant can differ, neither code fires any more, and **a service line whose `SVx` kind disagrees with the declaration is no longer decoded** - its `charge` and `units` read `undefined`, the rest of the service segment is undecoded, and `X12_837_SERVICE_LINE_NOT_DECODED` is raised at that line's `LX`, on a document that may have been silent before. **Gate on the warning, not on a slot:** an undecoded line SEEDS its identity fields, so `procedureCode` is `""` on a P or D line and `revenueCode` is `""` on an I one. Read all of that as **one property and not a closed list**: where ST-03 is recognised, the document's own declaration decides the variant instead of its first service segment. The set is a list of cited identifiers, never a pattern: a reference outside it, in a different case, or padded still falls through exactly as before. **🩺 When the fallback is what decided and the body contradicts itself, that is reported too.** The fallback takes the **first** `SV1` / `SV2` / `SV3` in the transaction body, whether or not a Loop 2400 was open at it, so one stray `SV2` ahead of a Professional claim re-types the whole submission Institutional. Where the body carries service segments for more than one variant and the fallback is what resolved it, `X12_837_AMBIGUOUS_VARIANT` is raised at the `ST`: `submission.variant` is a guess between contradictory evidence, and **which segment is the stray one is not decided**, because this reader cannot tell a stray service segment from a conformant one. Re-read with the `type` option to decode against a variant you trust. Read the bound as a property of the **resolution**: a `type` you pass, or an `ST-03` naming a known convention, means no guess was made and the code is not raised however mixed the body is. It is additive, so every existing warning on such a document still fires exactly where it did. The reference wins over the segments when the two disagree, and the `type` option wins over both. A service line whose `SVx` then does not match is **not** decoded (reading an `SV2` into a Professional line would mis-read the charge), so its `charge` and `units` read `undefined` and `X12_837_SERVICE_LINE_NOT_DECODED` is raised against the `LX` that opened it. Gate on that warning before you post a line amount, alongside `X12_837_SERVICE_SEGMENT_REPEATED` (a **second** service segment inside one Loop 2400: the line carries only the last matching one, so a charge and a procedure code the sender also sent are not on the model - see below), `X12_UNPARSEABLE_DECIMAL` (the `SVx` decoded but the amount itself did not) and `X12_AMOUNT_ROW_DROPPED` (an `AMT` whose own amount decoded nothing, so the whole supplemental-amount row is off the model - off the **line** where one is open, and off the claim otherwise). Gate on `X12_STATED_AMOUNT_DISCARDED` beside it: an `AMT` arriving while a Loop 2430 adjudication is open decodes fine and is discarded anyway, so that row is off the model even though nothing failed to read. That is specific to this route: read the code's own bounds before generalising it, because its other route reports rows whose bytes may not decode at all. The two are disjoint and never name the same segment. None of them is a complete account of every way an amount can fail to reach the model, and [Decimal-exact money](https://docs.cosyte.com/x12/spec-notes-money.md) states the guarantee in the only direction it holds. A third code covers one more way a line goes missing: `X12_837_SERVICE_LINE_DROPPED`, raised at an `LX` that opened no Loop 2400, either because no `CLM` was open or because the variant is not one of `P` / `I` / `D`. **An empty `serviceLines` is therefore not on its own evidence that the claim had none** - check the warning channel before concluding it. Read that code's scope literally: it is anchored at the `LX`, so an `SVx` arriving with **no Loop 2400 open** is reported by a fourth code, `X12_837_SERVICE_SEGMENT_WITHOUT_LX`, anchored at the service segment itself (through `0.0.9` that case was dropped silently). It does **not** travel with `X12_837_UNKNOWN_VARIANT` (a caller-supplied `type` outside the union reaches the same route without it, so read `submission.variant`); and what becomes of a `DTP` / `AMT` / `NTE` / `REF` after a dropped `LX` is route-dependent, so do not assume it is simply absent (see [Troubleshooting](https://docs.cosyte.com/x12/troubleshooting.md) and the package's `KNOWN-LIMITATIONS.md`). A fifth code covers the cost of that route. Where the dropped `LX` had **no `CLM` open** and landed inside an entity loop, it closes that loop, so an `N3` / `N4` / `PER` / `REF` following it reaches no party at all while nothing since has opened a loop, which the next `NM1` / `HL` / `CLM` does. Each such segment raises `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, anchored at the segment itself. Read its bound literally: it reports only that route, and only for that stretch, so it is not a general "this segment reached no party" report. It reports that the segment reached **no** party, and not that it would otherwise have reached one: this reader surfaces neither a `PER` on a patient nor one on a pay-to address on any release, so on those the alternative was also no party. The bytes stay verbatim on `tx.segments`, which is still the only complete account of the document. **🩺 A sixth code covers the one that costs money on a line that decoded perfectly well.** Where a second `SV1` / `SV2` / `SV3` arrives inside a Loop 2400 that is **already open**, `X12_837_SERVICE_SEGMENT_REPEATED` is raised at that repeated segment. The line carries one service segment's worth of slots, so it holds what the **last** segment matching the resolved variant wrote, and every decoder writes all of the slots its kind writes: `SV1*HC:99213*8500*…` followed by `SV1*HC:99999*12*…` leaves one line reading `charge` `12` and `procedureCode` `99999`, and through `0.0.13` it did so with `warnings: []`. Where the repeat's own charge element is **absent** it writes `undefined` over the amount the first one stated, and `X12_837_SERVICE_LINE_NOT_DECODED` does **not** fire there, because a service segment did decode. A repeat whose kind does not match the resolved variant is read into nothing and overwrites nothing, and is reported the same way. **Which of them the sender meant is not decided** - this reader cannot tell a stray service segment from a conformant one - and the decode is byte-for-byte what it was at `0.0.13`, so gate on the code and read the segments off `tx.segments` rather than expecting the model to reconcile them. It never names the same segment as `X12_837_SERVICE_SEGMENT_WITHOUT_LX`, which requires that **no** Loop 2400 be open where this one requires that one is. Read that as disjointness and not as coverage: a service segment following an `LX` that opened no line is named by neither, because `X12_837_SERVICE_LINE_DROPPED` at that `LX` already reports the loss. ```ts import { parseX12, get837Claims, HL_LEVEL_CODES, WARNING_CODES } from "@cosyte/x12"; const ix = parseX12(raw837); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "837"); const sub = tx ? get837Claims(ix.delimiters, tx) : undefined; if (sub === undefined) throw new Error("not an 837"); sub.variant; // "P" | "I" | "D" // Walk the HL hierarchy. HL-02 parent pointers are validated, never // re-numbered; a broken pointer surfaces as X12_HL_PARENT_MISMATCH. for (const hl of sub.hierarchies) { hl.hlId; // "1", "2", ... hl.parentHlId; // undefined at the top (Information Source) hl.levelCode; // compare against HL_LEVEL_CODES.* const isSource = hl.levelCode === HL_LEVEL_CODES.INFORMATION_SOURCE; const isSubscriber = hl.levelCode === HL_LEVEL_CODES.SUBSCRIBER; } for (const claim of sub.claims) { claim.billingProvider?.name; // "BILLING CLINIC INC" claim.billingProvider?.idCode; // "1234567890" (NPI) claim.subscriber?.info.claimFilingIndicator; // "MB" (Medicare Part B) // HI diagnoses: the qualifier tells you the code system AND the role. for (const dx of claim.diagnoses) { dx.qualifier; // "ABK" (principal ICD-10-CM), "ABF" (other), "ABJ" (admitting)... dx.codeSystem; // "ICD-10-CM" | "ICD-10-PCS" | ... | "unknown" dx.category; // "principal-diagnosis" | "other-diagnosis" | ... } for (const line of claim.serviceLines) { // Variant-discriminated union: narrow on `line.variant` before reading SVx fields. if (line.variant === "P") line.diagnosisPointers; // e.g. ["1"] } } // An unknown HI qualifier is preserved verbatim (codeSystem: "unknown") + warns. const unknownHi = sub.warnings.some((w) => w.code === WARNING_CODES.X12_UNKNOWN_HI_QUALIFIER); ``` ### 🩺 Building one: declare the guide your partner asked for `build837P` / `build837I` / `build837D` default ST-03 and GS-08 to `005010X222A2` / `005010X223A3` / `005010X224A2`. **Those defaults are not what every partner accepts** - CMS and several state Medicaid companion guides require `005010X222A1` on professional and `005010X223A2` on institutional claims, and a partner on one of those rejects a file declaring the default. Which published guide identifier a partner accepts is a **partner fact rather than a spec fact**, so this library will not choose for you: state it on the envelope and one value reaches both elements. ```ts runnable import { build837P, get837Claims, X12Decimal, type Build837Spec } from "@cosyte/x12"; const spec: Build837Spec = { envelope: { senderId: "SUBMITTER", receiverId: "RECEIVER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", // Omit this and you get 005010X222A2, exactly as before it existed. implementationConventionReference: "005010X222A1", }, submitter: { entityIdentifierCode: "41", entityTypeQualifier: "2", name: "SUBMITTER ONE", idQualifier: "46", idCode: "SUB001", }, receiver: { entityIdentifierCode: "40", entityTypeQualifier: "2", name: "RECEIVER ONE", idQualifier: "46", idCode: "REC001", }, billingProviders: [ { provider: { entityIdentifierCode: "85", entityTypeQualifier: "2", name: "BILLING CLINIC INC", idQualifier: "XX", idCode: "1234567890", }, subscribers: [ { info: { payerResponsibilityCode: "P", individualRelationshipCode: "18", claimFilingIndicator: "MB", }, subscriber: { entityIdentifierCode: "IL", entityTypeQualifier: "1", name: "PATIENT", firstName: "TEST", idQualifier: "MI", idCode: "MEMBER001", }, payer: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "PAYER ONE", idQualifier: "PI", idCode: "PAYER01", }, claims: [ { claimId: "PT-ACCT-001", totalCharge: X12Decimal.fromString("150.00")!, diagnoses: [{ qualifier: "ABK", code: "J20.9" }], serviceLines: [ { variant: "P", procedureQualifier: "HC", procedureCode: "99213", charge: X12Decimal.fromString("150.00")!, unitOfMeasure: "UN", units: X12Decimal.fromString("1")!, diagnosisPointers: ["1"], }, ], }, ], }, ], }, ], }; const built = build837P(spec); // One value, BOTH elements - GS-08 and ST-03 never disagree. built.groups[0]?.gs.elements[8]; // => "005010X222A1" built.groups[0]?.transactions[0]?.st.elements[3]; // => "005010X222A1" // And it still reads back as a Professional claim, charge intact. const builtTx = built.groups[0]?.transactions[0]; const readBack = builtTx ? get837Claims(built.delimiters, builtTx) : undefined; readBack?.variant; // => "P" readBack?.claims[0]?.serviceLines[0]?.charge?.toString(); // => "150.00" ``` **What it refuses of its own**, all `Claim837BuildError` with code `X12_837_BUILD_INVALID_SPEC`, and no refusal message echoes the value you passed: an **empty** reference (a trailing empty element is not emitted, so it would delete ST-03 and GS-08 rather than send them empty); one carrying an **active delimiter or the release character** (this library's own reader would now carry it - the envelope splitter honours the release escape in current releases - but a trading partner's parser is not obliged to, and a guide identifier has no legitimate use for a delimiter, so the refusal is kept); and one this library's own reader resolves to a **different 837 variant**, such as `005010X223A2` handed to `build837P`, which would emit a file declaring one variant and carrying another's service segments. Those are on top of the element-type guard every string slot in every builder already has, which refuses a non-string with the same code, so read the list as what this field adds rather than as everything that can refuse. **Anything else is emitted as given.** The set of published errata is not provably exhaustive, so an identifier this library does not recognise is your call, not an error - with one honest cost: on reading such a file back, this library falls through to the `SVx` scan for the variant, exactly as it does for any unrecognised ST-03. The **length** is not bounded either, and the two elements' maxima differ: GS-08 is data element 480 (`AN 1/12`), ST-03 is element 1705 (`AN 1/35`). **🩺 A guard on this element still cannot make the element trustworthy, but the reason narrowed.** An active delimiter in a _different_ envelope field (a control number, an application sender code) used to split its own segment and shift every element after it, so ST-03 and GS-08 were read out of a neighbour's slot, mostly with nothing warned. A **release-escaped** one no longer does: the envelope splitter honours the escape, so a sender who escapes correctly is now read correctly. An **unescaped** one still ends its element, because that is what a delimiter is, and no refusal here can reach it. **And that change is symmetric**: an envelope element ending in a **literal `?`** now merges with its successor, which can destroy ST-03 outright and send the document to the `SVx` variant fallback. The full behaviour change on already-published decoding, both directions, is measured in `KNOWN-LIMITATIONS.md`. --- ## 6. Read a 278 services review: the certification decision **The problem:** you asked a utilization management organization to authorize a service, and the 278 response came back. You need the **certification action** (was it certified, pended, denied?) and the authorization number to put on the claim you are about to send. `get278Request(delimiters, tx)` and `get278Response(delimiters, tx)` read the two directions of one implementation guide, `005010X217`. Both are 278s, so route on the direction the model reports rather than on `ST-01` alone; a reader hands back `undefined` for a transaction it does not decode. A 278 declaring any other guide is still read, and carries `X12_GUIDE_NOT_IMPLEMENTED`; that includes `005010X216`, the 278 notification guide. **The `HCR-01` certification action is the field this library places verbatim and never infers.** It is response-only: a request carries no decision at all, and `review.decision` is `undefined` there rather than defaulted to anything. ```ts runnable import { parseX12, get278Response } from "@cosyte/x12"; const raw278 = [ "ISA*00* *00* *ZZ*UMOPAYER *ZZ*SUBMITTER *260601*1230*^*00501*000000002*0*P*:~", "GS*HI*UMOPAYER*SUBMITTER*20260601*1230*1*X*005010X217~", "ST*278*0002*005010X217~", "BHT*0078*11*AUTHRESP-202606*20260601*1230~", "HL*1**20*1~", "NM1*X3*2*UTILIZATION REVIEW CO*****PI*UMO001~", "HL*2*1*21*1~", "NM1*1P*2*RENDERING CLINIC*****XX*1234567893~", "HL*3*2*22*1~", "NM1*IL*1*DOE*JANE****MI*MBR0001~", "DMG*D8*19850515*F~", "HL*4*3*EV*1~", "TRN*1*AUTHREQ-202606-0001*9SUBMITTER~", "UM*HS*I*1~", "HCR*A1*AUTH123456~", "DTP*472*RD8*20260601-20260605~", "HI*ABK:E1165~", "SE*16*0002~", "GE*1*1~", "IEA*1*000000002~", ].join(""); const ix278 = parseX12(raw278); const tx278 = ix278.groups[0]?.transactions.find((t) => t.st.elements[1] === "278"); const review278 = tx278 ? get278Response(ix278.delimiters, tx278) : undefined; review278?.direction; // => "response" review278?.warnings.length; // => 0 review278?.utilizationManagementOrganization?.name; // => "UTILIZATION REVIEW CO" review278?.subscriber?.idCode; // => "MBR0001" const review = review278?.reviews[0]; // The trace that reassociates this answer to the request you sent. review?.traces[0]?.referenceId; // => "AUTHREQ-202606-0001" review?.requestCategoryCode; // => "HS" review?.certificationTypeCode; // => "I" // HCR-01: the certification action, verbatim, never inferred from anything else. review?.decision?.actionCode; // => "A1" review?.decision?.reviewIdentificationNumber; // => "AUTH123456" // The diagnosis carries its code system, resolved from the HI qualifier. review?.diagnoses[0]?.qualifier; // => "ABK" review?.diagnoses[0]?.code; // => "E1165" review?.diagnoses[0]?.codeSystem; // => "ICD-10-CM" ``` **One thing to know before you build a 278 rather than read one.** Every `HL-03` in this library is a library constant chosen by tree position, with a single exception: `Build278ReviewSpec.levelCode`, which you supply and which the guides limit to `EV` (patient event) and `SS` (service). A level outside those two emits a perfectly well-formed document whose review loop no reader opens, so the review **and its certification decision** would come back absent with no warning. `build278Request` and `build278Response` therefore **refuse** an out-of-enum level rather than emit a document that loses it. Omit `levelCode` and it defaults to `EV`. --- ## 7. Walk an 834 enrollment roster: one member at a time **The problem:** a plan sponsor sent a benefit enrollment and maintenance file. You need the header (who sponsored it, who the payer is), then each member, each member's coverage, and above all what to **do** with each one: add, change, or terminate. The 834 splits into two calls, because the two halves have different shapes. `get834Header(delimiters, tx)` is a small synchronous read that stops at the first `INS`. `get834Enrollments(delimiters, tx)` is an **async iterable** yielding one decoded member per `INS` loop, so a consumer of a large roster holds one member at a time rather than the whole decoded file. ```ts runnable import { parseX12, get834Header, get834Enrollments, type X12Enrollment } from "@cosyte/x12"; const raw834 = [ "ISA*00* *00* *ZZ*EMPLOYERCO *ZZ*MEDPAY *260601*1200*^*00501*000000001*0*P*:~", "GS*BE*EMPLOYERCO*MEDPAY*20260601*1200*1*X*005010X220A1~", "ST*834*0001~", "BGN*00*ENR-202606*20260601*1200****2~", "REF*38*POLICY-0001~", "DTP*007*D8*20260601~", "N1*P5*EMPLOYER CO*FI*444556666~", "N1*IN*MEDPAY INSURANCE*FI*111223333~", "INS*Y*18*021*EC*A***FT~", "REF*0F*MBR0001~", "REF*1L*GROUP-0001~", "DTP*356*D8*20260101~", "NM1*IL*1*DOE*JANE*A***MI*MBR0001~", "N3*100 MAIN ST~", "N4*COLUMBUS*OH*43215~", "DMG*D8*19850515*F~", "HD*021**HLT*GOLD PPO*FAM~", "DTP*348*D8*20260101~", "AMT*P3*125.00~", "INS*N*01*024*XN*A***TE~", "REF*0F*MBR0002~", "NM1*IL*1*ROE*JOHN****MI*MBR0002~", "DMG*D8*19900101*M~", "HD*024**DEN~", "DTP*349*D8*20260531~", "SE*23*0001~", "GE*1*1~", "IEA*1*000000001~", ].join(""); const ix834 = parseX12(raw834); const tx834 = ix834.groups[0]?.transactions.find((t) => t.st.elements[1] === "834"); if (tx834 === undefined) throw new Error("no 834 in interchange"); const header = get834Header(ix834.delimiters, tx834); header?.transactionSetPurposeCode; // => "00" header?.referenceId; // => "ENR-202606" header?.sponsor?.name; // => "EMPLOYER CO" header?.payer?.name; // => "MEDPAY INSURANCE" // One member at a time, in transmitted order. const members: X12Enrollment[] = []; for await (const member of get834Enrollments(ix834.delimiters, tx834)) members.push(member); members.length; // => 2 // INS-03 is the maintenance type: the safety-critical field of this document. // The verbatim code is ALWAYS on the model; the description is looked up beside // it and never in place of it. const added = members[0]; added?.subscriberIndicator; // => "Y" added?.maintenanceTypeCode; // => "021" added?.maintenanceTypeDescription; // => "Addition" added?.member?.lastName; // => "DOE" added?.member?.idCode; // => "MBR0001" // Coverage sits under the member, with its own dates and amounts. Money is // X12Decimal here as everywhere. added?.healthCoverages[0]?.insuranceLineCode; // => "HLT" added?.healthCoverages[0]?.planCoverageDescription; // => "GOLD PPO" added?.healthCoverages[0]?.amounts[0]?.amount.toString(); // => "125.00" const terminated = members[1]; terminated?.maintenanceTypeCode; // => "024" terminated?.maintenanceTypeDescription; // => "Cancellation or Termination" terminated?.healthCoverages[0]?.insuranceLineCode; // => "DEN" ``` **Never infer an action for a maintenance-type code this library does not recognise.** An unknown `INS-03` or `HD-01` keeps its verbatim code, gets **no** description, and raises `X12_834_UNKNOWN_MAINTENANCE_TYPE` on **that member's own** `warnings` rather than on the interchange's. Terminating coverage because a code did not resolve is the harm this rule exists to prevent, so gate on the warning and route the member to a human. Two honest bounds on the stream. It iterates an **already-parsed** transaction set, so the file is fully parsed into `tx.segments` before iteration begins: the memory win is on the result side, not the input side, and this is not a byte-streaming reader for arbitrarily large files. And a coverage `AMT` whose amount does not decode loses its whole row, reported by `X12_AMOUNT_ROW_DROPPED` on that member. --- ## 8. Parse a 999 acknowledgment: disposition + segment errors **The problem:** you submitted an 837 and got a 999 back. Was the batch accepted? If not, which segments and elements failed, and where? `parse999(raw)` takes the raw bytes directly (it parses the envelope for you) and returns an `X12Ack999`, or `undefined` if there's no 999 in the interchange. ```ts import { parse999, isAcceptDisposition, X12_ACK_DISPOSITION_CODES } from "@cosyte/x12"; const ack = parse999(raw999); if (ack === undefined) throw new Error("no 999 in interchange"); // AK9: the functional-group disposition + counts. ack.ak9.disposition; // "A" accepted | "E" accepted-with-errors | "R" rejected | ... ack.ak9.numberOfReceivedTransactionSets; // e.g. 1 ack.ak9.numberOfAcceptedTransactionSets; // e.g. 0 // One boolean for "did this pass?": accept dispositions are A / E / P. if (!isAcceptDisposition(ack.ak9.disposition)) { // The group was rejected. Dig into the per-transaction responses. } for (const response of ack.transactionResponses) { response.ak2.transactionSetIdCode; // "837" response.ak2.transactionSetControlNumber; // "0001": matches your ST-02 response.ik5.disposition; // per-transaction disposition (=== X12_ACK_DISPOSITION_CODES.R?) // IK3: segment-level error notes. for (const segNote of response.segmentNotes) { segNote.ik3.segmentIdCode; // "NM1": which segment segNote.ik3.segmentPositionInTransactionSet; // 8 segNote.ik3.loopIdentifier; // "2010BA" segNote.ik3.syntaxErrorCode; // "8" (segment has data element errors) // IK4: element-level notes nested under the segment. for (const elemNote of segNote.elementNotes) { elemNote.ik4.position.element; // 1 elemNote.ik4.position.component; // 2 (composite subelement) elemNote.ik4.syntaxErrorCode; // "7" (invalid code value) } } } ``` `X12_ACK_DISPOSITION_CODES` is the code registry if you prefer explicit comparisons (`ack.ak9.disposition === X12_ACK_DISPOSITION_CODES.R`). ### The TA1 interchange acknowledgment sits one envelope out A 999 acknowledges a functional group. A **TA1** acknowledges the **interchange** itself: it says whether the ISA / IEA envelope was readable at all, and it is the answer you get when the file never reached the point of having a functional group to report on. It has its own pair, `parseTA1(ix)` and `buildTA1(spec)`, and `parseTA1` takes the parsed interchange rather than raw bytes because a TA1 is an envelope-level segment. ```ts runnable import { parseX12, parseTA1, isAcceptDisposition, TA1_ACK_CODES } from "@cosyte/x12"; // An interchange carrying nothing but a TA1: no functional group at all. const rawTa1 = "ISA*00* *00* *ZZ*RECEIVER *ZZ*SENDER " + "*220101*1230*^*00501*000000020*0*P*:~" + "TA1*000000019*220101*1200*A*000~" + "IEA*0*000000020~"; const ixTa1 = parseX12(rawTa1); const ta1 = parseTA1(ixTa1); ixTa1.groups.length; // => 0 ta1?.interchangeControlNumber; // => "000000019" ta1?.ackCode; // => TA1_ACK_CODES.A ta1?.noteCodeRaw; // => "000" // TA1-01 is the reassociation key: it echoes the ISA-13 of the interchange // being acknowledged, verbatim, and is how you match this answer to the file // you sent. It is NOT this interchange's own control number. ta1?.interchangeControlNumber === ixTa1.isa.elements[13]; // => false // One boolean for "did the envelope pass?", shared with the 999 dispositions. isAcceptDisposition(ta1?.ackCode ?? ""); // => true // The five decoded fields are post-`?`-unescape; `ta1.raw` is the segment as // transmitted, so do not apply `unescapeRelease` to the decoded ones yourself. ta1?.raw.raw; // => "TA1*000000019*220101*1200*A*000" ``` `buildTA1` is the emit half, and it is a pure function like every other builder: it never auto-sends, opens a socket, or touches the filesystem. It refuses an empty element at all five slots rather than emitting a short segment, and it releases an active delimiter in a value rather than letting it shift the disposition element. What it cannot verify is the envelope you will embed the segment in, so state your delimiter set on `BuildTA1Options` if it differs from the default, or a value carrying a byte that is a delimiter there and not here comes back with a stray release character. --- ## 9. Handle warnings: the lenient, never-throw contract **The problem:** you want to log or triage every tolerated deviation without your pipeline throwing on a vendor quirk. `@cosyte/x12` is liberal on input: **only four Tier-3 structural errors ever throw**; everything else is a warning carrying a stable code and positional context. Every warning is collected on the returned model (`ix.warnings`, `remit.warnings`, `sub.warnings`, …). You can also stream them live via the `onWarning` callback: ```ts import { parseX12, WARNING_CODES, type X12ParseWarning } from "@cosyte/x12"; const seen: X12ParseWarning[] = []; const ix = parseX12(raw, { onWarning: (w) => { seen.push(w); // w.code: a stable string from WARNING_CODES // w.message: a frozen-registry entry, never built from your document // w.position: where in the interchange it occurred }, }); // Or read them after the fact: for (const w of ix.warnings) { if (w.code === WARNING_CODES.X12_PRE_005010) { // sender is on a pre-005010 version family: tolerated, not fatal } } ``` **Escalate when you want strictness.** Pass `{ strict: true }` to turn every tolerated deviation into a thrown `X12ParseError` carrying the same warning code, useful as a gate on a trusted trading partner. **The four fatal codes.** These are unrecoverable structural corruption and always throw an `X12ParseError` regardless of `strict`: ```ts import { parseX12, FATAL_CODES, X12ParseError } from "@cosyte/x12"; try { parseX12(maybeGarbage); } catch (err) { if (err instanceof X12ParseError) { switch (err.code) { case FATAL_CODES.X12_EMPTY_INPUT: // nothing to parse case FATAL_CODES.X12_NO_ISA_HEADER: // not an X12 interchange at all case FATAL_CODES.X12_ISA_TOO_SHORT: // ISA truncated below 106 bytes case FATAL_CODES.X12_INVALID_DELIMITERS: // delimiters unrecoverable from ISA // A malformed interchange: the bytes aren't X12. break; } } } ``` Everything a real-world payer or clearinghouse does short of that (miscounts, dangling release chars, unknown CARC/RARC/HI codes, HL parent mismatches, balance mismatches, pre-005010 versions) is a warning you triage, not an exception you catch. --- ## 10. Read a date off any typed reader **The problem:** you have a service date, a statement period or an eligibility date on a parsed model, and you need it as a `Date`, an ISO string or a set of calendar fields. Every reader surfaces its dates verbatim, as the qualifier the sender used plus the bytes it carried, so that nothing is normalized behind your back. `toObject`, `toISO` and `toDate` are the three ways to read one. Every `@cosyte/*` parser exports the same three names with the same return shapes and the same timezone rule, so an application holding both an X12 date and an HL7 timestamp reads them the same way. ```ts runnable import { toObject, toISO, toDate } from "@cosyte/x12"; // The shape every X12*Date carrier has: claim.dates[0], eligibility.dates[0], and so on. const day = { formatQualifier: "D8", value: "20260601" }; toObject(day); // => { year: 2026, month: 6, day: 1 } toISO(day); // => "2026-06-01" // A calendar day is not an instant. Without a stated offset there is no answer. toDate(day); // => undefined toDate(day, { assumeOffsetMinutes: 0 })?.toISOString(); // => "2026-06-01T00:00:00.000Z" toDate(day, { assumeOffsetMinutes: -300 })?.toISOString(); // => "2026-06-01T05:00:00.000Z" // A range converts to nothing: an interval is not a point in time. const span = { formatQualifier: "RD8", value: "20260601-20260605" }; toISO(span); // => undefined // Split it yourself and convert whichever end you meant. const [from, through] = span.value.split("-"); toISO({ formatQualifier: "D8", value: from ?? "" }); // => "2026-06-01" toISO({ formatQualifier: "D8", value: through ?? "" }); // => "2026-06-05" // Anything this library does not decode answers undefined, and never throws. toISO({ formatQualifier: "D8", value: "20260230" }); // => undefined toISO({ formatQualifier: "D6", value: "202606" }); // => undefined toISO(undefined); // => undefined ``` **The decoded format qualifier set is `D8` and `RD8`, and nothing else.** Those are the two DTP-02 values this library already parses and builds: `D8` is a single `CCYYMMDD` day, `RD8` is a `CCYYMMDD-CCYYMMDD` range. A qualifier outside that pair, an absent one, a value whose digits do not match the shape its qualifier declares, and a day that is not on the calendar all convert to `undefined`. Two carriers, `X12PremiumDate` and `X12EnrollmentDate`, do not surface a format qualifier at all, so they convert to `undefined` too: the format is not knowable from what the model holds, and guessing it would turn an eligibility span into a single date. **`toDate` never guesses a timezone.** No X12 date element this library decodes carries a UTC offset, so `assumeOffsetMinutes` (signed minutes east of UTC) is the only route one takes into the result. Without it the answer is `undefined`: the host machine's timezone is never read and UTC is never assumed. The same midnight is two different instants either side of a border, and a date of birth read in the wrong zone is a day out. **`toObject` returns only the components the value stated.** `month` is spec-native 1 to 12 rather than the JavaScript `Date` 0 to 11, the names are singular, and there is no `precision`, `raw` or `valid` key, so `Object.keys()` of the result tells you the precision. Delete `offsetMinutes` and what is left is accepted by `Temporal.PlainDateTime.from` and by luxon's `DateTime.fromObject` with no renaming. Neither library is a dependency here. `parseDocumentDate`, behind the date-aware code-list queries, keeps its own contract and still throws on input it refuses. That divergence is deliberate: it answers a caller who asked a validity question and could not state the day, while these three read a document that was already parsed leniently, where a warning has already been raised. --- URL: https://docs.cosyte.com/x12/api Markdown: https://docs.cosyte.com/x12/api.md # @cosyte/x12 ## Classes ### AckBuildError Thrown by `build999` / `buildTA1` when the caller asks the library to fabricate an inconsistent acknowledgment (most notably an accept paired with errors). Carries a stable `code` for programmatic narrowing. Throwing here is the documented contract for the cosyte ack archetype: the library mechanically builds the disposition it is told; if the disposition is internally inconsistent it refuses, surfacing the bug at the call site rather than silently lying to the inbound sender. `AckBuildError` deliberately does NOT extend `X12ParseError` - the parser-vs-builder distinction matters at the type level (a parser catch should never catch a builder bug, and vice versa). #### Example ```ts import { AckBuildError, ACK_BUILD_ERROR_CODES } from "@cosyte/x12"; try { build999({ ... ack: { ..., functional: { disposition: "A", ... } }, ... }); } catch (err) { if (err instanceof AckBuildError) { // err.code is one of ACK_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AckBuildError**(`code`, `message`): [`AckBuildError`](#ackbuilderror) **`Internal`** Construct a new `AckBuildError`. Both fields required so every thrower pins a stable code. ###### Parameters ###### code [`AckBuildErrorCode`](#ackbuilderrorcode-1) ###### message `string` ###### Returns [`AckBuildError`](#ackbuilderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`AckBuildErrorCode`](#ackbuilderrorcode-1) *** ### Attachment275BuildError Thrown by `build275` when a spec cannot be emitted as a 275 whose every BDS-02 is true of its BDS-03. No interchange is returned. The message carries no attachment octet and no other document value. #### Example ```ts import { Attachment275BuildError } from "@cosyte/x12"; try { // build275(spec); } catch (err) { if (err instanceof Attachment275BuildError) console.error(err.code); } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Attachment275BuildError**(`code`, `message`): [`Attachment275BuildError`](#attachment275builderror) **`Internal`** ###### Parameters ###### code [`Attachment275BuildErrorCode`](#attachment275builderrorcode-1) ###### message `string` ###### Returns [`Attachment275BuildError`](#attachment275builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`Attachment275BuildErrorCode`](#attachment275builderrorcode-1) *** ### Claim837BuildError Thrown by "./build-837.js".build837P / `build837I` / `build837D` when the supplied claim spec cannot be emitted as a conformant, self-consistent 837 - most importantly when the HL hierarchy spine is structurally impossible. Carries a stable `code` for programmatic narrowing. Messages are PHI-clean: they name structural positions (indices, level codes) and counts, never patient names or member ids. #### Example ```ts import { Claim837BuildError } from "@cosyte/x12"; try { build837P(spec); } catch (err) { if (err instanceof Claim837BuildError) { // err.code is one of CLAIM_837_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Claim837BuildError**(`code`, `message`): [`Claim837BuildError`](#claim837builderror) **`Internal`** ###### Parameters ###### code [`Claim837BuildErrorCode`](#claim837builderrorcode-1) ###### message `string` ###### Returns [`Claim837BuildError`](#claim837builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`Claim837BuildErrorCode`](#claim837builderrorcode-1) *** ### ClaimStatus276BuildError Thrown by "./build-276.js".build276 when the supplied request spec cannot be emitted as a conformant, self-consistent 276. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError`: the domain-refusal distinction matters at the type level. #### Example ```ts import { ClaimStatus276BuildError, build276 } from "@cosyte/x12"; try { build276(spec); } catch (err) { if (err instanceof ClaimStatus276BuildError) { // err.code is one of CLAIM_STATUS_276_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new ClaimStatus276BuildError**(`code`, `message`): [`ClaimStatus276BuildError`](#claimstatus276builderror) **`Internal`** ###### Parameters ###### code [`ClaimStatus276BuildErrorCode`](#claimstatus276builderrorcode-1) ###### message `string` ###### Returns [`ClaimStatus276BuildError`](#claimstatus276builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`ClaimStatus276BuildErrorCode`](#claimstatus276builderrorcode-1) *** ### ClaimStatus277BuildError Thrown by "./build-277.js".build277 / "./build-277.js".build277CA when the supplied claim-status spec cannot be emitted as a conformant, self-consistent 277 - most importantly when its nested tree cannot form a valid HL hierarchy. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError` - the domain-refusal distinction matters at the type level. #### Example ```ts import { ClaimStatus277BuildError } from "@cosyte/x12"; try { build277(spec); } catch (err) { if (err instanceof ClaimStatus277BuildError) { // err.code is one of CLAIM_STATUS_277_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new ClaimStatus277BuildError**(`code`, `message`): [`ClaimStatus277BuildError`](#claimstatus277builderror) **`Internal`** ###### Parameters ###### code [`ClaimStatus277BuildErrorCode`](#claimstatus277builderrorcode-1) ###### message `string` ###### Returns [`ClaimStatus277BuildError`](#claimstatus277builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`ClaimStatus277BuildErrorCode`](#claimstatus277builderrorcode-1) *** ### CoreCodeCombinationTableError Thrown by "./core-code-combinations.js".checkCoreCodeCombination when the supplied combination table is malformed. Carries a stable `code` for programmatic narrowing, the `field` it refused and, for a row-level fault, the zero-based `rowIndex`. Unlike "../../code-lists/errors.js".X12CodeListError it carries no rendering of the refused value, on `message` or anywhere else. Deliberately does NOT extend `X12ParseError` or `X12BuildError`: nothing here parses a document or builds one, and the three stay tellable apart at the type level. #### Example ```ts import { CoreCodeCombinationTableError, checkCoreCodeCombination } from "@cosyte/x12"; try { checkCoreCodeCombination({ table, scenario, adjustment }); } catch (err) { if (err instanceof CoreCodeCombinationTableError) { err.field; // e.g. "groupCode" err.rowIndex; // e.g. 3, or undefined for a table-level fault } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new CoreCodeCombinationTableError**(`code`, `message`, `field`, `rowIndex`): [`CoreCodeCombinationTableError`](#corecodecombinationtableerror) **`Internal`** ###### Parameters ###### code `"X12_CORE_COMBINATION_TABLE_INVALID"` ###### message `string` ###### field [`CoreCodeCombinationTableField`](#corecodecombinationtablefield) ###### rowIndex `number` \| `undefined` ###### Returns [`CoreCodeCombinationTableError`](#corecodecombinationtableerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: `"X12_CORE_COMBINATION_TABLE_INVALID"` ##### field > `readonly` **field**: [`CoreCodeCombinationTableField`](#corecodecombinationtablefield) The part of the table that was refused. ##### rowIndex > `readonly` **rowIndex**: `number` \| `undefined` Zero-based index into `table.rows` of the refused row, or `undefined` when the fault is in the table itself rather than in one row. *** ### Eligibility270BuildError Thrown by "./build-270.js".build270 when the supplied inquiry spec cannot be emitted as a conformant, self-consistent 270. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError`: the domain-refusal distinction matters at the type level. #### Example ```ts import { Eligibility270BuildError, build270 } from "@cosyte/x12"; try { build270(spec); } catch (err) { if (err instanceof Eligibility270BuildError) { // err.code is one of ELIGIBILITY_270_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Eligibility270BuildError**(`code`, `message`): [`Eligibility270BuildError`](#eligibility270builderror) **`Internal`** ###### Parameters ###### code [`Eligibility270BuildErrorCode`](#eligibility270builderrorcode-1) ###### message `string` ###### Returns [`Eligibility270BuildError`](#eligibility270builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`Eligibility270BuildErrorCode`](#eligibility270builderrorcode-1) *** ### Eligibility271BuildError Thrown by "./build-271.js".build271 when the supplied eligibility spec cannot be emitted as a conformant, self-consistent 271 - most importantly when its nested tree cannot form a valid HL hierarchy. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError` - the domain-refusal distinction matters at the type level. #### Example ```ts import { Eligibility271BuildError } from "@cosyte/x12"; try { build271(spec); } catch (err) { if (err instanceof Eligibility271BuildError) { // err.code is one of ELIGIBILITY_271_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Eligibility271BuildError**(`code`, `message`): [`Eligibility271BuildError`](#eligibility271builderror) **`Internal`** ###### Parameters ###### code [`Eligibility271BuildErrorCode`](#eligibility271builderrorcode-1) ###### message `string` ###### Returns [`Eligibility271BuildError`](#eligibility271builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`Eligibility271BuildErrorCode`](#eligibility271builderrorcode-1) *** ### Enrollment834BuildError Thrown by "./build-834.js".build834 when the supplied enrollment spec cannot be emitted as a conformant, self-consistent 834 - most importantly when a maintenance type code is outside the validated X12 875 subset. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError` - the domain-refusal distinction matters at the type level. #### Example ```ts import { Enrollment834BuildError } from "@cosyte/x12"; try { build834(spec); } catch (err) { if (err instanceof Enrollment834BuildError) { // err.code is one of ENROLLMENT_834_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Enrollment834BuildError**(`code`, `message`): [`Enrollment834BuildError`](#enrollment834builderror) **`Internal`** ###### Parameters ###### code [`Enrollment834BuildErrorCode`](#enrollment834builderrorcode-1) ###### message `string` ###### Returns [`Enrollment834BuildError`](#enrollment834builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`Enrollment834BuildErrorCode`](#enrollment834builderrorcode-1) *** ### LoopSpecDefinitionError Thrown by [defineLoopSpec](#defineloopspec) when the supplied spec is structurally invalid. Carries the offending path so a consumer fixing a typo doesn't have to hunt: e.g. `"segments[3].id"` or `"children[0].trigger"`. #### Example ```ts import { defineLoopSpec, LoopSpecDefinitionError } from "@cosyte/x12"; try { defineLoopSpec({ id: "2300", trigger: "clm", segments: [{ id: "clm", usage: "required", max: 1 }], }); } catch (err) { if (err instanceof LoopSpecDefinitionError) { // err.path === "trigger" } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new LoopSpecDefinitionError**(`path`, `message`): [`LoopSpecDefinitionError`](#loopspecdefinitionerror) **`Internal`** Construct a new `LoopSpecDefinitionError` with the offending path. ###### Parameters ###### path `string` ###### message `string` ###### Returns [`LoopSpecDefinitionError`](#loopspecdefinitionerror) ###### Overrides `Error.constructor` #### Properties ##### path > `readonly` **path**: `string` *** ### Premium820BuildError Thrown by "./build-820.js".build820 when the supplied premium spec cannot be emitted as a conformant, self-consistent 820. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError` - the domain-refusal distinction matters at the type level. #### Example ```ts import { Premium820BuildError } from "@cosyte/x12"; try { build820(spec); } catch (err) { if (err instanceof Premium820BuildError) { // err.code is one of PREMIUM_820_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Premium820BuildError**(`code`, `message`): [`Premium820BuildError`](#premium820builderror) **`Internal`** ###### Parameters ###### code `"X12_820_BUILD_INVALID_SPEC"` ###### message `string` ###### Returns [`Premium820BuildError`](#premium820builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: `"X12_820_BUILD_INVALID_SPEC"` *** ### Remit835BuildError Thrown by "./build-835.js".build835 when the supplied remittance spec cannot be emitted as a conformant, self-consistent 835 - most importantly when it fails a §1.10.2 balance invariant. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError` - the domain-refusal distinction matters at the type level. #### Example ```ts import { Remit835BuildError } from "@cosyte/x12"; try { build835(spec); } catch (err) { if (err instanceof Remit835BuildError) { // err.code is one of REMIT_835_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Remit835BuildError**(`code`, `message`): [`Remit835BuildError`](#remit835builderror) **`Internal`** ###### Parameters ###### code [`Remit835BuildErrorCode`](#remit835builderrorcode-1) ###### message `string` ###### Returns [`Remit835BuildError`](#remit835builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`Remit835BuildErrorCode`](#remit835builderrorcode-1) *** ### Rfai277BuildError Thrown by `build277RequestForAdditionalInformation` when a spec cannot be emitted as a 277 the base X12 006020 standard accepts. No interchange is returned. The message carries no document value. #### Example ```ts import { Rfai277BuildError } from "@cosyte/x12"; try { // build277RequestForAdditionalInformation(spec); } catch (err) { if (err instanceof Rfai277BuildError) console.error(err.code); } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new Rfai277BuildError**(`code`, `message`): [`Rfai277BuildError`](#rfai277builderror) **`Internal`** ###### Parameters ###### code [`Rfai277BuildErrorCode`](#rfai277builderrorcode-1) ###### message `string` ###### Returns [`Rfai277BuildError`](#rfai277builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`Rfai277BuildErrorCode`](#rfai277builderrorcode-1) *** ### ServicesReview278BuildError Thrown by "./build-278.js".build278Request / "./build-278.js".build278Response when the supplied services-review spec cannot be emitted as a conformant, self-consistent 278 - most importantly when its nested tree cannot form a valid HL hierarchy, or when a response's `HCR` certification action is missing the verbatim `actionCode` the builder is forbidden to infer. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` or `X12BuildError` - the domain-refusal distinction matters at the type level. #### Example ```ts import { ServicesReview278BuildError } from "@cosyte/x12"; try { build278Request(spec); } catch (err) { if (err instanceof ServicesReview278BuildError) { // err.code is one of AUTH_278_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new ServicesReview278BuildError**(`code`, `message`): [`ServicesReview278BuildError`](#servicesreview278builderror) **`Internal`** ###### Parameters ###### code [`ServicesReview278BuildErrorCode`](#servicesreview278builderrorcode-1) ###### message `string` ###### Returns [`ServicesReview278BuildError`](#servicesreview278builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`ServicesReview278BuildErrorCode`](#servicesreview278builderrorcode-1) *** ### X12AttachmentData Binary data from a BDS-03, held so that it cannot be printed by accident. `String(data)`, a template literal, `JSON.stringify` and Node's `util.inspect` (and so `console.log`) all render `[X12AttachmentData: octets withheld]`. The octets themselves are held in a private field that no enumeration, spread or structured clone reaches, and are returned only by [readOctets](#readoctets), exactly as the parser framed them: one character per octet, no filter applied, no release unescape, no split. #### Example ```ts import { parseX12, get275Attachments } from "@cosyte/x12"; const ix = parseX12(buffer); const tx = ix.groups[0]?.transactions[0]; const reading = tx === undefined ? undefined : get275Attachments(ix.delimiters, tx); const data = reading?.attachments[0]?.data; String(data); // "[X12AttachmentData: 1336 octets withheld]" data?.octetCount; // 1336 data?.readOctets(); // the 1336 octets, one character each, verbatim ``` #### Constructors ##### Constructor > **new X12AttachmentData**(`octets`): [`X12AttachmentData`](#x12attachmentdata) **`Internal`** Wrap `octets`, one character per octet. Readers construct these; a caller building a 275 passes plain data to the builder instead. ###### Parameters ###### octets `string` ###### Returns [`X12AttachmentData`](#x12attachmentdata) #### Accessors ##### octetCount ###### Get Signature > **get** **octetCount**(): `number` How many characters, one per octet, the data holds. ###### Returns `number` #### Methods ##### \[toPrimitive\]() > **\[toPrimitive\]**(): `string` The withheld form for every coercion hint. ###### Returns `string` ##### readOctets() > **readOctets**(): `string` The data, verbatim: exactly the characters the parser framed as BDS-03, one per octet. This is the only route to them. ###### Returns `string` ###### Example ```ts import type { X12AttachmentData } from "@cosyte/x12"; declare const data: X12AttachmentData; const octets = data.readOctets(); Buffer.from(octets, "latin1"); // the bytes, where every character is at or below U+00FF ``` ##### toJSON() > **toJSON**(): `string` The withheld form, so `JSON.stringify` carries no octet. ###### Returns `string` ##### toString() > **toString**(): `string` The withheld form, never the data. ###### Returns `string` *** ### X12BuildError Thrown by "./build-interchange.js".buildInterchange when the supplied envelope spec is structurally impossible. Carries a stable `code` for programmatic narrowing. Deliberately does NOT extend `X12ParseError` - the parser-vs-builder distinction matters at the type level. #### Example ```ts import { X12BuildError } from "@cosyte/x12"; try { buildInterchange({ ...spec, interchangeControlNumber: "0123456789" }); } catch (err) { if (err instanceof X12BuildError) { // err.code is one of X12_BUILD_ERROR_CODES } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new X12BuildError**(`code`, `message`): [`X12BuildError`](#x12builderror) **`Internal`** ###### Parameters ###### code `"X12_BUILD_INVALID_SPEC"` ###### message `string` ###### Returns [`X12BuildError`](#x12builderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: `"X12_BUILD_INVALID_SPEC"` *** ### X12CodeListError Thrown by the date-aware code-list queries when the supplied document date is not a calendar day this package can read. Carries a stable `code` for programmatic narrowing and the rejected value, so a caller can see WHICH value was refused without re-deriving it from the message. Deliberately does NOT extend `X12ParseError` or `X12BuildError`: nothing here parses a document or builds one, and the three are meant to stay tellable apart at the type level. #### Example ```ts import { X12CodeListError, checkRarcValidity } from "@cosyte/x12"; try { checkRarcValidity("N4", "not-a-date"); } catch (err) { if (err instanceof X12CodeListError) { err.rejectedValue; // '"not-a-date"' } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new X12CodeListError**(`code`, `message`, `rejectedValue`): [`X12CodeListError`](#x12codelisterror) **`Internal`** ###### Parameters ###### code `"X12_CODE_LIST_INVALID_DOCUMENT_DATE"` ###### message `string` ###### rejectedValue `string` ###### Returns [`X12CodeListError`](#x12codelisterror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: `"X12_CODE_LIST_INVALID_DOCUMENT_DATE"` ##### rejectedValue > `readonly` **rejectedValue**: `string` The refused value as it appears in `message`: a BOUNDED rendering, not the caller's own object. It goes through the same renderer and the same ceiling every other refusal in this library uses, so an over-long value cannot make an unbounded `Error.message`, and `null`, `"null"` and an absent value stay tellable apart. *** ### X12Decimal String-backed decimal for X12 R-type elements. Immutable; arithmetic returns a new [X12Decimal](#x12decimal). Equality is mathematical (`"0.00"` equals `"0"`), not lexical - use `toString()` for byte-exact comparison. #### Example ```ts import { X12Decimal } from "@cosyte/x12"; const charge = X12Decimal.fromString("500.00"); const paid = X12Decimal.fromString("450.00"); const due = charge?.subtract(paid!); due?.toString(); // "50.00" due?.isZero(); // false ``` #### Properties ##### ZERO > `readonly` `static` **ZERO**: [`X12Decimal`](#x12decimal) Canonical zero with `scale: 0` - `"0"`. Used as the additive identity for balance reductions. Use `X12Decimal.fromBigInt(0n, n)` for a zero at a specific scale. ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.ZERO.toString(); // "0" X12Decimal.ZERO.isZero(); // true ``` #### Methods ##### abs() > **abs**(): [`X12Decimal`](#x12decimal) Absolute value. Scale and lexical form may change (canonical rendering drops the leading `-`). ###### Returns [`X12Decimal`](#x12decimal) ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("-50.00")?.abs().toString(); // "50.00" ``` ##### add() > **add**(`other`): [`X12Decimal`](#x12decimal) Add two `X12Decimal` values exactly. Result scale is `max(this.scale, other.scale)`; result lexical form is canonical. ###### Parameters ###### other [`X12Decimal`](#x12decimal) ###### Returns [`X12Decimal`](#x12decimal) ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; const a = X12Decimal.fromString("0.1")!; const b = X12Decimal.fromString("0.2")!; a.add(b).toString(); // "0.3" - exact, never 0.30000000000000004 ``` ##### compareTo() > **compareTo**(`other`): `-1` \| `0` \| `1` Three-way compare: `-1` if `this < other`, `0` if equal, `1` if `this > other`. Mathematical compare across scales. ###### Parameters ###### other [`X12Decimal`](#x12decimal) ###### Returns `-1` \| `0` \| `1` ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("10")!.compareTo(X12Decimal.fromString("9.99")!); // 1 ``` ##### equals() > **equals**(`other`): `boolean` Mathematical equality across scales (`"0.00"` equals `"0"`). For byte-exact comparison use `a.toString() === b.toString()`. ###### Parameters ###### other [`X12Decimal`](#x12decimal) ###### Returns `boolean` ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; const a = X12Decimal.fromString("0.00")!; const b = X12Decimal.fromString("0")!; a.equals(b); // true a.toString() === b.toString();// false ("0.00" vs "0") ``` ##### isZero() > **isZero**(): `boolean` True when this value is exactly zero (any scale). ###### Returns `boolean` ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("0.00")?.isZero(); // true X12Decimal.fromString("-50")?.isZero(); // false ``` ##### negate() > **negate**(): [`X12Decimal`](#x12decimal) Arithmetic negation. `abs() === negate()` for negative values; for positive values flips sign; zero is its own negation. ###### Returns [`X12Decimal`](#x12decimal) ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("50.00")?.negate().toString(); // "-50.00" X12Decimal.fromString("-50.00")?.negate().toString(); // "50.00" ``` ##### signum() > **signum**(): `-1` \| `0` \| `1` `-1` / `0` / `1` indicating the sign of the value. Zero values (including `"0.00"` and `"-0.00"`) always return `0`. ###### Returns `-1` \| `0` \| `1` ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("-50")?.signum(); // -1 X12Decimal.fromString("0.00")?.signum(); // 0 X12Decimal.fromString("0.01")?.signum(); // 1 ``` ##### subtract() > **subtract**(`other`): [`X12Decimal`](#x12decimal) Subtract `other` from `this` exactly. Result scale is `max(this.scale, other.scale)`. ###### Parameters ###### other [`X12Decimal`](#x12decimal) ###### Returns [`X12Decimal`](#x12decimal) ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("500.00")!.subtract(X12Decimal.fromString("50")!).toString(); // "450.00" ``` ##### toNumber() > **toNumber**(): `number` Lossy conversion to JS `number`. **JSDoc warns:** `number` cannot represent every X12 monetary value exactly (`0.1 + 0.2 !== 0.3`). Helpers return `X12Decimal`, not `number`; use this only for display or when the loss is acceptable. Magnitudes large enough to lose precision (> `Number.MAX_SAFE_INTEGER` after scaling) still convert without throwing - silently lossy. ###### Returns `number` ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("1234.56")?.toNumber(); // 1234.56 (typically exact) X12Decimal.fromString("0.1")?.toNumber(); // 0.1 (lossy at higher precision) ``` ##### toString() > **toString**(): `string` Return the verbatim lexical form (for [X12Decimal.fromString](#fromstring)) or the canonical rendering (for arithmetic results / `fromBigInt`). Round-trip is byte-exact when the value was produced by `fromString`. ###### Returns `string` ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("0050.00")?.toString(); // "0050.00" (verbatim) X12Decimal.fromBigInt(5000n, 2).toString(); // "50.00" (canonical) ``` ##### fromBigInt() > `static` **fromBigInt**(`value`, `scale`): [`X12Decimal`](#x12decimal) Construct an `X12Decimal` from a `BigInt` magnitude + scale (decimal places). Useful when arithmetic produces a result whose lexical form the caller wants to control. `raw` is rendered canonically (sign + integer + optional `.fraction`). ###### Parameters ###### value `bigint` ###### scale `number` ###### Returns [`X12Decimal`](#x12decimal) ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromBigInt(123456n, 2).toString(); // "1234.56" X12Decimal.fromBigInt(-50n, 0).toString(); // "-50" X12Decimal.fromBigInt(0n, 2).toString(); // "0.00" ``` ##### fromString() > `static` **fromString**(`raw`): [`X12Decimal`](#x12decimal) \| `undefined` Decode an X12 R-type decimal element into an `X12Decimal`, or `undefined` when the input is empty or does not match the shape `[+-]?digits(.digits?)?`. **Empty string returns `undefined`, never zero** - "not supplied" and "zero" are spec-distinct. ###### Parameters ###### raw `string` ###### Returns [`X12Decimal`](#x12decimal) \| `undefined` ###### Example ```ts import { X12Decimal } from "@cosyte/x12"; X12Decimal.fromString("1234.56")?.toString(); // "1234.56" X12Decimal.fromString("-50")?.signum(); // -1 X12Decimal.fromString(""); // undefined X12Decimal.fromString("1,234.56"); // undefined (X12 forbids thousands sep) ``` *** ### X12ParseError Thrown by `parseX12` when the input violates one of the 4 unrecoverable Tier-3 structural rules (missing ISA, truncated ISA, invalid ISA delimiters, or empty input). Carries positional context plus a short snippet of the offending input so consumers can log actionable errors. #### Remarks `snippet` is the library's ONE deliberate PHI exception, and it exists for the four Tier-3 structural fatals only. Those are raised before the envelope is readable and are undebuggable without a few bytes of context. It may contain PHI/PII when parsing real interchanges (member IDs appear in ISA-06/08 only as trading-partner IDs, not patient identity, but a real claim / eligibility body elsewhere in the input can carry PHI). Redact at the call site if required by your compliance posture; the library does not redact snippets itself. A strict-mode escalation of a Tier-2 warning carries `snippet: ""`. That error's `message` is the same frozen-registry entry the warning carried and `position` locates it, so there is nothing to attach and nothing to redact. #### Example ```ts import { parseX12, X12ParseError } from "@cosyte/x12"; try { parseX12(""); } catch (err) { if (err instanceof X12ParseError && err.code === "X12_EMPTY_INPUT") { // handle empty input - err.position, err.snippet available } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new X12ParseError**(`code`, `message`, `position`, `snippetText`): [`X12ParseError`](#x12parseerror) **`Internal`** Construct a new `X12ParseError`. All four fields are required so every thrower populates full positional context. ###### Parameters ###### code [`X12FatalCode`](#x12fatalcode) ###### message `string` ###### position [`X12Position`](#x12position) ###### snippetText `string` ###### Returns [`X12ParseError`](#x12parseerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`X12FatalCode`](#x12fatalcode) ##### position > `readonly` **position**: [`X12Position`](#x12position) ##### snippet > `readonly` **snippet**: `string` *** ### X12ProfileError Thrown by `defineProfile()` and profile-validation code when a profile definition is structurally invalid. Carries the offending profile name (when known) so consumers can pinpoint which definition failed. #### Example ```ts import { defineProfile, X12ProfileError } from "@cosyte/x12"; try { defineProfile({ name: "" }); } catch (err) { if (err instanceof X12ProfileError) { console.error(err.message, err.profileName); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new X12ProfileError**(`message`, `profileName?`): [`X12ProfileError`](#x12profileerror) **`Internal`** Construct a new `X12ProfileError`. `profileName` is optional so the name validator can throw before a usable name is available. ###### Parameters ###### message `string` ###### profileName? `string` ###### Returns [`X12ProfileError`](#x12profileerror) ###### Overrides `Error.constructor` #### Properties ##### profileName > `readonly` **profileName**: `string` \| `undefined` ## Interfaces ### AaaCodeListMeta Provenance for a bundled AAA code list. The FOUR parts a shipped description requires all live here, beside the codes they describe, so a consumer or a conformance test can assert the record by reading the artifact alone. `source` and `snapshotDate` are the source and the capture date, exposed the way every other bundled snapshot exposes its own. `maintainingOrganization` and `redistributionTerms` are `undefined` where that part could not be established, which is a different statement from an empty string and is meant to be read as one. #### Example ```ts import { AAA_REJECT_REASON_CODES } from "@cosyte/x12"; AAA_REJECT_REASON_CODES.meta.maintainingOrganization; // "ASC X12" AAA_REJECT_REASON_CODES.meta.redistributionPermitsBundledDescriptions; // false ``` #### Properties ##### description > `readonly` **description**: `string` ##### id > `readonly` **id**: `string` ##### maintainingOrganization > `readonly` **maintainingOrganization**: `string` \| `undefined` Part 3: who maintains the list, or `undefined` where unestablished. ##### redistributionPermitsBundledDescriptions > `readonly` **redistributionPermitsBundledDescriptions**: `boolean` Whether part 4, as recorded, permits these descriptions to ship inside the published package. Recording terms is not the same as being allowed to bundle under them, and this is the field that says which. ##### redistributionTerms > `readonly` **redistributionTerms**: `string` \| `undefined` Part 4: the terms, quoted or cited, or `undefined` where unestablished. ##### snapshotDate > `readonly` **snapshotDate**: `string` Part 2: the date this package captured the list. ##### source > `readonly` **source**: `string` Part 1: the canonical source the entries were captured from. *** ### AaaCodeListSnapshot A bundled AAA code list: its four-part provenance record plus the codes it was permitted to ship. `codes` is empty whenever the record is incomplete. #### Example ```ts import { AAA_FOLLOW_UP_ACTION_CODES } from "@cosyte/x12"; Object.keys(AAA_FOLLOW_UP_ACTION_CODES.codes).length; // 0 ``` #### Properties ##### codes > `readonly` **codes**: `Readonly`\<`Record`\<`string`, `string`\>\> ##### meta > `readonly` **meta**: [`AaaCodeListMeta`](#aaacodelistmeta) *** ### Build270AddressSpec A postal address (N3 + N4). Mirrors "./inquiry-types.js".X12InquiryAddress. #### Example ```ts import type { Build270AddressSpec } from "@cosyte/x12"; const a: Build270AddressSpec = { lines: ["100 MAIN ST"], city: "COLUMBUS", state: "OH" }; ``` #### Properties ##### city? > `readonly` `optional` **city?**: `string` N4-01 - city. ##### countryCode? > `readonly` `optional` **countryCode?**: `string` N4-04 - country code. ##### lines > `readonly` **lines**: readonly `string`[] N3 address lines (1-2). ##### postalCode? > `readonly` `optional` **postalCode?**: `string` N4-03 - postal code. ##### state? > `readonly` `optional` **state?**: `string` N4-02 - state or province. *** ### Build270DateSpec A DTP date or date range. `formatQualifier` (DTP-02) says which: `D8` is a single date, `RD8` a range. Mirrors "./inquiry-types.js".X12InquiryDate. #### Example ```ts import type { Build270DateSpec } from "@cosyte/x12"; const d: Build270DateSpec = { qualifier: "291", formatQualifier: "D8", value: "20260601" }; ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time period format qualifier. ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier. ##### value > `readonly` **value**: `string` DTP-03 - the date or range. *** ### Build270DependentSpec One dependent (Loop 2000D / 2100D / 2110D) - a patient who cannot be identified as a subscriber in their own right. Carries its OWN name, traces and inquiries. Mirrors "./inquiry-types.js".X12InquiryDependent. #### Example ```ts import type { Build270DependentSpec } from "@cosyte/x12"; const d: Build270DependentSpec = { name: { entityIdentifierCode: "03", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "BABY" }, inquiries: [{ serviceTypeCodes: [{ code: "35" }] }], }; ``` #### Properties ##### dates? > `readonly` `optional` **dates?**: readonly [`Build270DateSpec`](#build270datespec)[] Loop 2100D DTP dates. ##### inquiries > `readonly` **inquiries**: readonly [`Build270InquirySpec`](#build270inquiryspec)[] Loop 2110D inquiries. At least one is required. ##### name > `readonly` **name**: [`Build270NameSpec`](#build270namespec) Loop 2100D dependent name. Required: a level with no name is refused. ##### references? > `readonly` `optional` **references?**: readonly [`Build270ReferenceSpec`](#build270referencespec)[] Loop 2100D REF identifiers. ##### traces? > `readonly` `optional` **traces?**: readonly [`Build270TraceSpec`](#build270tracespec)[] Loop 2000D TRN reassociation traces. *** ### Build270EnvelopeSpec Interchange, group and transaction identity for the built 270. Mirrors "./build-271-types.js".Build271EnvelopeSpec; the builder fixes GS-01 to `"HS"` (Eligibility, Coverage or Benefit Inquiry) and the version and release to `"005010X279A1"`, so the caller never hand-codes them. #### Example ```ts import type { Build270EnvelopeSpec } from "@cosyte/x12"; const env: Build270EnvelopeSpec = { senderId: "ANYTOWNCLINIC", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build270HeaderSpec The BHT beginning-of-hierarchical-transaction header. Every field has a builder default so a caller that has nothing to say about the header can omit it, and the defaults are library constants or values already on the envelope: nothing is invented out of the inquiry's content. #### Example ```ts import type { Build270HeaderSpec } from "@cosyte/x12"; const h: Build270HeaderSpec = { referenceId: "REQ-0001" }; ``` #### Properties ##### date? > `readonly` `optional` **date?**: `string` BHT-04 - creation date CCYYMMDD. Default: the group date. ##### hierarchicalStructureCode? > `readonly` `optional` **hierarchicalStructureCode?**: `string` BHT-01 - hierarchical structure code. Default `"0022"`. ##### purposeCode? > `readonly` `optional` **purposeCode?**: `string` BHT-02 - transaction set purpose code. Default `"13"` (request). ##### referenceId? > `readonly` `optional` **referenceId?**: `string` BHT-03 - submitter transaction identifier. ##### time? > `readonly` `optional` **time?**: `string` BHT-05 - creation time HHMM. Default: the group time. *** ### Build270InformationReceiverSpec One information receiver (Loop 2000B / 2100B) - the provider asking. #### Example ```ts import type { Build270InformationReceiverSpec } from "@cosyte/x12"; const r: Build270InformationReceiverSpec = { name: { entityIdentifierCode: "1P", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC" }, subscribers: [], }; ``` #### Properties ##### name > `readonly` **name**: [`Build270NameSpec`](#build270namespec) Loop 2100B information-receiver name. ##### references? > `readonly` `optional` **references?**: readonly [`Build270ReferenceSpec`](#build270referencespec)[] Loop 2100B REF identifiers. ##### subscribers > `readonly` **subscribers**: readonly [`Build270SubscriberSpec`](#build270subscriberspec)[] Loop 2000C subscribers (at least one required - a receiver with none is refused). *** ### Build270InformationSourceSpec One information source (Loop 2000A / 2100A) - the payer being asked. #### Example ```ts import type { Build270InformationSourceSpec } from "@cosyte/x12"; const src: Build270InformationSourceSpec = { name: { entityIdentifierCode: "PR", entityTypeQualifier: "2", lastNameOrOrganizationName: "MEDPAY INSURANCE" }, receivers: [], }; ``` #### Properties ##### name > `readonly` **name**: [`Build270NameSpec`](#build270namespec) Loop 2100A information-source name. ##### receivers > `readonly` **receivers**: readonly [`Build270InformationReceiverSpec`](#build270informationreceiverspec)[] Loop 2000B receivers (at least one required - a source with none is refused). ##### references? > `readonly` `optional` **references?**: readonly [`Build270ReferenceSpec`](#build270referencespec)[] Loop 2100A REF identifiers. *** ### Build270InquirySpec One eligibility or benefit inquiry (`EQ`, Loop 2110C / 2110D) - a single thing being asked. At least one Service Type Code or a procedure is required: an EQ that asks nothing is refused rather than emitted. Mirrors "./inquiry-types.js".X12InquiryRequest minus each service type's derived description. #### Example ```ts import type { Build270InquirySpec } from "@cosyte/x12"; const q: Build270InquirySpec = { serviceTypeCodes: [{ code: "30" }], coverageLevelCode: "IND", }; ``` #### Properties ##### coverageLevelCode? > `readonly` `optional` **coverageLevelCode?**: `string` EQ-03 - coverage level code. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build270DateSpec`](#build270datespec)[] DTP dates under this inquiry. ##### diagnosisCodePointers? > `readonly` `optional` **diagnosisCodePointers?**: readonly `string`[] EQ-05 - diagnosis code pointers, as separated components. ##### insuranceTypeCode? > `readonly` `optional` **insuranceTypeCode?**: `string` EQ-04 - insurance type code. ##### procedure? > `readonly` `optional` **procedure?**: [`Build270ProcedureSpec`](#build270procedurespec) EQ-02 - the requested procedure, as separated components. ##### references? > `readonly` `optional` **references?**: readonly [`Build270ReferenceSpec`](#build270referencespec)[] REF identifiers under this inquiry. ##### serviceTypeCodes? > `readonly` `optional` **serviceTypeCodes?**: readonly [`Build270ServiceTypeSpec`](#build270servicetypespec)[] EQ-01 - one or more requested Service Type Codes (a repeating element). *** ### Build270NameSpec An NM1 name loop, with the N3 / N4 address and DMG demographics that follow it. Mirrors "./inquiry-types.js".X12InquiryName. One type for every level, for the reason the read model gives: NM1-03 is "name last or organization name" and the same segment carries both. #### Example ```ts import type { Build270NameSpec } from "@cosyte/x12"; const m: Build270NameSpec = { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001", }; ``` #### Properties ##### address? > `readonly` `optional` **address?**: [`Build270AddressSpec`](#build270addressspec) N3 + N4 postal address. ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: `string` DMG-02 - date of birth (emitted with DMG-01 = `D8`). ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`PR` payer, `1P` provider, `IL` insured, `03` dependent). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### genderCode? > `readonly` `optional` **genderCode?**: `string` DMG-03 - gender code. ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier. ##### lastNameOrOrganizationName? > `readonly` `optional` **lastNameOrOrganizationName?**: `string` NM1-03 - last name, or the organization name for a non-person. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build270ProcedureSpec The EQ-02 composite medical procedure identifier, supplied as its separated components. The builder joins them with the declared component separator, so a caller never hand-codes a delimiter. Mirrors "./inquiry-types.js".X12InquiryProcedure. #### Example ```ts import type { Build270ProcedureSpec } from "@cosyte/x12"; const p: Build270ProcedureSpec = { qualifier: "HC", code: "99213", modifiers: ["25"] }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: `string` EQ-02-2 - the procedure code. ##### description? > `readonly` `optional` **description?**: `string` EQ-02-7 - procedure description. ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] EQ-02-3 through EQ-02-6 - procedure modifiers, in order. ##### qualifier > `readonly` **qualifier**: `string` EQ-02-1 - product or service id qualifier. *** ### Build270ReferenceSpec A REF supplemental identifier. Mirrors "./inquiry-types.js".X12InquiryReference. #### Example ```ts import type { Build270ReferenceSpec } from "@cosyte/x12"; const r: Build270ReferenceSpec = { qualifier: "6P", value: "GROUP0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description. ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification. *** ### Build270ServiceTypeSpec One requested Service Type Code (EQ-01). Only the verbatim `code` is supplied: the read side looks up its description from the bundled snapshot, so the spec deliberately omits it. #### Example ```ts import type { Build270ServiceTypeSpec } from "@cosyte/x12"; const st: Build270ServiceTypeSpec = { code: "30" }; ``` #### Properties ##### code > `readonly` **code**: `string` EQ-01 - a single Service Type Code. *** ### Build270Spec The complete spec for "./build-270.js".build270: the envelope, an optional BHT header, and the nested informationSources / receivers / subscribers / (dependents) tree the builder walks depth-first to compute the HL spine. #### Example ```ts import { build270, type Build270Spec } from "@cosyte/x12"; const spec: Build270Spec = { envelope: { senderId: "ANYTOWNCLINIC", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ name: { entityIdentifierCode: "PR", entityTypeQualifier: "2", lastNameOrOrganizationName: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "PAYER01" }, receivers: [{ name: { entityIdentifierCode: "1P", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ traces: [{ traceTypeCode: "1", referenceId: "ELIG0001" }], name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, inquiries: [{ serviceTypeCodes: [{ code: "30" }] }], }], }], }], }; const ix = build270(spec); ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build270EnvelopeSpec`](#build270envelopespec) Interchange, group and transaction identity. ##### header? > `readonly` `optional` **header?**: [`Build270HeaderSpec`](#build270headerspec) The BHT header. Every field defaults; the whole object may be omitted. ##### informationSources > `readonly` **informationSources**: readonly [`Build270InformationSourceSpec`](#build270informationsourcespec)[] Loop 2000A information sources (at least one required). *** ### Build270SubscriberSpec One subscriber (Loop 2000C / 2100C / 2110C). Mirrors "./inquiry-types.js".X12InquirySubscriber. #### Example ```ts import type { Build270SubscriberSpec } from "@cosyte/x12"; const s: Build270SubscriberSpec = { traces: [{ traceTypeCode: "1", referenceId: "ELIG0001" }], name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE" }, inquiries: [{ serviceTypeCodes: [{ code: "30" }] }], }; ``` #### Properties ##### dates? > `readonly` `optional` **dates?**: readonly [`Build270DateSpec`](#build270datespec)[] Loop 2100C DTP dates. ##### dependents? > `readonly` `optional` **dependents?**: readonly [`Build270DependentSpec`](#build270dependentspec)[] Loop 2000D dependents (a non-empty list sets the subscriber HL-04 to `"1"`). ##### inquiries? > `readonly` `optional` **inquiries?**: readonly [`Build270InquirySpec`](#build270inquiryspec)[] Loop 2110C inquiries. At least one is required UNLESS this subscriber carries dependents, in which case the inquiry may sit on the dependent instead and the subscriber is the identifying level only. ##### name > `readonly` **name**: [`Build270NameSpec`](#build270namespec) Loop 2100C subscriber name. Required: a level with no name is refused. ##### references? > `readonly` `optional` **references?**: readonly [`Build270ReferenceSpec`](#build270referencespec)[] Loop 2100C REF identifiers. ##### traces? > `readonly` `optional` **traces?**: readonly [`Build270TraceSpec`](#build270tracespec)[] Loop 2000C TRN reassociation traces. *** ### Build270TraceSpec A reassociation trace (TRN). TRN-02 is the value the answering 271 echoes back verbatim. Mirrors "./inquiry-types.js".X12InquiryTrace. #### Example ```ts import type { Build270TraceSpec } from "@cosyte/x12"; const t: Build270TraceSpec = { traceTypeCode: "1", referenceId: "ELIG20260601001" }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03 - originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - reference identification. ##### supplementalReferenceId? > `readonly` `optional` **supplementalReferenceId?**: `string` TRN-04 - supplemental reference identifier. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code. *** ### Build271AddressSpec A postal address (N3 + N4) attached to a subscriber / dependent name. Mirrors "./types.js".X12EligibilityAddress. #### Example ```ts import type { Build271AddressSpec } from "@cosyte/x12"; const a: Build271AddressSpec = { lines: ["123 MAIN ST"], city: "ANYTOWN", state: "CA", postalCode: "90001", }; ``` #### Properties ##### city? > `readonly` `optional` **city?**: `string` N4-01 - city. ##### countryCode? > `readonly` `optional` **countryCode?**: `string` N4-04 - country code. ##### lines > `readonly` **lines**: readonly `string`[] N3 address lines (1-2). ##### postalCode? > `readonly` `optional` **postalCode?**: `string` N4-03 - postal code. ##### state? > `readonly` `optional` **state?**: `string` N4-02 - state / province. *** ### Build271BenefitSpec One eligibility-or-benefit line (EB, Loop 2110C/2110D). EB-01 is the eligibility code; EB-03 carries one-or-more Service Type Codes (emitted as a repeating simple element). Monetary + percent + quantity are [X12Decimal](#x12decimal). Mirrors "./types.js".X12EligibilityBenefit minus each service type's derived `description` and the procedure's `description`. #### Example ```ts import type { Build271BenefitSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const b: Build271BenefitSpec = { eligibilityCode: "1", coverageLevelCode: "IND", serviceTypeCodes: [{ code: "30" }], inPlanNetwork: "Y", monetaryAmount: X12Decimal.fromString("1000.00")!, }; ``` #### Properties ##### authorizationRequired? > `readonly` `optional` **authorizationRequired?**: `string` EB-11 - authorization / certification indicator. ##### coverageLevelCode? > `readonly` `optional` **coverageLevelCode?**: `string` EB-02 - coverage level code (`IND`, `FAM`, …). ##### dates? > `readonly` `optional` **dates?**: readonly [`Build271DateSpec`](#build271datespec)[] Loop 2120 benefit-related DTP dates. ##### eligibilityCode > `readonly` **eligibilityCode**: `string` EB-01 - eligibility or benefit information code. ##### inPlanNetwork? > `readonly` `optional` **inPlanNetwork?**: `string` EB-12 - in-plan-network indicator (`Y` / `N` / `U` / `W`). ##### insuranceTypeCode? > `readonly` `optional` **insuranceTypeCode?**: `string` EB-04 - insurance type code. ##### messages? > `readonly` `optional` **messages?**: readonly `string`[] MSG free-form benefit messages. ##### monetaryAmount? > `readonly` `optional` **monetaryAmount?**: [`X12Decimal`](#x12decimal) EB-07 - monetary amount. ##### percent? > `readonly` `optional` **percent?**: [`X12Decimal`](#x12decimal) EB-08 - percent. ##### planCoverageDescription? > `readonly` `optional` **planCoverageDescription?**: `string` EB-05 - plan coverage description. ##### procedure? > `readonly` `optional` **procedure?**: [`Build271ProcedureSpec`](#build271procedurespec) EB-13 - the procedure this benefit line applies to. ##### quantity? > `readonly` `optional` **quantity?**: [`X12Decimal`](#x12decimal) EB-10 - quantity. ##### quantityQualifier? > `readonly` `optional` **quantityQualifier?**: `string` EB-09 - quantity qualifier. ##### references? > `readonly` `optional` **references?**: readonly [`Build271ReferenceSpec`](#build271referencespec)[] Loop 2120 benefit-related REF identifiers. ##### relatedEntities? > `readonly` `optional` **relatedEntities?**: readonly [`Build271EntitySpec`](#build271entityspec)[] Loop 2120C/D benefit-related entities (NM1). ##### serviceTypeCodes? > `readonly` `optional` **serviceTypeCodes?**: readonly [`Build271ServiceTypeSpec`](#build271servicetypespec)[] EB-03 - one-or-more Service Type Codes (emitted as a repeating element). ##### timePeriodQualifier? > `readonly` `optional` **timePeriodQualifier?**: `string` EB-06 - time period qualifier. *** ### Build271DateSpec A DTP date / date-range on a subscriber, dependent, or benefit line. Mirrors "./types.js".X12EligibilityDate. #### Example ```ts import type { Build271DateSpec } from "@cosyte/x12"; const d: Build271DateSpec = { qualifier: "307", formatQualifier: "D8", value: "20260101" }; ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time format qualifier (`D8` / `RD8`). ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier. ##### value > `readonly` **value**: `string` DTP-03 - date/time value. *** ### Build271DependentSpec One dependent (Loop 2000D / 2100D) - a patient who is not the subscriber. Same benefit-bearing shape as a subscriber minus the nested dependents. Mirrors "./types.js".X12EligibilityDependent. #### Example ```ts import type { Build271DependentSpec } from "@cosyte/x12"; const d: Build271DependentSpec = { name: { entityIdentifierCode: "03", entityTypeQualifier: "1", lastName: "DOE", firstName: "JUNIOR" }, benefits: [{ eligibilityCode: "1", coverageLevelCode: "IND" }], }; ``` #### Properties ##### benefits? > `readonly` `optional` **benefits?**: readonly [`Build271BenefitSpec`](#build271benefitspec)[] Loop 2110D eligibility / benefit lines. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build271DateSpec`](#build271datespec)[] Loop 2100D DTP dates. ##### name? > `readonly` `optional` **name?**: [`Build271MemberSpec`](#build271memberspec) Loop 2100D dependent name (NM1 + DMG + N3/N4). ##### references? > `readonly` `optional` **references?**: readonly [`Build271ReferenceSpec`](#build271referencespec)[] Loop 2100D REF identifiers. ##### traces? > `readonly` `optional` **traces?**: readonly [`Build271TraceSpec`](#build271tracespec)[] Loop 2000D TRN reassociation traces. *** ### Build271EntitySpec A non-person entity (NM1) - the information-source payer (Loop 2100A) or information-receiver provider (Loop 2100B), or a benefit-related entity (Loop 2120C). Mirrors "./types.js".X12EligibilityEntity. #### Example ```ts import type { Build271EntitySpec } from "@cosyte/x12"; const payer: Build271EntitySpec = { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`PR` payer, `1P` provider, …). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier. ##### name > `readonly` **name**: `string` NM1-03 - organization / last name. *** ### Build271EnvelopeSpec Interchange + group + transaction identity for the built 271. Mirrors the "../remit/build-835-types.js".Build835EnvelopeSpec; the builder fixes GS-01 to `"HB"` and the version/release to `"005010X279A1"` (the 271 functional group + TR3) so the caller never hand-codes them. #### Example ```ts import type { Build271EnvelopeSpec } from "@cosyte/x12"; const env: Build271EnvelopeSpec = { senderId: "MEDPAY", receiverId: "PROVIDER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build271InformationReceiverSpec One information receiver (Loop 2000B / 2100B) - the provider that requested eligibility. Carries the provider entity (NM1) and its subscribers. #### Example ```ts import type { Build271InformationReceiverSpec } from "@cosyte/x12"; const r: Build271InformationReceiverSpec = { entity: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [], }; ``` #### Properties ##### entity > `readonly` **entity**: [`Build271EntitySpec`](#build271entityspec) Loop 2100B information-receiver provider entity (NM1). ##### subscribers > `readonly` **subscribers**: readonly [`Build271SubscriberSpec`](#build271subscriberspec)[] Loop 2000C subscribers (at least one required - a receiver with none is refused). *** ### Build271InformationSourceSpec One information source (Loop 2000A / 2100A) - the payer answering the eligibility request. Carries the payer entity (NM1) and its receivers. #### Example ```ts import type { Build271InformationSourceSpec } from "@cosyte/x12"; const src: Build271InformationSourceSpec = { entity: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123" }, receivers: [], }; ``` #### Properties ##### entity > `readonly` **entity**: [`Build271EntitySpec`](#build271entityspec) Loop 2100A information-source payer entity (NM1). ##### receivers > `readonly` **receivers**: readonly [`Build271InformationReceiverSpec`](#build271informationreceiverspec)[] Loop 2000B receivers (at least one required - a source with none is refused). *** ### Build271MemberSpec A person (subscriber Loop 2100C / dependent Loop 2100D) decoded from NM1 + the optional DMG demographics + N3/N4 address. `idCode` is the member identifier (NM1-09) - synthetic-only in fixtures. Mirrors "./types.js".X12EligibilityMember. #### Example ```ts import type { Build271MemberSpec } from "@cosyte/x12"; const m: Build271MemberSpec = { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001", dateOfBirth: "19800101", genderCode: "F", }; ``` #### Properties ##### address? > `readonly` `optional` **address?**: [`Build271AddressSpec`](#build271addressspec) N3 + N4 postal address. ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: `string` DMG-02 - date of birth (CCYYMMDD; emitted with DMG-01 = `D8`). ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`IL` insured / subscriber, `03` dependent). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### genderCode? > `readonly` `optional` **genderCode?**: `string` DMG-03 - gender code (`M` / `F` / `U`). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code (the member id). ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`MI` member id). ##### lastName? > `readonly` `optional` **lastName?**: `string` NM1-03 - last name. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build271ProcedureSpec The EB-13 composite medical procedure identifier, supplied as its separated components. The builder joins them with the declared component separator, so a caller never hand-codes a delimiter. Mirrors "./types.js".X12EligibilityProcedure minus the description, which the builder does not emit. Refused via `X12_271_BUILD_INVALID_SPEC`: an empty `qualifier` or `code`, an empty modifier, or more than four modifiers (C003 has four modifier positions, and a fifth would land in the description position). #### Example ```ts import type { Build271ProcedureSpec } from "@cosyte/x12"; const p: Build271ProcedureSpec = { qualifier: "AD", code: "D2150" }; ``` #### Properties ##### code > `readonly` **code**: `string` EB-13-2 - the procedure code. ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] EB-13-3 through EB-13-6 - up to four procedure modifiers, in order. ##### qualifier > `readonly` **qualifier**: `string` EB-13-1 - product or service id qualifier (`AD` for CDT, `HC` for HCPCS, ...). *** ### Build271ReferenceSpec A REF supplemental identifier on a subscriber, dependent, or benefit line. Mirrors "./types.js".X12EligibilityReference. #### Example ```ts import type { Build271ReferenceSpec } from "@cosyte/x12"; const r: Build271ReferenceSpec = { qualifier: "6P", value: "GRP0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description. ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification. *** ### Build271ServiceTypeSpec One Service Type Code (EB-03, X12 external code source 1365). Only the verbatim `code` is supplied - the read side looks up its `description` from the bundled snapshot, so the spec deliberately omits it. #### Example ```ts import type { Build271ServiceTypeSpec } from "@cosyte/x12"; const st: Build271ServiceTypeSpec = { code: "30" }; ``` #### Properties ##### code > `readonly` **code**: `string` EB-03 - a single Service Type Code. *** ### Build271Spec The complete spec for "./build-271.js".build271: the envelope plus the nested informationSources → receivers → subscribers → (dependents) tree the builder walks depth-first to compute the HL spine. #### Example ```ts import { build271, X12Decimal, type Build271Spec } from "@cosyte/x12"; const spec: Build271Spec = { envelope: { senderId: "MEDPAY", receiverId: "PROVIDER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ entity: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123" }, receivers: [{ entity: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ traces: [{ traceTypeCode: "2", referenceId: "ELIG001" }], name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, benefits: [{ eligibilityCode: "1", coverageLevelCode: "IND", serviceTypeCodes: [{ code: "30" }], monetaryAmount: X12Decimal.fromString("1000.00")! }], }], }], }], }; const ix = build271(spec); ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build271EnvelopeSpec`](#build271envelopespec) Interchange + group + transaction identity. ##### informationSources > `readonly` **informationSources**: readonly [`Build271InformationSourceSpec`](#build271informationsourcespec)[] Loop 2000A information sources (at least one required). *** ### Build271SubscriberSpec One subscriber (Loop 2000C / 2100C). Holds the echoed TRN traces, the subscriber name + demographics, the eligibility/benefit lines, and any non-subscriber dependents. Mirrors "./types.js".X12EligibilitySubscriber. #### Example ```ts import type { Build271SubscriberSpec } from "@cosyte/x12"; const s: Build271SubscriberSpec = { traces: [{ traceTypeCode: "2", referenceId: "ELIG001" }], name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE" }, benefits: [{ eligibilityCode: "1", coverageLevelCode: "IND" }], }; ``` #### Properties ##### benefits? > `readonly` `optional` **benefits?**: readonly [`Build271BenefitSpec`](#build271benefitspec)[] Loop 2110C eligibility / benefit lines. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build271DateSpec`](#build271datespec)[] Loop 2100C DTP dates. ##### dependents? > `readonly` `optional` **dependents?**: readonly [`Build271DependentSpec`](#build271dependentspec)[] Loop 2000D dependents (a non-empty list sets the subscriber HL-04 to `"1"`). ##### name? > `readonly` `optional` **name?**: [`Build271MemberSpec`](#build271memberspec) Loop 2100C subscriber name (NM1 + DMG + N3/N4). ##### references? > `readonly` `optional` **references?**: readonly [`Build271ReferenceSpec`](#build271referencespec)[] Loop 2100C REF identifiers. ##### traces? > `readonly` `optional` **traces?**: readonly [`Build271TraceSpec`](#build271tracespec)[] Loop 2000C TRN reassociation traces. *** ### Build271TraceSpec A reassociation trace (TRN). **The verbatim echo of the requesting 270's trace number** - `referenceId` (TRN-02) is the value a provider matches against the trace it sent. Mirrors "./types.js".X12EligibilityTrace. #### Example ```ts import type { Build271TraceSpec } from "@cosyte/x12"; const t: Build271TraceSpec = { traceTypeCode: "2", referenceId: "ELIG20260627001" }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03 - originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - reference identification (echoed verbatim from the 270). ##### supplementalReferenceId? > `readonly` `optional` **supplementalReferenceId?**: `string` TRN-04 - supplemental reference identifier. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code (`2` referenced - added by the payer in the 271). *** ### Build275AttachmentSpec One attachment, written as one BDS. `data` is the octets to carry: a `Uint8Array` (a `Buffer` is one), or a string holding one character per octet, every character at or below U+00FF. It is written verbatim and never encoded; `filterCode` names the filter the caller already applied. #### Example ```ts import type { Build275AttachmentSpec } from "@cosyte/x12"; const a: Build275AttachmentSpec = { filterCode: "B64", data: "U1lOVEhFVElD" }; ``` #### Properties ##### data > `readonly` **data**: `string` \| `Uint8Array`\<`ArrayBufferLike`\> BDS-03, at least one octet. ##### filterCode > `readonly` **filterCode**: `string` BDS-01, exactly three characters. *** ### Build275BeginningSpec The BGN, each element written as given. #### Example ```ts import type { Build275BeginningSpec } from "@cosyte/x12"; const bgn: Build275BeginningSpec = { transactionSetPurposeCode: "02", referenceId: "ATTACH-0001", date: "20260601" }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` BGN-08. ##### date? > `readonly` `optional` **date?**: `string` BGN-03. ##### referenceId? > `readonly` `optional` **referenceId?**: `string` BGN-02. ##### secondReferenceId? > `readonly` `optional` **secondReferenceId?**: `string` BGN-06. ##### securityLevelCode? > `readonly` `optional` **securityLevelCode?**: `string` BGN-09. ##### time? > `readonly` `optional` **time?**: `string` BGN-04. ##### timeCode? > `readonly` `optional` **timeCode?**: `string` BGN-05. ##### transactionSetPurposeCode > `readonly` **transactionSetPurposeCode**: `string` BGN-01. ##### transactionTypeCode? > `readonly` `optional` **transactionTypeCode?**: `string` BGN-07. *** ### Build275EntitySpec One NM1 of the heading, every element written as given. #### Example ```ts import type { Build275EntitySpec } from "@cosyte/x12"; const payer: Build275EntitySpec = { entityIdentifierCode: "PR", entityTypeQualifier: "2", lastOrOrganizationName: "PAYER ONE" }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01. ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02. ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04. ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08. ##### lastOrOrganizationName? > `readonly` `optional` **lastOrOrganizationName?**: `string` NM1-03. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05. ##### namePrefix? > `readonly` `optional` **namePrefix?**: `string` NM1-06. ##### nameSuffix? > `readonly` `optional` **nameSuffix?**: `string` NM1-07. *** ### Build275EnvelopeSpec The envelope. `interchangeControlVersion` is ISA-12: the builder writes the package's default, `00501`, unless given one, because no carried source states which ISA-12 a 006020 interchange carries. #### Example ```ts import type { Build275EnvelopeSpec } from "@cosyte/x12"; const envelope: Build275EnvelopeSpec = { senderId: "CLINIC", receiverId: "PAYER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03, application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02, application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02, group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04, group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05, group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02, interchange control number (zero-padded to 9 on emit). ##### interchangeControlVersion? > `readonly` `optional` **interchangeControlVersion?**: `string` ISA-12, interchange control version number. Default `"00501"`. ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09, interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10, interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08, interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07, interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator. Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06, interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05, interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02, transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15, usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build275LineSpec One LX line: its TRN, STC and REF segments, then one BDS per attachment. #### Example ```ts import type { Build275LineSpec } from "@cosyte/x12"; const line: Build275LineSpec = { trace: { traceTypeCode: "2", referenceId: "TRACE-0001" }, references: [{ qualifier: "1K", value: "PCN0001" }], attachments: [{ filterCode: "B64", data: "U1lOVEhFVElD" }], }; ``` #### Properties ##### attachments? > `readonly` `optional` **attachments?**: readonly [`Build275AttachmentSpec`](#build275attachmentspec)[] The line's attachments, one BDS each, written last. ##### references? > `readonly` `optional` **references?**: readonly [`Build275ReferenceSpec`](#build275referencespec)[] The line's REF segments. ##### status? > `readonly` `optional` **status?**: [`Build275StatusSpec`](#build275statusspec) The line's STC. ##### trace? > `readonly` `optional` **trace?**: [`Build275TraceSpec`](#build275tracespec) The line's TRN. *** ### Build275ReferenceSpec One REF. #### Example ```ts import type { Build275ReferenceSpec } from "@cosyte/x12"; const pcn: Build275ReferenceSpec = { qualifier: "1K", value: "PCN0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03. ##### qualifier > `readonly` **qualifier**: `string` REF-01. ##### value > `readonly` **value**: `string` REF-02. *** ### Build275Spec The whole build request. #### Example ```ts import type { Build275Spec } from "@cosyte/x12"; const spec: Build275Spec = { envelope: { senderId: "CLINIC", receiverId: "PAYER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, beginning: { transactionSetPurposeCode: "02", referenceId: "ATTACH-0001" }, lines: [{ trace: { traceTypeCode: "2", referenceId: "TRACE-0001" }, attachments: [{ filterCode: "B64", data: "U1lOVEhFVElD" }], }], }; ``` #### Properties ##### beginning? > `readonly` `optional` **beginning?**: [`Build275BeginningSpec`](#build275beginningspec) The heading's BGN, written first where given. ##### entities? > `readonly` `optional` **entities?**: readonly [`Build275EntitySpec`](#build275entityspec)[] The heading's NM1 names, written before the first line. ##### envelope > `readonly` **envelope**: [`Build275EnvelopeSpec`](#build275envelopespec) The interchange, group and transaction set envelope. ##### lines > `readonly` **lines**: readonly [`Build275LineSpec`](#build275linespec)[] The LX lines, each carrying its attachments. At least one attachment in all. *** ### Build275StatusCodeSpec One C043 composite. #### Example ```ts import type { Build275StatusCodeSpec } from "@cosyte/x12"; const code: Build275StatusCodeSpec = { categoryCode: "R4", statusCode: "18842-5", codeListQualifier: "LOI" }; ``` #### Properties ##### categoryCode > `readonly` **categoryCode**: `string` C043-01. ##### codeListQualifier? > `readonly` `optional` **codeListQualifier?**: `string` C043-04. ##### entityCode? > `readonly` `optional` **entityCode?**: `string` C043-03. ##### statusCode > `readonly` **statusCode**: `string` C043-02. *** ### Build275StatusSpec One STC, its composites written to STC-01, STC-10 and STC-11 in order, every component as given. #### Example ```ts import type { Build275StatusSpec } from "@cosyte/x12"; const status: Build275StatusSpec = { codes: [{ categoryCode: "R4", statusCode: "18842-5", codeListQualifier: "LOI" }], }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` STC-03. ##### codes > `readonly` **codes**: readonly [`Build275StatusCodeSpec`](#build275statuscodespec)[] One to three C043 composites. ##### message? > `readonly` `optional` **message?**: `string` STC-12. ##### statusEffectiveDate? > `readonly` `optional` **statusEffectiveDate?**: `string` STC-02. *** ### Build275TraceSpec One TRN. #### Example ```ts import type { Build275TraceSpec } from "@cosyte/x12"; const trace: Build275TraceSpec = { traceTypeCode: "2", referenceId: "TRACE-0001" }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03. ##### referenceId > `readonly` **referenceId**: `string` TRN-02. ##### supplementalReferenceId? > `readonly` `optional` **supplementalReferenceId?**: `string` TRN-04. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01. *** ### Build276AmountSpec An AMT amount row on a claim (e.g. `AMT*T3` total submitted charges). Mirrors "./status-inquiry-types.js".X12StatusInquiryAmount. #### Example ```ts import { X12Decimal, type Build276AmountSpec } from "@cosyte/x12"; const a: Build276AmountSpec = { qualifier: "T3", amount: X12Decimal.fromString("150.00")! }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) AMT-02 - the monetary amount, NEVER a JavaScript `number`. ##### qualifier > `readonly` **qualifier**: `string` AMT-01 - amount qualifier code. *** ### Build276ClaimSpec One claim the submitter is asking about (Loop 2200). The trace is REQUIRED: a TRN is what opens the loop on read, so a claim without one would fold its identifiers, amounts and dates into the claim before it. #### Example ```ts import type { Build276ClaimSpec } from "@cosyte/x12"; const c: Build276ClaimSpec = { trace: { traceTypeCode: "1", referenceId: "STATUS0001" }, references: [{ qualifier: "1K", value: "PCN0001" }], }; ``` #### Properties ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build276AmountSpec`](#build276amountspec)[] Loop 2200 AMT amount rows. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build276DateSpec`](#build276datespec)[] Loop 2200 DTP dates. ##### references? > `readonly` `optional` **references?**: readonly [`Build276ReferenceSpec`](#build276referencespec)[] Loop 2200 REF identifiers. ##### serviceLines? > `readonly` `optional` **serviceLines?**: readonly [`Build276ServiceLineSpec`](#build276servicelinespec)[] Loop 2210 service lines. ##### trace > `readonly` **trace**: [`Build276TraceSpec`](#build276tracespec) Loop 2200 TRN. Required: a claim with no trace is refused. *** ### Build276DateSpec A DTP date or date range. `formatQualifier` (DTP-02) says which: `D8` is a single date, `RD8` a range. Mirrors "./status-inquiry-types.js".X12StatusInquiryDate. #### Example ```ts import type { Build276DateSpec } from "@cosyte/x12"; const d: Build276DateSpec = { qualifier: "472", formatQualifier: "D8", value: "20260520" }; ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time period format qualifier. ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier. ##### value > `readonly` **value**: `string` DTP-03 - the date or range. *** ### Build276DependentSpec One dependent (Loop 2000E / 2100E / 2200E) - a patient who cannot be identified as a subscriber in their own right. Carries its OWN name and claims. Mirrors "./status-inquiry-types.js".X12StatusInquiryDependent. #### Example ```ts import type { Build276DependentSpec } from "@cosyte/x12"; const d: Build276DependentSpec = { name: { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "BABY" }, claims: [{ trace: { traceTypeCode: "1", referenceId: "STATUS0002" } }], }; ``` #### Properties ##### claims > `readonly` **claims**: readonly [`Build276ClaimSpec`](#build276claimspec)[] Loop 2200E claims. At least one is required. ##### name > `readonly` **name**: [`Build276NameSpec`](#build276namespec) Loop 2100E dependent name. Required: a level with no name is refused. *** ### Build276EnvelopeSpec Interchange, group and transaction identity for the built 276. Mirrors "./build-277-types.js".Build277EnvelopeSpec; the builder fixes the version and release to `"005010X212"` and takes GS-01 from the cited data element 479 table at `src/code-lists/functional-identifier.ts`, so the caller never hand-codes either. #### Example ```ts import type { Build276EnvelopeSpec } from "@cosyte/x12"; const env: Build276EnvelopeSpec = { senderId: "ANYTOWNCLINIC", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build276HeaderSpec The BHT beginning-of-hierarchical-transaction header. Every field has a builder default so a caller that has nothing to say about the header can omit it, and the defaults are library constants or values already on the envelope: nothing is invented out of the request's content. #### Example ```ts import type { Build276HeaderSpec } from "@cosyte/x12"; const h: Build276HeaderSpec = { referenceId: "STATUS-0001" }; ``` #### Properties ##### date? > `readonly` `optional` **date?**: `string` BHT-04 - creation date CCYYMMDD. Default: the group date. ##### hierarchicalStructureCode? > `readonly` `optional` **hierarchicalStructureCode?**: `string` BHT-01 - hierarchical structure code. Default `"0010"`. ##### purposeCode? > `readonly` `optional` **purposeCode?**: `string` BHT-02 - transaction set purpose code. Default `"13"` (request). ##### referenceId? > `readonly` `optional` **referenceId?**: `string` BHT-03 - submitter transaction identifier. ##### time? > `readonly` `optional` **time?**: `string` BHT-05 - creation time HHMM. Default: the group time. *** ### Build276InformationReceiverSpec One information receiver (Loop 2000B / 2100B) - the party the answer goes back to. #### Example ```ts import type { Build276InformationReceiverSpec } from "@cosyte/x12"; const r: Build276InformationReceiverSpec = { name: { entityIdentifierCode: "41", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC" }, providers: [], }; ``` #### Properties ##### name > `readonly` **name**: [`Build276NameSpec`](#build276namespec) Loop 2100B information-receiver name. ##### providers > `readonly` **providers**: readonly [`Build276ProviderSpec`](#build276providerspec)[] Loop 2000C service providers (at least one required - a receiver with none is refused). *** ### Build276InformationSourceSpec One information source (Loop 2000A / 2100A) - the payer being asked. #### Example ```ts import type { Build276InformationSourceSpec } from "@cosyte/x12"; const src: Build276InformationSourceSpec = { name: { entityIdentifierCode: "PR", entityTypeQualifier: "2", lastNameOrOrganizationName: "MEDPAY INSURANCE" }, receivers: [], }; ``` #### Properties ##### name > `readonly` **name**: [`Build276NameSpec`](#build276namespec) Loop 2100A information-source name. ##### receivers > `readonly` **receivers**: readonly [`Build276InformationReceiverSpec`](#build276informationreceiverspec)[] Loop 2000B receivers (at least one required - a source with none is refused). *** ### Build276NameSpec An NM1 name loop, with the DMG demographics that follow it. Mirrors "./status-inquiry-types.js".X12StatusInquiryName. One type for every level, for the reason the read model gives: NM1-03 is "name last or organization name" and the same segment carries both. #### Example ```ts import type { Build276NameSpec } from "@cosyte/x12"; const m: Build276NameSpec = { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001", }; ``` #### Properties ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: `string` DMG-02 - date of birth (emitted with DMG-01 = `D8`). ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`PR` payer, `41` receiver, `1P` provider, `IL` insured, `QC` patient). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### genderCode? > `readonly` `optional` **genderCode?**: `string` DMG-03 - gender code. ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier. ##### lastNameOrOrganizationName? > `readonly` `optional` **lastNameOrOrganizationName?**: `string` NM1-03 - last name, or the organization name for a non-person. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build276ProcedureSpec The SVC-01 composite medical procedure identifier, supplied as its separated components. The builder joins them with the declared component separator, so a caller never hand-codes a delimiter. Mirrors "./status-inquiry-types.js".X12StatusInquiryProcedure. #### Example ```ts import type { Build276ProcedureSpec } from "@cosyte/x12"; const p: Build276ProcedureSpec = { qualifier: "HC", code: "99213", modifiers: ["25"] }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: `string` SVC-01-2 - the procedure code. ##### description? > `readonly` `optional` **description?**: `string` SVC-01-7 - procedure description. ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] SVC-01-3 through SVC-01-6 - procedure modifiers, in order. ##### qualifier > `readonly` **qualifier**: `string` SVC-01-1 - product or service id qualifier. *** ### Build276ProviderSpec One service provider (Loop 2000C / 2100C) - the provider whose claim is being asked about. This level is the one the 270's spine does not have. #### Example ```ts import type { Build276ProviderSpec } from "@cosyte/x12"; const p: Build276ProviderSpec = { name: { entityIdentifierCode: "1P", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC" }, subscribers: [], }; ``` #### Properties ##### name > `readonly` **name**: [`Build276NameSpec`](#build276namespec) Loop 2100C provider name. ##### subscribers > `readonly` **subscribers**: readonly [`Build276SubscriberSpec`](#build276subscriberspec)[] Loop 2000D subscribers (at least one required - a provider with none is refused). *** ### Build276ReferenceSpec A REF supplemental identifier. Mirrors "./status-inquiry-types.js".X12StatusInquiryReference. #### Example ```ts import type { Build276ReferenceSpec } from "@cosyte/x12"; const r: Build276ReferenceSpec = { qualifier: "1K", value: "PCN0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description. ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification. *** ### Build276ServiceLineSpec One service line the request asks about (Loop 2210). A line that identifies nothing - no procedure and no revenue code - is refused rather than emitted: an SVC that names no service is a question no payer can answer. #### Example ```ts import { X12Decimal, type Build276ServiceLineSpec } from "@cosyte/x12"; const l: Build276ServiceLineSpec = { procedure: { qualifier: "HC", code: "99213" }, lineChargeAmount: X12Decimal.fromString("150.00")!, }; ``` #### Properties ##### dates? > `readonly` `optional` **dates?**: readonly [`Build276DateSpec`](#build276datespec)[] DTP dates under this service line. ##### lineChargeAmount? > `readonly` `optional` **lineChargeAmount?**: [`X12Decimal`](#x12decimal) SVC-02 - line item charge amount. ##### procedure? > `readonly` `optional` **procedure?**: [`Build276ProcedureSpec`](#build276procedurespec) SVC-01 - the billed procedure, as separated components. ##### references? > `readonly` `optional` **references?**: readonly [`Build276ReferenceSpec`](#build276referencespec)[] REF identifiers under this service line. ##### revenueCode? > `readonly` `optional` **revenueCode?**: `string` SVC-04 - revenue code. ##### unitsOfService? > `readonly` `optional` **unitsOfService?**: [`X12Decimal`](#x12decimal) SVC-07 - units of service count. *** ### Build276Spec The complete spec for "./build-276.js".build276: the envelope, an optional BHT header, and the nested informationSources / receivers / providers / subscribers / (dependents) tree the builder walks depth-first to compute the HL spine. #### Example ```ts import { build276, type Build276Spec } from "@cosyte/x12"; const spec: Build276Spec = { envelope: { senderId: "ANYTOWNCLINIC", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ name: { entityIdentifierCode: "PR", entityTypeQualifier: "2", lastNameOrOrganizationName: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "PAYER01" }, receivers: [{ name: { entityIdentifierCode: "41", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC", idQualifier: "46", idCode: "RECVR01" }, providers: [{ name: { entityIdentifierCode: "1P", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, claims: [{ trace: { traceTypeCode: "1", referenceId: "STATUS0001" } }], }], }], }], }], }; const ix = build276(spec); ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build276EnvelopeSpec`](#build276envelopespec) Interchange, group and transaction identity. ##### header? > `readonly` `optional` **header?**: [`Build276HeaderSpec`](#build276headerspec) The BHT header. Every field defaults; the whole object may be omitted. ##### informationSources > `readonly` **informationSources**: readonly [`Build276InformationSourceSpec`](#build276informationsourcespec)[] Loop 2000A information sources (at least one required). *** ### Build276SubscriberSpec One subscriber (Loop 2000D / 2100D / 2200D). Mirrors "./status-inquiry-types.js".X12StatusInquirySubscriber. #### Example ```ts import type { Build276SubscriberSpec } from "@cosyte/x12"; const s: Build276SubscriberSpec = { name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE" }, claims: [{ trace: { traceTypeCode: "1", referenceId: "STATUS0001" } }], }; ``` #### Properties ##### claims? > `readonly` `optional` **claims?**: readonly [`Build276ClaimSpec`](#build276claimspec)[] Loop 2200D claims. At least one is required UNLESS this subscriber carries dependents, in which case the claim may sit on the dependent instead and the subscriber is the identifying level only. ##### dependents? > `readonly` `optional` **dependents?**: readonly [`Build276DependentSpec`](#build276dependentspec)[] Loop 2000E dependents (a non-empty list sets the subscriber HL-04 to `"1"`). ##### name > `readonly` **name**: [`Build276NameSpec`](#build276namespec) Loop 2100D subscriber name. Required: a level with no name is refused. *** ### Build276TraceSpec A reassociation trace (TRN). TRN-02 is the value the answering 277 echoes back verbatim. Mirrors "./status-inquiry-types.js".X12StatusInquiryTrace. #### Example ```ts import type { Build276TraceSpec } from "@cosyte/x12"; const t: Build276TraceSpec = { traceTypeCode: "1", referenceId: "STATUS20260601001" }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03 - originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - reference identification. ##### supplementalReferenceId? > `readonly` `optional` **supplementalReferenceId?**: `string` TRN-04 - supplemental reference identifier. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code. *** ### Build277ClaimSpec One claim status-tracking loop (Loop 2200). A claim opens on a TRN (claim-level reassociation trace) or - in a 277CA provider-level batch acknowledgment - on a standalone STC. Carries at most one trace, the claim-level STC statuses, supplemental REF / DTP, and any service-line statuses (Loop 2220). Mirrors "./types.js".X12ClaimStatus. #### Example ```ts import type { Build277ClaimSpec } from "@cosyte/x12"; const c: Build277ClaimSpec = { trace: { traceTypeCode: "2", referenceId: "CLAIM001" }, statuses: [{ statuses: [{ categoryCode: "A2", statusCode: "20" }] }], }; ``` #### Properties ##### dates? > `readonly` `optional` **dates?**: readonly [`Build277DateSpec`](#build277datespec)[] Claim-level DTP dates. ##### references? > `readonly` `optional` **references?**: readonly [`Build277ReferenceSpec`](#build277referencespec)[] Claim-level REF identifiers. ##### serviceLines? > `readonly` `optional` **serviceLines?**: readonly [`Build277ServiceLineSpec`](#build277servicelinespec)[] Loop 2220 service lines. ##### statuses? > `readonly` `optional` **statuses?**: readonly [`Build277StatusSpec`](#build277statusspec)[] Claim-level STC statuses. ##### trace? > `readonly` `optional` **trace?**: [`Build277TraceSpec`](#build277tracespec) Loop 2200 TRN reassociation trace (opens the claim; at most one). *** ### Build277DateSpec A DTP date / date-range on a claim or service-line status. Mirrors "./types.js".X12StatusDate. #### Example ```ts import type { Build277DateSpec } from "@cosyte/x12"; const d: Build277DateSpec = { qualifier: "472", formatQualifier: "RD8", value: "20260601-20260601" }; ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time format qualifier (`D8` / `RD8`). ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier. ##### value > `readonly` **value**: `string` DTP-03 - date/time value. *** ### Build277DependentSpec One dependent (Loop 2000E / 2100E) - a patient who is not the subscriber. Carries the optional member NM1 and the claims tracked for that dependent. #### Example ```ts import type { Build277DependentSpec } from "@cosyte/x12"; const d: Build277DependentSpec = { member: { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastName: "DOE", firstName: "JUNIOR" }, claims: [{ trace: { traceTypeCode: "2", referenceId: "CLAIM002" } }], }; ``` #### Properties ##### claims > `readonly` **claims**: readonly [`Build277ClaimSpec`](#build277claimspec)[] Loop 2200 claims tracked for the dependent (at least one required). ##### member? > `readonly` `optional` **member?**: [`Build277MemberSpec`](#build277memberspec) Loop 2100E dependent member (NM1). *** ### Build277EntitySpec A non-person entity (NM1) - the information-source payer (Loop 2100A), information receiver (2100B), or service provider (2100C). Mirrors "./types.js".X12StatusEntity. #### Example ```ts import type { Build277EntitySpec } from "@cosyte/x12"; const payer: Build277EntitySpec = { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`PR` payer, `41` receiver, `1P` provider). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier. ##### name > `readonly` **name**: `string` NM1-03 - organization name. *** ### Build277EnvelopeSpec Interchange + group + transaction identity for the built 277 / 277CA. Mirrors the "../remit/build-835-types.js".Build835EnvelopeSpec; the builder fixes GS-01 to `"HN"` and ST-01 to `"277"`. ST-03 / GS-08 are the version supplied by the builder entry point (`005010X212` for "./build-277.js".build277, `005010X214` for "./build-277.js".build277CA) so the caller never hand-codes them. #### Example ```ts import type { Build277EnvelopeSpec } from "@cosyte/x12"; const env: Build277EnvelopeSpec = { senderId: "MEDPAY", receiverId: "PROVIDER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build277InformationReceiverSpec One information receiver (Loop 2000B / 2100B). Carries the receiver entity (NM1) and its service providers. #### Example ```ts import type { Build277InformationReceiverSpec } from "@cosyte/x12"; const r: Build277InformationReceiverSpec = { entity: { entityIdentifierCode: "41", entityTypeQualifier: "2", name: "CLEARINGHOUSE", idQualifier: "46", idCode: "CH001" }, providers: [], }; ``` #### Properties ##### entity > `readonly` **entity**: [`Build277EntitySpec`](#build277entityspec) Loop 2100B information-receiver entity (NM1). ##### providers > `readonly` **providers**: readonly [`Build277ProviderSpec`](#build277providerspec)[] Loop 2000C service providers (at least one required - a receiver with none is refused). *** ### Build277InformationSourceSpec One information source (Loop 2000A / 2100A) - the payer answering the claim-status request. Carries the payer entity (NM1) and its receivers. #### Example ```ts import type { Build277InformationSourceSpec } from "@cosyte/x12"; const src: Build277InformationSourceSpec = { entity: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123" }, receivers: [], }; ``` #### Properties ##### entity > `readonly` **entity**: [`Build277EntitySpec`](#build277entityspec) Loop 2100A information-source payer entity (NM1). ##### receivers > `readonly` **receivers**: readonly [`Build277InformationReceiverSpec`](#build277informationreceiverspec)[] Loop 2000B receivers (at least one required - a source with none is refused). *** ### Build277MemberSpec A person (subscriber Loop 2100D / dependent Loop 2100E) decoded from an NM1. `idCode` is the member identifier (NM1-09) - synthetic-only in fixtures. Mirrors "./types.js".X12StatusMember. #### Example ```ts import type { Build277MemberSpec } from "@cosyte/x12"; const m: Build277MemberSpec = { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`QC` patient, `IL` insured). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code (the member id). ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`MI` member id). ##### lastName? > `readonly` `optional` **lastName?**: `string` NM1-03 - last name. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build277ProviderSpec One service provider (Loop 2000C / 2100C - HL level 19). Carries the provider entity (NM1) and its subscribers. #### Example ```ts import type { Build277ProviderSpec } from "@cosyte/x12"; const p: Build277ProviderSpec = { entity: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [], }; ``` #### Properties ##### entity > `readonly` **entity**: [`Build277EntitySpec`](#build277entityspec) Loop 2100C service-provider entity (NM1). ##### subscribers > `readonly` **subscribers**: readonly [`Build277SubscriberSpec`](#build277subscriberspec)[] Loop 2000D subscribers (at least one required - a provider with none is refused). *** ### Build277ReferenceSpec A REF supplemental identifier on a claim or service-line status. Mirrors "./types.js".X12StatusReference. #### Example ```ts import type { Build277ReferenceSpec } from "@cosyte/x12"; const r: Build277ReferenceSpec = { qualifier: "1K", value: "PCN0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description. ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification. *** ### Build277RfaiAmountSpec One AMT. #### Example ```ts import { X12Decimal, type Build277RfaiAmountSpec } from "@cosyte/x12"; const amount: Build277RfaiAmountSpec = { qualifier: "T3", amount: X12Decimal.fromString("150.00")! }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) AMT-02. ##### qualifier > `readonly` **qualifier**: `string` AMT-01. *** ### Build277RfaiDateSpec One DTP, the value written exactly as given. #### Example ```ts import type { Build277RfaiDateSpec } from "@cosyte/x12"; const service: Build277RfaiDateSpec = { qualifier: "472", formatQualifier: "D8", value: "20260501" }; ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02. ##### qualifier > `readonly` **qualifier**: `string` DTP-01. ##### value > `readonly` **value**: `string` DTP-03. *** ### Build277RfaiEntitySpec One NM1. Every element written as given. #### Example ```ts import type { Build277RfaiEntitySpec } from "@cosyte/x12"; const patient: Build277RfaiEntitySpec = { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastOrOrganizationName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01. ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02. ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04. ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08. ##### lastOrOrganizationName? > `readonly` `optional` **lastOrOrganizationName?**: `string` NM1-03. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05. ##### namePrefix? > `readonly` `optional` **namePrefix?**: `string` NM1-06. ##### nameSuffix? > `readonly` `optional` **nameSuffix?**: `string` NM1-07. *** ### Build277RfaiEnvelopeSpec The envelope. `interchangeControlVersion` is ISA-12: the builder writes the package's default, `00501`, unless given one, because no carried source states which ISA-12 a 006020 interchange carries. #### Example ```ts import type { Build277RfaiEnvelopeSpec } from "@cosyte/x12"; const envelope: Build277RfaiEnvelopeSpec = { senderId: "PAYER", receiverId: "CLINIC", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03, application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02, application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02, group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04, group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05, group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02, interchange control number (zero-padded to 9 on emit). ##### interchangeControlVersion? > `readonly` `optional` **interchangeControlVersion?**: `string` ISA-12, interchange control version number. Default `"00501"`. ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09, interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10, interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08, interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07, interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator. Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06, interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05, interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02, transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15, usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build277RfaiHeaderSpec The BHT, each element written as given; an omitted one is written empty. #### Example ```ts import type { Build277RfaiHeaderSpec } from "@cosyte/x12"; const header: Build277RfaiHeaderSpec = { hierarchicalStructureCode: "0010", transactionSetPurposeCode: "08", referenceId: "RFAI-0001", date: "20260601", time: "1200", }; ``` #### Properties ##### date? > `readonly` `optional` **date?**: `string` BHT-04. ##### hierarchicalStructureCode > `readonly` **hierarchicalStructureCode**: `string` BHT-01. ##### referenceId? > `readonly` `optional` **referenceId?**: `string` BHT-03. ##### time? > `readonly` `optional` **time?**: `string` BHT-05. ##### transactionSetPurposeCode > `readonly` **transactionSetPurposeCode**: `string` BHT-02. ##### transactionTypeCode? > `readonly` `optional` **transactionTypeCode?**: `string` BHT-06. *** ### Build277RfaiLevelSpec One hierarchical level. HL-03 and HL-04 are written as given; HL-01 and HL-02 are computed from where the level sits in `levels`. #### Example ```ts import type { Build277RfaiLevelSpec } from "@cosyte/x12"; const level: Build277RfaiLevelSpec = { levelCode: "20", childCode: "1", children: [] }; ``` #### Properties ##### childCode? > `readonly` `optional` **childCode?**: `string` HL-04, the hierarchical child code. Omitted from the HL when absent. ##### children? > `readonly` `optional` **children?**: readonly [`Build277RfaiLevelSpec`](#build277rfailevelspec)[] The levels subordinate to this one, written after it. ##### entities? > `readonly` `optional` **entities?**: readonly [`Build277RfaiEntitySpec`](#build277rfaientityspec)[] The NM1 of each name loop under this level, written before its requests. ##### levelCode > `readonly` **levelCode**: `string` HL-03, the hierarchical level code. ##### requests? > `readonly` `optional` **requests?**: readonly [`Build277RfaiRequestSpec`](#build277rfairequestspec)[] The claim-level requests under this level. *** ### Build277RfaiQuantitySpec One QTY. #### Example ```ts import { X12Decimal, type Build277RfaiQuantitySpec } from "@cosyte/x12"; const quantity: Build277RfaiQuantitySpec = { qualifier: "90", quantity: X12Decimal.fromString("1")! }; ``` #### Properties ##### qualifier > `readonly` **qualifier**: `string` QTY-01. ##### quantity > `readonly` **quantity**: [`X12Decimal`](#x12decimal) QTY-02. *** ### Build277RfaiReferenceSpec One REF. #### Example ```ts import type { Build277RfaiReferenceSpec } from "@cosyte/x12"; const pcn: Build277RfaiReferenceSpec = { qualifier: "1K", value: "PCN0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03. ##### qualifier > `readonly` **qualifier**: `string` REF-01. ##### value > `readonly` **value**: `string` REF-02. *** ### Build277RfaiRequestSpec One claim-level request: the TRN that opens it and what is sent under it. #### Example ```ts import type { Build277RfaiRequestSpec } from "@cosyte/x12"; const request: Build277RfaiRequestSpec = { trace: { traceTypeCode: "1", referenceId: "TRACE-0001" }, references: [{ qualifier: "1K", value: "PCN0001" }], statuses: [{ codes: [{ categoryCode: "R4", statusCode: "18842-5", codeListQualifier: "LOI" }] }], }; ``` #### Properties ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build277RfaiAmountSpec`](#build277rfaiamountspec)[] Each AMT. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build277RfaiDateSpec`](#build277rfaidatespec)[] Each DTP. ##### quantities? > `readonly` `optional` **quantities?**: readonly [`Build277RfaiQuantitySpec`](#build277rfaiquantityspec)[] Each QTY. ##### references? > `readonly` `optional` **references?**: readonly [`Build277RfaiReferenceSpec`](#build277rfaireferencespec)[] Each REF. ##### serviceLines? > `readonly` `optional` **serviceLines?**: readonly [`Build277RfaiServiceLineSpec`](#build277rfaiservicelinespec)[] Each service line, written after everything above. ##### statuses? > `readonly` `optional` **statuses?**: readonly [`Build277RfaiStatusSpec`](#build277rfaistatusspec)[] Each STC, written after the TRN. ##### trace > `readonly` **trace**: [`Build277RfaiTraceSpec`](#build277rfaitracespec) The TRN. Required: the base 006020 277 opens this loop with one. *** ### Build277RfaiServiceLineSpec One service line: its SVC and the STC, REF and DTP segments sent under it. #### Example ```ts import type { Build277RfaiServiceLineSpec } from "@cosyte/x12"; const line: Build277RfaiServiceLineSpec = { service: { serviceIdQualifier: "HC", procedureCode: "99213" }, statuses: [{ codes: [{ categoryCode: "R4", statusCode: "11506-3", codeListQualifier: "LOI" }] }], }; ``` #### Properties ##### dates? > `readonly` `optional` **dates?**: readonly [`Build277RfaiDateSpec`](#build277rfaidatespec)[] Each DTP under the line. ##### references? > `readonly` `optional` **references?**: readonly [`Build277RfaiReferenceSpec`](#build277rfaireferencespec)[] Each REF under the line. ##### service > `readonly` **service**: [`Build277RfaiServiceSpec`](#build277rfaiservicespec) The SVC. Required: the base 006020 277 opens this loop with one. ##### statuses? > `readonly` `optional` **statuses?**: readonly [`Build277RfaiStatusSpec`](#build277rfaistatusspec)[] Each STC under the line. *** ### Build277RfaiServiceSpec The SVC elements this builder writes. SVC-06 is not written. #### Example ```ts import type { Build277RfaiServiceSpec } from "@cosyte/x12"; const svc: Build277RfaiServiceSpec = { serviceIdQualifier: "HC", procedureCode: "99213", modifiers: ["25"] }; ``` #### Properties ##### lineChargeAmount? > `readonly` `optional` **lineChargeAmount?**: [`X12Decimal`](#x12decimal) SVC-02. ##### linePaymentAmount? > `readonly` `optional` **linePaymentAmount?**: [`X12Decimal`](#x12decimal) SVC-03. ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] SVC-01-3 to SVC-01-6. ##### procedureCode > `readonly` **procedureCode**: `string` SVC-01-2. ##### quantity? > `readonly` `optional` **quantity?**: [`X12Decimal`](#x12decimal) SVC-05. ##### revenueCode? > `readonly` `optional` **revenueCode?**: `string` SVC-04. ##### serviceIdQualifier > `readonly` **serviceIdQualifier**: `string` SVC-01-1. ##### unitsOfService? > `readonly` `optional` **unitsOfService?**: [`X12Decimal`](#x12decimal) SVC-07. *** ### Build277RfaiSpec The whole build request. #### Example ```ts import type { Build277RfaiSpec } from "@cosyte/x12"; const spec: Build277RfaiSpec = { envelope: { senderId: "PAYER", receiverId: "CLINIC", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, header: { hierarchicalStructureCode: "0010", transactionSetPurposeCode: "08" }, levels: [{ levelCode: "20", entities: [{ entityIdentifierCode: "PR", entityTypeQualifier: "2", lastOrOrganizationName: "PAYER ONE" }], requests: [{ trace: { traceTypeCode: "1", referenceId: "TRACE-0001" }, statuses: [{ codes: [{ categoryCode: "R4", statusCode: "18842-5", codeListQualifier: "LOI" }] }], }], }], }; ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build277RfaiEnvelopeSpec`](#build277rfaienvelopespec) The interchange, group and transaction set envelope. ##### header > `readonly` **header**: [`Build277RfaiHeaderSpec`](#build277rfaiheaderspec) The BHT, every element written as given. ##### levels > `readonly` **levels**: readonly [`Build277RfaiLevelSpec`](#build277rfailevelspec)[] The top-level hierarchical levels, each with its subordinate levels nested. At least one. *** ### Build277RfaiStatusCodeSpec One C043 composite, all four components written verbatim. #### Example ```ts import type { Build277RfaiStatusCodeSpec } from "@cosyte/x12"; const code: Build277RfaiStatusCodeSpec = { categoryCode: "R4", statusCode: "18842-5", codeListQualifier: "LOI", }; ``` #### Properties ##### categoryCode > `readonly` **categoryCode**: `string` C043-01. Refused when empty. ##### codeListQualifier? > `readonly` `optional` **codeListQualifier?**: `string` C043-04, naming the code source of C043-02. ##### entityCode? > `readonly` `optional` **entityCode?**: `string` C043-03. ##### statusCode > `readonly` **statusCode**: `string` C043-02. Refused when empty. *** ### Build277RfaiStatusSpec One STC. `codes` are written to STC-01, STC-10 and STC-11 in that order, so it holds one to three composites. #### Example ```ts import type { Build277RfaiStatusSpec } from "@cosyte/x12"; const status: Build277RfaiStatusSpec = { codes: [{ categoryCode: "R4", statusCode: "18842-5", codeListQualifier: "LOI" }], statusEffectiveDate: "20260601", }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` STC-03. ##### checkIssueDate? > `readonly` `optional` **checkIssueDate?**: `string` STC-08. ##### checkNumber? > `readonly` `optional` **checkNumber?**: `string` STC-09. ##### codes > `readonly` **codes**: readonly [`Build277RfaiStatusCodeSpec`](#build277rfaistatuscodespec)[] The C043 composites, STC-01 first. At least one. ##### message? > `readonly` `optional` **message?**: `string` STC-12. ##### paymentAmount? > `readonly` `optional` **paymentAmount?**: [`X12Decimal`](#x12decimal) STC-05. ##### paymentDate? > `readonly` `optional` **paymentDate?**: `string` STC-06. ##### paymentMethodCode? > `readonly` `optional` **paymentMethodCode?**: `string` STC-07. ##### statusEffectiveDate? > `readonly` `optional` **statusEffectiveDate?**: `string` STC-02. ##### totalChargeAmount? > `readonly` `optional` **totalChargeAmount?**: [`X12Decimal`](#x12decimal) STC-04. *** ### Build277RfaiTraceSpec One TRN. #### Example ```ts import type { Build277RfaiTraceSpec } from "@cosyte/x12"; const trace: Build277RfaiTraceSpec = { traceTypeCode: "1", referenceId: "TRACE-0001" }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03. ##### referenceId > `readonly` **referenceId**: `string` TRN-02. ##### supplementalReferenceId? > `readonly` `optional` **supplementalReferenceId?**: `string` TRN-04. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01. *** ### Build277ServiceLineSpec One service-line status (Loop 2220). Triggered by an SVC; carries the procedure / revenue identification, line amounts, and its own STC statuses + REF / DTP. Mirrors "./types.js".X12ServiceLineStatus. #### Example ```ts import type { Build277ServiceLineSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const l: Build277ServiceLineSpec = { serviceIdQualifier: "HC", procedureCode: "99213", modifiers: ["25"], lineChargeAmount: X12Decimal.fromString("150.00")!, // SVC-07 is required in 005010X212, so `build277` refuses a line without it. unitsOfService: X12Decimal.fromString("1")!, statuses: [{ statuses: [{ categoryCode: "F2", statusCode: "65" }] }], }; ``` #### Properties ##### dates? > `readonly` `optional` **dates?**: readonly [`Build277DateSpec`](#build277datespec)[] Loop 2220 DTP dates. ##### lineChargeAmount? > `readonly` `optional` **lineChargeAmount?**: [`X12Decimal`](#x12decimal) SVC-02 - line charge amount. ##### linePaymentAmount? > `readonly` `optional` **linePaymentAmount?**: [`X12Decimal`](#x12decimal) SVC-03 - line payment amount. ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] SVC-01 components 3..6 - procedure modifiers. ##### procedureCode? > `readonly` `optional` **procedureCode?**: `string` SVC-01 component 2 - procedure code. ##### references? > `readonly` `optional` **references?**: readonly [`Build277ReferenceSpec`](#build277referencespec)[] Loop 2220 REF identifiers. ##### revenueCode? > `readonly` `optional` **revenueCode?**: `string` SVC-04 - revenue code. ##### serviceIdQualifier? > `readonly` `optional` **serviceIdQualifier?**: `string` SVC-01 component 1 - product/service id qualifier (`HC`, `NU`, …). ##### statuses? > `readonly` `optional` **statuses?**: readonly [`Build277StatusSpec`](#build277statusspec)[] Loop 2220 STC statuses. ##### unitsOfService? > `readonly` `optional` **unitsOfService?**: [`X12Decimal`](#x12decimal) SVC-07 - units of service count (data element 380). **Usage differs by TR3 and so does the builder.** In `005010X212` the element is REQUIRED, so "./build-277.js".build277 REFUSES a service line that omits it rather than emit a document short a required element. In `005010X214` it is SITUATIONAL, so "./build-277.js".build277CA emits it when supplied and omits it otherwise. Nothing is ever defaulted: a count the caller did not supply is a count nobody sent, and inventing one would put a quantity on the wire that no submitter stands behind. *** ### Build277Spec The complete spec for "./build-277.js".build277 / "./build-277.js".build277CA: the envelope plus the nested informationSources → receivers → providers → subscribers → (dependents) tree the builder walks depth-first to compute the HL spine. #### Example ```ts import { build277, X12Decimal, type Build277Spec } from "@cosyte/x12"; const spec: Build277Spec = { envelope: { senderId: "MEDPAY", receiverId: "PROVIDER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ entity: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123" }, receivers: [{ entity: { entityIdentifierCode: "41", entityTypeQualifier: "2", name: "CLEARINGHOUSE", idQualifier: "46", idCode: "CH001" }, providers: [{ entity: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ member: { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE" }, claims: [{ trace: { traceTypeCode: "2", referenceId: "CLAIM001" }, statuses: [{ statuses: [{ categoryCode: "A2", statusCode: "20" }], totalChargeAmount: X12Decimal.fromString("150.00")! }], }], }], }], }], }], }; const ix = build277(spec); ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build277EnvelopeSpec`](#build277envelopespec) Interchange + group + transaction identity. ##### informationSources > `readonly` **informationSources**: readonly [`Build277InformationSourceSpec`](#build277informationsourcespec)[] Loop 2000A information sources (at least one required). *** ### Build277StatusCodeSpec One Health Care Claim Status composite (C043 - STC-01 / STC-10 / STC-11). Pairs a CSCC (category, source 507) with a CSC (status, source 508) and the responsible entity. Only the verbatim codes are supplied - the read side resolves the descriptions. Mirrors "./types.js".X12StatusCode minus the derived descriptions. #### Example ```ts import type { Build277StatusCodeSpec } from "@cosyte/x12"; const c: Build277StatusCodeSpec = { categoryCode: "A2", statusCode: "20", entityCode: "PR" }; ``` #### Properties ##### categoryCode > `readonly` **categoryCode**: `string` C043-01 - Claim Status Category Code (CSCC). Required on the first composite. ##### entityCode? > `readonly` `optional` **entityCode?**: `string` C043-03 - responsible entity code. ##### statusCode? > `readonly` `optional` **statusCode?**: `string` C043-02 - Claim Status Code (CSC). *** ### Build277StatusSpec One decoded STC segment - the headline status fields plus the up-to-three [Build277StatusCodeSpec](#build277statuscodespec) composites (STC-01, STC-10, STC-11). The first composite (STC-01) is required and must carry a category code. Mirrors "./types.js".X12StatusInfo. #### Example ```ts import type { Build277StatusSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const s: Build277StatusSpec = { statuses: [{ categoryCode: "A2", statusCode: "20" }], statusEffectiveDate: "20260627", totalChargeAmount: X12Decimal.fromString("150.00")!, }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` STC-03 - action code. ##### adjudicationDate? > `readonly` `optional` **adjudicationDate?**: `string` STC-06 - adjudication / payment date. ##### message? > `readonly` `optional` **message?**: `string` STC-12 - free-form message. ##### paymentAmount? > `readonly` `optional` **paymentAmount?**: [`X12Decimal`](#x12decimal) STC-05 - claim payment amount. ##### statusEffectiveDate? > `readonly` `optional` **statusEffectiveDate?**: `string` STC-02 - status information effective date. ##### statuses > `readonly` **statuses**: readonly [`Build277StatusCodeSpec`](#build277statuscodespec)[] STC-01 / STC-10 / STC-11 - 1..3 status composites (first is required). ##### totalChargeAmount? > `readonly` `optional` **totalChargeAmount?**: [`X12Decimal`](#x12decimal) STC-04 - total claim charge amount. *** ### Build277SubscriberSpec One subscriber (Loop 2000D / 2100D). Carries the optional member NM1, the subscriber-level claims, and any non-subscriber dependents. #### Example ```ts import type { Build277SubscriberSpec } from "@cosyte/x12"; const s: Build277SubscriberSpec = { member: { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE" }, claims: [{ trace: { traceTypeCode: "2", referenceId: "CLAIM001" } }], }; ``` #### Properties ##### claims? > `readonly` `optional` **claims?**: readonly [`Build277ClaimSpec`](#build277claimspec)[] Loop 2200 subscriber-level claims. ##### dependents? > `readonly` `optional` **dependents?**: readonly [`Build277DependentSpec`](#build277dependentspec)[] Loop 2000E dependents (a non-empty list sets the subscriber HL-04 to `"1"`). ##### member? > `readonly` `optional` **member?**: [`Build277MemberSpec`](#build277memberspec) Loop 2100D subscriber member (NM1). *** ### Build277TraceSpec A reassociation trace (TRN). For a 277 claim status, **`referenceId` (TRN-02) echoes the requesting 276's trace number verbatim**. A claim opens on its trace, so a claim carries at most one trace. Mirrors "./types.js".X12StatusTrace. #### Example ```ts import type { Build277TraceSpec } from "@cosyte/x12"; const t: Build277TraceSpec = { traceTypeCode: "2", referenceId: "CLAIM20260627001" }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03 - originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - reference identification (echoed verbatim from the 276). ##### supplementalReferenceId? > `readonly` `optional` **supplementalReferenceId?**: `string` TRN-04 - supplemental reference identifier. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code. *** ### Build278DateSpec A DTP date / date-range on a review. Mirrors "./types.js".X12AuthDate. #### Example ```ts import type { Build278DateSpec } from "@cosyte/x12"; const d: Build278DateSpec = { qualifier: "472", formatQualifier: "RD8", value: "20260601-20260605" }; ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time format qualifier (`D8` / `RD8`). ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier (`435` admission, `472` service). ##### value > `readonly` **value**: `string` DTP-03 - date/time value. *** ### Build278DecisionSpec The HCR Health Care Services Review decision (RESPONSE ONLY). **`actionCode` (HCR-01) is the certification outcome and is emitted VERBATIM** - the builder never infers or upgrades it, so the response round-trips the exact decision the caller supplied. A request spec carrying a decision is REFUSED. Mirrors "./types.js".X12ReviewDecision. #### Example ```ts import type { Build278DecisionSpec } from "@cosyte/x12"; const d: Build278DecisionSpec = { actionCode: "A1", reviewIdentificationNumber: "AUTH123456" }; ``` #### Properties ##### actionCode > `readonly` **actionCode**: `string` HCR-01 - action code (`A1` certified, `A3` not certified, `A4` pended, `A6` modified). ##### reasonCode? > `readonly` `optional` **reasonCode?**: `string` HCR-03 - reason code. ##### reviewIdentificationNumber? > `readonly` `optional` **reviewIdentificationNumber?**: `string` HCR-02 - review / authorization identification number. ##### secondSurgicalOpinionCode? > `readonly` `optional` **secondSurgicalOpinionCode?**: `string` HCR-04 - second surgical opinion code. *** ### Build278DependentSpec One dependent (Loop 2000D / 2010D - HL level 23) - a patient who is not the subscriber. Carries the optional member NM1 + DMG and the reviews tracked for that dependent (at least one required). #### Example ```ts import type { Build278DependentSpec } from "@cosyte/x12"; const d: Build278DependentSpec = { member: { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastName: "DOE", firstName: "JUNIOR" }, reviews: [{ requestCategoryCode: "HS", certificationTypeCode: "I" }], }; ``` #### Properties ##### member? > `readonly` `optional` **member?**: [`Build278MemberSpec`](#build278memberspec) Loop 2010D dependent member (NM1 + DMG). ##### reviews > `readonly` **reviews**: readonly [`Build278ReviewSpec`](#build278reviewspec)[] Loop 2000E/2000F reviews tracked for the dependent (at least one required). *** ### Build278DiagnosisSpec One diagnosis composite emitted into the review's HI segment. Only the verbatim `qualifier` (HI-0x-01) + `code` (HI-0x-02) are supplied - the read side resolves the `codeSystem`. Mirrors "./types.js".X12AuthDiagnosis minus the derived `codeSystem`. #### Example ```ts import type { Build278DiagnosisSpec } from "@cosyte/x12"; const dx: Build278DiagnosisSpec = { qualifier: "ABK", code: "E1165" }; ``` #### Properties ##### code > `readonly` **code**: `string` HI-0x-02 - diagnosis code. ##### qualifier > `readonly` **qualifier**: `string` HI-0x-01 - diagnosis code-source qualifier (`ABK` ICD-10-CM principal). *** ### Build278EntitySpec A non-person entity (NM1) - the UMO (Loop 2010A), the requester (Loop 2010B), or a provider attached to a review. Mirrors "./types.js".X12AuthEntity. #### Example ```ts import type { Build278EntitySpec } from "@cosyte/x12"; const umo: Build278EntitySpec = { entityIdentifierCode: "X3", entityTypeQualifier: "2", name: "UTILIZATION REVIEW CO", idQualifier: "PI", idCode: "UMO001", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`X3` UMO, `1P` requester, `71` attending). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier. ##### name > `readonly` **name**: `string` NM1-03 - name (organization or last name). *** ### Build278EnvelopeSpec Interchange + group + transaction identity for the built 278. The builder fixes GS-01 to `"HI"` and ST-01 to `"278"`; ST-03 / GS-08 is `005010X217`, the one guide for both directions, written by "./build-278.js".build278Request and "./build-278.js".build278Response alike, so the caller never hand-codes them. #### Example ```ts import type { Build278EnvelopeSpec } from "@cosyte/x12"; const env: Build278EnvelopeSpec = { senderId: "SUBMITTER", receiverId: "UMOPAYER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build278HeaderSpec The BHT beginning-of-hierarchical-transaction header. Mirrors "./types.js".X12AuthHeader; `structurePurposeCode` (BHT-01, `"0078"` for a 278) is required. #### Example ```ts import type { Build278HeaderSpec } from "@cosyte/x12"; const h: Build278HeaderSpec = { structurePurposeCode: "0078", purposeCode: "13", referenceId: "AUTHREQ-202606", date: "20260601", time: "1200", }; ``` #### Properties ##### date? > `readonly` `optional` **date?**: `string` BHT-04 - transaction set creation date (CCYYMMDD). ##### purposeCode? > `readonly` `optional` **purposeCode?**: `string` BHT-02 - transaction set purpose code (`13` request / `11` response). ##### referenceId? > `readonly` `optional` **referenceId?**: `string` BHT-03 - submitter transaction reference (re-association key). ##### structurePurposeCode > `readonly` **structurePurposeCode**: `string` BHT-01 - hierarchical structure code (`0078` services review). ##### time? > `readonly` `optional` **time?**: `string` BHT-05 - transaction set creation time (HHMM). ##### transactionTypeCode? > `readonly` `optional` **transactionTypeCode?**: `string` BHT-06 - transaction type code. *** ### Build278MemberSpec A person (subscriber Loop 2010C / dependent Loop 2010D) - NM1 plus the optional DMG demographics. `idCode` (NM1-09) is the member identifier - synthetic-only in fixtures. Mirrors "./types.js".X12AuthMember. #### Example ```ts import type { Build278MemberSpec } from "@cosyte/x12"; const m: Build278MemberSpec = { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001", dateOfBirth: "19850515", genderCode: "F", }; ``` #### Properties ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: `string` DMG-02 - date of birth (CCYYMMDD). A DMG is emitted only when this or `genderCode` is set. ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`IL` insured, `QC` patient). ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### genderCode? > `readonly` `optional` **genderCode?**: `string` DMG-03 - gender code (`M` / `F` / `U`). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code (the member id). ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`MI` member id). ##### lastName? > `readonly` `optional` **lastName?**: `string` NM1-03 - last name. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build278ReferenceSpec A REF supplemental identifier on a review. Mirrors "./types.js".X12AuthReference. #### Example ```ts import type { Build278ReferenceSpec } from "@cosyte/x12"; const r: Build278ReferenceSpec = { qualifier: "BB", value: "PRIORAUTH-1" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description. ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification. *** ### Build278ReviewSpec One services-review item - a patient-event (`EV`, Loop 2000E) or service (`SS`, Loop 2000F) HL. Carries the UM review information, the optional HCR decision (response only), echoed TRN traces, HI diagnoses, attached provider NM1s, and the supplemental REF / DTP / MSG. Nested `reviews` become child HLs (an `SS` service under an `EV` event) parented to this review. Mirrors "./types.js".X12ServiceReview. #### Example ```ts import type { Build278ReviewSpec } from "@cosyte/x12"; const r: Build278ReviewSpec = { levelCode: "EV", requestCategoryCode: "HS", certificationTypeCode: "I", serviceTypeCode: "1", traces: [{ traceTypeCode: "1", referenceId: "AUTHREQ-202606-0001" }], diagnoses: [{ qualifier: "ABK", code: "E1165" }], dates: [{ qualifier: "472", formatQualifier: "RD8", value: "20260601-20260605" }], }; ``` #### Properties ##### certificationTypeCode? > `readonly` `optional` **certificationTypeCode?**: `string` UM-02 - certification type code (`I` initial / `R` renewal). ##### dates? > `readonly` `optional` **dates?**: readonly [`Build278DateSpec`](#build278datespec)[] Supplemental DTP dates. ##### decision? > `readonly` `optional` **decision?**: [`Build278DecisionSpec`](#build278decisionspec) HCR decision - RESPONSE ONLY (a request review carrying one is refused). ##### diagnoses? > `readonly` `optional` **diagnoses?**: readonly [`Build278DiagnosisSpec`](#build278diagnosisspec)[] HI diagnosis composites (emitted as one HI segment). ##### levelCode? > `readonly` `optional` **levelCode?**: `"EV"` \| `"SS"` HL-03 - review level code (`EV` patient event / `SS` service). Default `"EV"`. The only caller-supplied HL-03 in the library; every other level on every builder's spine is a module constant selected by tree position. A value outside `EV` / `SS` is **refused** (`X12_278_BUILD_INVALID_SPEC`) rather than emitted: it produces a well-formed document whose review loop no reader opens, so the review and its HCR-01 certification decision would fail to decode. An absent value still defaults to `EV`. ##### levelOfServiceCode? > `readonly` `optional` **levelOfServiceCode?**: `string` UM-06 - level of service code. ##### messages? > `readonly` `optional` **messages?**: readonly `string`[] MSG free-form messages. ##### providers? > `readonly` `optional` **providers?**: readonly [`Build278EntitySpec`](#build278entityspec)[] Attached provider NM1s (rendering / attending / operating). ##### references? > `readonly` `optional` **references?**: readonly [`Build278ReferenceSpec`](#build278referencespec)[] Supplemental REF identifiers. ##### requestCategoryCode > `readonly` **requestCategoryCode**: `string` UM-01 - request category code (required; a review with none is refused). ##### reviews? > `readonly` `optional` **reviews?**: readonly [`Build278ReviewSpec`](#build278reviewspec)[] Nested service (`SS`) reviews parented to this review. ##### serviceTypeCode? > `readonly` `optional` **serviceTypeCode?**: `string` UM-03 - service type code. ##### traces? > `readonly` `optional` **traces?**: readonly [`Build278TraceSpec`](#build278tracespec)[] Loop 2000E/2000F TRN traces. *** ### Build278Spec The complete spec for "./build-278.js".build278Request / "./build-278.js".build278Response: the envelope + BHT header plus the UMO → requester → subscriber → (dependent) → reviews tree the builder walks depth-first to compute the HL spine. #### Example ```ts import { build278Request, type Build278Spec } from "@cosyte/x12"; const spec: Build278Spec = { envelope: { senderId: "SUBMITTER", receiverId: "UMOPAYER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, header: { structurePurposeCode: "0078", purposeCode: "13", referenceId: "AUTHREQ-202606" }, utilizationManagementOrganization: { entityIdentifierCode: "X3", entityTypeQualifier: "2", name: "UTILIZATION REVIEW CO", idQualifier: "PI", idCode: "UMO001" }, requester: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "RENDERING CLINIC", idQualifier: "XX", idCode: "1234567893" }, subscriber: { member: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, reviews: [{ requestCategoryCode: "HS", certificationTypeCode: "I", serviceTypeCode: "1", traces: [{ traceTypeCode: "1", referenceId: "AUTHREQ-202606-0001" }], diagnoses: [{ qualifier: "ABK", code: "E1165" }], }], }, }; const ix = build278Request(spec); ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build278EnvelopeSpec`](#build278envelopespec) Interchange + group + transaction identity. ##### header > `readonly` **header**: [`Build278HeaderSpec`](#build278headerspec) BHT header. ##### requester > `readonly` **requester**: [`Build278EntitySpec`](#build278entityspec) Loop 2010B requester (HL level 21). ##### subscriber > `readonly` **subscriber**: [`Build278SubscriberSpec`](#build278subscriberspec) Loop 2000C subscriber (HL level 22). ##### utilizationManagementOrganization > `readonly` **utilizationManagementOrganization**: [`Build278EntitySpec`](#build278entityspec) Loop 2010A utilization management organization (HL level 20). *** ### Build278SubscriberSpec One subscriber (Loop 2000C / 2010C - HL level 22). Carries the optional member NM1 + DMG, the subscriber-level reviews, and an optional dependent. A subscriber with neither a review nor a dependent is REFUSED. #### Example ```ts import type { Build278SubscriberSpec } from "@cosyte/x12"; const s: Build278SubscriberSpec = { member: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE" }, reviews: [{ requestCategoryCode: "HS", certificationTypeCode: "I", serviceTypeCode: "1" }], }; ``` #### Properties ##### dependent? > `readonly` `optional` **dependent?**: [`Build278DependentSpec`](#build278dependentspec) Loop 2000D dependent (sets the subscriber HL-04 to `"1"`). ##### member? > `readonly` `optional` **member?**: [`Build278MemberSpec`](#build278memberspec) Loop 2010C subscriber member (NM1 + DMG). ##### reviews? > `readonly` `optional` **reviews?**: readonly [`Build278ReviewSpec`](#build278reviewspec)[] Loop 2000E/2000F subscriber-level reviews. *** ### Build278TraceSpec A reassociation trace (TRN) on a review. A 278 request carries a trace the response echoes VERBATIM so the requester can re-associate the certification outcome. Mirrors "./types.js".X12AuthTrace. #### Example ```ts import type { Build278TraceSpec } from "@cosyte/x12"; const t: Build278TraceSpec = { traceTypeCode: "1", referenceId: "AUTHREQ-202606-0001" }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03 - originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - reference identification (echoed verbatim between request/response). ##### supplementalReferenceId? > `readonly` `optional` **supplementalReferenceId?**: `string` TRN-04 - supplemental reference identifier. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code. *** ### Build820AddressSpec N3 + N4 address block on a party. Mirrors "./types.js".X12PremiumAddress. #### Example ```ts import type { Build820AddressSpec } from "@cosyte/x12"; const a: Build820AddressSpec = { lines: ["500 CORPORATE BLVD"], city: "COLUMBUS", state: "OH", postalCode: "43004", }; ``` #### Properties ##### city? > `readonly` `optional` **city?**: `string` N4-01 - city. ##### countryCode? > `readonly` `optional` **countryCode?**: `string` N4-04 - country code. ##### lines > `readonly` **lines**: readonly `string`[] N3 address lines (1-2). ##### postalCode? > `readonly` `optional` **postalCode?**: `string` N4-03 - postal code. ##### state? > `readonly` `optional` **state?**: `string` N4-02 - state / province. *** ### Build820AdjustmentSpec ADX adjustment to a premium remittance. Mirrors "./types.js".X12PremiumAdjustment. #### Example ```ts import type { Build820AdjustmentSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const a: Build820AdjustmentSpec = { amount: X12Decimal.fromString("-25.00")!, reasonCode: "53", }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) ADX-01 - signed monetary adjustment. ##### reasonCode > `readonly` **reasonCode**: `string` ADX-02 - adjustment reason code (`52` credit memo, `53` debit memo, …). ##### referenceId? > `readonly` `optional` **referenceId?**: `string` ADX-04 - reference id (situational). ##### referenceQualifier? > `readonly` `optional` **referenceQualifier?**: `string` ADX-03 - reference qualifier (situational). *** ### Build820DateSpec DTM date attached to a remittance loop. Mirrors "./types.js".X12PremiumDate. The 820 carries dates only inside a remittance loop - a header-level DTM is not part of the typed surface. #### Example ```ts import type { Build820DateSpec } from "@cosyte/x12"; const d: Build820DateSpec = { qualifier: "582", value: "20260601" }; ``` #### Properties ##### qualifier > `readonly` **qualifier**: `string` DTM-01 - date/time qualifier. ##### value > `readonly` **value**: `string` DTM-02 - verbatim CCYYMMDD date. *** ### Build820EntitySpec ENT entity - opens an organization-summary remittance loop (Loop 2000A). Mirrors "./types.js".X12PremiumEntity. All elements optional; the ENT's role is to open the loop. #### Example ```ts import type { Build820EntitySpec } from "@cosyte/x12"; const e: Build820EntitySpec = { assignedNumber: "1", entityIdentifierCode: "2J", idQualifier: "94", idCode: "GRP-0001", }; ``` #### Properties ##### assignedNumber? > `readonly` `optional` **assignedNumber?**: `string` ENT-01 - assigned number. ##### entityIdentifierCode? > `readonly` `optional` **entityIdentifierCode?**: `string` ENT-02 - entity identifier code. ##### idCode? > `readonly` `optional` **idCode?**: `string` ENT-04 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` ENT-03 - identification code qualifier. *** ### Build820EnvelopeSpec Interchange + group + transaction identity for the built 820. The builder fixes GS-01 to `"RA"` (Payment Order / Remittance Advice) and the version/release to `"005010X218"` (the 820 functional group + TR3) so the caller never hand-codes them. #### Example ```ts import type { Build820EnvelopeSpec } from "@cosyte/x12"; const env: Build820EnvelopeSpec = { senderId: "EMPLOYERCO", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build820OpenItemSpec RMR open item - the premium line unit: a policy / invoice reference plus the amount paid and (optionally) the amount due. Mirrors "./types.js".X12PremiumOpenItem. At least one of `qualifier` / `referenceId` must be non-empty (an RMR with no identity is dropped on the read side, so the builder refuses it). #### Example ```ts import type { Build820OpenItemSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const o: Build820OpenItemSpec = { qualifier: "AZ", referenceId: "POL-0001", amountPaid: X12Decimal.fromString("250.00")!, }; ``` #### Properties ##### amountDue? > `readonly` `optional` **amountDue?**: [`X12Decimal`](#x12decimal) RMR-05 - amount due / original (situational). ##### amountPaid > `readonly` **amountPaid**: [`X12Decimal`](#x12decimal) RMR-04 - amount paid. ##### paymentActionCode? > `readonly` `optional` **paymentActionCode?**: `string` RMR-03 - payment action code (situational). ##### qualifier > `readonly` **qualifier**: `string` RMR-01 - reference id qualifier (`11`, `IK`, `AZ`, …). ##### referenceId > `readonly` **referenceId**: `string` RMR-02 - reference id (policy / invoice number). *** ### Build820PartySpec Loop 1000A premium receiver (`N1*PE`) or Loop 1000B premium payer / remitter (`N1*PR` / `N1*RM`). Mirrors "./types.js".X12PremiumParty. #### Example ```ts import type { Build820PartySpec } from "@cosyte/x12"; const remitter: Build820PartySpec = { entityIdentifierCode: "PR", name: "EMPLOYER CO", idQualifier: "FI", idCode: "FEIN123", }; ``` #### Properties ##### address? > `readonly` `optional` **address?**: [`Build820AddressSpec`](#build820addressspec) N3 + N4 address block. ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` N1-01 - entity identifier code (`PE` receiver / `PR` / `RM` remitter). ##### idCode? > `readonly` `optional` **idCode?**: `string` N1-04 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` N1-03 - identification code qualifier. ##### name > `readonly` **name**: `string` N1-02 - party name. ##### references? > `readonly` `optional` **references?**: readonly [`Build820ReferenceSpec`](#build820referencespec)[] REF supplemental identifiers. *** ### Build820PaymentSpec BPR financial-information / payment header. `totalPremiumAmount` is the aggregate premium the bank moved (BPR-02). Identical segment shape to the 835 - but the 820 carries no balance equation, so this total is emitted verbatim, never reconciled against the remittance open items. #### Example ```ts import type { Build820PaymentSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const bpr: Build820PaymentSpec = { transactionHandlingCode: "I", totalPremiumAmount: X12Decimal.fromString("12500.00")!, creditDebitFlag: "C", method: "ACH", paymentDate: "20260601", }; ``` #### Properties ##### creditDebitFlag > `readonly` **creditDebitFlag**: `string` BPR-03 - credit/debit flag (`C` credit, `D` debit). ##### method > `readonly` **method**: `string` BPR-04 - payment method (`ACH`, `CHK`, `NON`, …). ##### paymentDate > `readonly` **paymentDate**: `string` BPR-16 - payment effective date (CCYYMMDD). ##### paymentFormatCode? > `readonly` `optional` **paymentFormatCode?**: `string` BPR-05 - payment format code (situational). ##### totalPremiumAmount > `readonly` **totalPremiumAmount**: [`X12Decimal`](#x12decimal) BPR-02 - total premium amount the bank moved. ##### transactionHandlingCode > `readonly` **transactionHandlingCode**: `string` BPR-01 - transaction handling code (`I` remittance + payment, `H` notification, …). *** ### Build820PersonSpec NM1 individual (member) inside a remittance loop. Mirrors "./types.js".X12PremiumPerson. PHI surface: every field carries PHI. The builder emits NM1-02 (entity type qualifier) as `"1"` (person); the read side does not interpret it. #### Example ```ts import type { Build820PersonSpec } from "@cosyte/x12"; const p: Build820PersonSpec = { entityIdentifierCode: "IL", lastName: "DOE", firstName: "JANE", idQualifier: "34", idCode: "MBR0001", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`IL` insured, …). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code (verbatim member id). ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`34` SSN, `ZZ` mutually defined, …). ##### lastName? > `readonly` `optional` **lastName?**: `string` NM1-03 - last name. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build820ReferenceSpec REF supplemental identifier on a party or remittance loop. Mirrors "./types.js".X12PremiumReference. `description` (REF-03) is emitted only when supplied. #### Example ```ts import type { Build820ReferenceSpec } from "@cosyte/x12"; const ref: Build820ReferenceSpec = { qualifier: "38", value: "POL-0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description (situational). ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification value. *** ### Build820RemittanceSpec One Loop 2000 remittance - an organization summary (`entity`), an individual (`individual`), or both. Mirrors "./types.js".X12PremiumRemittance. Structural preconditions the builder enforces so the loop round-trips through `get820Payments`: - at least one of `entity` / `individual` must be present (a remittance needs an `ENT` or `NM1` to open its loop), and - at least one `openItems` entry must be present (a premium remittance always carries an `RMR` line; an item-less loop would also merge into a following individual remittance on the read side). #### Example ```ts import type { Build820RemittanceSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const r: Build820RemittanceSpec = { individual: { entityIdentifierCode: "IL", lastName: "DOE", idQualifier: "34", idCode: "MBR0001" }, openItems: [{ qualifier: "AZ", referenceId: "POL-0001", amountPaid: X12Decimal.fromString("250.00")! }], }; ``` #### Properties ##### adjustments? > `readonly` `optional` **adjustments?**: readonly [`Build820AdjustmentSpec`](#build820adjustmentspec)[] ADX adjustments for the loop. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build820DateSpec`](#build820datespec)[] DTM dates for the loop. ##### entity? > `readonly` `optional` **entity?**: [`Build820EntitySpec`](#build820entityspec) ENT entity (Loop 2000A organization summary). ##### individual? > `readonly` `optional` **individual?**: [`Build820PersonSpec`](#build820personspec) NM1 individual (member). ##### openItems > `readonly` **openItems**: readonly [`Build820OpenItemSpec`](#build820openitemspec)[] RMR open items (≥ 1 required). ##### references? > `readonly` `optional` **references?**: readonly [`Build820ReferenceSpec`](#build820referencespec)[] REF supplemental identifiers for the loop. *** ### Build820Spec The full input to "./build-820.js".build820: the envelope, the payment header, ≥ 1 trace, optional receiver / remitter parties, and ≥ 1 remittance loop. A well-formed spec round-trips through `get820Payments` field-for-field; a structurally impossible one is REFUSED with a "./build-errors.js".Premium820BuildError. #### Example ```ts import { build820, X12Decimal, type Build820Spec } from "@cosyte/x12"; const spec: Build820Spec = { envelope: { senderId: "EMPLOYERCO", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, payment: { transactionHandlingCode: "I", totalPremiumAmount: X12Decimal.fromString("250.00")!, creditDebitFlag: "C", method: "ACH", paymentDate: "20260601", }, traces: [{ traceTypeCode: "1", referenceId: "PREM-202606" }], remitter: { entityIdentifierCode: "PR", name: "EMPLOYER CO" }, receiver: { entityIdentifierCode: "PE", name: "MEDPAY INSURANCE" }, remittances: [ { individual: { entityIdentifierCode: "IL", lastName: "DOE", idQualifier: "34", idCode: "MBR0001" }, openItems: [{ qualifier: "AZ", referenceId: "POL-0001", amountPaid: X12Decimal.fromString("250.00")! }], }, ], }; const ix = build820(spec); ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build820EnvelopeSpec`](#build820envelopespec) Interchange / group / transaction identity. ##### payment > `readonly` **payment**: [`Build820PaymentSpec`](#build820paymentspec) BPR payment header. ##### receiver? > `readonly` `optional` **receiver?**: [`Build820PartySpec`](#build820partyspec) Loop 1000A premium receiver (N1*PE). ##### remittances > `readonly` **remittances**: readonly [`Build820RemittanceSpec`](#build820remittancespec)[] Loop 2000 remittance detail (≥ 1 required). ##### remitter? > `readonly` `optional` **remitter?**: [`Build820PartySpec`](#build820partyspec) Loop 1000B premium payer / remitter (N1*PR / N1*RM). ##### traces > `readonly` **traces**: readonly [`Build820TraceSpec`](#build820tracespec)[] TRN traces (≥ 1 required). *** ### Build820TraceSpec TRN reassociation trace - pairs the 820 to the originating ACH / check so the receiver can reconcile the premium deposit. At least one is required (TR3 005010X218 mandates the header TRN). #### Example ```ts import type { Build820TraceSpec } from "@cosyte/x12"; const trn: Build820TraceSpec = { traceTypeCode: "1", referenceId: "PREM-202606", originatingCompanyId: "1512345678", }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03 - originating company identifier. ##### originatingCompanySupplementalCode? > `readonly` `optional` **originatingCompanySupplementalCode?**: `string` TRN-04 - originating company supplemental code. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - reference identification (the trace / check number). ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code (`1` current transaction trace numbers). *** ### Build834AddressSpec N3 + N4 address block on a member. Mirrors "./types.js".X12EnrollmentAddress. #### Example ```ts import type { Build834AddressSpec } from "@cosyte/x12"; const a: Build834AddressSpec = { lines: ["100 MAIN ST"], city: "COLUMBUS", state: "OH", postalCode: "43004", }; ``` #### Properties ##### city? > `readonly` `optional` **city?**: `string` N4-01 - city. ##### countryCode? > `readonly` `optional` **countryCode?**: `string` N4-04 - country code. ##### lines > `readonly` **lines**: readonly `string`[] N3 address lines (1-2). ##### postalCode? > `readonly` `optional` **postalCode?**: `string` N4-03 - postal code. ##### state? > `readonly` `optional` **state?**: `string` N4-02 - state / province. *** ### Build834AmountSpec AMT amount on a health-coverage loop. Mirrors "./types.js".X12EnrollmentAmount. #### Example ```ts import type { Build834AmountSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const a: Build834AmountSpec = { qualifier: "P3", amount: X12Decimal.fromString("125.00")! }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) AMT-02 - monetary amount. ##### qualifier > `readonly` **qualifier**: `string` AMT-01 - amount qualifier code (`P3` premium, `B9` co-insurance, …). *** ### Build834CoordinationOfBenefitsSpec Loop 2320 coordination of benefits (`COB`). Mirrors "./types.js".X12CoordinationOfBenefits. #### Example ```ts import type { Build834CoordinationOfBenefitsSpec } from "@cosyte/x12"; const c: Build834CoordinationOfBenefitsSpec = { payerResponsibility: "P", referenceId: "OTHERGRP-1", coordinationOfBenefitsCode: "1", }; ``` #### Properties ##### coordinationOfBenefitsCode? > `readonly` `optional` **coordinationOfBenefitsCode?**: `string` COB-03 - coordination of benefits code. ##### payerResponsibility? > `readonly` `optional` **payerResponsibility?**: `string` COB-01 - payer responsibility (`P` primary, `S` secondary, `T` tertiary). ##### referenceId? > `readonly` `optional` **referenceId?**: `string` COB-02 - the other payer's group / policy number. *** ### Build834CoverageSpec Loop 2300 health coverage (`HD`) plus its dates (`DTP`) and amounts (`AMT`). Mirrors "./types.js".X12HealthCoverage. `maintenanceTypeCode` (HD-01, X12 875) is validated when present - an unknown code is REFUSED. #### Example ```ts import type { Build834CoverageSpec } from "@cosyte/x12"; const c: Build834CoverageSpec = { maintenanceTypeCode: "021", insuranceLineCode: "HLT", planCoverageDescription: "GOLD PPO", coverageLevelCode: "FAM", dates: [{ qualifier: "348", value: "20260101" }], }; ``` #### Properties ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build834AmountSpec`](#build834amountspec)[] AMT coverage amounts. ##### coverageLevelCode? > `readonly` `optional` **coverageLevelCode?**: `string` HD-05 - coverage level code (`IND`, `FAM`, `EMP`, …). ##### dates? > `readonly` `optional` **dates?**: readonly [`Build834DateSpec`](#build834datespec)[] DTP coverage dates. ##### insuranceLineCode? > `readonly` `optional` **insuranceLineCode?**: `string` HD-03 - insurance line code (`HLT`, `DEN`, `VIS`, …). ##### maintenanceTypeCode? > `readonly` `optional` **maintenanceTypeCode?**: `string` HD-01 - maintenance type code (X12 875; validated when present). ##### planCoverageDescription? > `readonly` `optional` **planCoverageDescription?**: `string` HD-04 - plan coverage description. *** ### Build834DateSpec DTP date on a member or a health-coverage loop. Mirrors "./types.js".X12EnrollmentDate. `value` (DTP-03) is the verbatim CCYYMMDD or range; `formatQualifier` (DTP-02) defaults to `"D8"` (single date) - pass `"RD8"` for a range. The read model surfaces only the qualifier + value, so the format qualifier does not affect a round-trip. #### Example ```ts import type { Build834DateSpec } from "@cosyte/x12"; const d: Build834DateSpec = { qualifier: "356", value: "20260101" }; ``` #### Properties ##### formatQualifier? > `readonly` `optional` **formatQualifier?**: `string` DTP-02 - date/time format qualifier. Default `"D8"`. ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier (`356` eligibility begin, `357` end, …). ##### value > `readonly` **value**: `string` DTP-03 - verbatim CCYYMMDD or range. *** ### Build834EnvelopeSpec Interchange + group + transaction identity for the built 834. The builder fixes GS-01 to `"BE"` (Benefit Enrollment and Maintenance) and the version/release to `"005010X220A1"` (the 834 functional group + TR3) so the caller never hand-codes them. #### Example ```ts import type { Build834EnvelopeSpec } from "@cosyte/x12"; const env: Build834EnvelopeSpec = { senderId: "EMPLOYERCO", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build834HeaderSpec The 834 header - BGN beginning segment + sponsor (`N1*P5`) + payer (`N1*IN`) + header REF / DTP. Mirrors "./types.js".X12EnrollmentHeader minus the read-only `warnings`. #### Example ```ts import type { Build834HeaderSpec } from "@cosyte/x12"; const header: Build834HeaderSpec = { transactionSetPurposeCode: "00", referenceId: "FILE-202606", date: "20260601", sponsor: { entityIdentifierCode: "P5", name: "EMPLOYER CO" }, payer: { entityIdentifierCode: "IN", name: "MEDPAY INSURANCE" }, }; ``` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` BGN-08 - action code (`2` change / `4` verify / `RX` replace, …). ##### date? > `readonly` `optional` **date?**: `string` BGN-03 - transaction set creation date (CCYYMMDD). ##### dates? > `readonly` `optional` **dates?**: readonly [`Build834DateSpec`](#build834datespec)[] Header DTP dates. ##### payer? > `readonly` `optional` **payer?**: [`Build834PartySpec`](#build834partyspec) Loop 1000B payer (N1*IN). ##### referenceId? > `readonly` `optional` **referenceId?**: `string` BGN-02 - reference identification (the file / batch id). ##### references? > `readonly` `optional` **references?**: readonly [`Build834ReferenceSpec`](#build834referencespec)[] Header REF identifiers. ##### sponsor? > `readonly` `optional` **sponsor?**: [`Build834PartySpec`](#build834partyspec) Loop 1000A sponsor (N1*P5). ##### time? > `readonly` `optional` **time?**: `string` BGN-04 - transaction set creation time (HHMM). ##### transactionSetPurposeCode > `readonly` **transactionSetPurposeCode**: `string` BGN-01 - transaction set purpose code (`00` original, `15` re-submission, …). *** ### Build834MemberNameSpec Loop 2100A member name (`NM1*IL`) + demographics (`DMG`) + address (`N3`/`N4`). Mirrors "./types.js".X12EnrollmentMember. PHI surface: every field carries PHI. `entityIdentifierCode` defaults to `"IL"` (insured) - the read side captures only the IL member name, so a different qualifier would not round-trip. #### Example ```ts import type { Build834MemberNameSpec } from "@cosyte/x12"; const m: Build834MemberNameSpec = { lastName: "DOE", firstName: "JANE", idQualifier: "34", idCode: "MBR0001", dateOfBirth: "19850515", genderCode: "F", }; ``` #### Properties ##### address? > `readonly` `optional` **address?**: [`Build834AddressSpec`](#build834addressspec) N3 + N4 address block. ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: `string` DMG-02 - date of birth (CCYYMMDD). ##### entityIdentifierCode? > `readonly` `optional` **entityIdentifierCode?**: `string` NM1-01 - entity identifier code. Default `"IL"` (insured). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### genderCode? > `readonly` `optional` **genderCode?**: `string` DMG-03 - gender code (`M` / `F` / `U`). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code (verbatim member id). ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`34` SSN, `ZZ` mutually defined, …). ##### lastName? > `readonly` `optional` **lastName?**: `string` NM1-03 - last name. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build834MemberSpec One Loop 2000 member-level detail (`INS`) - a single member's enrollment action. Mirrors "./types.js".X12Enrollment minus the looked-up `maintenanceTypeDescription`. `maintenanceTypeCode` (INS-03, X12 875) is REQUIRED and safety-critical - it is emitted verbatim and an unknown code is REFUSED. #### Example ```ts import type { Build834MemberSpec } from "@cosyte/x12"; const member: Build834MemberSpec = { subscriberIndicator: "Y", relationshipCode: "18", maintenanceTypeCode: "021", member: { lastName: "DOE", firstName: "JANE", idQualifier: "34", idCode: "MBR0001" }, healthCoverages: [{ maintenanceTypeCode: "021", insuranceLineCode: "HLT" }], }; ``` #### Properties ##### benefitStatusCode? > `readonly` `optional` **benefitStatusCode?**: `string` INS-05 - benefit status code (`A` active, `C` COBRA, …). ##### coordinationOfBenefits? > `readonly` `optional` **coordinationOfBenefits?**: readonly [`Build834CoordinationOfBenefitsSpec`](#build834coordinationofbenefitsspec)[] Loop 2320 coordination of benefits. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build834DateSpec`](#build834datespec)[] DTP member-level dates (eligibility begin / end). ##### employmentStatusCode? > `readonly` `optional` **employmentStatusCode?**: `string` INS-08 - employment status code (`FT`, `PT`, …). ##### healthCoverages? > `readonly` `optional` **healthCoverages?**: readonly [`Build834CoverageSpec`](#build834coveragespec)[] Loop 2300 health coverages. ##### maintenanceReasonCode? > `readonly` `optional` **maintenanceReasonCode?**: `string` INS-04 - maintenance reason code. ##### maintenanceTypeCode > `readonly` **maintenanceTypeCode**: `string` INS-03 - maintenance type code (X12 875; required, validated). ##### member? > `readonly` `optional` **member?**: [`Build834MemberNameSpec`](#build834membernamespec) Loop 2100A member name + DMG + address. ##### references? > `readonly` `optional` **references?**: readonly [`Build834ReferenceSpec`](#build834referencespec)[] REF supplemental identifiers (subscriber id, group/policy). ##### relationshipCode? > `readonly` `optional` **relationshipCode?**: `string` INS-02 - relationship code (`18` self, `01` spouse, `19` child, …). ##### subscriberIndicator? > `readonly` `optional` **subscriberIndicator?**: `string` INS-01 - subscriber indicator (`Y` subscriber / `N` dependent). *** ### Build834PartySpec N1 party - sponsor (Loop 1000A, `N1*P5`) or payer (Loop 1000B, `N1*IN`). Mirrors "./types.js".X12EnrollmentParty. #### Example ```ts import type { Build834PartySpec } from "@cosyte/x12"; const sponsor: Build834PartySpec = { entityIdentifierCode: "P5", name: "EMPLOYER CO", idQualifier: "FI", idCode: "FEIN123", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` N1-01 - entity identifier code (`P5` sponsor / `IN` payer). ##### idCode? > `readonly` `optional` **idCode?**: `string` N1-04 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` N1-03 - identification code qualifier. ##### name > `readonly` **name**: `string` N1-02 - party name. *** ### Build834ReferenceSpec REF supplemental identifier on the header or a member. Mirrors "./types.js".X12EnrollmentReference. `description` (REF-03) is emitted only when supplied. #### Example ```ts import type { Build834ReferenceSpec } from "@cosyte/x12"; const ref: Build834ReferenceSpec = { qualifier: "0F", value: "MBR0001" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description (situational). ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification value. *** ### Build834Spec The full input to "./build-834.js".build834: the envelope, the header, and ≥ 1 member loop. A well-formed spec round-trips through `get834Header` / `get834Enrollments` field-for-field; a spec with an unknown maintenance type or a missing member loop is REFUSED with an "./build-errors.js".Enrollment834BuildError. #### Example ```ts import { build834, type Build834Spec } from "@cosyte/x12"; const spec: Build834Spec = { envelope: { senderId: "EMPLOYERCO", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, header: { transactionSetPurposeCode: "00", referenceId: "FILE-202606", date: "20260601", sponsor: { entityIdentifierCode: "P5", name: "EMPLOYER CO" }, payer: { entityIdentifierCode: "IN", name: "MEDPAY INSURANCE" }, }, members: [ { subscriberIndicator: "Y", relationshipCode: "18", maintenanceTypeCode: "021", member: { lastName: "DOE", firstName: "JANE", idQualifier: "34", idCode: "MBR0001" }, healthCoverages: [{ maintenanceTypeCode: "021", insuranceLineCode: "HLT" }], }, ], }; const ix = build834(spec); ``` #### Properties ##### envelope > `readonly` **envelope**: [`Build834EnvelopeSpec`](#build834envelopespec) Interchange / group / transaction identity. ##### header > `readonly` **header**: [`Build834HeaderSpec`](#build834headerspec) BGN header + sponsor / payer parties. ##### members > `readonly` **members**: readonly [`Build834MemberSpec`](#build834memberspec)[] Loop 2000 member-level detail (≥ 1 required). *** ### Build835AddressSpec N3 + N4 address block on a party. Mirrors "./types.js".X12RemitAddress. #### Example ```ts import type { Build835AddressSpec } from "@cosyte/x12"; const a: Build835AddressSpec = { lines: ["123 PAYER WAY"], city: "BALTIMORE", state: "MD", postalCode: "21244", }; ``` #### Properties ##### city? > `readonly` `optional` **city?**: `string` N4-01 - city. ##### countryCode? > `readonly` `optional` **countryCode?**: `string` N4-04 - country code. ##### lines > `readonly` **lines**: readonly `string`[] N3 address lines (1-2). ##### postalCode? > `readonly` `optional` **postalCode?**: `string` N4-03 - postal code. ##### state? > `readonly` `optional` **state?**: `string` N4-02 - state / province. *** ### Build835AdjustmentSpec One CAS adjustment (one reason / amount / quantity triple under a group code). Mirrors "./types.js".X12RemitAdjustment minus the looked-up `reasonDescription`. The builder re-chunks adjustments that share a `groupCode` into CAS segments (≤ 6 triples each). #### Example ```ts import type { Build835AdjustmentSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const cas: Build835AdjustmentSpec = { groupCode: "PR", reasonCode: "1", amount: X12Decimal.fromString("50.00")!, }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) Adjustment amount. ##### groupCode > `readonly` **groupCode**: `string` CAS-01 - claim adjustment group code (`CO`, `PR`, `OA`, `PI`). ##### quantity? > `readonly` `optional` **quantity?**: [`X12Decimal`](#x12decimal) Adjustment quantity (situational). ##### reasonCode > `readonly` **reasonCode**: `string` Adjustment reason code (CARC). *** ### Build835AmountSpec AMT supplemental amount on a claim / service line. Mirrors "./types.js".X12RemitAmount. #### Example ```ts import type { Build835AmountSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const amt: Build835AmountSpec = { qualifier: "B6", amount: X12Decimal.fromString("450.00")! }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) AMT-02 - monetary amount. ##### qualifier > `readonly` **qualifier**: `string` AMT-01 - amount qualifier code (`AU`, `B6`, …). *** ### Build835ClaimSpec Loop 2100 claim-payment spec. Subject to the claim balance invariant `CLP-03 == CLP-04 + Σ(claim CAS + line CAS)` - the builder REFUSES an out-of-balance claim. Mirrors "./types.js".X12RemitClaim minus the looked-up `claimStatusDescription`. #### Example ```ts import type { Build835ClaimSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const claim: Build835ClaimSpec = { patientControlNumber: "PT-ACCT-001", claimStatusCode: "1", totalChargeAmount: X12Decimal.fromString("500.00")!, totalPaymentAmount: X12Decimal.fromString("450.00")!, patientResponsibilityAmount: X12Decimal.fromString("50.00")!, }; ``` #### Properties ##### adjustments? > `readonly` `optional` **adjustments?**: readonly [`Build835AdjustmentSpec`](#build835adjustmentspec)[] Claim-level CAS adjustments. ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build835AmountSpec`](#build835amountspec)[] Claim-level AMT amounts. ##### claimFilingIndicatorCode? > `readonly` `optional` **claimFilingIndicatorCode?**: `string` CLP-06 - claim filing indicator code. ##### claimFrequencyCode? > `readonly` `optional` **claimFrequencyCode?**: `string` CLP-08-3 - claim frequency code. ##### claimStatusCode > `readonly` **claimStatusCode**: `string` CLP-02 - claim status code. ##### correctedPatient? > `readonly` `optional` **correctedPatient?**: [`Build835PersonSpec`](#build835personspec) NM1*74 corrected patient. ##### facilityTypeCode? > `readonly` `optional` **facilityTypeCode?**: `string` CLP-08-1 - facility type code. ##### patient? > `readonly` `optional` **patient?**: [`Build835PersonSpec`](#build835personspec) NM1*QC patient. ##### patientControlNumber > `readonly` **patientControlNumber**: `string` CLP-01 - patient control number. ##### patientResponsibilityAmount > `readonly` **patientResponsibilityAmount**: [`X12Decimal`](#x12decimal) CLP-05 - patient responsibility amount (informational, not balanced). ##### payerClaimControlNumber? > `readonly` `optional` **payerClaimControlNumber?**: `string` CLP-07 - payer claim control number. ##### references? > `readonly` `optional` **references?**: readonly [`Build835ReferenceSpec`](#build835referencespec)[] Claim-level REF identifiers. ##### remarks? > `readonly` `optional` **remarks?**: readonly [`Build835RemarkSpec`](#build835remarkspec)[] Claim-level LQ remarks. ##### renderingProvider? > `readonly` `optional` **renderingProvider?**: [`Build835ProviderSpec`](#build835providerspec) A second NM1*82 (rendering provider). ##### serviceLines? > `readonly` `optional` **serviceLines?**: readonly [`Build835ServiceLineSpec`](#build835servicelinespec)[] Loop 2110 service lines. ##### servicePeriodEnd? > `readonly` `optional` **servicePeriodEnd?**: `string` DTM*233 statement-to date. ##### servicePeriodStart? > `readonly` `optional` **servicePeriodStart?**: `string` DTM*232 statement-from date. ##### serviceProvider? > `readonly` `optional` **serviceProvider?**: [`Build835ProviderSpec`](#build835providerspec) NM1*82 service provider. ##### subscriber? > `readonly` `optional` **subscriber?**: [`Build835PersonSpec`](#build835personspec) NM1*IL subscriber. ##### totalChargeAmount > `readonly` **totalChargeAmount**: [`X12Decimal`](#x12decimal) CLP-03 - total submitted charge amount. ##### totalPaymentAmount > `readonly` **totalPaymentAmount**: [`X12Decimal`](#x12decimal) CLP-04 - total claim payment amount. *** ### Build835ContactSpec PER contact on a party (Loop 1000A/1000B). Each contact may carry up to three communication channels. Mirrors "./types.js".X12RemitContact. #### Example ```ts import type { Build835ContactSpec } from "@cosyte/x12"; const per: Build835ContactSpec = { contactFunctionCode: "BL", name: "JANE COORDINATOR", communications: [{ qualifier: "TE", value: "5551234567" }], }; ``` #### Properties ##### communications? > `readonly` `optional` **communications?**: readonly `object`[] Up to 3 communication channels (PER-03/04, 05/06, 07/08). ##### contactFunctionCode > `readonly` **contactFunctionCode**: `string` PER-01 - contact function code (`BL` technical, `CX` claim office, …). ##### name? > `readonly` `optional` **name?**: `string` PER-02 - contact name. *** ### Build835EnvelopeSpec Interchange + group + transaction identity for the built 835. Mirrors the `build999` envelope spec; the builder fixes GS-01 to `"HP"` and the version/release to `"005010X221A1"` (the 835 functional group + TR3) so the caller never hand-codes them. #### Example ```ts import type { Build835EnvelopeSpec } from "@cosyte/x12"; const env: Build835EnvelopeSpec = { senderId: "MEDICARE", receiverId: "SUBMITTER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build835PartySpec N1 party - payer (Loop 1000A, `entityIdentifierCode: "PR"`) or payee (Loop 1000B, `"PE"`). Mirrors "./types.js".X12RemitParty. #### Example ```ts import type { Build835PartySpec } from "@cosyte/x12"; const payer: Build835PartySpec = { entityIdentifierCode: "PR", name: "MEDICARE PART A", address: { lines: ["123 PAYER WAY"], city: "BALTIMORE", state: "MD", postalCode: "21244" }, }; ``` #### Properties ##### additionalIdentifiers? > `readonly` `optional` **additionalIdentifiers?**: readonly [`Build835ReferenceSpec`](#build835referencespec)[] REF additional identifiers. ##### address? > `readonly` `optional` **address?**: [`Build835AddressSpec`](#build835addressspec) N3 + N4 address block. ##### contacts? > `readonly` `optional` **contacts?**: readonly [`Build835ContactSpec`](#build835contactspec)[] PER contacts. ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` N1-01 - entity identifier code (`PR` payer / `PE` payee). ##### idCode? > `readonly` `optional` **idCode?**: `string` N1-04 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` N1-03 - identification code qualifier. ##### name > `readonly` **name**: `string` N1-02 - party name. *** ### Build835PaymentSpec BPR financial-information / payment header. `totalActualPayment` is the sum the bank moved - it is the right-hand side of the top-of-remit balance invariant `BPR-02 == Σ(CLP-04) − Σ(PLB)` the builder enforces. #### Example ```ts import type { Build835PaymentSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const bpr: Build835PaymentSpec = { transactionHandlingCode: "I", totalActualPayment: X12Decimal.fromString("450.00")!, creditDebitFlag: "C", method: "ACH", paymentDate: "20260601", }; ``` #### Properties ##### creditDebitFlag > `readonly` **creditDebitFlag**: `string` BPR-03 - credit/debit flag (`C` credit, `D` debit). ##### method > `readonly` **method**: `string` BPR-04 - payment method (`ACH`, `CHK`, `NON`, `BOP`, `FWT`). ##### paymentDate > `readonly` **paymentDate**: `string` BPR-16 - payment effective date (CCYYMMDD). ##### paymentFormatCode? > `readonly` `optional` **paymentFormatCode?**: `string` BPR-05 - payment format code (situational). ##### totalActualPayment > `readonly` **totalActualPayment**: [`X12Decimal`](#x12decimal) BPR-02 - total actual provider payment amount. ##### transactionHandlingCode > `readonly` **transactionHandlingCode**: `string` BPR-01 - transaction handling code (`I` remittance + payment, `H` notification, …). *** ### Build835PersonSpec NM1 person on a claim - patient (`QC`), subscriber (`IL`), or corrected patient (`74`). Mirrors "./types.js".X12RemitPerson. NM1-02 (entity type qualifier) is emitted as `"1"` (person). #### Example ```ts import type { Build835PersonSpec } from "@cosyte/x12"; const patient: Build835PersonSpec = { entityIdentifierCode: "QC", lastName: "PATIENT", firstName: "TEST", idQualifier: "MI", idCode: "MEMBER001", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`QC` patient / `IL` insured / `74` corrected). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name. ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`MI`, `34`, …). ##### lastName? > `readonly` `optional` **lastName?**: `string` NM1-03 - last name / organization name. ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix. *** ### Build835ProviderAdjustmentSpec PLB provider-level adjustment. **Sign convention (raw EDI):** a POSITIVE amount REDUCES the provider payment (take-back / recoupment); a NEGATIVE amount ADDS to it (interest / advance). The top-of-remit invariant `BPR-02 == Σ(CLP-04) − Σ(PLB)` relies on this sign. Mirrors "./types.js".X12RemitProviderAdjustment. #### Example ```ts import type { Build835ProviderAdjustmentSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const plb: Build835ProviderAdjustmentSpec = { providerId: "1234567890", fiscalPeriodDate: "20261231", reasonCode: "WO", subCode: "PRIOR-CLAIM-X", amount: X12Decimal.fromString("50.00")!, }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) Adjustment amount (raw EDI sign). ##### fiscalPeriodDate > `readonly` **fiscalPeriodDate**: `string` PLB-02 - fiscal period date (CCYYMMDD). ##### providerId > `readonly` **providerId**: `string` PLB-01 - provider identifier. ##### reasonCode > `readonly` **reasonCode**: `string` Adjustment reason code (PLB composite component 1). ##### subCode? > `readonly` `optional` **subCode?**: `string` Adjustment reference / sub code (PLB composite component 2). *** ### Build835ProviderSpec NM1 provider on a claim - service provider (`82`). Mirrors "./types.js".X12RemitProvider. NM1-02 is emitted as `"2"` (non-person / organization). #### Example ```ts import type { Build835ProviderSpec } from "@cosyte/x12"; const prov: Build835ProviderSpec = { entityIdentifierCode: "82", name: "RENDERING PROVIDER INC", idQualifier: "XX", idCode: "1234567890", }; ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code (`82` service provider). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`XX` NPI). ##### name? > `readonly` `optional` **name?**: `string` NM1-03 - organization name. *** ### Build835ReferenceSpec REF additional identifier on a party / claim / service line. Mirrors "./types.js".X12RemitReference. `description` (REF-03) is emitted only when supplied. #### Example ```ts import type { Build835ReferenceSpec } from "@cosyte/x12"; const ref: Build835ReferenceSpec = { qualifier: "TJ", value: "123456789" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description (situational). ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification value. *** ### Build835RemarkSpec One remark (LQ). Mirrors "./types.js".X12RemitRemark minus the looked-up `description`. Emitted as `LQ*{system}*{code}`. Note: the read side also surfaces MIA/MOA remark codes as `system: "HE"` remarks - the builder emits all remarks via LQ, so a round-trip reproduces the `{ system, code }` pair (the equivalent model), not the original MIA/MOA segment. #### Example ```ts import type { Build835RemarkSpec } from "@cosyte/x12"; const lq: Build835RemarkSpec = { system: "HE", code: "N4" }; ``` #### Properties ##### code > `readonly` **code**: `string` LQ-02 - industry code value. ##### system > `readonly` **system**: `string` LQ-01 - code list qualifier code (`HE` healthcare remark / RARC, …). *** ### Build835ServiceLineSpec Loop 2110 service-line spec. Subject to the per-line balance invariant `SVC-02 == SVC-03 + Σ(line CAS)` - the builder REFUSES an out-of-balance line. Mirrors "./types.js".X12RemitServiceLine. #### Example ```ts import type { Build835ServiceLineSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const line: Build835ServiceLineSpec = { productServiceIdQualifier: "HC", productServiceId: "99213", chargeAmount: X12Decimal.fromString("500.00")!, paymentAmount: X12Decimal.fromString("450.00")!, adjustments: [{ groupCode: "PR", reasonCode: "1", amount: X12Decimal.fromString("50.00")! }], }; ``` #### Properties ##### adjustments? > `readonly` `optional` **adjustments?**: readonly [`Build835AdjustmentSpec`](#build835adjustmentspec)[] Line-level CAS adjustments. ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build835AmountSpec`](#build835amountspec)[] Line-level AMT amounts. ##### chargeAmount > `readonly` **chargeAmount**: [`X12Decimal`](#x12decimal) SVC-02 - line item charge amount. ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] SVC-01-3..6 - procedure modifiers. ##### originalServiceId? > `readonly` `optional` **originalServiceId?**: `string` SVC-06-2 - original (submitted) product/service ID. ##### originalServiceIdQualifier? > `readonly` `optional` **originalServiceIdQualifier?**: `string` SVC-06-1 - original product/service ID qualifier. ##### originalUnitsOfService? > `readonly` `optional` **originalUnitsOfService?**: [`X12Decimal`](#x12decimal) SVC-07 - Original Units of Service Count: the units as SUBMITTED, sent only when they differ from the paid count in SVC-05. ##### paidUnitsOfService? > `readonly` `optional` **paidUnitsOfService?**: [`X12Decimal`](#x12decimal) SVC-05 - Units of Service Paid Count. The count the payer actually adjudicated. Distinct from [Build835ServiceLineSpec.originalUnitsOfService](#originalunitsofservice), which is what was submitted. ##### paymentAmount > `readonly` **paymentAmount**: [`X12Decimal`](#x12decimal) SVC-03 - line item provider payment amount. ##### productServiceId > `readonly` **productServiceId**: `string` SVC-01-2 - product/service ID (the procedure code). ##### productServiceIdQualifier > `readonly` **productServiceIdQualifier**: `string` SVC-01-1 - product/service ID qualifier (`HC`, `AD`, `N4`, `WK`, `IV`). ##### references? > `readonly` `optional` **references?**: readonly [`Build835ReferenceSpec`](#build835referencespec)[] Line-level REF identifiers. ##### remarks? > `readonly` `optional` **remarks?**: readonly [`Build835RemarkSpec`](#build835remarkspec)[] Line-level LQ remarks. ##### revenueCode? > `readonly` `optional` **revenueCode?**: `string` SVC-04 - NUBC revenue code (institutional). ##### serviceDateEnd? > `readonly` `optional` **serviceDateEnd?**: `string` Service date end (DTM*151). ##### serviceDateStart? > `readonly` `optional` **serviceDateStart?**: `string` Service date start (DTM*150, or single DTM*472 when start == end). *** ### Build835Spec The full input to "./build-835.js".build835: the envelope, the payment header, ≥ 1 trace, optional payer/payee parties, the claims, and optional provider-level adjustments. A balanced spec round-trips through `get835` field-for-field; an imbalanced one is REFUSED with a "./build-errors.js".Remit835BuildError. #### Example ```ts import { build835, X12Decimal, type Build835Spec } from "@cosyte/x12"; const spec: Build835Spec = { envelope: { senderId: "MEDICARE", receiverId: "SUBMITTER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, payment: { transactionHandlingCode: "I", totalActualPayment: X12Decimal.fromString("450.00")!, creditDebitFlag: "C", method: "ACH", paymentDate: "20260601", }, traces: [{ traceTypeCode: "1", referenceId: "0012345", originatingCompanyId: "1512345678" }], claims: [ { patientControlNumber: "PT-ACCT-001", claimStatusCode: "1", totalChargeAmount: X12Decimal.fromString("500.00")!, totalPaymentAmount: X12Decimal.fromString("450.00")!, patientResponsibilityAmount: X12Decimal.fromString("50.00")!, adjustments: [{ groupCode: "PR", reasonCode: "1", amount: X12Decimal.fromString("50.00")! }], }, ], }; const ix = build835(spec); ``` #### Properties ##### claims > `readonly` **claims**: readonly [`Build835ClaimSpec`](#build835claimspec)[] Loop 2100 claim payments. ##### envelope > `readonly` **envelope**: [`Build835EnvelopeSpec`](#build835envelopespec) Interchange / group / transaction identity. ##### payee? > `readonly` `optional` **payee?**: [`Build835PartySpec`](#build835partyspec) Loop 1000B payee party (N1*PE). ##### payer? > `readonly` `optional` **payer?**: [`Build835PartySpec`](#build835partyspec) Loop 1000A payer party (N1*PR). ##### payment > `readonly` **payment**: [`Build835PaymentSpec`](#build835paymentspec) BPR payment header. ##### providerAdjustments? > `readonly` `optional` **providerAdjustments?**: readonly [`Build835ProviderAdjustmentSpec`](#build835provideradjustmentspec)[] PLB provider-level adjustments. ##### traces > `readonly` **traces**: readonly [`Build835TraceSpec`](#build835tracespec)[] TRN traces (≥ 1 required). *** ### Build835TraceSpec TRN reassociation trace. At least one is required by the TR3 (it pairs the 835 to the payment artifact a cash-poster reconciles against). #### Example ```ts import type { Build835TraceSpec } from "@cosyte/x12"; const trn: Build835TraceSpec = { traceTypeCode: "1", referenceId: "0012345", originatingCompanyId: "1512345678", }; ``` #### Properties ##### originatingCompanyId? > `readonly` `optional` **originatingCompanyId?**: `string` TRN-03 - originating company identifier. ##### originatingCompanySupplementalCode? > `readonly` `optional` **originatingCompanySupplementalCode?**: `string` TRN-04 - originating company supplemental code. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - reference identification (the trace / check number). ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code (`1` current transaction trace numbers). *** ### Build837AddressSpec N3 + N4 address block on an entity. Mirrors "./types.js".X12ClaimAddress. #### Example ```ts import type { Build837AddressSpec } from "@cosyte/x12"; const a: Build837AddressSpec = { lines: ["123 BILLING WAY"], city: "CLEVELAND", state: "OH", postalCode: "44113", }; ``` #### Properties ##### city? > `readonly` `optional` **city?**: `string` N4-01 - city. ##### countryCode? > `readonly` `optional` **countryCode?**: `string` N4-04 - country code. ##### lines > `readonly` **lines**: readonly `string`[] N3 address lines (1-2). ##### postalCode? > `readonly` `optional` **postalCode?**: `string` N4-03 - postal code. ##### state? > `readonly` `optional` **state?**: `string` N4-02 - state / province. *** ### Build837AdjudicationSpec SVD + adjacent CAS / DTP - Loop 2430 Line Adjudication Information (a prior payer's adjudication of this line, for COB). Mirrors "./types.js".X12LineAdjudication. #### Example ```ts import type { Build837AdjudicationSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const svd: Build837AdjudicationSpec = { otherPayerId: "PAYER02", amountPaid: X12Decimal.fromString("50.00")!, procedureQualifier: "HC", procedureCode: "99213", adjustments: [{ groupCode: "CO", reasonCode: "45", amount: X12Decimal.fromString("20.00")! }], dateAdjudicated: "20260520", }; ``` #### Properties ##### adjustments? > `readonly` `optional` **adjustments?**: readonly [`Build837AdjustmentSpec`](#build837adjustmentspec)[] CAS adjustments under this adjudication. ##### amountPaid > `readonly` **amountPaid**: [`X12Decimal`](#x12decimal) SVD-02 - amount the other payer paid for this line. ##### dateAdjudicated? > `readonly` `optional` **dateAdjudicated?**: `string` DTP*573 - adjudication / payment date (CCYYMMDD). ##### otherPayerId > `readonly` **otherPayerId**: `string` SVD-01 - other payer identifier. ##### paidUnits? > `readonly` `optional` **paidUnits?**: [`X12Decimal`](#x12decimal) SVD-05 - paid units of service. ##### procedureCode? > `readonly` `optional` **procedureCode?**: `string` SVD-03-2 - adjudicated procedure code. ##### procedureQualifier? > `readonly` `optional` **procedureQualifier?**: `string` SVD-03-1 - adjudicated procedure qualifier. *** ### Build837AdjustmentSpec One CAS adjustment triple inside a Loop 2430 line adjudication. Mirrors the remit `X12RemitAdjustment` shape (CAS semantics are identical). #### Example ```ts import type { Build837AdjustmentSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const cas: Build837AdjustmentSpec = { groupCode: "CO", reasonCode: "45", amount: X12Decimal.fromString("20.00")! }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) Adjustment amount. ##### groupCode > `readonly` **groupCode**: `string` CAS-01 - claim adjustment group code (`CO`, `PR`, `OA`, `PI`). ##### quantity? > `readonly` `optional` **quantity?**: [`X12Decimal`](#x12decimal) Adjustment quantity (situational). ##### reasonCode > `readonly` **reasonCode**: `string` Adjustment reason code (CARC). *** ### Build837AmountSpec AMT supplemental amount on a claim / service line. Mirrors "./types.js".X12ClaimAmount. #### Example ```ts import type { Build837AmountSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const amt: Build837AmountSpec = { qualifier: "F5", amount: X12Decimal.fromString("25.00")! }; ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) AMT-02 - monetary amount. ##### qualifier > `readonly` **qualifier**: `string` AMT-01 - amount qualifier code. *** ### Build837BillingProviderSpec Loop 2000A billing provider. Emits an HL level-20 top-of-tree node, computed by the builder. Requires ≥ 1 subscriber. #### Example ```ts import type { Build837BillingProviderSpec } from "@cosyte/x12"; declare const sub: import("@cosyte/x12").Build837SubscriberSpec; const bp: Build837BillingProviderSpec = { provider: { entityIdentifierCode: "85", entityTypeQualifier: "2", name: "BILLING CLINIC INC", idQualifier: "XX", idCode: "1234567890", address: { lines: ["123 BILLING WAY"], city: "CLEVELAND", state: "OH", postalCode: "44113" }, references: [{ qualifier: "EI", value: "987654321" }], }, subscribers: [sub], }; ``` #### Properties ##### payToAddress? > `readonly` `optional` **payToAddress?**: [`Build837AddressSpec`](#build837addressspec) NM1*87 - pay-to address (N3/N4 round-trip; the name is not re-surfaced). ##### payToPlan? > `readonly` `optional` **payToPlan?**: [`Build837EntitySpec`](#build837entityspec) NM1*PE - pay-to plan (837I only). ##### provider > `readonly` **provider**: [`Build837EntitySpec`](#build837entityspec) NM1*85 - the billing provider. ##### subscribers > `readonly` **subscribers**: readonly [`Build837SubscriberSpec`](#build837subscriberspec)[] Loop 2000B subscribers (≥ 1 required). *** ### Build837ClaimSpec Loop 2300 claim. The builder REFUSES a claim with an empty `claimId` or no service lines (a CLM requires ≥ 1 LX/SVx loop). Mirrors "./types.js".X12Claim minus the HL-resolved context the spine supplies. #### Example ```ts import type { Build837ClaimSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const claim: Build837ClaimSpec = { claimId: "PT-ACCT-001", totalCharge: X12Decimal.fromString("150.00")!, placeOfServiceCode: "11", facilityCodeQualifier: "B", claimFrequencyCode: "1", providerSignatureOnFile: "Y", providerAcceptAssignment: "A", benefitsAssignment: "Y", releaseOfInformationCode: "Y", diagnoses: [{ qualifier: "ABK", code: "J20.9" }], serviceLines: [{ variant: "P", procedureQualifier: "HC", procedureCode: "99213", charge: X12Decimal.fromString("150.00")!, unitOfMeasure: "UN", units: X12Decimal.fromString("1")!, diagnosisPointers: ["1"], }], }; ``` #### Properties ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build837AmountSpec`](#build837amountspec)[] AMT claim-level amounts. ##### benefitsAssignment? > `readonly` `optional` **benefitsAssignment?**: `string` CLM-08 - benefits assignment certification indicator. ##### claimFrequencyCode? > `readonly` `optional` **claimFrequencyCode?**: `string` CLM-05-3 - claim frequency type code. ##### claimId > `readonly` **claimId**: `string` CLM-01 - patient account / claim id. ##### dates? > `readonly` `optional` **dates?**: readonly [`Build837DateSpec`](#build837datespec)[] DTP claim-level dates. ##### diagnoses? > `readonly` `optional` **diagnoses?**: readonly [`Build837HiCodeSpec`](#build837hicodespec)[] HI diagnoses. ##### facilityCodeQualifier? > `readonly` `optional` **facilityCodeQualifier?**: `string` CLM-05-2 - facility code qualifier. ##### notes? > `readonly` `optional` **notes?**: readonly [`Build837NoteSpec`](#build837notespec)[] NTE claim-level notes. ##### otherHi? > `readonly` `optional` **otherHi?**: readonly [`Build837HiCodeSpec`](#build837hicodespec)[] HI other code-set entries (value information, condition, occurrence, …). ##### otherSubscribers? > `readonly` `optional` **otherSubscribers?**: readonly [`Build837OtherSubscriberSpec`](#build837othersubscriberspec)[] Loop 2320 other subscribers. ##### placeOfServiceCode? > `readonly` `optional` **placeOfServiceCode?**: `string` CLM-05-1 - place of service code. ##### procedures? > `readonly` `optional` **procedures?**: readonly [`Build837HiCodeSpec`](#build837hicodespec)[] HI procedures. ##### providerAcceptAssignment? > `readonly` `optional` **providerAcceptAssignment?**: `string` CLM-07 - provider accept assignment code. ##### providers? > `readonly` `optional` **providers?**: readonly [`Build837EntitySpec`](#build837entityspec)[] Loop 2310x claim provider names (NM1 fields round-trip). ##### providerSignatureOnFile? > `readonly` `optional` **providerSignatureOnFile?**: `string` CLM-06 - provider/supplier signature on file. ##### references? > `readonly` `optional` **references?**: readonly [`Build837ReferenceSpec`](#build837referencespec)[] REF claim-level identifiers. ##### releaseOfInformationCode? > `readonly` `optional` **releaseOfInformationCode?**: `string` CLM-09 - release of information code. ##### serviceLines > `readonly` **serviceLines**: readonly [`Build837ServiceLineSpec`](#build837servicelinespec)[] Loop 2400 service lines (≥ 1 required). ##### totalCharge > `readonly` **totalCharge**: [`X12Decimal`](#x12decimal) CLM-02 - total claim charge amount. *** ### Build837ContactSpec PER contact on an entity (Loop 1000A submitter, etc.). Each contact may carry up to three communication channels. Mirrors "./types.js".X12ClaimContact. #### Example ```ts import type { Build837ContactSpec } from "@cosyte/x12"; const per: Build837ContactSpec = { contactFunctionCode: "IC", name: "JANE SUBMITTER", communications: [{ qualifier: "TE", value: "5551234567" }], }; ``` #### Properties ##### communications? > `readonly` `optional` **communications?**: readonly `object`[] Up to 3 communication channels (PER-03/04, 05/06, 07/08). ##### contactFunctionCode > `readonly` **contactFunctionCode**: `string` PER-01 - contact function code (`IC` information contact, …). ##### name? > `readonly` `optional` **name?**: `string` PER-02 - contact name. *** ### Build837DateSpec DTP date - claim-level or service-line-level. Mirrors "./types.js".X12ClaimDate. #### Example ```ts import type { Build837DateSpec } from "@cosyte/x12"; const d: Build837DateSpec = { qualifier: "472", formatQualifier: "D8", value: "20260601" }; ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time format qualifier (`D8` CCYYMMDD, `RD8` range). ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier (`472` service, `431` onset, …). ##### value > `readonly` **value**: `string` DTP-03 - the date value verbatim. *** ### Build837DrugSpec LIN + CTP - Loop 2410 Drug Identification (837P). Mirrors "./types.js".X12LineDrug. #### Example ```ts import type { Build837DrugSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const drug: Build837DrugSpec = { qualifier: "N4", code: "00093721410", quantity: X12Decimal.fromString("1.5")!, unitOfMeasure: "ML", }; ``` #### Properties ##### code > `readonly` **code**: `string` LIN-03 - the drug code (NDC). ##### qualifier > `readonly` **qualifier**: `string` LIN-02 - product id qualifier (`N4` NDC, …). ##### quantity? > `readonly` `optional` **quantity?**: [`X12Decimal`](#x12decimal) CTP-04 - dispensed quantity (situational). ##### unitOfMeasure? > `readonly` `optional` **unitOfMeasure?**: `string` CTP-05-1 - UCUM unit of measure (situational). *** ### Build837EntitySpec NM1 entity used throughout the 837 - submitter (`41`), receiver (`40`), billing provider (`85`), subscriber (`IL`), payer (`PR`), patient (`QC`), and the 2310x / 2420x provider roles. Mirrors "./types.js".X12ClaimEntity. Note: when used as a claim-level (2310x) or service-line (2420x) provider, only the NM1 fields round-trip through `get837Claims`; an attached `address` / `contacts` / `references` is emitted but the read side does not re-surface it on the provider (a documented read limitation). #### Example ```ts import type { Build837EntitySpec } from "@cosyte/x12"; const billing: Build837EntitySpec = { entityIdentifierCode: "85", entityTypeQualifier: "2", name: "BILLING CLINIC INC", idQualifier: "XX", idCode: "1234567890", address: { lines: ["123 BILLING WAY"], city: "CLEVELAND", state: "OH", postalCode: "44113" }, references: [{ qualifier: "EI", value: "987654321" }], }; ``` #### Properties ##### address? > `readonly` `optional` **address?**: [`Build837AddressSpec`](#build837addressspec) N3 + N4 address block. ##### contacts? > `readonly` `optional` **contacts?**: readonly [`Build837ContactSpec`](#build837contactspec)[] PER contacts. ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code. ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### firstName? > `readonly` `optional` **firstName?**: `string` NM1-04 - first name (person). ##### idCode? > `readonly` `optional` **idCode?**: `string` NM1-09 - identification code. ##### idQualifier? > `readonly` `optional` **idQualifier?**: `string` NM1-08 - identification code qualifier (`XX` NPI, `MI` member id, …). ##### middleName? > `readonly` `optional` **middleName?**: `string` NM1-05 - middle name (person). ##### name > `readonly` **name**: `string` NM1-03 - last name / organization name. ##### references? > `readonly` `optional` **references?**: readonly [`Build837ReferenceSpec`](#build837referencespec)[] REF additional identifiers. ##### suffix? > `readonly` `optional` **suffix?**: `string` NM1-07 - name suffix (person). *** ### Build837EnvelopeSpec Interchange + group + transaction identity for the built 837. The builder fixes GS-01 to `"HC"`, and defaults the ST-03 / GS-08 reference to the variant's TR3 (`005010X222A2` / `X223A3` / `X224A2`) so the caller never has to hand-code it. **🩺 That default is not what every trading partner accepts, so [Build837EnvelopeSpec.implementationConventionReference](#implementationconventionreference) overrides it.** The three defaults are published errata guides, but they are neither the identifiers HIPAA adopts at 45 CFR 162.1102 nor the ones payer companion guides commonly require in ST-03 / GS-08 (CMS and several state Medicaid guides state `005010X222A1` for Professional and `005010X223A2` for Institutional). **A partner that requires one of those will reject a Professional or Institutional 837 built on the default**, and the override is how you send what your partner asked for. The default itself was deliberately NOT re-stamped: which published guide identifier a partner accepts is a partner fact rather than a spec fact, and changing bytes this library already emitted would break the partners it works with today. The READ side is unaffected - `get837Claims` recognises all of these references. #### Example ```ts import type { Build837EnvelopeSpec } from "@cosyte/x12"; const env: Build837EnvelopeSpec = { senderId: "SUBMITTER", receiverId: "RECEIVER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Default: the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Default: the interchange sender id. ##### claimOrEncounterIndicator? > `readonly` `optional` **claimOrEncounterIndicator?**: `string` BHT-06 - claim/encounter indicator (`CH` chargeable, `RP` reporting). Default `"CH"`. ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number. ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Default: century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Default: the interchange time. ##### implementationConventionReference? > `readonly` `optional` **implementationConventionReference?**: `string` 🩺 ST-03 **and** GS-08 - the implementation convention reference this 837 declares. Default: the variant's TR3 (`005010X222A2` Professional, `005010X223A3` Institutional, `005010X224A2` Dental). **Set it to whatever your trading partner's companion guide requires**, for example `005010X222A1` on a professional claim to a payer that asks for it. Which published identifier a partner accepts is a partner fact, so this library will not choose for you: it defaults to a real published guide and gets out of the way. **One value is written to both elements**, because this builder has always written the same reference to ST-03 and GS-08 and nothing grounds a caller making them differ. That is a statement about the bytes this field emits, not about what each element may legally hold: GS-08 is data element 480 (`AN 1/12`) and ST-03 is element 1705 (`AN 1/35`), and nothing here bounds the length. Refused, all `X12_837_BUILD_INVALID_SPEC`. An **empty** string, because a trailing empty element is dropped on emit, so it would delete ST-03 and GS-08 rather than send them empty. One carrying an **active delimiter or the release character**, because these two elements cannot carry it even escaped: the ST and GS segments are read by a splitter that is not release-aware, so the declaration would silently become two elements. And one **this library's own reader resolves to a different variant** (`005010X223A2` handed to `build837P`): the file would declare one variant and carry another's service segments, and `get837Claims` would then decode none of its service lines. Those are what this field adds, on top of the element-type guard every string slot already has, which refuses a non-string with the same code - no total is published. **Anything else is emitted as given** - the set of published errata is not provably exhaustive, so an identifier this library does not carry is the caller's call, not an error. 🩺 A guard on this element cannot make the element trustworthy. An active delimiter in a DIFFERENT envelope field splits its own segment and shifts every element after it, so ST-03 / GS-08 are then read out of a neighbour's slot. `KNOWN-LIMITATIONS.md` carries the measurement. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### transactionDate? > `readonly` `optional` **transactionDate?**: `string` BHT-04 - transaction creation date CCYYMMDD. Default: the group date. ##### transactionReferenceId? > `readonly` `optional` **transactionReferenceId?**: `string` BHT-03 - originator application reference id. Default: the transaction set control number. ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number. ##### transactionTime? > `readonly` `optional` **transactionTime?**: `string` BHT-05 - transaction creation time HHMM. Default: the group time. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. *** ### Build837HiCodeSpec One HI diagnosis / procedure composite. Mirrors "./types.js".X12ClaimHiCode minus the looked-up `codeSystem` / `category` (the builder emits the qualifier; the read side resolves the system). #### Example ```ts import type { Build837HiCodeSpec } from "@cosyte/x12"; const dx: Build837HiCodeSpec = { qualifier: "ABK", code: "J20.9" }; ``` #### Properties ##### code > `readonly` **code**: `string` Composite component 2 - the diagnosis / procedure code. ##### date? > `readonly` `optional` **date?**: `string` Composite component 4 - date value (situational). ##### dateQualifier? > `readonly` `optional` **dateQualifier?**: `string` Composite component 3 - date/time format qualifier (situational). ##### monetaryAmount? > `readonly` `optional` **monetaryAmount?**: [`X12Decimal`](#x12decimal) Composite component 5 - monetary amount (situational). ##### poaIndicator? > `readonly` `optional` **poaIndicator?**: `string` Composite component 9 - present-on-admission indicator (837I). ##### qualifier > `readonly` **qualifier**: `string` Composite component 1 - code-list qualifier (`ABK`, `ABF`, `BBR`, …). ##### quantity? > `readonly` `optional` **quantity?**: [`X12Decimal`](#x12decimal) Composite component 6 - quantity (situational). ##### versionId? > `readonly` `optional` **versionId?**: `string` Composite component 7 - version id (situational). *** ### Build837NoteSpec NTE free-text note. Mirrors "./types.js".X12ClaimNote. NTE-02 may carry incidental PHI - synthetic-only fixtures. #### Example ```ts import type { Build837NoteSpec } from "@cosyte/x12"; const n: Build837NoteSpec = { noteReferenceCode: "ADD", description: "SUPPLEMENTAL INFO" }; ``` #### Properties ##### description > `readonly` **description**: `string` NTE-02 - free text. ##### noteReferenceCode > `readonly` **noteReferenceCode**: `string` NTE-01 - note reference code (`ADD`, `CER`, …). *** ### Build837OtherSubscriberSpec Loop 2320 Other Subscriber Information (COB surface). Mirrors "./types.js".X12OtherSubscriber. #### Example ```ts import type { Build837OtherSubscriberSpec } from "@cosyte/x12"; const oth: Build837OtherSubscriberSpec = { payerResponsibilityCode: "S", individualRelationshipCode: "01", otherSubscriber: { entityIdentifierCode: "IL", entityTypeQualifier: "1", name: "SPOUSE" }, otherPayer: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "SECONDARY PLAN" }, }; ``` #### Properties ##### claimFilingIndicator? > `readonly` `optional` **claimFilingIndicator?**: `string` SBR-09 - claim filing indicator code. ##### individualRelationshipCode? > `readonly` `optional` **individualRelationshipCode?**: `string` SBR-02 - individual relationship code. ##### otherPayer? > `readonly` `optional` **otherPayer?**: [`Build837EntitySpec`](#build837entityspec) NM1*PR - the other payer. ##### otherSubscriber? > `readonly` `optional` **otherSubscriber?**: [`Build837EntitySpec`](#build837entityspec) NM1*IL / NM1*QC - the other subscriber. ##### payerResponsibilityCode > `readonly` **payerResponsibilityCode**: `string` SBR-01 - payer responsibility code (`P`/`S`/`T`). *** ### Build837PatientSpec Loop 2000C dependent patient (patient ≠ subscriber). Emits an HL level-23 child of the enclosing subscriber HL, computed by the builder. Requires ≥ 1 claim. #### Example ```ts import type { Build837PatientSpec } from "@cosyte/x12"; declare const claim: import("@cosyte/x12").Build837ClaimSpec; const pat: Build837PatientSpec = { individualRelationshipCode: "19", patient: { entityIdentifierCode: "QC", entityTypeQualifier: "1", name: "CHILD", firstName: "TEST" }, claims: [claim], }; ``` #### Properties ##### claims > `readonly` **claims**: readonly [`Build837ClaimSpec`](#build837claimspec)[] Loop 2300 claims under this patient. ##### individualRelationshipCode? > `readonly` `optional` **individualRelationshipCode?**: `string` PAT-01 - individual relationship code (`19` child, …). ##### patient > `readonly` **patient**: [`Build837EntitySpec`](#build837entityspec) NM1*QC - the patient. *** ### Build837ReferenceSpec REF additional identifier on an entity / claim / service line. Mirrors "./types.js".X12ClaimReference. `description` (REF-03) is emitted only when supplied. #### Example ```ts import type { Build837ReferenceSpec } from "@cosyte/x12"; const ref: Build837ReferenceSpec = { qualifier: "EI", value: "987654321" }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` REF-03 - description (situational). ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification value. *** ### Build837ServiceLineBaseSpec Fields shared across every service-line variant spec. #### Extended by - [`Build837ServiceLineDentalSpec`](#build837servicelinedentalspec) - [`Build837ServiceLineInstitutionalSpec`](#build837servicelineinstitutionalspec) - [`Build837ServiceLineProfessionalSpec`](#build837servicelineprofessionalspec) #### Properties ##### adjudications? > `readonly` `optional` **adjudications?**: readonly [`Build837AdjudicationSpec`](#build837adjudicationspec)[] Loop 2430 line adjudications. ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build837AmountSpec`](#build837amountspec)[] Line-level AMT amounts. ##### charge > `readonly` **charge**: [`X12Decimal`](#x12decimal) SVx charge amount - SV1-02 (P), SV2-03 (I) or SV3-02 (D). ##### dates? > `readonly` `optional` **dates?**: readonly [`Build837DateSpec`](#build837datespec)[] DTP service-line dates. ##### lineNumber? > `readonly` `optional` **lineNumber?**: `string` LX-01 - line number. Default: the 1-based index within the claim. ##### notes? > `readonly` `optional` **notes?**: readonly [`Build837NoteSpec`](#build837notespec)[] Line-level NTE notes. ##### providers? > `readonly` `optional` **providers?**: readonly [`Build837EntitySpec`](#build837entityspec)[] Loop 2420 service-line provider names (NM1 fields round-trip). ##### references? > `readonly` `optional` **references?**: readonly [`Build837ReferenceSpec`](#build837referencespec)[] Line-level REF identifiers. ##### unitOfMeasure? > `readonly` `optional` **unitOfMeasure?**: `string` SVx unit/basis-of-measurement code (`UN`, `MJ`, …). ##### units > `readonly` **units**: [`X12Decimal`](#x12decimal) SVx units of service - SV1-04 (P), SV2-05 (I) or SV3-06 (D). **Required as of `0.0.13`.** Through `0.0.12` this was optional and an omitted value was emitted as the literal `"0"`, so the builder stated a service unit count the caller never supplied. It now refuses with `X12_837_BUILD_INVALID_SPEC` instead, the same stance `build277` takes for SVC-07: this builder will not invent a count, and will not leave the element empty for the receiver to guess at either. *** ### Build837ServiceLineDentalSpec 837D service line (SV3). Mirrors "./types.js".X12\_837ServiceLineDental. #### Example ```ts import type { Build837ServiceLineDentalSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const sl: Build837ServiceLineDentalSpec = { variant: "D", procedureQualifier: "AD", procedureCode: "D2391", charge: X12Decimal.fromString("180.00")!, units: X12Decimal.fromString("1")!, placeOfServiceCode: "11", toothInformation: [{ qualifier: "JP", toothCode: "14", surfaces: ["O"] }], }; ``` #### Extends - [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec) #### Properties ##### adjudications? > `readonly` `optional` **adjudications?**: readonly [`Build837AdjudicationSpec`](#build837adjudicationspec)[] Loop 2430 line adjudications. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`adjudications`](#adjudications) ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build837AmountSpec`](#build837amountspec)[] Line-level AMT amounts. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`amounts`](#amounts-6) ##### charge > `readonly` **charge**: [`X12Decimal`](#x12decimal) SVx charge amount - SV1-02 (P), SV2-03 (I) or SV3-02 (D). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`charge`](#charge) ##### dates? > `readonly` `optional` **dates?**: readonly [`Build837DateSpec`](#build837datespec)[] DTP service-line dates. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`dates`](#dates-18) ##### lineNumber? > `readonly` `optional` **lineNumber?**: `string` LX-01 - line number. Default: the 1-based index within the claim. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`lineNumber`](#linenumber) ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] SV3-01-3..6 - procedure modifiers. ##### notes? > `readonly` `optional` **notes?**: readonly [`Build837NoteSpec`](#build837notespec)[] Line-level NTE notes. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`notes`](#notes-1) ##### oralCavityArea? > `readonly` `optional` **oralCavityArea?**: readonly `string`[] SV3-04 - oral cavity area designation codes. ##### placeOfServiceCode? > `readonly` `optional` **placeOfServiceCode?**: `string` SV3-03 - place of service. ##### procedureCode > `readonly` **procedureCode**: `string` SV3-01-2 - procedure code. ##### procedureQualifier > `readonly` **procedureQualifier**: `string` SV3-01-1 - procedure qualifier (`AD` ADA/CDT). ##### prosthesisCrownInlayCode? > `readonly` `optional` **prosthesisCrownInlayCode?**: `string` SV3-05 - prosthesis / crown / inlay code. ##### providers? > `readonly` `optional` **providers?**: readonly [`Build837EntitySpec`](#build837entityspec)[] Loop 2420 service-line provider names (NM1 fields round-trip). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`providers`](#providers-4) ##### references? > `readonly` `optional` **references?**: readonly [`Build837ReferenceSpec`](#build837referencespec)[] Line-level REF identifiers. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`references`](#references-24) ##### toothInformation? > `readonly` `optional` **toothInformation?**: readonly [`Build837ToothSpec`](#build837toothspec)[] Loop 2400 TOO tooth information. ##### unitOfMeasure? > `readonly` `optional` **unitOfMeasure?**: `string` SVx unit/basis-of-measurement code (`UN`, `MJ`, …). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`unitOfMeasure`](#unitofmeasure-1) ##### units > `readonly` **units**: [`X12Decimal`](#x12decimal) SVx units of service - SV1-04 (P), SV2-05 (I) or SV3-06 (D). **Required as of `0.0.13`.** Through `0.0.12` this was optional and an omitted value was emitted as the literal `"0"`, so the builder stated a service unit count the caller never supplied. It now refuses with `X12_837_BUILD_INVALID_SPEC` instead, the same stance `build277` takes for SVC-07: this builder will not invent a count, and will not leave the element empty for the receiver to guess at either. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`units`](#units) ##### variant > `readonly` **variant**: `"D"` *** ### Build837ServiceLineInstitutionalSpec 837I service line (SV2). Mirrors "./types.js".X12\_837ServiceLineInstitutional. #### Example ```ts import type { Build837ServiceLineInstitutionalSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const sl: Build837ServiceLineInstitutionalSpec = { variant: "I", revenueCode: "0120", procedureQualifier: "HC", procedureCode: "99221", charge: X12Decimal.fromString("1500.00")!, unitOfMeasure: "UN", units: X12Decimal.fromString("1")!, }; ``` #### Extends - [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec) #### Properties ##### adjudications? > `readonly` `optional` **adjudications?**: readonly [`Build837AdjudicationSpec`](#build837adjudicationspec)[] Loop 2430 line adjudications. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`adjudications`](#adjudications) ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build837AmountSpec`](#build837amountspec)[] Line-level AMT amounts. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`amounts`](#amounts-6) ##### charge > `readonly` **charge**: [`X12Decimal`](#x12decimal) SVx charge amount - SV1-02 (P), SV2-03 (I) or SV3-02 (D). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`charge`](#charge) ##### dates? > `readonly` `optional` **dates?**: readonly [`Build837DateSpec`](#build837datespec)[] DTP service-line dates. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`dates`](#dates-18) ##### lineNumber? > `readonly` `optional` **lineNumber?**: `string` LX-01 - line number. Default: the 1-based index within the claim. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`lineNumber`](#linenumber) ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] SV2-02-3..6 - procedure modifiers. ##### nonCoveredCharge? > `readonly` `optional` **nonCoveredCharge?**: [`X12Decimal`](#x12decimal) SV2-07 - non-covered charge amount. ##### notes? > `readonly` `optional` **notes?**: readonly [`Build837NoteSpec`](#build837notespec)[] Line-level NTE notes. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`notes`](#notes-1) ##### procedureCode? > `readonly` `optional` **procedureCode?**: `string` SV2-02-2 - procedure code (situational). ##### procedureQualifier? > `readonly` `optional` **procedureQualifier?**: `string` SV2-02-1 - procedure qualifier (situational). ##### providers? > `readonly` `optional` **providers?**: readonly [`Build837EntitySpec`](#build837entityspec)[] Loop 2420 service-line provider names (NM1 fields round-trip). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`providers`](#providers-4) ##### references? > `readonly` `optional` **references?**: readonly [`Build837ReferenceSpec`](#build837referencespec)[] Line-level REF identifiers. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`references`](#references-24) ##### revenueCode > `readonly` **revenueCode**: `string` SV2-01 - NUBC revenue code. ##### serviceLineRate? > `readonly` `optional` **serviceLineRate?**: [`X12Decimal`](#x12decimal) SV2-06 - line item rate. ##### unitOfMeasure? > `readonly` `optional` **unitOfMeasure?**: `string` SVx unit/basis-of-measurement code (`UN`, `MJ`, …). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`unitOfMeasure`](#unitofmeasure-1) ##### units > `readonly` **units**: [`X12Decimal`](#x12decimal) SVx units of service - SV1-04 (P), SV2-05 (I) or SV3-06 (D). **Required as of `0.0.13`.** Through `0.0.12` this was optional and an omitted value was emitted as the literal `"0"`, so the builder stated a service unit count the caller never supplied. It now refuses with `X12_837_BUILD_INVALID_SPEC` instead, the same stance `build277` takes for SVC-07: this builder will not invent a count, and will not leave the element empty for the receiver to guess at either. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`units`](#units) ##### variant > `readonly` **variant**: `"I"` *** ### Build837ServiceLineProfessionalSpec 837P service line (SV1). Mirrors "./types.js".X12\_837ServiceLineProfessional. #### Example ```ts import type { Build837ServiceLineProfessionalSpec } from "@cosyte/x12"; import { X12Decimal } from "@cosyte/x12"; const sl: Build837ServiceLineProfessionalSpec = { variant: "P", procedureQualifier: "HC", procedureCode: "99213", modifiers: ["25"], charge: X12Decimal.fromString("150.00")!, unitOfMeasure: "UN", units: X12Decimal.fromString("1")!, diagnosisPointers: ["1"], dates: [{ qualifier: "472", formatQualifier: "D8", value: "20260601" }], }; ``` #### Extends - [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec) #### Properties ##### adjudications? > `readonly` `optional` **adjudications?**: readonly [`Build837AdjudicationSpec`](#build837adjudicationspec)[] Loop 2430 line adjudications. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`adjudications`](#adjudications) ##### amounts? > `readonly` `optional` **amounts?**: readonly [`Build837AmountSpec`](#build837amountspec)[] Line-level AMT amounts. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`amounts`](#amounts-6) ##### charge > `readonly` **charge**: [`X12Decimal`](#x12decimal) SVx charge amount - SV1-02 (P), SV2-03 (I) or SV3-02 (D). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`charge`](#charge) ##### dates? > `readonly` `optional` **dates?**: readonly [`Build837DateSpec`](#build837datespec)[] DTP service-line dates. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`dates`](#dates-18) ##### diagnosisPointers? > `readonly` `optional` **diagnosisPointers?**: readonly `string`[] SV1-07 - diagnosis code pointers (1-4). ##### drug? > `readonly` `optional` **drug?**: [`Build837DrugSpec`](#build837drugspec) Loop 2410 drug identification. ##### emergencyIndicator? > `readonly` `optional` **emergencyIndicator?**: `string` SV1-09 - emergency indicator. ##### epsdtIndicator? > `readonly` `optional` **epsdtIndicator?**: `string` SV1-11 - EPSDT indicator. ##### familyPlanningIndicator? > `readonly` `optional` **familyPlanningIndicator?**: `string` SV1-12 - family planning indicator. ##### lineNumber? > `readonly` `optional` **lineNumber?**: `string` LX-01 - line number. Default: the 1-based index within the claim. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`lineNumber`](#linenumber) ##### modifiers? > `readonly` `optional` **modifiers?**: readonly `string`[] SV1-01-3..6 - procedure modifiers. ##### notes? > `readonly` `optional` **notes?**: readonly [`Build837NoteSpec`](#build837notespec)[] Line-level NTE notes. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`notes`](#notes-1) ##### placeOfServiceCode? > `readonly` `optional` **placeOfServiceCode?**: `string` SV1-05 - place of service (overrides claim-level). ##### procedureCode > `readonly` **procedureCode**: `string` SV1-01-2 - procedure code. ##### procedureQualifier > `readonly` **procedureQualifier**: `string` SV1-01-1 - procedure qualifier (`HC` HCPCS/CPT, …). ##### providers? > `readonly` `optional` **providers?**: readonly [`Build837EntitySpec`](#build837entityspec)[] Loop 2420 service-line provider names (NM1 fields round-trip). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`providers`](#providers-4) ##### references? > `readonly` `optional` **references?**: readonly [`Build837ReferenceSpec`](#build837referencespec)[] Line-level REF identifiers. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`references`](#references-24) ##### unitOfMeasure? > `readonly` `optional` **unitOfMeasure?**: `string` SVx unit/basis-of-measurement code (`UN`, `MJ`, …). ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`unitOfMeasure`](#unitofmeasure-1) ##### units > `readonly` **units**: [`X12Decimal`](#x12decimal) SVx units of service - SV1-04 (P), SV2-05 (I) or SV3-06 (D). **Required as of `0.0.13`.** Through `0.0.12` this was optional and an omitted value was emitted as the literal `"0"`, so the builder stated a service unit count the caller never supplied. It now refuses with `X12_837_BUILD_INVALID_SPEC` instead, the same stance `build277` takes for SVC-07: this builder will not invent a count, and will not leave the element empty for the receiver to guess at either. ###### Inherited from [`Build837ServiceLineBaseSpec`](#build837servicelinebasespec).[`units`](#units) ##### variant > `readonly` **variant**: `"P"` *** ### Build837Spec The full input to "./build-837.js".build837P / `build837I` / `build837D`: the envelope, the submitter (Loop 1000A) + receiver (Loop 1000B), and the nested billing-provider → subscriber → (claims | patient) tree from which the builder COMPUTES the HL spine. A well-formed spec round-trips through `get837Claims` field-for-field; a structurally impossible tree is REFUSED with a "./build-errors.js".Claim837BuildError. #### Example ```ts import { build837P, X12Decimal, type Build837Spec } from "@cosyte/x12"; const spec: Build837Spec = { envelope: { senderId: "SUBMITTER", receiverId: "RECEIVER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, submitter: { entityIdentifierCode: "41", entityTypeQualifier: "2", name: "SUBMITTER ONE", idQualifier: "46", idCode: "SUB001" }, receiver: { entityIdentifierCode: "40", entityTypeQualifier: "2", name: "RECEIVER ONE", idQualifier: "46", idCode: "REC001" }, billingProviders: [{ provider: { entityIdentifierCode: "85", entityTypeQualifier: "2", name: "BILLING CLINIC INC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ info: { payerResponsibilityCode: "P", individualRelationshipCode: "18", claimFilingIndicator: "MB" }, subscriber: { entityIdentifierCode: "IL", entityTypeQualifier: "1", name: "PATIENT", firstName: "TEST", idQualifier: "MI", idCode: "MEMBER001" }, payer: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "PAYER ONE", idQualifier: "PI", idCode: "PAYER01" }, claims: [{ claimId: "PT-ACCT-001", totalCharge: X12Decimal.fromString("150.00")!, diagnoses: [{ qualifier: "ABK", code: "J20.9" }], serviceLines: [{ variant: "P", procedureQualifier: "HC", procedureCode: "99213", charge: X12Decimal.fromString("150.00")!, unitOfMeasure: "UN", units: X12Decimal.fromString("1")!, diagnosisPointers: ["1"] }], }], }], }], }; const ix = build837P(spec); ``` #### Properties ##### billingProviders > `readonly` **billingProviders**: readonly [`Build837BillingProviderSpec`](#build837billingproviderspec)[] Loop 2000A billing providers (≥ 1 required). ##### envelope > `readonly` **envelope**: [`Build837EnvelopeSpec`](#build837envelopespec) Interchange / group / transaction identity. ##### receiver > `readonly` **receiver**: [`Build837EntitySpec`](#build837entityspec) Loop 1000B receiver (NM1*40). ##### submitter > `readonly` **submitter**: [`Build837EntitySpec`](#build837entityspec) Loop 1000A submitter (NM1*41). *** ### Build837SubscriberInfoSpec SBR - Subscriber Information for a Loop 2000B subscriber. Mirrors "./types.js".X12SubscriberInfo. #### Example ```ts import type { Build837SubscriberInfoSpec } from "@cosyte/x12"; const info: Build837SubscriberInfoSpec = { payerResponsibilityCode: "P", individualRelationshipCode: "18", groupNumber: "GROUP123", claimFilingIndicator: "MB", }; ``` #### Properties ##### claimFilingIndicator? > `readonly` `optional` **claimFilingIndicator?**: `string` SBR-09 - claim filing indicator code. ##### groupName? > `readonly` `optional` **groupName?**: `string` SBR-04 - group name. ##### groupNumber? > `readonly` `optional` **groupNumber?**: `string` SBR-03 - group / policy number. ##### individualRelationshipCode? > `readonly` `optional` **individualRelationshipCode?**: `string` SBR-02 - individual relationship code (`18` self, …). ##### payerResponsibilityCode > `readonly` **payerResponsibilityCode**: `string` SBR-01 - payer responsibility code (`P`/`S`/`T`). *** ### Build837SubscriberSpec Loop 2000B subscriber. Emits an HL level-22 child of the enclosing billing provider HL, computed by the builder. Carries either direct `claims` (patient = subscriber, SBR-02 `18`) or dependent `patients`. #### Example ```ts import type { Build837SubscriberSpec } from "@cosyte/x12"; declare const claim: import("@cosyte/x12").Build837ClaimSpec; const sub: Build837SubscriberSpec = { info: { payerResponsibilityCode: "P", individualRelationshipCode: "18", claimFilingIndicator: "MB" }, subscriber: { entityIdentifierCode: "IL", entityTypeQualifier: "1", name: "PATIENT", firstName: "TEST", idQualifier: "MI", idCode: "MEMBER001" }, payer: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "PAYER ONE", idQualifier: "PI", idCode: "PAYER01" }, claims: [claim], }; ``` #### Properties ##### claims? > `readonly` `optional` **claims?**: readonly [`Build837ClaimSpec`](#build837claimspec)[] Loop 2300 claims directly under the subscriber (patient = subscriber). ##### info > `readonly` **info**: [`Build837SubscriberInfoSpec`](#build837subscriberinfospec) SBR subscriber information. ##### patients? > `readonly` `optional` **patients?**: readonly [`Build837PatientSpec`](#build837patientspec)[] Loop 2000C dependent patients. ##### payer > `readonly` **payer**: [`Build837EntitySpec`](#build837entityspec) NM1*PR - the payer. ##### subscriber > `readonly` **subscriber**: [`Build837EntitySpec`](#build837entityspec) NM1*IL - the subscriber. *** ### Build837ToothSpec TOO - Tooth Information (837D Loop 2400). Mirrors "./types.js".X12ToothInformation. #### Example ```ts import type { Build837ToothSpec } from "@cosyte/x12"; const too: Build837ToothSpec = { qualifier: "JP", toothCode: "14", surfaces: ["O"] }; ``` #### Properties ##### qualifier > `readonly` **qualifier**: `string` TOO-01 - tooth-numbering code list qualifier (`JP` ADA universal, …). ##### surfaces? > `readonly` `optional` **surfaces?**: readonly `string`[] TOO-03 - per-surface codes (`M`, `O`, `D`, …). ##### toothCode > `readonly` **toothCode**: `string` TOO-02 - tooth identifier. *** ### Build999ElementErrorSpec Element-level error spec - the input shape for an IK4 the library will build. `copyOfBadDataElement` is OPTIONAL by design: when the offending value is PHI (medical record number, name, date of birth), callers SHOULD omit it. The library never auto-populates this field. #### Properties ##### contexts? > `readonly` `optional` **contexts?**: readonly `string`[] Optional CTX context strings emitted between this IK4 and the next. Each entry becomes a single CTX segment with the given value as its first composite element. Pass `[]` (default) for no context. ##### copyOfBadDataElement? > `readonly` `optional` **copyOfBadDataElement?**: `string` ##### dataElementReferenceNumber? > `readonly` `optional` **dataElementReferenceNumber?**: `string` ##### position > `readonly` **position**: `object` ###### component? > `readonly` `optional` **component?**: `number` ###### element > `readonly` **element**: `number` ###### repetition? > `readonly` `optional` **repetition?**: `number` ##### syntaxErrorCode > `readonly` **syntaxErrorCode**: [`Ik403Code`](#ik403code) *** ### Build999EnvelopeSpec Envelope spec for `build999` - the ISA + GS + ST + matching trailers that wrap the 999 transaction set. Defaults are spec-conformant for a minimal 999 envelope: - `senderQualifier` / `receiverQualifier` default to `ZZ` (mutually defined). - `usageIndicator` defaults to `P` (production). - `repetitionSeparator` defaults to `^`; `componentSeparator` defaults to `:`; `segmentTerminator` defaults to `~`. Override for trading- partner companion-guide requirements (e.g. Medicare uses `:` component, BCBS some shapes use `\\`). - `interchangeControlNumber` is the 9-character ISA-13 value (zero- padded if needed). `groupControlNumber` is the GS-06 numeric string (1 to 9 digits). `transactionSetControlNumber` is the ST-02 string (4 to 9 digits per X12 .5). #### Properties ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` ##### groupControlNumber > `readonly` **groupControlNumber**: `string` ##### groupDate? > `readonly` `optional` **groupDate?**: `string` ##### groupResponsibleAgency? > `readonly` `optional` **groupResponsibleAgency?**: `string` ##### groupTime? > `readonly` `optional` **groupTime?**: `string` ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ##### interchangeDate > `readonly` **interchangeDate**: `string` ##### interchangeTime > `readonly` **interchangeTime**: `string` ##### receiverId > `readonly` **receiverId**: `string` ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` ##### senderId > `readonly` **senderId**: `string` ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `"P"` \| `"T"` *** ### Build999FunctionalGroupSpec Functional-group response spec - input for the AK1 + AK9 pair. #### Properties ##### disposition > `readonly` **disposition**: [`X12AckDispositionCode`](#x12ackdispositioncode) ##### functionalIdCode > `readonly` **functionalIdCode**: `string` ##### groupControlNumber > `readonly` **groupControlNumber**: `string` ##### numberOfAcceptedTransactionSets > `readonly` **numberOfAcceptedTransactionSets**: `number` ##### numberOfReceivedTransactionSets > `readonly` **numberOfReceivedTransactionSets**: `number` ##### numberOfTransactionSets > `readonly` **numberOfTransactionSets**: `number` ##### syntaxErrorCodes? > `readonly` `optional` **syntaxErrorCodes?**: readonly `string`[] ##### transactionResponses > `readonly` **transactionResponses**: readonly [`Build999TransactionResponseSpec`](#build999transactionresponsespec)[] ##### versionRelease > `readonly` **versionRelease**: `string` *** ### Build999SegmentErrorSpec Segment-level error spec - input for an IK3 (with optional nested IK4s). #### Properties ##### contexts? > `readonly` `optional` **contexts?**: readonly `string`[] Optional CTX context strings emitted between this IK3 and the first nested IK4 (or the next IK3 / IK5 if no IK4s). Each entry becomes a single CTX segment with the given value as its first composite element. ##### elementErrors? > `readonly` `optional` **elementErrors?**: readonly [`Build999ElementErrorSpec`](#build999elementerrorspec)[] ##### loopIdentifier? > `readonly` `optional` **loopIdentifier?**: `string` ##### segmentIdCode > `readonly` **segmentIdCode**: `string` ##### segmentPositionInTransactionSet > `readonly` **segmentPositionInTransactionSet**: `number` ##### syntaxErrorCode? > `readonly` `optional` **syntaxErrorCode?**: [`Ik304Code`](#ik304code) *** ### Build999Spec Top-level spec for `build999`. #### Properties ##### envelope > `readonly` **envelope**: [`Build999EnvelopeSpec`](#build999envelopespec) ##### functionalGroup > `readonly` **functionalGroup**: [`Build999FunctionalGroupSpec`](#build999functionalgroupspec) *** ### Build999TransactionResponseSpec Transaction-set response spec - input for one AK2..IK5 block. #### Properties ##### disposition > `readonly` **disposition**: [`X12AckDispositionCode`](#x12ackdispositioncode) ##### implementationConventionReference? > `readonly` `optional` **implementationConventionReference?**: `string` ##### segmentErrors? > `readonly` `optional` **segmentErrors?**: readonly [`Build999SegmentErrorSpec`](#build999segmenterrorspec)[] ##### syntaxErrorCodes? > `readonly` `optional` **syntaxErrorCodes?**: readonly `string`[] ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ##### transactionSetIdCode > `readonly` **transactionSetIdCode**: `string` *** ### BuildTA1Options Options accepted by [buildTA1](#buildta1): the delimiter set the returned segment will be read against. Every field is optional and defaults to the cosyte parser archetype (`*` element, `^` repetition, `:` component, `~` segment), which is the same four `build999` takes on "./types.js".Build999EnvelopeSpec. **The three non-element fields exist for ESCAPING and nothing else.** `buildTA1` still emits no segment terminator, no repetition and no composite. It has to know them because a caller value carrying one of them has to be released, and because escaping a byte that is NOT a delimiter where the segment lands corrupts the value: `unescapeRelease` preserves `?X` verbatim for any `X` outside the declared set, so a value released against a guessed delimiter comes back carrying a stray `?`. **If you embed a TA1 in an envelope whose delimiters are not the archetype, state them here** - the defaults are an assumption this function cannot verify, exactly as they were before it escaped anything. #### Properties ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` *** ### BuildTA1Spec Top-level spec for `buildTA1`. #### Properties ##### ackCode > `readonly` **ackCode**: [`Ta1AckCode`](#ta1ackcode) ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ##### interchangeDate > `readonly` **interchangeDate**: `string` ##### interchangeTime > `readonly` **interchangeTime**: `string` ##### noteCode > `readonly` **noteCode**: [`Ta1NoteCode`](#ta1notecode) *** ### CodeListEntry One entry returned from a code-list `lookup(code)`. The inbound code value is echoed verbatim (so a caller that branches on `entry.code` is comparing exactly the bytes that came in, never a normalized form); `description` is the bundled human-readable text from the snapshot. Future fields (`isObsolete`, `replacedBy`) are tracked by the roadmap; v0.0.x snapshots ship code + description only. `dates` is present only on the snapshots that carry per-code dates (CARC and RARC today) and only for a code that HAS one there, so an entry from a snapshot without them keeps exactly the two properties it has always had. Reading a description has never meant the code was valid on any particular day; [CodeValidityResult](#codevalidityresult) is where that question is answered. #### Example ```ts import { lookupCarc } from "@cosyte/x12"; const entry = lookupCarc("45"); entry?.code; // "45" entry?.description; // "Charge exceeds fee schedule/maximum allowable..." entry?.dates?.start; // "1995-01-01" ``` #### Properties ##### code > `readonly` **code**: `string` ##### dates? > `readonly` `optional` **dates?**: [`CodeListEntryDates`](#codelistentrydates-1) ##### description > `readonly` **description**: `string` *** ### CodeListEntryDates The dates a code-list maintainer publishes for ONE code, as ISO-8601 calendar days in `YYYY-MM-DD` form. `start` is the first day the code was valid, `stop` the FIRST day it is no longer valid (the interval is half-open, and `KNOWN-LIMITATIONS.md` records why this package reads the published Stop date that way), and `lastModified` the day its description last changed. Each is absent when the maintainer publishes no such date for that code. An absent `start` is the one that matters: validity on a supplied day cannot be decided without it, and the answer is `indeterminate` rather than either verdict. #### Example ```ts import { CARC } from "@cosyte/x12"; CARC.dates["1"]?.start; // "1995-01-01" CARC.dates["15"]?.stop; // "2018-05-01" (deactivated code) CARC.dates["1"]?.stop; // undefined (still current) ``` #### Properties ##### lastModified? > `readonly` `optional` **lastModified?**: `string` ##### start? > `readonly` `optional` **start?**: `string` ##### stop? > `readonly` `optional` **stop?**: `string` *** ### CodeListMeta Metadata header attached to every bundled code-list snapshot. Surfaces the snapshot's identity + provenance + freshness so consumers can decide whether a stale description matters for their use case, and its maintaining organisation + redistribution record so they can decide whether displaying, caching or re-publishing a description is theirs to do. Those last two are SEPARATE readings: who keeps the list and what may be done with its text are different questions with different answers, and the bundled lists really do disagree on both. #### Example ```ts import { CARC } from "@cosyte/x12"; CARC.meta.id; // "CARC" CARC.meta.snapshotDate; // ISO date string this snapshot was captured CARC.meta.publishedDate; // ISO date string of the underlying publication CARC.meta.maintainingOrganization; // "ASC X12" CARC.meta.redistribution?.status; // "licence-required" CARC.meta.completeness; // "cited-subset" ``` #### Extended by - [`DatedCodeListMeta`](#datedcodelistmeta) #### Properties ##### completeness > `readonly` **completeness**: [`CodeListCompleteness`](#codelistcompleteness) Whether `codes` is the complete published list or a cited part of it, so a code this package does not know can be told from a code the publisher never issued. ##### description > `readonly` **description**: `string` ##### id > `readonly` **id**: `string` ##### maintainingOrganization > `readonly` **maintainingOrganization**: `string` \| `undefined` Who maintains the published list, read off a source carried for this package. `undefined` means the list carries NO maintainer at all, which snapshot validation refuses rather than tolerates: a list nobody is recorded as maintaining is a list nobody can be asked about. ##### note? > `readonly` `optional` **note?**: `string` ##### publishedDate > `readonly` **publishedDate**: `string` ##### redistribution > `readonly` **redistribution**: [`CodeListRedistribution`](#codelistredistribution) \| `undefined` What may be done with this list's descriptions, and whom to approach where the answer is not "anything". `undefined` means the list carries no record at all, which snapshot validation refuses. An unsettled question is recorded as `status: "not-established"` instead, which IS a record. ##### snapshotDate > `readonly` **snapshotDate**: `string` ##### source > `readonly` **source**: `string` *** ### CodeListRedistribution A bundled list's redistribution record: the status, the terms it was read off, and the party who must be approached where the descriptions are not free to redistribute. `approach` is `undefined` only where nobody needs to be asked, which is to say only where `status` is `"permitted"`. #### Example ```ts import { CARC } from "@cosyte/x12"; CARC.meta.redistribution?.approach; // names ASC X12 and its licensing page ``` #### Properties ##### approach > `readonly` **approach**: `string` \| `undefined` The licensor to approach, or `undefined` where none need be. ##### status > `readonly` **status**: [`CodeListRedistributionStatus`](#codelistredistributionstatus-1) What the evidence establishes, or that it establishes nothing. ##### terms > `readonly` **terms**: `string` The terms, quoted or cited from the source that carries them. *** ### CodeListSnapshot A complete bundled code-list snapshot. `meta` carries provenance; `codes` is a frozen plain object so consumers can iterate or build their own lookups without going through the helper. Internal use prefers the per-snapshot `lookup*` helpers - they return a frozen [CodeListEntry](#codelistentry) ergonomic for the helper APIs. #### Example ```ts import { CARC } from "@cosyte/x12"; Object.keys(CARC.codes).length; // count of bundled CARC codes CARC.codes["45"]; // raw description string (or undefined) ``` #### Extended by - [`DatedCodeListSnapshot`](#datedcodelistsnapshot) #### Properties ##### codes > `readonly` **codes**: `Readonly`\<`Record`\<`string`, `string`\>\> ##### meta > `readonly` **meta**: [`CodeListMeta`](#codelistmeta) *** ### CodeValidityResult The answer to "was this code valid on the day this document was produced". `code` is the inbound value byte for byte, exactly as the lookup helpers echo it, so a code outside the bundled subset is never lost on the way through. `description` is `undefined` for such a code: an absent code gets no description and no validity claim, only its own bytes back. #### Example ```ts import { checkRarcValidity } from "@cosyte/x12"; const answer = checkRarcValidity("N4", "20260627"); answer.code; // "N4" answer.documentDate; // "2026-06-27" (normalised from the wire form) answer.validity; // "valid" answer.reason; // undefined ``` #### Properties ##### code > `readonly` **code**: `string` The inbound code, byte for byte as it was supplied. ##### dates > `readonly` **dates**: [`CodeListEntryDates`](#codelistentrydates-1) \| `undefined` The maintainer's dates for this code, where the snapshot has them. ##### description > `readonly` **description**: `string` \| `undefined` The bundled description, or `undefined` outside the bundled subset. ##### documentDate > `readonly` **documentDate**: `string` The supplied document date, normalised to `YYYY-MM-DD`. ##### reason > `readonly` **reason**: [`CodeValidityReason`](#codevalidityreason) \| `undefined` Why the answer is `indeterminate`; `undefined` for the other two. ##### validity > `readonly` **validity**: [`CodeValidity`](#codevalidity) `valid`, `not-valid` or `indeterminate`. *** ### CoreCodeCombinationQuery The input to [checkCoreCodeCombination](#checkcorecodecombination). `adjustment` and `remarks` take the decoded `X12RemitAdjustment` and `X12RemitRemark` as `get835` returns them, or any object carrying the same fields. The 835 carries remarks per claim and per service line rather than per adjustment, so which remarks accompany an adjustment is the caller's choice; pass none to judge the group and reason pair alone. #### Example ```ts import type { CoreCodeCombinationQuery } from "@cosyte/x12"; const query: CoreCodeCombinationQuery = { table, scenario: "scenario-3", adjustment: { groupCode: "CO", reasonCode: "ZZ901" }, remarks: [{ system: "HE", code: "ZZ-R1" }], }; ``` #### Properties ##### adjustment > `readonly` **adjustment**: `Pick`\<[`X12RemitAdjustment`](#x12remitadjustment), `"groupCode"` \| `"reasonCode"`\> ##### remarks? > `readonly` `optional` **remarks?**: readonly `Pick`\<[`X12RemitRemark`](#x12remitremark), `"system"` \| `"code"`\>[] The remarks that accompany the adjustment. Absent means none. ##### scenario > `readonly` **scenario**: [`CoreBusinessScenario`](#corebusinessscenario) ##### table? > `readonly` `optional` **table?**: [`CoreCodeCombinationTable`](#corecodecombinationtable) The caller's table. Absent means no table: the answer is `unevaluated`. *** ### CoreCodeCombinationRow One row of a [CoreCodeCombinationTable](#corecodecombinationtable): one scenario, one group code, one reason code and at most one remark code. A row with no `remarkCode` lists the group and reason pair alone; the table lists a pair with several remark codes as several rows. Every code is compared exactly as written here: no case folding and no trimming. #### Example ```ts import type { CoreCodeCombinationRow } from "@cosyte/x12"; const row: CoreCodeCombinationRow = { scenario: "scenario-3", groupCode: "CO", reasonCode: "ZZ901", remarkCode: "ZZ-R1", }; ``` #### Properties ##### groupCode > `readonly` **groupCode**: `string` The Claim Adjustment Group Code (CAS-01): a non-empty string. ##### reasonCode > `readonly` **reasonCode**: `string` The Claim Adjustment Reason Code: a non-empty string. ##### remarkCode? > `readonly` `optional` **remarkCode?**: `string` One Remittance Advice Remark Code, or absent for the pair alone. ##### scenario > `readonly` **scenario**: [`CoreBusinessScenario`](#corebusinessscenario) *** ### CoreCodeCombinationTable A combination table the CALLER supplies, typically transcribed from the CORE Code Combinations version its posting system follows. `version` is a label of the caller's own choosing (such as the published version it transcribed) and comes back unchanged beside every answer given against this table. A table with no version label is refused. #### Example ```ts import type { CoreCodeCombinationTable } from "@cosyte/x12"; const table: CoreCodeCombinationTable = { version: "my-transcription-2026-02", rows: [ { scenario: "scenario-3", groupCode: "CO", reasonCode: "ZZ901" }, { scenario: "scenario-3", groupCode: "CO", reasonCode: "ZZ901", remarkCode: "ZZ-R1" }, ], }; ``` #### Properties ##### rows > `readonly` **rows**: readonly [`CoreCodeCombinationRow`](#corecodecombinationrow)[] ##### version > `readonly` **version**: `string` The caller's label for this table's version: a non-empty string. *** ### DatedCodeListMeta Provenance for a snapshot that ALSO carries per-code validity dates. The two new fields are deliberately separate from `publishedDate` and `snapshotDate`: those describe the DESCRIPTIONS this package bundled, and a consumer has to be able to tell how fresh the validity data is without inferring it from how fresh the descriptions are. The two are captured from different pages on different days and neither implies the other. #### Example ```ts import { CARC } from "@cosyte/x12"; CARC.meta.snapshotDate; // when the descriptions were captured CARC.meta.datesCapturedAt; // when the per-code validity dates were captured CARC.meta.datesSource; // the maintainer page they were read from ``` #### Extends - [`CodeListMeta`](#codelistmeta) #### Properties ##### completeness > `readonly` **completeness**: [`CodeListCompleteness`](#codelistcompleteness) Whether `codes` is the complete published list or a cited part of it, so a code this package does not know can be told from a code the publisher never issued. ###### Inherited from [`CodeListMeta`](#codelistmeta).[`completeness`](#completeness) ##### datesCapturedAt > `readonly` **datesCapturedAt**: `string` The day the per-code dates below were read off the maintainer page. ##### datesSource > `readonly` **datesSource**: `string` The maintainer page URL those dates were read from. ##### description > `readonly` **description**: `string` ###### Inherited from [`CodeListMeta`](#codelistmeta).[`description`](#description-16) ##### id > `readonly` **id**: `string` ###### Inherited from [`CodeListMeta`](#codelistmeta).[`id`](#id-1) ##### maintainingOrganization > `readonly` **maintainingOrganization**: `string` \| `undefined` Who maintains the published list, read off a source carried for this package. `undefined` means the list carries NO maintainer at all, which snapshot validation refuses rather than tolerates: a list nobody is recorded as maintaining is a list nobody can be asked about. ###### Inherited from [`CodeListMeta`](#codelistmeta).[`maintainingOrganization`](#maintainingorganization-1) ##### note? > `readonly` `optional` **note?**: `string` ###### Inherited from [`CodeListMeta`](#codelistmeta).[`note`](#note) ##### publishedDate > `readonly` **publishedDate**: `string` ###### Inherited from [`CodeListMeta`](#codelistmeta).[`publishedDate`](#publisheddate) ##### redistribution > `readonly` **redistribution**: [`CodeListRedistribution`](#codelistredistribution) \| `undefined` What may be done with this list's descriptions, and whom to approach where the answer is not "anything". `undefined` means the list carries no record at all, which snapshot validation refuses. An unsettled question is recorded as `status: "not-established"` instead, which IS a record. ###### Inherited from [`CodeListMeta`](#codelistmeta).[`redistribution`](#redistribution) ##### snapshotDate > `readonly` **snapshotDate**: `string` ###### Inherited from [`CodeListMeta`](#codelistmeta).[`snapshotDate`](#snapshotdate-1) ##### source > `readonly` **source**: `string` ###### Inherited from [`CodeListMeta`](#codelistmeta).[`source`](#source-1) *** ### DatedCodeListSnapshot A bundled snapshot that carries the maintainer's per-code validity dates beside the descriptions. `dates` is keyed by the same code strings as `codes`, and a code may be present in `codes` while absent from `dates`: that means no date was published for it, never that it has none. #### Example ```ts import { RARC } from "@cosyte/x12"; RARC.codes["N4"]; // the bundled description RARC.dates["N4"]?.start; // "2000-01-01" ``` #### Extends - [`CodeListSnapshot`](#codelistsnapshot) #### Properties ##### codes > `readonly` **codes**: `Readonly`\<`Record`\<`string`, `string`\>\> ###### Inherited from [`CodeListSnapshot`](#codelistsnapshot).[`codes`](#codes-3) ##### dates > `readonly` **dates**: `Readonly`\<`Record`\<`string`, [`CodeListEntryDates`](#codelistentrydates-1)\>\> ##### meta > `readonly` **meta**: [`DatedCodeListMeta`](#datedcodelistmeta) ###### Overrides [`CodeListSnapshot`](#codelistsnapshot).[`meta`](#meta-1) *** ### DateParts The calendar components a value actually stated, and only those. A component the value did not state is ABSENT: the key is not there at all, rather than present holding `undefined`. So `Object.keys()` of a result is exactly the set of stated components and the value's precision is recoverable from it. `month` is spec-native 1 to 12, never the JavaScript `Date` 0 to 11, and the names are singular. Deleting `offsetMinutes` leaves an object `Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject` both accept with no key rename and no value adjustment. That compatibility is the reason for this shape. Neither library is a dependency of this package and neither is used at run time. The type carries every component the shared shape defines, so the same declaration serves each `@cosyte/*` parser. From an X12 date element this package populates `year`, `month` and `day` and nothing else: the two format qualifiers it decodes are a whole day and a range of whole days, so no value it reads states a time of day, a fraction of a second or a UTC offset. #### Example ```ts import { toObject, type DateParts } from "@cosyte/x12"; const parts: DateParts | undefined = toObject({ formatQualifier: "D8", value: "20260601" }); Object.keys(parts ?? {}); // ["year", "month", "day"] parts?.month; // 6, not 5 ``` #### Properties ##### day? > `readonly` `optional` **day?**: `number` ##### hour? > `readonly` `optional` **hour?**: `number` ##### millisecond? > `readonly` `optional` **millisecond?**: `number` ##### minute? > `readonly` `optional` **minute?**: `number` ##### month? > `readonly` `optional` **month?**: `number` ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` ##### second? > `readonly` `optional` **second?**: `number` ##### year? > `readonly` `optional` **year?**: `number` *** ### DefineLoopSpecInput Input shape accepted by `defineLoopSpec`. `children` is optional; `LoopSpec.children` always materializes as a frozen `[]` when omitted so consumer code can iterate without an `?? []` guard. #### Example ```ts import type { DefineLoopSpecInput } from "@cosyte/x12"; const input: DefineLoopSpecInput = { id: "2110", trigger: "SVC", segments: [{ id: "SVC", usage: "required", max: 1 }], }; ``` #### Properties ##### children? > `readonly` `optional` **children?**: readonly [`LoopSpec`](#loopspec)[] ##### description? > `readonly` `optional` **description?**: `string` ##### id > `readonly` **id**: `string` ##### segments > `readonly` **segments**: readonly [`LoopSegmentSpec`](#loopsegmentspec)[] ##### trigger > `readonly` **trigger**: `string` *** ### Delimiters The four X12 delimiter classes discovered from fixed byte positions inside the ISA envelope. The parser detects all four from the ISA itself - they are NEVER assumed (in particular, `component` is rarely `:` outside Medicare). - `element` - ISA byte 4 (1-indexed); separates the 16 ISA elements. - `repetition` - ISA-11 (byte 83, 1-indexed); separates repetitions inside an element. Carries the legacy Control Standards Identifier (typically `U`) for pre-005010 inputs; the parser surfaces it verbatim. - `component` - ISA-16 (byte 105, 1-indexed); separates sub-elements of a composite. Real-world senders use `:`, `\\`, `^`, `|`, and more. - `segment` - the byte immediately after ISA-16 (byte 106, 1-indexed); terminates each segment. Typically `~`, often followed by optional `\r\n` which is silently tolerated. #### Example ```ts import type { Delimiters } from "@cosyte/x12"; const medicare: Delimiters = { element: "*", repetition: "^", component: ":", segment: "~", }; ``` #### Properties ##### component > `readonly` **component**: `string` ##### element > `readonly` **element**: `string` ##### repetition > `readonly` **repetition**: `string` ##### segment > `readonly` **segment**: `string` *** ### FunctionalGroupSpec A single GS..GE functional group. The builder emits the GS header, each transaction set, then `GE{count}{control}` with GE-01 (the transaction count) computed for you. #### Example ```ts const group: FunctionalGroupSpec = { functionalIdCode: "HC", groupControlNumber: "1", versionRelease: "005010X222A2", transactions: [tx], }; ``` #### Properties ##### applicationReceiverCode? > `readonly` `optional` **applicationReceiverCode?**: `string` GS-03 - application receiver code. Defaults to the interchange receiver id. ##### applicationSenderCode? > `readonly` `optional` **applicationSenderCode?**: `string` GS-02 - application sender code. Defaults to the interchange sender id. ##### functionalIdCode > `readonly` **functionalIdCode**: `string` GS-01 - functional identifier code (`HC` claims, `HP` remittance, …). ##### groupControlNumber > `readonly` **groupControlNumber**: `string` GS-06 / GE-02 - group control number (echoed on GE-02). ##### groupDate? > `readonly` `optional` **groupDate?**: `string` GS-04 - group date CCYYMMDD. Defaults to the century-expanded ISA-09. ##### groupTime? > `readonly` `optional` **groupTime?**: `string` GS-05 - group time HHMM. Defaults to the interchange time (ISA-10). ##### responsibleAgencyCode? > `readonly` `optional` **responsibleAgencyCode?**: `string` GS-07 - responsible agency code. Defaults to `"X"` (ASC X12). ##### transactions > `readonly` **transactions**: readonly [`TransactionSetSpec`](#transactionsetspec)[] The ordered ST..SE transaction sets inside this group. ##### versionRelease > `readonly` **versionRelease**: `string` GS-08 - version / release / industry identifier code (e.g. `"005010X222A2"`). *** ### GeSegment The decoded GE functional group trailer. GE-01 is the transaction count, which reconciles against the number of ST/SE pairs in the group, and GE-02 the group control number, which reconciles against GS-06. `elements` is 1-indexed (`elements[0]` = `"GE"`, `elements[1]` = GE-01, `elements[2]` = GE-02). **Element values are stored RAW, pre-`?`-unescape**, so an element is the framed byte text of its slot and not necessarily the value the sender stated. #### Example ```ts import type { GeSegment } from "@cosyte/x12"; declare const ge: GeSegment; ge.raw; // verbatim segment text ge.elements[2]; // raw text of GE-02 (post-element-split, pre-?-unescape) ``` #### Properties ##### elements > `readonly` **elements**: readonly `string`[] ##### raw > `readonly` **raw**: `string` *** ### GsSegment The decoded GS functional group header. GS-01 is the functional ID code (`HC` for claims, `HP` for remittance, etc.), GS-06 the group control number, which reconciles against GE-02, and GS-08 the version / implementation convention reference (e.g. `005010X222A2`). `elements` is 1-indexed (`elements[0]` = `"GS"`, `elements[1]` = GS-01, ..., `elements[8]` = GS-08). **Element values are stored RAW, pre-`?`-unescape**, so an element is the framed byte text of its slot and not necessarily the value the sender stated. #### Example ```ts import type { GsSegment } from "@cosyte/x12"; declare const gs: GsSegment; gs.raw; // verbatim segment text gs.elements[6]; // raw text of GS-06 (post-element-split, pre-?-unescape) ``` #### Properties ##### elements > `readonly` **elements**: readonly `string`[] ##### raw > `readonly` **raw**: `string` *** ### IeaSegment The decoded IEA interchange trailer. IEA-01 is the group count and IEA-02 the interchange control number, which the envelope walker reconciles against ISA-13. `raw` is the exact segment string (without the segment terminator) and `elements` is 1-indexed (`elements[0]` = `"IEA"`, `elements[1]` = IEA-01, `elements[2]` = IEA-02). **Element values are stored RAW, pre-`?`-unescape**, so an element is the framed byte text of its slot and not necessarily the value the sender stated. #### Example ```ts import type { IeaSegment } from "@cosyte/x12"; declare const iea: IeaSegment; iea.raw; // verbatim segment text iea.elements[2]; // raw text of IEA-02 (post-element-split, pre-?-unescape) ``` #### Properties ##### elements > `readonly` **elements**: readonly `string`[] ##### raw > `readonly` **raw**: `string` *** ### InterchangeSpec The top-level spec for "./build-interchange.js".buildInterchange. The required fields name the interchange identity; everything else has a conformant default (`*^:~` delimiters, `ZZ` qualifiers, `P` usage indicator, `00501` version). #### Example ```ts const spec: InterchangeSpec = { senderId: "SENDER", receiverId: "RECEIVER", interchangeDate: "250101", interchangeTime: "1200", interchangeControlNumber: "000000001", groups: [group], }; ``` #### Properties ##### componentSeparator? > `readonly` `optional` **componentSeparator?**: `string` Component (sub-element) separator (ISA-16). Default `":"`. ##### elementSeparator? > `readonly` `optional` **elementSeparator?**: `string` Element separator (ISA byte 4). Default `"*"`. ##### groups > `readonly` **groups**: readonly [`FunctionalGroupSpec`](#functionalgroupspec)[] The ordered GS..GE functional groups inside this interchange. ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ISA-13 / IEA-02 - interchange control number (zero-padded to 9 on emit). ##### interchangeDate > `readonly` **interchangeDate**: `string` ISA-09 - interchange date YYMMDD. ##### interchangeTime > `readonly` **interchangeTime**: `string` ISA-10 - interchange time HHMM. ##### receiverId > `readonly` **receiverId**: `string` ISA-08 - interchange receiver id (padded to 15 on emit). ##### receiverQualifier? > `readonly` `optional` **receiverQualifier?**: `string` ISA-07 - interchange receiver qualifier. Default `"ZZ"`. ##### repetitionSeparator? > `readonly` `optional` **repetitionSeparator?**: `string` Repetition separator (ISA-11). Default `"^"`. ##### segmentTerminator? > `readonly` `optional` **segmentTerminator?**: `string` Segment terminator (ISA byte 106). Default `"~"`. ##### senderId > `readonly` **senderId**: `string` ISA-06 - interchange sender id (padded to 15 on emit). ##### senderQualifier? > `readonly` `optional` **senderQualifier?**: `string` ISA-05 - interchange sender qualifier. Default `"ZZ"`. ##### usageIndicator? > `readonly` `optional` **usageIndicator?**: `string` ISA-15 - usage indicator (`P` production, `T` test). Default `"P"`. ##### version? > `readonly` `optional` **version?**: `string` ISA-12 - interchange control version number. Default `"00501"`. *** ### IsaSegment The decoded ISA interchange header. ISA-12 is the interchange control version number, `00501` being the HIPAA-mandated baseline, and ISA-13 the interchange control number, which the envelope walker reconciles against IEA-02. `raw` preserves the exact 106-byte ISA + terminator string from input so round-trip serialization is byte-exact regardless of any lenient normalization downstream. `elements` is what splitting that span on the element separator produced, `elements[0]` being the literal `"ISA"` name placeholder. **Element values are stored RAW**, so an entry is byte text and not necessarily the value the sender stated. The 1-indexed mapping onto ISA-01..ISA-16 holds where that split produced exactly 17 entries; `X12_ISA_EXTRA_ELEMENT_SEPARATOR` reports where it did not, and `raw` carries all 106 bytes either way. #### Example ```ts import type { IsaSegment } from "@cosyte/x12"; declare const isa: IsaSegment; isa.raw; // verbatim 106-byte header isa.elements[13]; // raw text at index 13 of the ISA split ``` #### Properties ##### elements > `readonly` **elements**: readonly `string`[] ##### raw > `readonly` **raw**: `string` *** ### LoopSegmentSpec A segment slot inside a loop body. Carries the segment id, its TR3 usage + cardinality, and an optional position label (e.g. `"Loop 2300 #2"`) used by loop-walker diagnostics. Stored verbatim by `defineLoopSpec`; no validation beyond the structural shape so consumers can author payer-specific segment specs without re-deriving each rule. #### Example ```ts import type { LoopSegmentSpec } from "@cosyte/x12"; const clm: LoopSegmentSpec = { id: "CLM", usage: "required", max: 1 }; ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` ##### id > `readonly` **id**: `string` ##### max > `readonly` **max**: [`LoopMax`](#loopmax) ##### position? > `readonly` `optional` **position?**: `string` ##### usage > `readonly` **usage**: [`LoopUsage`](#loopusage) *** ### LoopSpec A declarative loop description used by the transaction extractors and consumer-authored payer profiles alike. Frozen by `defineLoopSpec` so a `LoopSpec` is safe to share across calls. #### Example ```ts import { defineLoopSpec } from "@cosyte/x12"; const Loop2300 = defineLoopSpec({ id: "2300", description: "837 Claim Information", trigger: "CLM", segments: [ { id: "CLM", usage: "required", max: 1 }, { id: "DTP", usage: "situational", max: ">1" }, { id: "HI", usage: "situational", max: ">1" }, ], }); ``` #### Properties ##### children > `readonly` **children**: readonly [`LoopSpec`](#loopspec)[] ##### description? > `readonly` `optional` **description?**: `string` ##### id > `readonly` **id**: `string` ##### segments > `readonly` **segments**: readonly [`LoopSegmentSpec`](#loopsegmentspec)[] ##### trigger > `readonly` **trigger**: `string` *** ### SerializeOptions Options accepted by [serializeX12](#serializex12). Every field is optional; `serializeX12(ix)` produces the byte-faithful reconstruction with no reconciliation. #### Remarks With `exactOptionalPropertyTypes: true`, do not pass `specClean: undefined` explicitly - omit the key instead. #### Example ```ts import { serializeX12, parseX12 } from "@cosyte/x12"; const ix = parseX12(raw); // Spec-clean emit that fixes any stale envelope counts: const out = serializeX12(ix, { specClean: true, recomputeCounts: true, onWarning: (w) => console.warn(w.code, w.message), }); ``` #### Properties ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) Invoked once per reconciliation warning (spec-clean mode only). ##### recomputeCounts? > `readonly` `optional` **recomputeCounts?**: `boolean` Substitute the recomputed SE-01 / GE-01 / IEA-01 counts into the emitted control segments (only meaningful with `specClean: true`). Default `false` - the serializer warns on a count mismatch but emits the model's verbatim value. Control NUMBERS are never rewritten regardless. ##### specClean? > `readonly` `optional` **specClean?**: `boolean` Reconcile envelope counts + control-number pairs and surface any mismatch via [onWarning](#onwarning). Default `false` (pure byte-faithful reconstruction, no reconciliation, no warnings). *** ### Ta1Segment A decoded envelope-level TA1 Interchange Acknowledgment segment. TA1 is NOT a transaction set - per the ASC X12 standard it lives at the envelope level, between ISA and the first GS, or alone inside an ISA..IEA with no GS at all (a TA1-only interchange). One interchange may carry multiple TA1 segments, each acknowledging a prior inbound interchange. TA1-01 is the interchange control number of the acknowledged interchange and the reassociation key. TA1-02 and TA1-03 are its interchange date (YYMMDD) and time (HHMM). TA1-04 is the Interchange Acknowledgment Code, code list I13 (`A` accepted, `E` accepted with errors, `R` rejected), and TA1-05 the Interchange Note Code, code list I18. `elements` is 1-indexed (`elements[0]` = `"TA1"`, `elements[1]` = TA1-01, ..., `elements[5]` = TA1-05). **Element values are stored RAW, pre-`?`-unescape**, so an element is the framed byte text of its slot and not necessarily the value the sender stated. The envelope walker captures TA1 segments here verbatim; the typed-ack model is built on top by `parseTA1`, whose five decoded fields are POST-`?`-unescape. `parseTA1` decodes the FIRST TA1 on an interchange, so on a multi-TA1 inbound it does not necessarily describe the segment you are holding. TA1 contains only structural control / disposition codes - by spec it carries NO PHI. #### Example ```ts import type { Ta1Segment } from "@cosyte/x12"; declare const ta1: Ta1Segment; ta1.raw; // verbatim segment text ta1.elements[1]; // raw text of TA1-01 (post-element-split, pre-?-unescape) ``` #### Properties ##### elements > `readonly` **elements**: readonly `string`[] ##### raw > `readonly` **raw**: `string` *** ### ToDateOptions The one option [toDate](#todate) takes, and the only way a zone reaches it. `assumeOffsetMinutes` is signed minutes east of UTC, so `0` means "treat this calendar day as UTC" and `-300` means UTC-5. It is the caller stating a fact this library does not have; without it there is no instant to return. #### Example ```ts import { toDate, type ToDateOptions } from "@cosyte/x12"; const easternStandard: ToDateOptions = { assumeOffsetMinutes: -300 }; toDate({ formatQualifier: "D8", value: "20260601" }, easternStandard); // 2026-06-01T05:00:00.000Z ``` #### Properties ##### assumeOffsetMinutes? > `readonly` `optional` **assumeOffsetMinutes?**: `number` *** ### TransactionSetSpec A single ST..SE transaction set. The builder emits `ST{idCode}{control}`, then each body [SegmentSpec](#segmentspec), then `SE{count}{control}` with the segment count (ST..SE inclusive) computed for you. #### Example ```ts const tx: TransactionSetSpec = { transactionSetIdCode: "837", transactionSetControlNumber: "0001", implementationConventionReference: "005010X222A2", segments: [["BHT", "0019", "00", "REF", "20250101", "1200", "CH"]], }; ``` #### Properties ##### implementationConventionReference? > `readonly` `optional` **implementationConventionReference?**: `string` ST-03 - implementation convention reference (optional, e.g. `"005010X222A2"`). ##### segments > `readonly` **segments**: readonly [`SegmentSpec`](#segmentspec)[] Body segments between ST and SE (excludes ST and SE themselves). ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ST-02 / SE-02 - transaction set control number (echoed on SE-02). ##### transactionSetIdCode > `readonly` **transactionSetIdCode**: `string` ST-01 - transaction set ID code (e.g. `"837"`, `"835"`, `"271"`). *** ### X12\_837ServiceLineDental 837D service line (SV3). Dental lines carry the ADA-coded procedure (qualifier `AD`, CDT code) plus optional tooth + surface detail captured on the per-line TOO segments (Loop 2400 in X224A2). #### Example ```ts import type { X12_837ServiceLineDental } from "@cosyte/x12"; declare const sl: X12_837ServiceLineDental; sl.procedureQualifier; // "AD" sl.procedureCode; // "D2391" composite resin sl.charge.toString(); // "180.00" sl.toothInformation[0]?.toothCode; // "14" sl.toothInformation[0]?.surfaces; // ["O"] ``` #### Extends - `X12_837ServiceLineBase` #### Properties ##### adjudications > `readonly` **adjudications**: readonly [`X12LineAdjudication`](#x12lineadjudication)[] ###### Inherited from `X12_837ServiceLineBase.adjudications` ##### amounts > `readonly` **amounts**: readonly [`X12ClaimAmount`](#x12claimamount)[] ###### Inherited from `X12_837ServiceLineBase.amounts` ##### charge > `readonly` **charge**: [`X12Decimal`](#x12decimal) \| `undefined` The service line's charge - SV1-02 (professional), SV2-03 (institutional) or SV3-02 (dental). `undefined` where this library decoded no value from that element: it was absent or empty, it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL`), or no SVx for the resolved variant was decoded onto the line at all (which emits `X12_837_SERVICE_LINE_NOT_DECODED` at the LX). Through `0.0.12` all three read `X12Decimal.ZERO`, and a consumer could not tell any of them from a line the sender charged nothing for. ###### Inherited from `X12_837ServiceLineBase.charge` ##### dates > `readonly` **dates**: readonly [`X12ClaimDate`](#x12claimdate)[] ###### Inherited from `X12_837ServiceLineBase.dates` ##### drug > `readonly` **drug**: [`X12LineDrug`](#x12linedrug) \| `undefined` ###### Inherited from `X12_837ServiceLineBase.drug` ##### lineNumber > `readonly` **lineNumber**: `string` ###### Inherited from `X12_837ServiceLineBase.lineNumber` ##### modifiers > `readonly` **modifiers**: readonly `string`[] ##### notes > `readonly` **notes**: readonly [`X12ClaimNote`](#x12claimnote)[] ###### Inherited from `X12_837ServiceLineBase.notes` ##### oralCavityArea > `readonly` **oralCavityArea**: readonly `string`[] ##### placeOfServiceCode > `readonly` **placeOfServiceCode**: `string` \| `undefined` ###### Inherited from `X12_837ServiceLineBase.placeOfServiceCode` ##### procedureCode > `readonly` **procedureCode**: `string` ##### procedureQualifier > `readonly` **procedureQualifier**: `string` ##### prosthesisCrownInlayCode > `readonly` **prosthesisCrownInlayCode**: `string` \| `undefined` ##### providers > `readonly` **providers**: readonly [`X12ClaimEntity`](#x12claimentity)[] ###### Inherited from `X12_837ServiceLineBase.providers` ##### references > `readonly` **references**: readonly [`X12ClaimReference`](#x12claimreference)[] ###### Inherited from `X12_837ServiceLineBase.references` ##### toothInformation > `readonly` **toothInformation**: readonly [`X12ToothInformation`](#x12toothinformation)[] ##### unitOfMeasure > `readonly` **unitOfMeasure**: `string` \| `undefined` ###### Inherited from `X12_837ServiceLineBase.unitOfMeasure` ##### units > `readonly` **units**: [`X12Decimal`](#x12decimal) \| `undefined` The service line's unit count - SV1-04 (professional), SV2-05 (institutional) or SV3-06 (dental). `undefined` on the same three conditions as [X12\_837ServiceLineBase.charge](#charge-4), and for the same reason: a fabricated count is a count the sender never sent. ###### Inherited from `X12_837ServiceLineBase.units` ##### variant > `readonly` **variant**: `"D"` *** ### X12\_837ServiceLineInstitutional 837I service line (SV2). Institutional lines lead with a `revenueCode` (SV2-01, NUBC 4-digit revenue code - what *kind* of service this is); the procedure code (HCPCS) and modifiers in SV2-02 are situational. `nonCoveredCharge` (SV2-07) is the portion of the line the provider has marked as not covered before the payer adjudicates. #### Example ```ts import type { X12_837ServiceLineInstitutional } from "@cosyte/x12"; declare const sl: X12_837ServiceLineInstitutional; sl.revenueCode; // "0260" IV therapy sl.procedureCode; // "J7030" sl.charge.toString(); // "780.00" sl.nonCoveredCharge?.toString();// "0.00" ``` #### Extends - `X12_837ServiceLineBase` #### Properties ##### adjudications > `readonly` **adjudications**: readonly [`X12LineAdjudication`](#x12lineadjudication)[] ###### Inherited from `X12_837ServiceLineBase.adjudications` ##### amounts > `readonly` **amounts**: readonly [`X12ClaimAmount`](#x12claimamount)[] ###### Inherited from `X12_837ServiceLineBase.amounts` ##### charge > `readonly` **charge**: [`X12Decimal`](#x12decimal) \| `undefined` The service line's charge - SV1-02 (professional), SV2-03 (institutional) or SV3-02 (dental). `undefined` where this library decoded no value from that element: it was absent or empty, it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL`), or no SVx for the resolved variant was decoded onto the line at all (which emits `X12_837_SERVICE_LINE_NOT_DECODED` at the LX). Through `0.0.12` all three read `X12Decimal.ZERO`, and a consumer could not tell any of them from a line the sender charged nothing for. ###### Inherited from `X12_837ServiceLineBase.charge` ##### dates > `readonly` **dates**: readonly [`X12ClaimDate`](#x12claimdate)[] ###### Inherited from `X12_837ServiceLineBase.dates` ##### drug > `readonly` **drug**: [`X12LineDrug`](#x12linedrug) \| `undefined` ###### Inherited from `X12_837ServiceLineBase.drug` ##### lineNumber > `readonly` **lineNumber**: `string` ###### Inherited from `X12_837ServiceLineBase.lineNumber` ##### modifiers > `readonly` **modifiers**: readonly `string`[] ##### nonCoveredCharge > `readonly` **nonCoveredCharge**: [`X12Decimal`](#x12decimal) \| `undefined` ##### notes > `readonly` **notes**: readonly [`X12ClaimNote`](#x12claimnote)[] ###### Inherited from `X12_837ServiceLineBase.notes` ##### placeOfServiceCode > `readonly` **placeOfServiceCode**: `string` \| `undefined` ###### Inherited from `X12_837ServiceLineBase.placeOfServiceCode` ##### procedureCode > `readonly` **procedureCode**: `string` \| `undefined` ##### procedureQualifier > `readonly` **procedureQualifier**: `string` \| `undefined` ##### providers > `readonly` **providers**: readonly [`X12ClaimEntity`](#x12claimentity)[] ###### Inherited from `X12_837ServiceLineBase.providers` ##### references > `readonly` **references**: readonly [`X12ClaimReference`](#x12claimreference)[] ###### Inherited from `X12_837ServiceLineBase.references` ##### revenueCode > `readonly` **revenueCode**: `string` ##### serviceLineRate > `readonly` **serviceLineRate**: [`X12Decimal`](#x12decimal) \| `undefined` ##### unitOfMeasure > `readonly` **unitOfMeasure**: `string` \| `undefined` ###### Inherited from `X12_837ServiceLineBase.unitOfMeasure` ##### units > `readonly` **units**: [`X12Decimal`](#x12decimal) \| `undefined` The service line's unit count - SV1-04 (professional), SV2-05 (institutional) or SV3-06 (dental). `undefined` on the same three conditions as [X12\_837ServiceLineBase.charge](#charge-4), and for the same reason: a fabricated count is a count the sender never sent. ###### Inherited from `X12_837ServiceLineBase.units` ##### variant > `readonly` **variant**: `"I"` *** ### X12\_837ServiceLineProfessional 837P service line (SV1). `procedureQualifier` is the HCPCS / CPT qualifier (typically `HC` for HCPCS / CPT-4; `ER` jurisdiction-specific procedure code; `IV` HIPPS-based rate code); `procedureCode` is the verbatim code; `modifiers` are the 1-4 procedure modifiers from SV1-01-3 through SV1-01-6 (e.g. `25`, `59`, `LT`, `RT`). `diagnosisPointers` carry up to 4 pointers (SV1-07's composite of positional indexes into the claim's HI diagnoses). The pointer `"1"` refers to the first principal-diagnosis HI composite, `"2"` the second, etc.; verbatim string preserved. #### Example ```ts import type { X12_837ServiceLineProfessional } from "@cosyte/x12"; declare const sl: X12_837ServiceLineProfessional; sl.procedureQualifier; // "HC" sl.procedureCode; // "99213" sl.modifiers; // ["25"] sl.diagnosisPointers; // ["1"] sl.placeOfServiceCode; // "11" office (overrides claim-level) sl.charge.toString(); // "150.00" ``` #### Extends - `X12_837ServiceLineBase` #### Properties ##### adjudications > `readonly` **adjudications**: readonly [`X12LineAdjudication`](#x12lineadjudication)[] ###### Inherited from `X12_837ServiceLineBase.adjudications` ##### amounts > `readonly` **amounts**: readonly [`X12ClaimAmount`](#x12claimamount)[] ###### Inherited from `X12_837ServiceLineBase.amounts` ##### charge > `readonly` **charge**: [`X12Decimal`](#x12decimal) \| `undefined` The service line's charge - SV1-02 (professional), SV2-03 (institutional) or SV3-02 (dental). `undefined` where this library decoded no value from that element: it was absent or empty, it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL`), or no SVx for the resolved variant was decoded onto the line at all (which emits `X12_837_SERVICE_LINE_NOT_DECODED` at the LX). Through `0.0.12` all three read `X12Decimal.ZERO`, and a consumer could not tell any of them from a line the sender charged nothing for. ###### Inherited from `X12_837ServiceLineBase.charge` ##### dates > `readonly` **dates**: readonly [`X12ClaimDate`](#x12claimdate)[] ###### Inherited from `X12_837ServiceLineBase.dates` ##### diagnosisPointers > `readonly` **diagnosisPointers**: readonly `string`[] ##### drug > `readonly` **drug**: [`X12LineDrug`](#x12linedrug) \| `undefined` ###### Inherited from `X12_837ServiceLineBase.drug` ##### emergencyIndicator > `readonly` **emergencyIndicator**: `string` \| `undefined` ##### epsdtIndicator > `readonly` **epsdtIndicator**: `string` \| `undefined` ##### familyPlanningIndicator > `readonly` **familyPlanningIndicator**: `string` \| `undefined` ##### lineNumber > `readonly` **lineNumber**: `string` ###### Inherited from `X12_837ServiceLineBase.lineNumber` ##### modifiers > `readonly` **modifiers**: readonly `string`[] ##### notes > `readonly` **notes**: readonly [`X12ClaimNote`](#x12claimnote)[] ###### Inherited from `X12_837ServiceLineBase.notes` ##### placeOfServiceCode > `readonly` **placeOfServiceCode**: `string` \| `undefined` ###### Inherited from `X12_837ServiceLineBase.placeOfServiceCode` ##### procedureCode > `readonly` **procedureCode**: `string` ##### procedureQualifier > `readonly` **procedureQualifier**: `string` ##### providers > `readonly` **providers**: readonly [`X12ClaimEntity`](#x12claimentity)[] ###### Inherited from `X12_837ServiceLineBase.providers` ##### references > `readonly` **references**: readonly [`X12ClaimReference`](#x12claimreference)[] ###### Inherited from `X12_837ServiceLineBase.references` ##### unitOfMeasure > `readonly` **unitOfMeasure**: `string` \| `undefined` ###### Inherited from `X12_837ServiceLineBase.unitOfMeasure` ##### units > `readonly` **units**: [`X12Decimal`](#x12decimal) \| `undefined` The service line's unit count - SV1-04 (professional), SV2-05 (institutional) or SV3-06 (dental). `undefined` on the same three conditions as [X12\_837ServiceLineBase.charge](#charge-4), and for the same reason: a fabricated count is a count the sender never sent. ###### Inherited from `X12_837ServiceLineBase.units` ##### variant > `readonly` **variant**: `"P"` *** ### X12\_837Submission The top-level result returned by `get837Claims()`. Carries the submitter (Loop 1000A) and receiver (Loop 1000B) parties, the HL hierarchy walk (Loops 2000A/B/C - every HL segment captured with parent-pointer provenance), every claim payment loop (Loop 2300), and every warning surfaced during the walk - including the safety-critical `X12_HL_PARENT_MISMATCH` and `X12_HL_PARENT_LEVEL_INVALID`. #### Example ```ts import { parseX12, get837Claims } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "837"); if (tx !== undefined) { const sub = get837Claims(ix.delimiters, tx); sub?.variant; // "P" | "I" | "D" | "unknown" sub?.claims.length; // count of CLM segments encountered for (const claim of sub?.claims ?? []) { claim.totalCharge.toString(); claim.diagnoses[0]?.codeSystem; // "ICD-10-CM" etc. } } ``` #### Properties ##### claims > `readonly` **claims**: readonly [`X12Claim`](#x12claim)[] ##### hierarchies > `readonly` **hierarchies**: readonly [`X12HierarchicalLevel`](#x12hierarchicallevel)[] ##### implementationConventionReference > `readonly` **implementationConventionReference**: `string` \| `undefined` ##### receiver > `readonly` **receiver**: [`X12ClaimEntity`](#x12claimentity) \| `undefined` ##### submitter > `readonly` **submitter**: [`X12ClaimEntity`](#x12claimentity) \| `undefined` ##### variant > `readonly` **variant**: [`X12Claim837Variant`](#x12claim837variant) ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12AaaCode One AAA code as it reached the reader: the VERBATIM inbound value, plus the bundled description where one resolves. `description` is `undefined` for a code outside the bundled snapshot, and the snapshots ship empty, so today it is `undefined` for every code. The code itself is never normalised, never defaulted and never dropped. #### Example ```ts import type { X12AaaCode } from "@cosyte/x12"; declare const c: X12AaaCode; c.code; // "42", exactly the bytes the payer sent c.description; // undefined (the bundled snapshot is empty) ``` #### Properties ##### code > `readonly` **code**: `string` ##### description > `readonly` **description**: `string` \| `undefined` *** ### X12AaaCondition One AAA request-validation segment, surfaced on the typed 271 result. **This is the distinction between "the payer rejected the inquiry" and "the member has no benefits".** Both used to read as an empty benefit collection; only the first produces an entry here. The reject reason and follow-up action codes are the payer's own, echoed verbatim, with a description only where the bundled snapshot has one. Only the two code positions are read, because only those two have a recorded source. Nothing else in the segment is assigned a meaning, and an element past them raises `X12_271_AAA_SEGMENT_MALFORMED` rather than being read. #### Example ```ts import type { X12AaaCondition } from "@cosyte/x12"; declare const c: X12AaaCondition; c.key.level; // "subscriber" c.rejectReasonCode?.code; // "42", verbatim c.followUpActionCode?.code; // "C", verbatim c.position.segmentIndex; // where in the transaction set to read it ``` #### Properties ##### followUpActionCode > `readonly` **followUpActionCode**: [`X12AaaCode`](#x12aaacode) \| `undefined` AAA-04, verbatim, or `undefined` where the payer stated none. ##### key > `readonly` **key**: [`X12AaaConditionKey`](#x12aaaconditionkey-1) ##### position > `readonly` **position**: [`X12Position`](#x12position) Where the segment sits, for a consumer reading `tx.segments` beside it. ##### rejectReasonCode > `readonly` **rejectReasonCode**: [`X12AaaCode`](#x12aaacode) \| `undefined` AAA-03, verbatim, or `undefined` where the payer stated none. *** ### X12AaaConditionKey Which loop occurrence an AAA was transmitted under. Three parts, and a consumer reads all three: - `level` - the hierarchical level, or `undefined` where no level of a named kind encloses the segment. Never guessed. - `hierarchyId` - the identifier the DOCUMENT assigns that loop (HL-01), as [X12Eligibility.hierarchies](#hierarchies-2) reports it, or `undefined` where the loop states none. Never synthesised. - `occurrenceIndex` - the zero-based index of that loop occurrence among the occurrences of the SAME level, counted DOCUMENT-WIDE in document order and never restarted per enclosing loop. The third dependent loop in a document is index 2 whether it is the first dependent of the second subscriber or the third dependent of the first. Where `level` is `undefined` the index counts within the AAA segments whose level is likewise unknown. It is always determinable and is never absent. A per-parent index is deliberately NOT what this is: it is not unique document-wide, so it could not stand in for `hierarchyId` where a loop states none. #### Example ```ts import type { X12AaaConditionKey } from "@cosyte/x12"; declare const k: X12AaaConditionKey; k.level; // "dependent" k.hierarchyId; // "4" (the HL-01 the document assigned) k.occurrenceIndex; // 1 (the second dependent loop in the document) ``` #### Properties ##### hierarchyId > `readonly` **hierarchyId**: `string` \| `undefined` ##### level > `readonly` **level**: [`X12AaaConditionLevel`](#x12aaaconditionlevel) \| `undefined` ##### occurrenceIndex > `readonly` **occurrenceIndex**: `number` *** ### X12Ack999 The decoded 999 Implementation Acknowledgment. Returned by `parse999(raw)`. Carries every loop the wire produced and the underlying parsed envelope (so byte-exact round-trip is reachable through `interchange.isa.raw`). `warnings` collects both envelope-level warnings (from `parseX12`) AND any 999-specific warnings (e.g. an unknown disposition code, an AK9 count mismatch). The set is additive - never throws on a real-world 999. #### Properties ##### ak1 > `readonly` **ak1**: [`X12Ack999Ak1`](#x12ack999ak1-1) ##### ak9 > `readonly` **ak9**: [`X12Ack999Ak9`](#x12ack999ak9-1) ##### interchange > `readonly` **interchange**: [`X12Interchange`](#x12interchange) ##### transactionResponses > `readonly` **transactionResponses**: readonly [`X12Ack999TransactionResponse`](#x12ack999transactionresponse)[] ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12Ack999Ak1 The decoded AK1 functional group response header (loop 1). - `functionalIdCode` - AK1-01: echoes the inbound GS-01 (e.g. `HC` for a claim group). - `groupControlNumber` - AK1-02: echoes the inbound GS-06. - `versionRelease` - AK1-03: echoes the inbound GS-08 (e.g. `005010X222A2`). Situational at the standard level, ALWAYS present in X231A1; surfaced as `string | undefined` to remain lenient on parse. #### Example ```ts import type { X12Ack999Ak1 } from "@cosyte/x12"; declare const ak1: X12Ack999Ak1; ak1.functionalIdCode; // "HC" ak1.versionRelease; // "005010X222A2" | undefined ``` #### Properties ##### functionalIdCode > `readonly` **functionalIdCode**: `string` ##### groupControlNumber > `readonly` **groupControlNumber**: `string` ##### versionRelease > `readonly` **versionRelease**: `string` \| `undefined` *** ### X12Ack999Ak2 The decoded AK2 transaction set response header (loop 2000). - `transactionSetIdCode` - AK2-01: echoes the inbound ST-01 (e.g. `837`, `270`). - `transactionSetControlNumber` - AK2-02: echoes the inbound ST-02. - `implementationConventionReference` - AK2-03: echoes the inbound ST-03 (the TR3 ID, e.g. `005010X222A2`); situational. #### Example ```ts import type { X12Ack999Ak2 } from "@cosyte/x12"; declare const ak2: X12Ack999Ak2; ak2.transactionSetIdCode; // "837" ``` #### Properties ##### implementationConventionReference > `readonly` **implementationConventionReference**: `string` \| `undefined` ##### transactionSetControlNumber > `readonly` **transactionSetControlNumber**: `string` ##### transactionSetIdCode > `readonly` **transactionSetIdCode**: `string` *** ### X12Ack999Ak9 The decoded AK9 functional group response trailer. - `disposition` - AK9-01: functional-group-level disposition. - `numberOfTransactionSets` - AK9-02: echoes the inbound GE-01. - `numberOfReceivedTransactionSets` - AK9-03: how many ST..SE pairs actually arrived. - `numberOfAcceptedTransactionSets` - AK9-04: how many were accepted. By construction `0 <= accepted <= received <= numberOfTransactionSets`. - `syntaxErrorCodes` - AK9-05..AK9-09: situational; up to five functional-group syntax error codes (code list 716). #### Properties ##### disposition > `readonly` **disposition**: [`X12AckDispositionCode`](#x12ackdispositioncode) ##### numberOfAcceptedTransactionSets > `readonly` **numberOfAcceptedTransactionSets**: `number` ##### numberOfReceivedTransactionSets > `readonly` **numberOfReceivedTransactionSets**: `number` ##### numberOfTransactionSets > `readonly` **numberOfTransactionSets**: `number` ##### syntaxErrorCodes > `readonly` **syntaxErrorCodes**: readonly `string`[] *** ### X12Ack999ElementNote One IK4 element-level error wrapped with its (optional) IK3-paired CTX context strings - surfaced verbatim because CTX uses a composite syntax the X231A1 implementer typically writes as `ELEMENT*NM1*8*1`. The library does not try to over-decompose the CTX value. #### Properties ##### contexts > `readonly` **contexts**: readonly `string`[] ##### ik4 > `readonly` **ik4**: [`X12Ack999Ik4`](#x12ack999ik4) *** ### X12Ack999Ik3 The decoded IK3 implementation data segment note (loop 2100). - `segmentIdCode` - IK3-01: the X12 segment identifier (e.g. `NM1`, `CLP`, `HL`). - `segmentPositionInTransactionSet` - IK3-02: 1-indexed position of the offending segment inside the inbound ST..SE. - `loopIdentifier` - IK3-03: situational; the loop identifier (e.g. `2010BA`) where the offending segment lives. - `syntaxErrorCode` - IK3-04: situational; one of [Ik304Code](#ik304code). `undefined` when the parent IK3 is purely a context wrapper for nested IK4s (the per-element error path). #### Example ```ts import type { X12Ack999SegmentNote } from "@cosyte/x12"; declare const note: X12Ack999SegmentNote; note.ik3.segmentIdCode; // "NM1" note.ik3.segmentPositionInTransactionSet; // 8 ``` #### Properties ##### loopIdentifier > `readonly` **loopIdentifier**: `string` \| `undefined` ##### segmentIdCode > `readonly` **segmentIdCode**: `string` ##### segmentPositionInTransactionSet > `readonly` **segmentPositionInTransactionSet**: `number` ##### syntaxErrorCode > `readonly` **syntaxErrorCode**: [`Ik304Code`](#ik304code) \| `undefined` *** ### X12Ack999Ik4 The decoded IK4 implementation data element note (loop 2110). - `position` - IK4-01: composite carrying element / component / repetition (1-indexed). - `dataElementReferenceNumber` - IK4-02: situational; the ASC X12 element reference number (e.g. `66` for NM1-08). - `syntaxErrorCode` - IK4-03: required; one of [Ik403Code](#ik403code). - `copyOfBadDataElement` - IK4-04: situational; the verbatim offending element value. By spec it may be omitted entirely; callers building a 999 SHOULD omit it whenever the offending bytes are PHI (medical IDs, names, dates). The library never auto-populates this field. #### Example ```ts import type { X12Ack999ElementNote } from "@cosyte/x12"; declare const note: X12Ack999ElementNote; note.ik4.syntaxErrorCode; // "7" (invalid code value) ``` #### Properties ##### copyOfBadDataElement > `readonly` **copyOfBadDataElement**: `string` \| `undefined` ##### dataElementReferenceNumber > `readonly` **dataElementReferenceNumber**: `string` \| `undefined` ##### position > `readonly` **position**: [`X12Ack999Ik4Position`](#x12ack999ik4position-1) ##### syntaxErrorCode > `readonly` **syntaxErrorCode**: [`Ik403Code`](#ik403code) *** ### X12Ack999Ik4Position Position of an element/component/repetition inside a parent segment for an IK4 element note. Element is 1-indexed (the TR3 convention); component and repetition are surfaced exactly as encoded in IK4-01's composite - see TR3 `005010X231A1` §IK4. #### Example ```ts import type { X12Ack999Ik4Position } from "@cosyte/x12"; const pos: X12Ack999Ik4Position = { element: 1, component: 2 }; ``` #### Properties ##### component > `readonly` **component**: `number` \| `undefined` ##### element > `readonly` **element**: `number` ##### repetition > `readonly` **repetition**: `number` \| `undefined` *** ### X12Ack999Ik5 The decoded IK5 implementation transaction set response trailer. - `disposition` - IK5-01: one of [X12AckDispositionCode](#x12ackdispositioncode). - `syntaxErrorCodes` - IK5-02..IK5-06: situational; up to five transaction-set syntax error codes (code list 718). #### Properties ##### disposition > `readonly` **disposition**: [`X12AckDispositionCode`](#x12ackdispositioncode) ##### syntaxErrorCodes > `readonly` **syntaxErrorCodes**: readonly `string`[] *** ### X12Ack999SegmentNote One IK3 segment-level error wrapped with its (optional) CTX context strings and any nested IK4 element-level errors. Lifting the IK3 / IK4 groups into nested arrays mirrors the X231A1 loop hierarchy exactly so the typed model maps 1:1 onto the wire shape. #### Properties ##### contexts > `readonly` **contexts**: readonly `string`[] ##### elementNotes > `readonly` **elementNotes**: readonly [`X12Ack999ElementNote`](#x12ack999elementnote)[] ##### ik3 > `readonly` **ik3**: [`X12Ack999Ik3`](#x12ack999ik3) *** ### X12Ack999TransactionResponse One AK2..IK5 transaction-set response inside a 999. #### Properties ##### ak2 > `readonly` **ak2**: [`X12Ack999Ak2`](#x12ack999ak2) ##### ik5 > `readonly` **ik5**: [`X12Ack999Ik5`](#x12ack999ik5) ##### segmentNotes > `readonly` **segmentNotes**: readonly [`X12Ack999SegmentNote`](#x12ack999segmentnote)[] *** ### X12AckTA1 The decoded TA1 Interchange Acknowledgment. - `interchangeControlNumber` - TA1-01: echoes the inbound ISA-13. - `interchangeDate` - TA1-02: YYMMDD (echoes inbound ISA-09). - `interchangeTime` - TA1-03: HHMM (echoes inbound ISA-10). - `ackCode` - TA1-04: [Ta1AckCode](#ta1ackcode). - `noteCode` - TA1-05: typed when the value is a known I18 code ([Ta1NoteCode](#ta1notecode)); `noteCodeRaw` carries the un-narrowed string alongside so unknown extensions survive. **A clause here said that string is preserved VERBATIM and is deleted, not reworded:** a later release made it post-unescape, and `raw.elements[5]` is the verbatim surface. - `noteCodeRaw` - the un-narrowed TA1-05 string. Equal to `noteCode` when the value is a known I18 code; equal to the decoded inbound text when not. - `raw` - the underlying envelope-level [Ta1Segment](#ta1segment), for byte-exact round-trip. **The five decoded fields are POST-`?`-unescape; `raw` is the verbatim byte surface and is not.** A later release moved the decoded half: before it, a released TA1-01 read back carrying its `?` and matched no ISA-13, while every dot-path read of the same element already unescaped. Read `raw.elements` when you want the bytes. #### Example ```ts import type { X12AckTA1 } from "@cosyte/x12"; declare const ta1: X12AckTA1; ta1.ackCode; // "A" | "E" | "R" ta1.noteCode; // "000" | "001" | ... ``` #### Properties ##### ackCode > `readonly` **ackCode**: [`Ta1AckCode`](#ta1ackcode) ##### interchangeControlNumber > `readonly` **interchangeControlNumber**: `string` ##### interchangeDate > `readonly` **interchangeDate**: `string` ##### interchangeTime > `readonly` **interchangeTime**: `string` ##### noteCode > `readonly` **noteCode**: [`Ta1NoteCode`](#ta1notecode) \| `undefined` ##### noteCodeRaw > `readonly` **noteCodeRaw**: `string` ##### raw > `readonly` **raw**: [`Ta1Segment`](#ta1segment) *** ### X12AdditionalInformationRequest A 277 request for additional information read by `get277RequestForAdditionalInformation`: the BHT header and every hierarchical level in document order, each carrying the entities and the claim-level requests sent under it. #### Example ```ts import { parseX12, get277RequestForAdditionalInformation } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions[0]; const rfai = tx === undefined ? undefined : get277RequestForAdditionalInformation(ix.delimiters, tx); rfai?.transactionType; // "request-for-additional-information" rfai?.levels[0]?.requests[0]?.statuses[0]?.codes[0]?.codeListQualifier; // "LOI" ``` #### Properties ##### header > `readonly` **header**: [`X12AdditionalInformationRequestHeader`](#x12additionalinformationrequestheader-1) \| `undefined` The BHT, or `undefined` where the transaction set carries none. ##### implementationConventionReference > `readonly` **implementationConventionReference**: `string` \| `undefined` ST-03, decoded of any release escape. ##### levels > `readonly` **levels**: readonly [`X12AdditionalInformationRequestLevel`](#x12additionalinformationrequestlevel)[] Every HL, in document order. Empty where the transaction set carries none. ##### transactionType > `readonly` **transactionType**: `"request-for-additional-information"` Always `"request-for-additional-information"`. Never a claim status label. ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] Every warning raised while reading, anchored at the segment it concerns. *** ### X12AdditionalInformationRequestAmount One AMT Monetary Amount Information, the amount exact. #### Example ```ts import type { X12AdditionalInformationRequestAmount } from "@cosyte/x12"; declare const a: X12AdditionalInformationRequestAmount; a.amount?.toString(); // AMT-02, exact ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) \| `undefined` AMT-02, the amount; `undefined` where absent or not a decimal. ##### qualifier > `readonly` **qualifier**: `string` \| `undefined` AMT-01, amount qualifier code. *** ### X12AdditionalInformationRequestClaim One claim-level request (the 2200 loop): the TRN that opens it, and the STC, REF, DTP, QTY and AMT segments and SVC service lines sent under it. A request opened by an STC or SVC with no TRN before it carries an empty `traces` list rather than an invented one. #### Example ```ts import type { X12AdditionalInformationRequestClaim } from "@cosyte/x12"; declare const r: X12AdditionalInformationRequestClaim; r.references.find((ref) => ref.qualifier === "1K")?.value; // payer claim control number r.statuses[0]?.codes[0]?.statusCode; // the requested item's code ``` #### Properties ##### amounts > `readonly` **amounts**: readonly [`X12AdditionalInformationRequestAmount`](#x12additionalinformationrequestamount)[] Every AMT, in document order. ##### dates > `readonly` **dates**: readonly [`X12AdditionalInformationRequestDate`](#x12additionalinformationrequestdate)[] Every claim-level DTP, in document order. ##### quantities > `readonly` **quantities**: readonly [`X12AdditionalInformationRequestQuantity`](#x12additionalinformationrequestquantity)[] Every QTY, in document order. ##### references > `readonly` **references**: readonly [`X12AdditionalInformationRequestReference`](#x12additionalinformationrequestreference)[] Every claim-level REF, in document order. ##### serviceLines > `readonly` **serviceLines**: readonly [`X12AdditionalInformationRequestServiceLine`](#x12additionalinformationrequestserviceline)[] Every SVC service line, in document order. ##### statuses > `readonly` **statuses**: readonly [`X12AdditionalInformationRequestStatus`](#x12additionalinformationrequeststatus)[] Every claim-level STC, in document order. ##### traces > `readonly` **traces**: readonly [`X12AdditionalInformationRequestTrace`](#x12additionalinformationrequesttrace)[] The TRN that opened this request; empty where an STC or SVC opened it. *** ### X12AdditionalInformationRequestDate One DTP date or period, verbatim: the value keeps exactly the precision and format the sender stated, and the format qualifier that says which is carried beside it. A partial date is never completed. #### Example ```ts import type { X12AdditionalInformationRequestDate } from "@cosyte/x12"; declare const d: X12AdditionalInformationRequestDate; d.formatQualifier; // "RD8" d.value; // "20260501-20260502" ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` \| `undefined` DTP-02, date time period format qualifier. ##### qualifier > `readonly` **qualifier**: `string` \| `undefined` DTP-01, date or time qualifier. ##### value > `readonly` **value**: `string` \| `undefined` DTP-03, the date or period, as sent. *** ### X12AdditionalInformationRequestEntity One NM1 Individual or Organizational Name, the elements a party is named and identified by, verbatim. #### Example ```ts import type { X12AdditionalInformationRequestEntity } from "@cosyte/x12"; declare const e: X12AdditionalInformationRequestEntity; e.entityIdentifierCode; // NM1-01 e.lastOrOrganizationName; // NM1-03 ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` \| `undefined` NM1-01, entity identifier code. ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` \| `undefined` NM1-02, entity type qualifier. ##### firstName > `readonly` **firstName**: `string` \| `undefined` NM1-04, first name. ##### idCode > `readonly` **idCode**: `string` \| `undefined` NM1-09, identification code. ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` NM1-08, identification code qualifier. ##### lastOrOrganizationName > `readonly` **lastOrOrganizationName**: `string` \| `undefined` NM1-03, last name or organization name. ##### middleName > `readonly` **middleName**: `string` \| `undefined` NM1-05, middle name. ##### namePrefix > `readonly` **namePrefix**: `string` \| `undefined` NM1-06, name prefix. ##### nameSuffix > `readonly` **nameSuffix**: `string` \| `undefined` NM1-07, name suffix. *** ### X12AdditionalInformationRequestHeader The BHT Beginning of Hierarchical Transaction, every element verbatim. #### Example ```ts import type { X12AdditionalInformationRequestHeader } from "@cosyte/x12"; declare const h: X12AdditionalInformationRequestHeader; h.referenceId; // BHT-03, as sent ``` #### Properties ##### date > `readonly` **date**: `string` \| `undefined` BHT-04, date, as sent. ##### hierarchicalStructureCode > `readonly` **hierarchicalStructureCode**: `string` \| `undefined` BHT-01, hierarchical structure code. ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` BHT-03, reference identification. ##### time > `readonly` **time**: `string` \| `undefined` BHT-05, time, as sent. ##### transactionSetPurposeCode > `readonly` **transactionSetPurposeCode**: `string` \| `undefined` BHT-02, transaction set purpose code. ##### transactionTypeCode > `readonly` **transactionTypeCode**: `string` \| `undefined` BHT-06, transaction type code. *** ### X12AdditionalInformationRequestLevel One HL hierarchical level, with the NM1 entities of its name loops and the claim-level requests sent under it. The HL elements are verbatim and are never re-numbered or re-parented; which level a code names is the sender's, because no carried source states the guide's level codes. #### Example ```ts import type { X12AdditionalInformationRequestLevel } from "@cosyte/x12"; declare const level: X12AdditionalInformationRequestLevel; level.levelCode; // HL-03, as sent level.entities[0]?.idCode; // NM1-09, as sent level.requests.length; // claim-level requests under this level ``` #### Properties ##### childCode > `readonly` **childCode**: `string` \| `undefined` HL-04, hierarchical child code. ##### entities > `readonly` **entities**: readonly [`X12AdditionalInformationRequestEntity`](#x12additionalinformationrequestentity)[] Every NM1 sent under this level before its first claim-level request. ##### id > `readonly` **id**: `string` \| `undefined` HL-01, hierarchical id number. ##### levelCode > `readonly` **levelCode**: `string` \| `undefined` HL-03, hierarchical level code. ##### parentId > `readonly` **parentId**: `string` \| `undefined` HL-02, hierarchical parent id number. ##### requests > `readonly` **requests**: readonly [`X12AdditionalInformationRequestClaim`](#x12additionalinformationrequestclaim)[] Every claim-level request sent under this level, in document order. *** ### X12AdditionalInformationRequestQuantity One QTY Quantity Information, the quantity exact. #### Example ```ts import type { X12AdditionalInformationRequestQuantity } from "@cosyte/x12"; declare const q: X12AdditionalInformationRequestQuantity; q.quantity?.toString(); // QTY-02, exact ``` #### Properties ##### qualifier > `readonly` **qualifier**: `string` \| `undefined` QTY-01, quantity qualifier. ##### quantity > `readonly` **quantity**: [`X12Decimal`](#x12decimal) \| `undefined` QTY-02, the quantity; `undefined` where absent or not a decimal. *** ### X12AdditionalInformationRequestReference One REF Reference Information, verbatim. A REF short of either element is still carried, with the absent one `undefined`. #### Example ```ts import type { X12AdditionalInformationRequestReference } from "@cosyte/x12"; declare const r: X12AdditionalInformationRequestReference; r.qualifier; // REF-01 r.value; // REF-02 ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` REF-03, description. ##### qualifier > `readonly` **qualifier**: `string` \| `undefined` REF-01, reference identification qualifier. ##### value > `readonly` **value**: `string` \| `undefined` REF-02, reference identification. *** ### X12AdditionalInformationRequestServiceLine One SVC service line (the 2220 loop) and the STC, REF and DTP segments sent under it. #### Example ```ts import type { X12AdditionalInformationRequestServiceLine } from "@cosyte/x12"; declare const l: X12AdditionalInformationRequestServiceLine; l.procedureCode; // SVC-01-2 l.lineChargeAmount?.toString(); // SVC-02, exact ``` #### Properties ##### dates > `readonly` **dates**: readonly [`X12AdditionalInformationRequestDate`](#x12additionalinformationrequestdate)[] Every DTP sent under this line. ##### lineChargeAmount > `readonly` **lineChargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-02, line charge amount. ##### linePaymentAmount > `readonly` **linePaymentAmount**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-03, line payment amount. ##### modifiers > `readonly` **modifiers**: readonly `string`[] SVC-01-3 to SVC-01-6, the modifiers present, in order. ##### procedureCode > `readonly` **procedureCode**: `string` \| `undefined` SVC-01-2, the procedure or service code. ##### quantity > `readonly` **quantity**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-05, quantity. ##### references > `readonly` **references**: readonly [`X12AdditionalInformationRequestReference`](#x12additionalinformationrequestreference)[] Every REF sent under this line. ##### revenueCode > `readonly` **revenueCode**: `string` \| `undefined` SVC-04, revenue code. ##### serviceIdQualifier > `readonly` **serviceIdQualifier**: `string` \| `undefined` SVC-01-1, product or service id qualifier. ##### statuses > `readonly` **statuses**: readonly [`X12AdditionalInformationRequestStatus`](#x12additionalinformationrequeststatus)[] Every STC sent under this line. ##### unitsOfService > `readonly` **unitsOfService**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-07, original units of service count. *** ### X12AdditionalInformationRequestStatus One STC Status Information segment: its up to three C043 composites (STC-01, STC-10, STC-11) and its other elements, verbatim. #### Example ```ts import type { X12AdditionalInformationRequestStatus } from "@cosyte/x12"; declare const s: X12AdditionalInformationRequestStatus; s.codes[0]?.categoryCode; // C043-01 of STC-01 s.totalChargeAmount?.toString(); // STC-04, exact ``` #### Properties ##### actionCode > `readonly` **actionCode**: `string` \| `undefined` STC-03, action code. ##### checkIssueDate > `readonly` **checkIssueDate**: `string` \| `undefined` STC-08, check issue date, as sent. ##### checkNumber > `readonly` **checkNumber**: `string` \| `undefined` STC-09, check number. ##### codes > `readonly` **codes**: readonly [`X12AdditionalInformationRequestStatusCode`](#x12additionalinformationrequeststatuscode)[] The C043 composites present, STC-01 first, then STC-10 and STC-11. ##### message > `readonly` **message**: `string` \| `undefined` STC-12, free-form message text. ##### paymentAmount > `readonly` **paymentAmount**: [`X12Decimal`](#x12decimal) \| `undefined` STC-05, amount paid. ##### paymentDate > `readonly` **paymentDate**: `string` \| `undefined` STC-06, paid date, as sent. ##### paymentMethodCode > `readonly` **paymentMethodCode**: `string` \| `undefined` STC-07, payment method code. ##### statusEffectiveDate > `readonly` **statusEffectiveDate**: `string` \| `undefined` STC-02, the status effective date, as sent. ##### totalChargeAmount > `readonly` **totalChargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` STC-04, total charge amount. *** ### X12AdditionalInformationRequestStatusCode One C043 Health Care Claim Status composite, all four components verbatim. C043-04 names the code source of C043-02. Where it is non-empty, C043-02 is a code from that source (a LOINC code naming the attachment requested, say) and is NOT looked up in the claim status code list, so `statusDescription` is `undefined` and no unknown-claim-status warning is raised for it. Where it is empty, C043-02 is a claim status code and is described from the bundled list when the list has it. C043-01 is always a claim status category code. #### Example ```ts import type { X12AdditionalInformationRequestStatusCode } from "@cosyte/x12"; declare const c: X12AdditionalInformationRequestStatusCode; c.codeListQualifier; // "LOI" where C043-02 is a LOINC code c.statusDescription; // undefined whenever codeListQualifier is present ``` #### Properties ##### categoryCode > `readonly` **categoryCode**: `string` \| `undefined` C043-01, the claim status category code. ##### categoryDescription > `readonly` **categoryDescription**: `string` \| `undefined` The bundled description of C043-01, where the list has it. ##### codeListQualifier > `readonly` **codeListQualifier**: `string` \| `undefined` C043-04, the code list qualifier naming the source of C043-02. ##### entityCode > `readonly` **entityCode**: `string` \| `undefined` C043-03, entity identifier code. ##### statusCode > `readonly` **statusCode**: `string` \| `undefined` C043-02, a code from the source C043-04 names, or a claim status code. ##### statusDescription > `readonly` **statusDescription**: `string` \| `undefined` The claim status description of C043-02; only where C043-04 is empty. *** ### X12AdditionalInformationRequestTrace One TRN Trace, verbatim. #### Example ```ts import type { X12AdditionalInformationRequestTrace } from "@cosyte/x12"; declare const t: X12AdditionalInformationRequestTrace; t.referenceId; // TRN-02 ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` TRN-03, originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` TRN-02, reference identification. ##### supplementalReferenceId > `readonly` **supplementalReferenceId**: `string` \| `undefined` TRN-04, supplemental reference identification. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` \| `undefined` TRN-01, trace type code. *** ### X12Attachment One BDS Binary Data Structure, read as one attachment. `lengthVerified` is `true` only where the parser framed BDS-03 by a valid BDS-02 count, the count was met exactly, the segment ended where the count said, and every character is one octet. Where any of those failed, the matching binary framing warning is on the reading's `warnings`, anchored at this BDS, and `lengthVerified` is `false`, so a caller can tell a length-verified attachment from the others without reading the warnings. BDS-02 is then still exactly what was sent, and the data is only the octets that were present. #### Example ```ts import type { X12Attachment } from "@cosyte/x12"; declare const a: X12Attachment; if (a.lengthVerified) a.data.readOctets(); // exactly BDS-02 octets ``` #### Properties ##### data > `readonly` **data**: [`X12AttachmentData`](#x12attachmentdata) BDS-03, withheld from printing; `readOctets()` returns it verbatim. ##### declaredLength > `readonly` **declaredLength**: `string` \| `undefined` BDS-02, the declared length, exactly as sent. ##### filterCode > `readonly` **filterCode**: `string` \| `undefined` BDS-01, the filter id code, as sent. Not applied. ##### lengthVerified > `readonly` **lengthVerified**: `boolean` Whether the data's length was verified against BDS-02 with no framing warning. ##### line > `readonly` **line**: [`X12AttachmentLine`](#x12attachmentline-1) \| `undefined` The LX line this BDS was sent under, or `undefined` before the first LX. ##### segmentIndex > `readonly` **segmentIndex**: `number` Where the BDS sits, transaction-relative, the ST being segment 0. *** ### X12AttachmentBeginning The heading's BGN Beginning Segment, verbatim. #### Example ```ts import type { X12AttachmentBeginning } from "@cosyte/x12"; declare const b: X12AttachmentBeginning; b.referenceId; // BGN-02 ``` #### Properties ##### actionCode > `readonly` **actionCode**: `string` \| `undefined` BGN-08, action code. ##### date > `readonly` **date**: `string` \| `undefined` BGN-03, date, as sent. ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` BGN-02, reference identification. ##### secondReferenceId > `readonly` **secondReferenceId**: `string` \| `undefined` BGN-06, a second reference identification. ##### securityLevelCode > `readonly` **securityLevelCode**: `string` \| `undefined` BGN-09, security level code. ##### time > `readonly` **time**: `string` \| `undefined` BGN-04, time, as sent. ##### timeCode > `readonly` **timeCode**: `string` \| `undefined` BGN-05, time code. ##### transactionSetPurposeCode > `readonly` **transactionSetPurposeCode**: `string` \| `undefined` BGN-01, transaction set purpose code. ##### transactionTypeCode > `readonly` **transactionTypeCode**: `string` \| `undefined` BGN-07, transaction type code. *** ### X12AttachmentEntity One NM1 of the heading, verbatim. #### Example ```ts import type { X12AttachmentEntity } from "@cosyte/x12"; declare const e: X12AttachmentEntity; e.entityIdentifierCode; // NM1-01 ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` \| `undefined` NM1-01, entity identifier code. ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` \| `undefined` NM1-02, entity type qualifier. ##### firstName > `readonly` **firstName**: `string` \| `undefined` NM1-04, first name. ##### idCode > `readonly` **idCode**: `string` \| `undefined` NM1-09, identification code. ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` NM1-08, identification code qualifier. ##### lastOrOrganizationName > `readonly` **lastOrOrganizationName**: `string` \| `undefined` NM1-03, last name or organization name. ##### middleName > `readonly` **middleName**: `string` \| `undefined` NM1-05, middle name. ##### namePrefix > `readonly` **namePrefix**: `string` \| `undefined` NM1-06, name prefix. ##### nameSuffix > `readonly` **nameSuffix**: `string` \| `undefined` NM1-07, name suffix. *** ### X12AttachmentLine One LX line: its number and the TRN, STC and REF segments sent under it, verbatim. The attachments sent under it point back at it through [X12Attachment.line](#line). #### Example ```ts import type { X12AttachmentLine } from "@cosyte/x12"; declare const line: X12AttachmentLine; line.lineNumber; // LX-01 line.statuses[0]?.codes[0]?.statusCode; // the requested item's code, echoed ``` #### Properties ##### lineNumber > `readonly` **lineNumber**: `string` \| `undefined` LX-01, assigned number. ##### references > `readonly` **references**: readonly [`X12AttachmentReference`](#x12attachmentreference)[] Every REF sent under this line. ##### statuses > `readonly` **statuses**: readonly [`X12AttachmentStatus`](#x12attachmentstatus)[] Every STC sent under this line. ##### traces > `readonly` **traces**: readonly [`X12AttachmentTrace`](#x12attachmenttrace)[] Every TRN sent under this line. *** ### X12AttachmentReference One REF, verbatim. #### Example ```ts import type { X12AttachmentReference } from "@cosyte/x12"; declare const r: X12AttachmentReference; r.value; // REF-02 ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` REF-03, description. ##### qualifier > `readonly` **qualifier**: `string` \| `undefined` REF-01, reference identification qualifier. ##### value > `readonly` **value**: `string` \| `undefined` REF-02, reference identification. *** ### X12AttachmentStatus One STC, its C043 composites verbatim. Nothing is looked up or described: on a 275 the STC echoes the item a request named, and C043-02 is whatever code source C043-04 names. #### Example ```ts import type { X12AttachmentStatus } from "@cosyte/x12"; declare const s: X12AttachmentStatus; s.codes[0]?.codeListQualifier; // C043-04 of STC-01 ``` #### Properties ##### actionCode > `readonly` **actionCode**: `string` \| `undefined` STC-03, action code. ##### checkIssueDate > `readonly` **checkIssueDate**: `string` \| `undefined` STC-08, check issue date, as sent. ##### checkNumber > `readonly` **checkNumber**: `string` \| `undefined` STC-09, check number. ##### codes > `readonly` **codes**: readonly [`X12AttachmentStatusCode`](#x12attachmentstatuscode)[] The C043 composites present, STC-01 first, then STC-10 and STC-11. ##### message > `readonly` **message**: `string` \| `undefined` STC-12, free-form message text. ##### paymentAmount > `readonly` **paymentAmount**: [`X12Decimal`](#x12decimal) \| `undefined` STC-05, amount paid. ##### paymentDate > `readonly` **paymentDate**: `string` \| `undefined` STC-06, paid date, as sent. ##### paymentMethodCode > `readonly` **paymentMethodCode**: `string` \| `undefined` STC-07, payment method code. ##### statusEffectiveDate > `readonly` **statusEffectiveDate**: `string` \| `undefined` STC-02, status effective date, as sent. ##### totalChargeAmount > `readonly` **totalChargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` STC-04, total charge amount. *** ### X12AttachmentStatusCode One C043 composite of a 275 STC, all four components verbatim. #### Example ```ts import type { X12AttachmentStatusCode } from "@cosyte/x12"; declare const c: X12AttachmentStatusCode; c.statusCode; // C043-02 ``` #### Properties ##### categoryCode > `readonly` **categoryCode**: `string` \| `undefined` C043-01. ##### codeListQualifier > `readonly` **codeListQualifier**: `string` \| `undefined` C043-04, naming the code source of C043-02. ##### entityCode > `readonly` **entityCode**: `string` \| `undefined` C043-03. ##### statusCode > `readonly` **statusCode**: `string` \| `undefined` C043-02. *** ### X12AttachmentSubmission A 275 read by `get275Attachments`: the heading's BGN and names, every LX line, and every BDS as one attachment, in document order. #### Example ```ts import { parseX12, get275Attachments } from "@cosyte/x12"; const ix = parseX12(buffer); const tx = ix.groups[0]?.transactions[0]; const reading = tx === undefined ? undefined : get275Attachments(ix.delimiters, tx); for (const attachment of reading?.attachments ?? []) { attachment.filterCode; // BDS-01, e.g. "B64" attachment.declaredLength; // BDS-02, as sent attachment.lengthVerified; // false where a framing warning was raised attachment.line?.lineNumber; // LX-01 of the line it was sent under } ``` #### Properties ##### attachments > `readonly` **attachments**: readonly [`X12Attachment`](#x12attachment)[] Every BDS, in document order. Empty where the transaction set carries none. ##### beginning > `readonly` **beginning**: [`X12AttachmentBeginning`](#x12attachmentbeginning) \| `undefined` The heading's BGN, or `undefined` where the transaction set carries none. ##### entities > `readonly` **entities**: readonly [`X12AttachmentEntity`](#x12attachmententity)[] Every NM1 in the heading, before the first LX, in document order. ##### implementationConventionReference > `readonly` **implementationConventionReference**: `string` \| `undefined` ST-03, decoded of any release escape. ##### lines > `readonly` **lines**: readonly [`X12AttachmentLine`](#x12attachmentline-1)[] Every LX line, in document order. ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] Every warning raised while reading, including each BDS's framing warnings. *** ### X12AttachmentTrace One TRN, verbatim. #### Example ```ts import type { X12AttachmentTrace } from "@cosyte/x12"; declare const t: X12AttachmentTrace; t.referenceId; // TRN-02 ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` TRN-03, originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` TRN-02, reference identification. ##### supplementalReferenceId > `readonly` **supplementalReferenceId**: `string` \| `undefined` TRN-04, supplemental reference identification. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` \| `undefined` TRN-01, trace type code. *** ### X12AuthDate A DTP date / date-range attached to a review item. `qualifier` is DTP-01 (e.g. `435` admission, `472` service); `value` is DTP-03 in the DTP-02 format (`D8` `CCYYMMDD` / `RD8` range). #### Example ```ts import type { X12AuthDate } from "@cosyte/x12"; declare const d: X12AuthDate; d.qualifier; // "472" d.formatQualifier; // "D8" d.value; // "20260601" ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12AuthDiagnosis One diagnosis from an HI segment (Loop 2000E). `qualifier` (HI-0x-01) is the X12 code-source qualifier (e.g. `ABK` ICD-10-CM principal); `code` (HI-0x-02) is the diagnosis code. `codeSystem` resolves the qualifier against the bundled `HI_QUALIFIERS` snapshot - `"unknown"` (with an `X12_UNKNOWN_HI_QUALIFIER` warning) when the qualifier is outside it. The verbatim qualifier + code are always preserved. #### Example ```ts import type { X12AuthDiagnosis } from "@cosyte/x12"; declare const dx: X12AuthDiagnosis; dx.qualifier; // "ABK" dx.code; // "E1165" dx.codeSystem; // "ICD-10-CM" ``` #### Properties ##### code > `readonly` **code**: `string` ##### codeSystem > `readonly` **codeSystem**: [`X12HiCodeSystem`](#x12hicodesystem) ##### qualifier > `readonly` **qualifier**: `string` *** ### X12AuthEntity A non-person entity (UMO in Loop 2010A, requester in Loop 2010B, or a provider attached to a review). Decoded from an NM1 - name + identifier, no demographics. #### Example ```ts import type { X12AuthEntity } from "@cosyte/x12"; declare const e: X12AuthEntity; e.entityIdentifierCode; // "X3" (UMO) / "1P" (provider) e.name; // "UTILIZATION REVIEW CO" ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### name > `readonly` **name**: `string` *** ### X12AuthHeader The BHT beginning-of-hierarchical-transaction header. `purposeCode` (BHT-02) is `00` (original) / `18` (reissue); `transactionTypeCode` (BHT-06) distinguishes a request (`RT` / cancel) from administrative variants. #### Example ```ts import type { X12AuthHeader } from "@cosyte/x12"; declare const h: X12AuthHeader; h.referenceId; // "AUTH-202606" h.date; // "20260601" (BHT-04, CCYYMMDD) ``` #### Properties ##### date > `readonly` **date**: `string` \| `undefined` ##### purposeCode > `readonly` **purposeCode**: `string` \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` ##### structurePurposeCode > `readonly` **structurePurposeCode**: `string` ##### time > `readonly` **time**: `string` \| `undefined` ##### transactionTypeCode > `readonly` **transactionTypeCode**: `string` \| `undefined` *** ### X12AuthMember A person (subscriber Loop 2010C / dependent Loop 2010D) decoded from NM1 + the optional DMG demographics. `idCode` (NM1-09) is the member identifier - synthetic-only in fixtures. #### Example ```ts import type { X12AuthMember } from "@cosyte/x12"; declare const m: X12AuthMember; m.lastName; // "DOE" m.dateOfBirth; // "19850515" (DMG-02, CCYYMMDD) ``` #### Properties ##### dateOfBirth > `readonly` **dateOfBirth**: `string` \| `undefined` ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` ##### firstName > `readonly` **firstName**: `string` \| `undefined` ##### genderCode > `readonly` **genderCode**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### lastName > `readonly` **lastName**: `string` \| `undefined` ##### middleName > `readonly` **middleName**: `string` \| `undefined` ##### suffix > `readonly` **suffix**: `string` \| `undefined` *** ### X12AuthReference A REF supplemental identifier attached to a review item. `qualifier` is REF-01; `value` is REF-02. #### Example ```ts import type { X12AuthReference } from "@cosyte/x12"; declare const r: X12AuthReference; r.qualifier; // "BB" (authorization number) r.value; // "PRIORAUTH-1" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12AuthTrace A reassociation trace (TRN). A 278 request carries a trace the response echoes verbatim so the requester can re-associate the certification outcome with the request it sent - the walker NEVER mutates it. #### Example ```ts import type { X12AuthTrace } from "@cosyte/x12"; declare const t: X12AuthTrace; t.traceTypeCode; // "1" (current transaction) t.referenceId; // "AUTHREQ-202606-0001" ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` ##### supplementalReferenceId > `readonly` **supplementalReferenceId**: `string` \| `undefined` ##### traceTypeCode > `readonly` **traceTypeCode**: `string` *** ### X12Claim Decoded CLM claim header + every claim-scoped loop. The `variant` mirrors the parent submission's variant; the `serviceLines` discriminate on the same variant so a consumer can do a single switch. PHI surface: `claimId` (provider's patient-account number), the subscriber/patient NM1 entities, member ID on subscriber/patient, NTE notes. All surfaced verbatim; warnings never echo values. #### Example ```ts import type { X12Claim } from "@cosyte/x12"; declare const c: X12Claim; c.variant; // "P" / "I" / "D" / "unknown" c.claimId; // CLM-01 (patient account number) c.totalCharge?.toString(); // CLM-02 as X12Decimal, or undefined if none decoded c.diagnoses[0]?.codeSystem; // "ICD-10-CM" c.serviceLines.length; // count of LX/SVx loops ``` #### Properties ##### amounts > `readonly` **amounts**: readonly [`X12ClaimAmount`](#x12claimamount)[] ##### benefitsAssignment > `readonly` **benefitsAssignment**: `string` \| `undefined` ##### billingProvider > `readonly` **billingProvider**: [`X12ClaimEntity`](#x12claimentity) \| `undefined` ##### claimFrequencyCode > `readonly` **claimFrequencyCode**: `string` \| `undefined` ##### claimId > `readonly` **claimId**: `string` ##### dates > `readonly` **dates**: readonly [`X12ClaimDate`](#x12claimdate)[] ##### diagnoses > `readonly` **diagnoses**: readonly [`X12ClaimHiCode`](#x12claimhicode)[] ##### facilityCodeQualifier > `readonly` **facilityCodeQualifier**: `string` \| `undefined` ##### hierarchy > `readonly` **hierarchy**: [`X12HierarchicalLevel`](#x12hierarchicallevel) \| `undefined` ##### notes > `readonly` **notes**: readonly [`X12ClaimNote`](#x12claimnote)[] ##### otherHi > `readonly` **otherHi**: readonly [`X12ClaimHiCode`](#x12claimhicode)[] ##### otherSubscribers > `readonly` **otherSubscribers**: readonly [`X12OtherSubscriber`](#x12othersubscriber)[] ##### patient > `readonly` **patient**: [`X12ClaimMember`](#x12claimmember) \| `undefined` ##### payer > `readonly` **payer**: [`X12ClaimEntity`](#x12claimentity) \| `undefined` ##### payToAddress > `readonly` **payToAddress**: [`X12ClaimAddress`](#x12claimaddress) \| `undefined` ##### payToPlan > `readonly` **payToPlan**: [`X12ClaimEntity`](#x12claimentity) \| `undefined` ##### placeOfServiceCode > `readonly` **placeOfServiceCode**: `string` \| `undefined` ##### procedures > `readonly` **procedures**: readonly [`X12ClaimHiCode`](#x12claimhicode)[] ##### providerAcceptAssignment > `readonly` **providerAcceptAssignment**: `string` \| `undefined` ##### providers > `readonly` **providers**: readonly [`X12ClaimEntity`](#x12claimentity)[] ##### providerSignatureOnFile > `readonly` **providerSignatureOnFile**: `string` \| `undefined` ##### references > `readonly` **references**: readonly [`X12ClaimReference`](#x12claimreference)[] ##### releaseOfInformationCode > `readonly` **releaseOfInformationCode**: `string` \| `undefined` ##### serviceLines > `readonly` **serviceLines**: readonly [`X12_837ServiceLine`](#x12_837serviceline)[] ##### subscriber > `readonly` **subscriber**: [`X12ClaimMember`](#x12claimmember) \| `undefined` ##### totalCharge > `readonly` **totalCharge**: [`X12Decimal`](#x12decimal) \| `undefined` CLM-02, the total claim charge. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from a claim whose charge really is zero. ##### variant > `readonly` **variant**: [`X12Claim837Variant`](#x12claim837variant) *** ### X12ClaimAddress Decoded N3 + N4 address block attached to an entity. Same shape as the 835's address (intentional - symmetry across helpers). All fields verbatim, no normalization. #### Example ```ts import type { X12ClaimAddress } from "@cosyte/x12"; declare const a: X12ClaimAddress; a.lines[0]; // "123 PROVIDER WAY" a.city; // "CLEVELAND" a.state; // "OH" a.postalCode; // "44113" ``` #### Properties ##### city > `readonly` **city**: `string` \| `undefined` ##### countryCode > `readonly` **countryCode**: `string` \| `undefined` ##### lines > `readonly` **lines**: readonly `string`[] ##### postalCode > `readonly` **postalCode**: `string` \| `undefined` ##### state > `readonly` **state**: `string` \| `undefined` *** ### X12ClaimAmount Decoded AMT segment - supplemental claim amount (patient-paid amount, coverage amount, etc.). `qualifier` from X12 522. Surfaced verbatim; never folded into a computed total (the 837 has no on-spec balance invariant analogous to the 835's CLP balance). #### Example ```ts import type { X12ClaimAmount } from "@cosyte/x12"; declare const a: X12ClaimAmount; a.qualifier; // "F5" patient amount paid a.amount.toString(); // "25.00" ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) ##### qualifier > `readonly` **qualifier**: `string` *** ### X12ClaimContact Decoded PER contact segment. `contactFunctionCode` = `IC` (Information Contact), `BL` (Technical), `AP` (Accounts Payable Contact); each carries up to 3 communication channels (`TE` telephone, `EM` email, `FX` fax, `EX` extension). #### Example ```ts import type { X12ClaimContact } from "@cosyte/x12"; declare const c: X12ClaimContact; c.contactFunctionCode; // "IC" c.communications[0]?.qualifier; // "TE" c.communications[0]?.value; // "5551234567" ``` #### Properties ##### communications > `readonly` **communications**: readonly `object`[] ##### contactFunctionCode > `readonly` **contactFunctionCode**: `string` ##### name > `readonly` **name**: `string` \| `undefined` *** ### X12ClaimDate Decoded DTP date - claim-level or service-line-level. `formatQualifier` is DTP-02 (`D8` single-date CCYYMMDD; `RD8` date range CCYYMMDD-CCYYMMDD). `value` is DTP-03 verbatim - the parser never normalizes the literal. Date-qualifier vocabulary on 837 (X12 374): `472` Service Date, `434` Statement Date, `435` Admission Date (837I), `096` Discharge Date (837I), `431` Onset of Current Illness, `454` Initial Treatment Date, `297` Last Worked Date, etc. #### Example ```ts import type { X12ClaimDate } from "@cosyte/x12"; declare const d: X12ClaimDate; d.qualifier; // "472" d.formatQualifier; // "D8" d.value; // "20260601" ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12ClaimEntity Generic NM1 entity used throughout the 837. Covers billing provider (`85`), pay-to address (`87`), submitter (`41`), receiver (`40`), subscriber name (`IL`), patient name (`QC`), payer (`PR`), and the long tail of 2310 / 2330 / 2420 provider / payer / facility roles. The `entityIdentifierCode` discriminates the role; consumers branching on roles should compare against the X12 0098 code list. PHI surface: `lastName`/`firstName`/`idCode` carry PHI when the role is a person (subscriber, patient, rendering provider as an individual). Surfaced verbatim - the parser never echoes them in warnings. #### Example ```ts import type { X12ClaimEntity } from "@cosyte/x12"; declare const e: X12ClaimEntity; e.entityIdentifierCode; // "85" billing provider e.name; // verbatim NM1-03 e.idQualifier; // "XX" (NPI) e.idCode; // verbatim NPI ``` #### Properties ##### address > `readonly` **address**: [`X12ClaimAddress`](#x12claimaddress) \| `undefined` ##### contacts > `readonly` **contacts**: readonly [`X12ClaimContact`](#x12claimcontact)[] ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` ##### firstName > `readonly` **firstName**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### middleName > `readonly` **middleName**: `string` \| `undefined` ##### name > `readonly` **name**: `string` ##### references > `readonly` **references**: readonly [`X12ClaimReference`](#x12claimreference)[] ##### suffix > `readonly` **suffix**: `string` \| `undefined` *** ### X12ClaimHiCode Decoded HI diagnosis or procedure composite. ONE entry per HI composite (HI-01..HI-12); the parser surfaces the verbatim qualifier AND the resolved [X12HiCodeSystem](#x12hicodesystem) so consumers can branch by system without re-deriving the mapping. The `category` discriminates the role (diagnosis / procedure / NUBC code-set entry). `poaIndicator` (Present-on-Admission) is HI-NN-9 - 837I institutional inpatient only. CMS-mandated values: `Y` Yes, `N` No, `U` Insufficient documentation, `W` Clinically undetermined, `1` Exempt from POA reporting. Verbatim - the parser preserves the value, never validates against the spec list (a profile may layer enforcement). #### Example ```ts import type { X12ClaimHiCode } from "@cosyte/x12"; declare const dx: X12ClaimHiCode; dx.qualifier; // "ABK" - principal diagnosis ICD-10-CM dx.codeSystem; // "ICD-10-CM" dx.category; // "principal-diagnosis" dx.code; // "J45.50" dx.poaIndicator; // "Y" (837I only) ``` #### Properties ##### category > `readonly` **category**: [`X12HiCategory`](#x12hicategory) ##### code > `readonly` **code**: `string` ##### codeSystem > `readonly` **codeSystem**: [`X12HiCodeSystem`](#x12hicodesystem) ##### date > `readonly` **date**: `string` \| `undefined` ##### dateQualifier > `readonly` **dateQualifier**: `string` \| `undefined` ##### monetaryAmount > `readonly` **monetaryAmount**: [`X12Decimal`](#x12decimal) \| `undefined` ##### poaIndicator > `readonly` **poaIndicator**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### quantity > `readonly` **quantity**: [`X12Decimal`](#x12decimal) \| `undefined` ##### versionId > `readonly` **versionId**: `string` \| `undefined` *** ### X12ClaimMember A claim's subscriber or patient - the NM1 entity plus the SBR/PAT metadata that wraps it. #### Example ```ts import type { X12ClaimMember } from "@cosyte/x12"; declare const m: X12ClaimMember; m.entity.name; // verbatim subscriber name (PHI) m.info.claimFilingIndicator; // "MB" ``` #### Properties ##### entity > `readonly` **entity**: [`X12ClaimEntity`](#x12claimentity) ##### info > `readonly` **info**: [`X12SubscriberInfo`](#x12subscriberinfo) *** ### X12ClaimNote Decoded NTE note - free-text annotation. `noteReferenceCode` (NTE-01) classifies the note ('ADD' Additional Information, 'CER' Certification, 'DCP' Goals/Rehabilitation/Discharge Plans, 'DGN' Diagnosis, 'DME' DME, 'MED' Medications, etc., X12 code list 363). NOTE: NTE-02 is free text supplied by the provider - it may include incidental PHI (a patient name in a clinical note). Surfaced verbatim for fidelity; the parser flags this surface in JSDoc but never redacts. Consumers should treat NTE-02 as PHI-bearing. #### Example ```ts import type { X12ClaimNote } from "@cosyte/x12"; declare const n: X12ClaimNote; n.noteReferenceCode; // "ADD" n.description; // verbatim - may include incidental PHI ``` #### Properties ##### description > `readonly` **description**: `string` ##### noteReferenceCode > `readonly` **noteReferenceCode**: `string` *** ### X12ClaimReference Decoded REF segment - additional identifier on an entity or claim. Verbatim across the table; the qualifier vocabulary depends on the context (`EI` Employer ID at the billing provider, `D9` Claim Number on an other-payer reference, `G1` Prior Authorization, etc.). #### Example ```ts import type { X12ClaimReference } from "@cosyte/x12"; declare const r: X12ClaimReference; r.qualifier; // "EI" r.value; // "123456789" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12ClaimStatus One claim status-tracking loop (Loop 2200). A claim opens on a TRN (claim-level reassociation trace) or - in a 277CA provider-level batch acknowledgment - on a standalone STC. Carries the resolved HL context, the echoed traces, the decoded STC statuses, supplemental REF / DTP, and any service-line statuses (Loop 2220). #### Example ```ts import type { X12ClaimStatus } from "@cosyte/x12"; declare const c: X12ClaimStatus; c.serviceProvider?.name; // "ANYTOWN CLINIC" c.subscriber?.lastName; // "DOE" c.statuses[0]?.totalChargeAmount?.toString(); // "150.00" ``` #### Properties ##### dates > `readonly` **dates**: readonly [`X12StatusDate`](#x12statusdate)[] ##### dependent > `readonly` **dependent**: [`X12StatusMember`](#x12statusmember) \| `undefined` ##### informationReceiver > `readonly` **informationReceiver**: [`X12StatusEntity`](#x12statusentity) \| `undefined` ##### informationSource > `readonly` **informationSource**: [`X12StatusEntity`](#x12statusentity) \| `undefined` ##### references > `readonly` **references**: readonly [`X12StatusReference`](#x12statusreference)[] ##### serviceLines > `readonly` **serviceLines**: readonly [`X12ServiceLineStatus`](#x12servicelinestatus)[] ##### serviceProvider > `readonly` **serviceProvider**: [`X12StatusEntity`](#x12statusentity) \| `undefined` ##### statuses > `readonly` **statuses**: readonly [`X12StatusInfo`](#x12statusinfo)[] ##### subscriber > `readonly` **subscriber**: [`X12StatusMember`](#x12statusmember) \| `undefined` ##### traces > `readonly` **traces**: readonly [`X12StatusTrace`](#x12statustrace)[] *** ### X12ClaimStatusResponse Top-level result of the 277 / 277CA walker. Claims are flattened: each [X12ClaimStatus](#x12claimstatus) carries its enclosing information source (payer), information receiver, service provider, and subscriber / dependent context resolved from the HL tree, mirroring how `get835` flattens claim payment loops. #### Example ```ts import { parseX12, get277Status } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "277"); if (tx !== undefined) { const status = get277Status(ix.delimiters, tx); for (const claim of status?.claims ?? []) { claim.traces[0]?.referenceId; // echoed 276 trace claim.statuses[0]?.statuses[0]?.categoryCode; // "A2" (acknowledgment) } } ``` #### Properties ##### claims > `readonly` **claims**: readonly [`X12ClaimStatus`](#x12claimstatus)[] ##### hierarchies > `readonly` **hierarchies**: readonly [`X12Hl`](#x12hl)[] ##### implementationConventionReference > `readonly` **implementationConventionReference**: `string` \| `undefined` ##### transactionType > `readonly` **transactionType**: `"claim-status"` \| `"claim-acknowledgment"` \| `"unrecognized-guide"` Which document this reading is. `"claim-status"` or `"claim-acknowledgment"` where the transaction set declares a guide the reader implements (ST-03 as framed reading `005010X214` is the acknowledgment); `"unrecognized-guide"` where it declares a guide outside that set, or none, in which case the reading also carries `X12_GUIDE_NOT_IMPLEMENTED` or `X12_GUIDE_NOT_DECLARED`. The reading is walked and returned either way, and ST-03 stays readable, decoded, on `implementationConventionReference`. ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12ClassifiedQuirk A quirk as [X12Profile.describe](#describe) RENDERS it: the authored quirk plus the conformance state it resolved to. `conformance` is required here and optional on [X12ProfileQuirk](#x12profilequirk), which is the whole difference - an unrecorded judgement is rendered as `"undetermined"` rather than left absent, so a reader never has to distinguish "no field" from "no claim". #### Example ```ts import { profiles } from "@cosyte/x12"; const d = profiles.bcbsCommon.describe(); d.relaxes[0]?.conformance; // "permitted" | "not-permitted" | "undetermined" ``` #### Extends - [`X12ProfileQuirk`](#x12profilequirk) #### Properties ##### conformance > `readonly` **conformance**: [`X12ProfileConformance`](#x12profileconformance) OPTIONAL recorded judgement of whether a trading partner may lawfully require this deviation under 45 CFR 162.915. Omit it and the quirk renders as [X12ProfileConformance](#x12profileconformance) `"undetermined"` - the fail-safe default - so a profile written before this field existed keeps defining and describing exactly as it did, and never gains a claim nobody made. ###### Overrides [`X12ProfileQuirk`](#x12profilequirk).[`conformance`](#conformance-2) ##### effect > `readonly` **effect**: [`X12ProfileEffect`](#x12profileeffect) Which `describe()` bucket this quirk renders into. ###### Inherited from [`X12ProfileQuirk`](#x12profilequirk).[`effect`](#effect-1) ##### expectedWarnings? > `readonly` `optional` **expectedWarnings?**: readonly [`X12WarningCode`](#x12warningcode)[] Warning codes this quirk leads a consumer to EXPECT when the deviation is present. Drives `partitionWarnings`. Often empty: the lenient parser absorbs most corpus deviations with zero warnings, and that "lossless, no warning" outcome is itself the documented behavior. ###### Inherited from [`X12ProfileQuirk`](#x12profilequirk).[`expectedWarnings`](#expectedwarnings-2) ##### fixture > `readonly` **fixture**: `string` Path to the Tier-2 fixture demonstrating the deviation, relative to `test/fixtures/` (e.g. `"remit/835-availity-quirk.edi"`). REQUIRED - the locked hard rule. The accuracy test parses this file and asserts the claimed deviation is present. ###### Inherited from [`X12ProfileQuirk`](#x12profilequirk).[`fixture`](#fixture-1) ##### id > `readonly` **id**: `string` Stable, kebab-case identifier - unique within a profile's quirk set. ###### Inherited from [`X12ProfileQuirk`](#x12profilequirk).[`id`](#id-8) ##### sourceCategory > `readonly` **sourceCategory**: `string` Where the deviation was observed (companion guide / corpus category). ###### Inherited from [`X12ProfileQuirk`](#x12profilequirk).[`sourceCategory`](#sourcecategory-1) ##### summary > `readonly` **summary**: `string` One-line human summary. NEVER contains PHI - describes structure only. ###### Inherited from [`X12ProfileQuirk`](#x12profilequirk).[`summary`](#summary-1) *** ### X12CoordinationOfBenefits Decoded Loop 2320 coordination of benefits (`COB`). `payerResponsibility` is COB-01 (`P` primary, `S` secondary, `T` tertiary); `referenceId` is COB-02 (the other payer's group/policy number); `serviceTypeCode` is COB-03. #### Example ```ts import type { X12CoordinationOfBenefits } from "@cosyte/x12"; declare const c: X12CoordinationOfBenefits; c.payerResponsibility; // "P" c.referenceId; // "OTHERGRP-1" ``` #### Properties ##### coordinationOfBenefitsCode > `readonly` **coordinationOfBenefitsCode**: `string` \| `undefined` ##### payerResponsibility > `readonly` **payerResponsibility**: `string` \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` *** ### X12DateValue Any date carrier this package's typed readers surface, read structurally rather than by name. Every `X12*Date` this package exports is accepted through this one shape: `X12ClaimDate`, `X12InquiryDate`, `X12EligibilityDate`, `X12StatusInquiryDate`, `X12StatusDate`, `X12PremiumDate`, `X12EnrollmentDate` and `X12AuthDate`. It is structural on purpose - a ninth carrier added later is accepted with no change here, and there is no per-transaction conversion variant to pick between. Both members are optional because two of those eight carriers state no format qualifier at all: `X12EnrollmentDate` and `X12PremiumDate` surface the qualifier and the verbatim value and nothing else. A carrier with no `formatQualifier` converts to `undefined`, which is the honest answer rather than a guess at which format the sender used. #### Example ```ts import { toISO, type X12DateValue } from "@cosyte/x12"; const day: X12DateValue = { formatQualifier: "D8", value: "20260601" }; toISO(day); // "2026-06-01" ``` #### Properties ##### formatQualifier? > `readonly` `optional` **formatQualifier?**: `string` DTP-02 / DTM-03 date/time period format qualifier, when the carrier states one. ##### value? > `readonly` `optional` **value?**: `string` The verbatim date element, in whatever form `formatQualifier` declares. *** ### X12DecimalRead The tri-state result of [readElementDecimal](#readelementdecimal): the decoded value (when there is one) alongside the reason there is not. #### Example ```ts import { readElementDecimal } from "@cosyte/x12"; const read = readElementDecimal(seg, 2, delim); if (read.status === "unparseable") { // the element was PRESENT and did not decode - do not read it as absent } ``` #### Properties ##### status > `readonly` **status**: [`X12DecimalStatus`](#x12decimalstatus) Which of the three spec-distinct outcomes this read hit. ##### value > `readonly` **value**: [`X12Decimal`](#x12decimal) \| `undefined` The decoded decimal, or `undefined` for both non-`"decoded"` states. *** ### X12DecimalWarningSink Where [elementDecimal](#elementdecimal) / [elementDecimalOrZero](#elementdecimalorzero) put an `X12_UNPARSEABLE_DECIMAL` warning, and the segment-level position it is anchored to. The helpers add the failing `elementIndex` themselves, so a caller passes the position of the SEGMENT it is decoding and never has to remember to narrow it per element. #### Example ```ts import { elementDecimalOrZero, type X12DecimalWarningSink } from "@cosyte/x12"; const sink: X12DecimalWarningSink = { warnings, position: { segmentIndex: 3 } }; elementDecimalOrZero(seg, 2, delim, sink); ``` #### Properties ##### position > `readonly` **position**: [`X12Position`](#x12position) Position of the segment being decoded; `elementIndex` is supplied by the helper. ##### warnings > `readonly` **warnings**: [`X12ParseWarning`](#x12parsewarning)[] The walker's warning accumulator. Appended to, never read. *** ### X12Eligibility Top-level result of "./get-271.js".get271Eligibility. Carries every subscriber loop (each with its enclosing information-source payer and information-receiver provider, its echoed TRN traces, name, and eligibility/benefit lines), the verbatim HL hierarchy, and every warning surfaced during the walk. #### Example ```ts import { parseX12, get271Eligibility } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "271"); if (tx !== undefined) { const elig = get271Eligibility(ix.delimiters, tx); for (const sub of elig.subscribers) { sub.traces[0]?.referenceId; // echoed 270 trace number sub.benefits[0]?.eligibilityCode; // "1" (Active Coverage) } } ``` #### Properties ##### aaaConditions > `readonly` **aaaConditions**: readonly [`X12AaaCondition`](#x12aaacondition)[] Every AAA request-validation segment the document carried, in document order. **ALWAYS PRESENT, and empty where the document carried none.** A present, empty collection is a stated zero; an absent field could not be told from a reader that does not surface AAA at all, which is the exact ambiguity this collection exists to remove. A payer that rejected the inquiry and a payer that found no benefits are different answers, and before this collection existed they read identically through this surface. ##### hierarchies > `readonly` **hierarchies**: readonly [`X12Hl`](#x12hl)[] ##### subscribers > `readonly` **subscribers**: readonly [`X12EligibilitySubscriber`](#x12eligibilitysubscriber)[] ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12EligibilityAddress A postal address (N3 + N4) attached to a subscriber / dependent name. #### Example ```ts import type { X12EligibilityAddress } from "@cosyte/x12"; declare const a: X12EligibilityAddress; a.lines[0]; // "123 MAIN ST" a.city; // "ANYTOWN" a.state; // "CA" a.postalCode; // "90001" ``` #### Properties ##### city > `readonly` **city**: `string` \| `undefined` ##### countryCode > `readonly` **countryCode**: `string` \| `undefined` ##### lines > `readonly` **lines**: readonly `string`[] ##### postalCode > `readonly` **postalCode**: `string` \| `undefined` ##### state > `readonly` **state**: `string` \| `undefined` *** ### X12EligibilityBenefit One eligibility-or-benefit line (EB, Loop 2110C/2110D). EB-01 is the eligibility code (`1` Active Coverage, `6` Inactive, `I` Non-Covered, …); EB-03 carries one-or-more Service Type Codes (each looked up against the bundled snapshot). Monetary + percent + quantity are [X12Decimal](#x12decimal). The walker preserves the verbatim EB-01 even when no description resolves. #### Example ```ts import type { X12EligibilityBenefit } from "@cosyte/x12"; declare const b: X12EligibilityBenefit; b.eligibilityCode; // "1" b.serviceTypeCodes[0]?.code; // "30" b.inPlanNetwork; // "Y" b.monetaryAmount?.toString(); // "1000.00" b.procedure?.code; // "D2150" (EB-13-2, when the line is procedure-specific) ``` #### Properties ##### authorizationRequired > `readonly` **authorizationRequired**: `string` \| `undefined` ##### coverageLevelCode > `readonly` **coverageLevelCode**: `string` \| `undefined` ##### dates > `readonly` **dates**: readonly [`X12EligibilityDate`](#x12eligibilitydate)[] ##### eligibilityCode > `readonly` **eligibilityCode**: `string` ##### inPlanNetwork > `readonly` **inPlanNetwork**: `string` \| `undefined` ##### insuranceTypeCode > `readonly` **insuranceTypeCode**: `string` \| `undefined` ##### messages > `readonly` **messages**: readonly `string`[] ##### monetaryAmount > `readonly` **monetaryAmount**: [`X12Decimal`](#x12decimal) \| `undefined` ##### percent > `readonly` **percent**: [`X12Decimal`](#x12decimal) \| `undefined` ##### planCoverageDescription > `readonly` **planCoverageDescription**: `string` \| `undefined` ##### procedure > `readonly` **procedure**: [`X12EligibilityProcedure`](#x12eligibilityprocedure) \| `undefined` EB-13 - the procedure this line applies to; `undefined` when EB-13 is absent or has no qualifier. ##### quantity > `readonly` **quantity**: [`X12Decimal`](#x12decimal) \| `undefined` ##### quantityQualifier > `readonly` **quantityQualifier**: `string` \| `undefined` ##### references > `readonly` **references**: readonly [`X12EligibilityReference`](#x12eligibilityreference)[] ##### relatedEntities > `readonly` **relatedEntities**: readonly [`X12EligibilityEntity`](#x12eligibilityentity)[] ##### serviceTypeCodes > `readonly` **serviceTypeCodes**: readonly [`X12EligibilityServiceType`](#x12eligibilityservicetype)[] ##### timePeriodQualifier > `readonly` **timePeriodQualifier**: `string` \| `undefined` *** ### X12EligibilityDate A DTP date / date-range attached to a subscriber, dependent, or benefit line. `qualifier` is DTP-01 (e.g. `307` Eligibility, `291` Plan); `value` is DTP-03 in the DTP-02 format (`D8` `CCYYMMDD` / `RD8` range). #### Example ```ts import type { X12EligibilityDate } from "@cosyte/x12"; declare const d: X12EligibilityDate; d.qualifier; // "307" d.formatQualifier; // "D8" d.value; // "20260101" ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12EligibilityDependent One dependent (Loop 2000D / 2100D) - a patient who is not the subscriber (relationship is carried at the HL level). Same benefit-bearing shape as a subscriber minus the nested dependents. #### Example ```ts import type { X12EligibilityDependent } from "@cosyte/x12"; declare const d: X12EligibilityDependent; d.name?.firstName; // "JUNIOR" d.benefits[0]?.coverageLevelCode; // "IND" ``` #### Properties ##### benefits > `readonly` **benefits**: readonly [`X12EligibilityBenefit`](#x12eligibilitybenefit)[] ##### dates > `readonly` **dates**: readonly [`X12EligibilityDate`](#x12eligibilitydate)[] ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) \| `undefined` ##### name > `readonly` **name**: [`X12EligibilityMember`](#x12eligibilitymember) \| `undefined` ##### references > `readonly` **references**: readonly [`X12EligibilityReference`](#x12eligibilityreference)[] ##### traces > `readonly` **traces**: readonly [`X12EligibilityTrace`](#x12eligibilitytrace)[] *** ### X12EligibilityEntity A non-person entity (payer in Loop 2100A, provider in Loop 2100B, or a benefit-related entity in Loop 2120C). Decoded from an NM1 - no demographics, just the organization / provider name + identifier. #### Example ```ts import type { X12EligibilityEntity } from "@cosyte/x12"; declare const e: X12EligibilityEntity; e.entityIdentifierCode; // "PR" (payer) / "1P" (provider) e.name; // "MEDPAY INSURANCE" e.idCode; // "00123" ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### name > `readonly` **name**: `string` *** ### X12EligibilityMember A person (subscriber / dependent) decoded from NM1 + the optional DMG demographics + N3/N4 address. `idCode` is the member identifier (NM1-09) - synthetic-only in fixtures. #### Example ```ts import type { X12EligibilityMember } from "@cosyte/x12"; declare const m: X12EligibilityMember; m.lastName; // "DOE" m.dateOfBirth; // "19800101" (DMG-02, CCYYMMDD) m.genderCode; // "F" ``` #### Properties ##### address > `readonly` **address**: [`X12EligibilityAddress`](#x12eligibilityaddress) \| `undefined` ##### dateOfBirth > `readonly` **dateOfBirth**: `string` \| `undefined` ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` ##### firstName > `readonly` **firstName**: `string` \| `undefined` ##### genderCode > `readonly` **genderCode**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### lastName > `readonly` **lastName**: `string` \| `undefined` ##### middleName > `readonly` **middleName**: `string` \| `undefined` ##### suffix > `readonly` **suffix**: `string` \| `undefined` *** ### X12EligibilityProcedure The EB-13 composite medical procedure identifier (C003), as its separated components, split on the component separator the interchange declares. C003-08 (range end) is not read. #### Example ```ts import type { X12EligibilityProcedure } from "@cosyte/x12"; declare const p: X12EligibilityProcedure; p.qualifier; // "AD" (EB-13-1, product/service id qualifier) p.code; // "D2150" (EB-13-2) p.modifiers[0]; // "26" (EB-13-3 onward) ``` #### Properties ##### code > `readonly` **code**: `string` \| `undefined` EB-13-2 - the procedure code. ##### description > `readonly` **description**: `string` \| `undefined` EB-13-7 - procedure description, when the sender transmitted one. ##### modifiers > `readonly` **modifiers**: readonly `string`[] EB-13-3 through EB-13-6 - procedure modifiers, in transmitted order. ##### qualifier > `readonly` **qualifier**: `string` EB-13-1 - product or service id qualifier. *** ### X12EligibilityReference A REF supplemental identifier attached to a subscriber, dependent, or benefit line. `qualifier` is REF-01; `value` is REF-02. #### Example ```ts import type { X12EligibilityReference } from "@cosyte/x12"; declare const r: X12EligibilityReference; r.qualifier; // "6P" (group number) r.value; // "GRP0001" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12EligibilityServiceType A decoded Service Type Code (EB-03, X12 external code source 1365). The verbatim code is always preserved; `description` resolves from the bundled snapshot (or `undefined` when outside the subset). #### Example ```ts import type { X12EligibilityServiceType } from "@cosyte/x12"; declare const st: X12EligibilityServiceType; st.code; // "30" st.description; // "Health Benefit Plan Coverage" ``` #### Properties ##### code > `readonly` **code**: `string` ##### description > `readonly` **description**: `string` \| `undefined` *** ### X12EligibilitySubscriber One subscriber (Loop 2000C / 2100C). Holds the enclosing information source (Loop 2100A payer) and information receiver (Loop 2100B provider) resolved from the HL tree, the verbatim echoed TRN traces, the subscriber name + demographics, and the eligibility/benefit lines. Non-subscriber patients hang off [dependents](#dependents-4). #### Example ```ts import type { X12EligibilitySubscriber } from "@cosyte/x12"; declare const s: X12EligibilitySubscriber; s.informationSource?.name; // "MEDPAY INSURANCE" s.name?.lastName; // "DOE" s.dependents.length; // 0 ``` #### Properties ##### benefits > `readonly` **benefits**: readonly [`X12EligibilityBenefit`](#x12eligibilitybenefit)[] ##### dates > `readonly` **dates**: readonly [`X12EligibilityDate`](#x12eligibilitydate)[] ##### dependents > `readonly` **dependents**: readonly [`X12EligibilityDependent`](#x12eligibilitydependent)[] ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) \| `undefined` ##### informationReceiver > `readonly` **informationReceiver**: [`X12EligibilityEntity`](#x12eligibilityentity) \| `undefined` ##### informationSource > `readonly` **informationSource**: [`X12EligibilityEntity`](#x12eligibilityentity) \| `undefined` ##### name > `readonly` **name**: [`X12EligibilityMember`](#x12eligibilitymember) \| `undefined` ##### references > `readonly` **references**: readonly [`X12EligibilityReference`](#x12eligibilityreference)[] ##### traces > `readonly` **traces**: readonly [`X12EligibilityTrace`](#x12eligibilitytrace)[] *** ### X12EligibilityTrace A reassociation trace (TRN). **The verbatim echo of the requesting 270's trace number** - `referenceId` (TRN-02) is the value a provider matches against the trace it sent. The walker NEVER mutates it. #### Example ```ts import type { X12EligibilityTrace } from "@cosyte/x12"; declare const t: X12EligibilityTrace; t.traceTypeCode; // "2" (referenced - added by the payer in the 271) t.referenceId; // "ELIG20260627001" (echoed verbatim from the 270) ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` ##### supplementalReferenceId > `readonly` **supplementalReferenceId**: `string` \| `undefined` ##### traceTypeCode > `readonly` **traceTypeCode**: `string` *** ### X12Enrollment One decoded Loop 2000 member-level detail (`INS`) - a single member's enrollment action. Carries the member identity (Loop 2100A NM1 + DMG + address), the supplemental ids (REF - subscriber id, group/policy), the dates (DTP - eligibility begin/end), the health-coverage loops (Loop 2300 HD), and the coordination-of-benefits loops (Loop 2320 COB). Any recoverable deviation (e.g. an unknown maintenance type) is surfaced on `warnings` for THIS member only. #### Example ```ts import type { X12Enrollment } from "@cosyte/x12"; declare const e: X12Enrollment; e.maintenanceTypeCode; // "021" addition e.maintenanceTypeDescription; // "Addition" e.member?.lastName; // verbatim member surname e.healthCoverages[0]?.insuranceLineCode; // "HLT" ``` #### Properties ##### benefitStatusCode > `readonly` **benefitStatusCode**: `string` \| `undefined` ##### coordinationOfBenefits > `readonly` **coordinationOfBenefits**: readonly [`X12CoordinationOfBenefits`](#x12coordinationofbenefits)[] ##### dates > `readonly` **dates**: readonly [`X12EnrollmentDate`](#x12enrollmentdate)[] ##### employmentStatusCode > `readonly` **employmentStatusCode**: `string` \| `undefined` ##### healthCoverages > `readonly` **healthCoverages**: readonly [`X12HealthCoverage`](#x12healthcoverage)[] ##### maintenanceReasonCode > `readonly` **maintenanceReasonCode**: `string` \| `undefined` ##### maintenanceTypeCode > `readonly` **maintenanceTypeCode**: `string` ##### maintenanceTypeDescription > `readonly` **maintenanceTypeDescription**: `string` \| `undefined` ##### member > `readonly` **member**: [`X12EnrollmentMember`](#x12enrollmentmember-1) \| `undefined` ##### references > `readonly` **references**: readonly [`X12EnrollmentReference`](#x12enrollmentreference)[] ##### relationshipCode > `readonly` **relationshipCode**: `string` \| `undefined` ##### subscriberIndicator > `readonly` **subscriberIndicator**: `string` \| `undefined` ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12EnrollmentAddress Decoded N3 + N4 address block attached to a member. `lines` is the N3 address lines (1-2 entries); `city` / `state` / `postalCode` come from N4. All verbatim - no normalization. #### Example ```ts import type { X12EnrollmentAddress } from "@cosyte/x12"; declare const a: X12EnrollmentAddress; a.lines[0]; // "100 MAIN ST" a.city; // "COLUMBUS" ``` #### Properties ##### city > `readonly` **city**: `string` \| `undefined` ##### countryCode > `readonly` **countryCode**: `string` \| `undefined` ##### lines > `readonly` **lines**: readonly `string`[] ##### postalCode > `readonly` **postalCode**: `string` \| `undefined` ##### state > `readonly` **state**: `string` \| `undefined` *** ### X12EnrollmentAmount Decoded AMT amount attached to a health-coverage loop. `qualifier` is the amount qualifier (AMT-01 - `"P3"` premium, `"B9"` co-insurance, …); `amount` is "../../decimal.js".X12Decimal (NEVER `number`). #### Example ```ts import type { X12EnrollmentAmount } from "@cosyte/x12"; declare const a: X12EnrollmentAmount; a.qualifier; // "P3" a.amount.toString(); // "125.00" ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) ##### qualifier > `readonly` **qualifier**: `string` *** ### X12EnrollmentDate Decoded DTP date attached to a member or a health-coverage loop. `qualifier` is the date/time qualifier (DTP-01); `value` is the verbatim CCYYMMDD or range (DTP-03). No normalization. #### Example ```ts import type { X12EnrollmentDate } from "@cosyte/x12"; declare const d: X12EnrollmentDate; d.qualifier; // "356" eligibility begin d.value; // "20260101" ``` #### Properties ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12EnrollmentHeader Decoded 834 header - the BGN Beginning Segment plus the sponsor (`N1*P5`) and payer (`N1*IN`) parties. Returned by "./get-834.js".get834Header; the per-member stream is separate so a consumer can read the file-level context without draining the (potentially huge) member roster. #### Example ```ts import type { X12EnrollmentHeader } from "@cosyte/x12"; declare const h: X12EnrollmentHeader; h.transactionSetPurposeCode; // "00" original h.sponsor?.name; // "EMPLOYER CO" h.payer?.name; // "MEDPAY INSURANCE" ``` #### Properties ##### actionCode > `readonly` **actionCode**: `string` \| `undefined` ##### date > `readonly` **date**: `string` \| `undefined` ##### dates > `readonly` **dates**: readonly [`X12EnrollmentDate`](#x12enrollmentdate)[] ##### payer > `readonly` **payer**: [`X12EnrollmentParty`](#x12enrollmentparty) \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` ##### references > `readonly` **references**: readonly [`X12EnrollmentReference`](#x12enrollmentreference)[] ##### sponsor > `readonly` **sponsor**: [`X12EnrollmentParty`](#x12enrollmentparty) \| `undefined` ##### time > `readonly` **time**: `string` \| `undefined` ##### transactionSetPurposeCode > `readonly` **transactionSetPurposeCode**: `string` ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12EnrollmentMember Decoded Loop 2100A member name (`NM1*IL`) + demographic (`DMG`) + address (`N3`/`N4`). PHI surface: every field carries PHI. #### Example ```ts import type { X12EnrollmentMember } from "@cosyte/x12"; declare const m: X12EnrollmentMember; m.lastName; // verbatim m.idCode; // verbatim member id m.dateOfBirth; // "19850515" (CCYYMMDD, verbatim) m.genderCode; // "F" ``` #### Properties ##### address > `readonly` **address**: [`X12EnrollmentAddress`](#x12enrollmentaddress) \| `undefined` ##### dateOfBirth > `readonly` **dateOfBirth**: `string` \| `undefined` ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### firstName > `readonly` **firstName**: `string` \| `undefined` ##### genderCode > `readonly` **genderCode**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### lastName > `readonly` **lastName**: `string` \| `undefined` ##### middleName > `readonly` **middleName**: `string` \| `undefined` ##### suffix > `readonly` **suffix**: `string` \| `undefined` *** ### X12EnrollmentParty Decoded Loop 1000 party (sponsor `N1*P5`, payer `N1*IN`, TPA/broker `N1*BO`/`N1*TV`). Organizational PII, not member PHI. #### Example ```ts import type { X12EnrollmentParty } from "@cosyte/x12"; declare const p: X12EnrollmentParty; p.entityIdentifierCode; // "P5" plan sponsor p.name; // "EMPLOYER CO" p.idCode; // "FEIN123" (verbatim) ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### name > `readonly` **name**: `string` *** ### X12EnrollmentReference Decoded REF - supplemental identifier (subscriber id `0F`, group/policy `1L`, member id `23`, …). `qualifier` is the X12 reference-identification qualifier; `value` is the verbatim id. #### Example ```ts import type { X12EnrollmentReference } from "@cosyte/x12"; declare const r: X12EnrollmentReference; r.qualifier; // "0F" subscriber number r.value; // "MBR0001" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12FunctionalGroup A single GS..GE functional group inside an interchange. `transactions` is the ordered list of ST..SE transaction sets inside it (opaque bodies at envelope decode - see [X12TransactionSet](#x12transactionset)). #### Example ```ts import type { X12FunctionalGroup } from "@cosyte/x12"; declare const group: X12FunctionalGroup; group.transactions.length; ``` #### Properties ##### ge > `readonly` **ge**: [`GeSegment`](#gesegment) \| `undefined` ##### gs > `readonly` **gs**: [`GsSegment`](#gssegment) ##### transactions > `readonly` **transactions**: readonly [`X12TransactionSet`](#x12transactionset)[] *** ### X12HealthCoverage Decoded Loop 2300 health coverage (`HD`) plus its attached dates (`DTP`) and monetary amounts (`AMT`). `insuranceLineCode` is HD-03 (`HLT`, `DEN`, `VIS`, …); `maintenanceTypeCode` is HD-01 - the per-coverage echo of the member action, validated against the same X12 0875 snapshot as INS-03. #### Example ```ts import type { X12HealthCoverage } from "@cosyte/x12"; declare const c: X12HealthCoverage; c.insuranceLineCode; // "HLT" c.planCoverageDescription; // "GOLD PPO" c.dates[0]?.qualifier; // "348" benefit begin ``` #### Properties ##### amounts > `readonly` **amounts**: readonly [`X12EnrollmentAmount`](#x12enrollmentamount)[] ##### coverageLevelCode > `readonly` **coverageLevelCode**: `string` \| `undefined` ##### dates > `readonly` **dates**: readonly [`X12EnrollmentDate`](#x12enrollmentdate)[] ##### insuranceLineCode > `readonly` **insuranceLineCode**: `string` \| `undefined` ##### maintenanceTypeCode > `readonly` **maintenanceTypeCode**: `string` \| `undefined` ##### maintenanceTypeDescription > `readonly` **maintenanceTypeDescription**: `string` \| `undefined` ##### planCoverageDescription > `readonly` **planCoverageDescription**: `string` \| `undefined` *** ### X12HierarchicalLevel One HL segment captured during the walk. The 837 HL hierarchy is the safety primitive: HL-01 is this level's id (sequential within the transaction); HL-02 is the parent's id (empty when this is a top-level HL); HL-03 is the level code (`20` Information Source = billing provider, `22` Subscriber, `23` Dependent/patient); HL-04 is `1` when any HL below it claims this as its parent, `0` otherwise. **Parent-pointer integrity is the #1 safety property.** The walker validates HL-02 references an earlier-emitted HL-01 in the same transaction AND that the parent's HL-03 level is consistent with this level (20 → 22; 22 → 23). Violations emit `X12_HL_PARENT_MISMATCH` and `X12_HL_PARENT_LEVEL_INVALID` - the parser NEVER silently re-numbers. #### Example ```ts import type { X12HierarchicalLevel } from "@cosyte/x12"; declare const hl: X12HierarchicalLevel; hl.hlId; // "1" (sequential within the transaction) hl.parentHlId; // undefined for billing-provider top level hl.levelCode; // "20" hl.hasChild; // "1" ``` #### Properties ##### hasChild > `readonly` **hasChild**: `string` ##### hlId > `readonly` **hlId**: `string` ##### levelCode > `readonly` **levelCode**: `string` ##### parentHlId > `readonly` **parentHlId**: `string` \| `undefined` *** ### X12Hl One HL segment captured during an eligibility / claim-status walk. The hierarchy is the structural safety primitive - see the module doc. The verbatim declared `parentHlId` is always preserved even when it fails validation (the parser never re-numbers). #### Example ```ts import type { X12Hl } from "@cosyte/x12"; declare const hl: X12Hl; hl.hlId; // "3" hl.parentHlId; // "2" (undefined at the information-source top level) hl.levelCode; // "22" (Subscriber) hl.hasChild; // "1" ``` #### Properties ##### hasChild > `readonly` **hasChild**: `string` ##### hlId > `readonly` **hlId**: `string` ##### levelCode > `readonly` **levelCode**: `string` ##### parentHlId > `readonly` **parentHlId**: `string` \| `undefined` *** ### X12Inquiry Top-level result of "./get-270.js".get270Inquiry. Carries the transaction header, the information-source roots of the transmitted hierarchy, every declared HL verbatim, and every warning raised while walking. #### Example ```ts import { parseX12, get270Inquiry } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "270"); if (tx !== undefined) { const inquiry = get270Inquiry(ix.delimiters, tx); const sub = inquiry?.informationSources[0]?.receivers[0]?.subscribers[0]; sub?.traces[0]?.referenceId; // trace the provider sent sub?.inquiries[0]?.serviceTypeCodes[0]?.code; // "30" } ``` #### Properties ##### header > `readonly` **header**: [`X12InquiryHeader`](#x12inquiryheader-1) \| `undefined` ##### hierarchies > `readonly` **hierarchies**: readonly [`X12Hl`](#x12hl)[] ##### informationSources > `readonly` **informationSources**: readonly [`X12InquirySource`](#x12inquirysource)[] ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12InquiryAddress A postal address (N3 + N4) attached to a name loop. #### Example ```ts import type { X12InquiryAddress } from "@cosyte/x12"; declare const a: X12InquiryAddress; a.lines[0]; // "100 MAIN ST" a.postalCode; // "43215" ``` #### Properties ##### city > `readonly` **city**: `string` \| `undefined` ##### countryCode > `readonly` **countryCode**: `string` \| `undefined` ##### lines > `readonly` **lines**: readonly `string`[] ##### postalCode > `readonly` **postalCode**: `string` \| `undefined` ##### state > `readonly` **state**: `string` \| `undefined` *** ### X12InquiryDate A DTP date or date range. `formatQualifier` (DTP-02) is the element that says WHICH: `D8` is a single `CCYYMMDD` date and `RD8` a `CCYYMMDD-CCYYMMDD` range. It is preserved verbatim beside the value, so a consumer never has to infer which one a `value` holds. #### Example ```ts import type { X12InquiryDate } from "@cosyte/x12"; declare const d: X12InquiryDate; d.qualifier; // "291" (plan) / "307" (eligibility) d.formatQualifier; // "D8" or "RD8" d.value; // "20260601" ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time period format qualifier (`D8` single, `RD8` range). ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier. ##### value > `readonly` **value**: `string` DTP-03 - the date or range, in the DTP-02 format. *** ### X12InquiryDependent One dependent (Loop 2000D / 2100D / 2110D) - a patient who cannot be identified as a subscriber in their own right. HL-03 is `23`. Carries its OWN traces and inquiries and is never merged onto the subscriber it hangs under. #### Example ```ts import type { X12InquiryDependent } from "@cosyte/x12"; declare const d: X12InquiryDependent; d.name?.firstName; // "BABY" d.inquiries.length; // the dependent's own requested service types ``` #### Properties ##### dates > `readonly` **dates**: readonly [`X12InquiryDate`](#x12inquirydate)[] ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### inquiries > `readonly` **inquiries**: readonly [`X12InquiryRequest`](#x12inquiryrequest)[] ##### name > `readonly` **name**: [`X12InquiryName`](#x12inquiryname) \| `undefined` ##### references > `readonly` **references**: readonly [`X12InquiryReference`](#x12inquiryreference)[] ##### traces > `readonly` **traces**: readonly [`X12InquiryTrace`](#x12inquirytrace)[] *** ### X12InquiryHeader The BHT beginning-of-hierarchical-transaction header. `purposeCode` (BHT-02) is `13` on a request; `referenceId` (BHT-03) is the submitter's own identifier for the inquiry. #### Example ```ts import type { X12InquiryHeader } from "@cosyte/x12"; declare const h: X12InquiryHeader; h.hierarchicalStructureCode; // "0022" h.purposeCode; // "13" ``` #### Properties ##### date > `readonly` **date**: `string` \| `undefined` BHT-04 - transaction creation date, CCYYMMDD. ##### hierarchicalStructureCode > `readonly` **hierarchicalStructureCode**: `string` BHT-01 - hierarchical structure code. ##### purposeCode > `readonly` **purposeCode**: `string` BHT-02 - transaction set purpose code. ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` BHT-03 - submitter transaction identifier. ##### time > `readonly` **time**: `string` \| `undefined` BHT-05 - transaction creation time. *** ### X12InquiryName An NM1 name loop, plus the N3 / N4 address and DMG demographics that follow it. ONE type covers every level, because NM1 is one segment: NM1-03 is "name last or organization name", so a payer fills it and a member fills it, and splitting the type by level would mean this reader deciding which kind of party a level holds. `entityTypeQualifier` (NM1-02) is the sender's own statement of that (`1` person, `2` non-person) and is preserved verbatim. #### Example ```ts import type { X12InquiryName } from "@cosyte/x12"; declare const n: X12InquiryName; n.entityIdentifierCode; // "IL" subscriber / "PR" payer / "1P" provider n.lastNameOrOrganizationName; // "DOE" or "MEDPAY INSURANCE" n.dateOfBirth; // "19850515" (DMG-02, CCYYMMDD) ``` #### Properties ##### address > `readonly` **address**: [`X12InquiryAddress`](#x12inquiryaddress) \| `undefined` N3 + N4 postal address, absent when the level transmitted neither. ##### dateOfBirth > `readonly` **dateOfBirth**: `string` \| `undefined` DMG-02 - date of birth. ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code. ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### firstName > `readonly` **firstName**: `string` \| `undefined` NM1-04 - first name. ##### genderCode > `readonly` **genderCode**: `string` \| `undefined` DMG-03 - gender code. ##### idCode > `readonly` **idCode**: `string` \| `undefined` NM1-09 - identification code. ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` NM1-08 - identification code qualifier. ##### lastNameOrOrganizationName > `readonly` **lastNameOrOrganizationName**: `string` \| `undefined` NM1-03 - last name, or the organization name for a non-person. ##### middleName > `readonly` **middleName**: `string` \| `undefined` NM1-05 - middle name. ##### suffix > `readonly` **suffix**: `string` \| `undefined` NM1-07 - name suffix. *** ### X12InquiryProcedure The EQ-02 composite medical procedure identifier, exposed as its SEPARATED components. It is never handed back as one joined string: the component separator is framing, so joining it into a value would make two documents that differ only in their declared delimiters decode to different models. #### Example ```ts import type { X12InquiryProcedure } from "@cosyte/x12"; declare const p: X12InquiryProcedure; p.qualifier; // "HC" (EQ-02-1, product/service id qualifier) p.code; // "99213" (EQ-02-2) p.modifiers[0]; // "25" (EQ-02-3 onward) ``` #### Properties ##### code > `readonly` **code**: `string` \| `undefined` EQ-02-2 - the procedure code. ##### description > `readonly` **description**: `string` \| `undefined` EQ-02-7 - procedure description. ##### modifiers > `readonly` **modifiers**: readonly `string`[] EQ-02-3 through EQ-02-6 - procedure modifiers, in transmitted order. ##### qualifier > `readonly` **qualifier**: `string` EQ-02-1 - product or service id qualifier. *** ### X12InquiryReceiver One information receiver (Loop 2000B / 2100B) - the provider asking. HL-03 is `21` and its HL-02 names an information source. #### Example ```ts import type { X12InquiryReceiver } from "@cosyte/x12"; declare const r: X12InquiryReceiver; r.name?.idCode; // the asking provider's identifier r.subscribers.length; // 1 ``` #### Properties ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### name > `readonly` **name**: [`X12InquiryName`](#x12inquiryname) \| `undefined` ##### references > `readonly` **references**: readonly [`X12InquiryReference`](#x12inquiryreference)[] ##### subscribers > `readonly` **subscribers**: readonly [`X12InquirySubscriber`](#x12inquirysubscriber)[] *** ### X12InquiryReference A REF supplemental identifier on a level or on an inquiry. #### Example ```ts import type { X12InquiryReference } from "@cosyte/x12"; declare const r: X12InquiryReference; r.qualifier; // "6P" (group number) r.value; // "GROUP0001" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` REF-03 - description. ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification. *** ### X12InquiryRequest One eligibility or benefit inquiry (`EQ`, Loop 2110C / 2110D) - a single thing the provider asked about, with the REF identifiers and DTP dates transmitted under it. #### Example ```ts import type { X12InquiryRequest } from "@cosyte/x12"; declare const q: X12InquiryRequest; q.serviceTypeCodes[0]?.code; // "30" q.serviceTypeCodes[0]?.description; // "Health Benefit Plan Coverage" q.coverageLevelCode; // "IND" ``` #### Properties ##### coverageLevelCode > `readonly` **coverageLevelCode**: `string` \| `undefined` EQ-03 - coverage level code. ##### dates > `readonly` **dates**: readonly [`X12InquiryDate`](#x12inquirydate)[] DTP dates transmitted under this inquiry. ##### diagnosisCodePointers > `readonly` **diagnosisCodePointers**: readonly `string`[] EQ-05 - diagnosis code pointers, as separated components. ##### insuranceTypeCode > `readonly` **insuranceTypeCode**: `string` \| `undefined` EQ-04 - insurance type code. ##### procedure > `readonly` **procedure**: [`X12InquiryProcedure`](#x12inquiryprocedure) \| `undefined` EQ-02 - the requested procedure, as its separated components. ##### references > `readonly` **references**: readonly [`X12InquiryReference`](#x12inquiryreference)[] REF identifiers transmitted under this inquiry. ##### serviceTypeCodes > `readonly` **serviceTypeCodes**: readonly [`X12InquiryServiceType`](#x12inquiryservicetype)[] EQ-01 - one or more requested Service Type Codes (a repeating element). *** ### X12InquiryServiceType A requested Service Type Code (EQ-01, X12 external code source 1365). The verbatim `code` is ALWAYS preserved; `description` resolves from the bundled snapshot the 271 reader already consumes, and is `undefined` outside it. The code is never replaced by its description. #### Example ```ts import type { X12InquiryServiceType } from "@cosyte/x12"; declare const st: X12InquiryServiceType; st.code; // "30" st.description; // "Health Benefit Plan Coverage" ``` #### Properties ##### code > `readonly` **code**: `string` ##### description > `readonly` **description**: `string` \| `undefined` *** ### X12InquirySource One information source (Loop 2000A / 2100A) - the payer the inquiry is put to. The root of a transmitted hierarchy: HL-03 is `20` and HL-02 is absent. #### Example ```ts import type { X12InquirySource } from "@cosyte/x12"; declare const s: X12InquirySource; s.name?.lastNameOrOrganizationName; // "MEDPAY INSURANCE" s.receivers.length; // 1 ``` #### Properties ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### name > `readonly` **name**: [`X12InquiryName`](#x12inquiryname) \| `undefined` ##### receivers > `readonly` **receivers**: readonly [`X12InquiryReceiver`](#x12inquiryreceiver)[] ##### references > `readonly` **references**: readonly [`X12InquiryReference`](#x12inquiryreference)[] *** ### X12InquirySubscriber One subscriber (Loop 2000C / 2100C / 2110C) - the member the inquiry is about, or the member a dependent hangs under. HL-03 is `22`. #### Example ```ts import type { X12InquirySubscriber } from "@cosyte/x12"; declare const s: X12InquirySubscriber; s.name?.idCode; // member identifier as transmitted s.inquiries[0]?.serviceTypeCodes[0]; // requested service type s.dependents.length; // 0 ``` #### Properties ##### dates > `readonly` **dates**: readonly [`X12InquiryDate`](#x12inquirydate)[] ##### dependents > `readonly` **dependents**: readonly [`X12InquiryDependent`](#x12inquirydependent)[] ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### inquiries > `readonly` **inquiries**: readonly [`X12InquiryRequest`](#x12inquiryrequest)[] ##### name > `readonly` **name**: [`X12InquiryName`](#x12inquiryname) \| `undefined` ##### references > `readonly` **references**: readonly [`X12InquiryReference`](#x12inquiryreference)[] ##### traces > `readonly` **traces**: readonly [`X12InquiryTrace`](#x12inquirytrace)[] *** ### X12InquiryTrace A reassociation trace (TRN) transmitted with the inquiry. **TRN-02 is the value the answering 271 must echo verbatim**, which is what lets the provider match the answer to this question. #### Example ```ts import type { X12InquiryTrace } from "@cosyte/x12"; declare const t: X12InquiryTrace; t.traceTypeCode; // "1" (current transaction trace numbers) t.referenceId; // "ELIG20260601001" ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` TRN-03 - originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - the trace number a 271 echoes back. ##### supplementalReferenceId > `readonly` **supplementalReferenceId**: `string` \| `undefined` TRN-04 - supplemental reference identifier. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code. *** ### X12Interchange The top-level X12 interchange returned by `parseX12`. `isa` carries the envelope header verbatim; `delimiters` is the four-class delimiter set detected from fixed positions inside `isa.raw`; `groups` is the ordered GS..GE list; `orphanSegments` holds any segment that fell outside every transaction set (empty for a well-formed interchange); `warnings` accumulates every Tier-2 deviation observed during the parse (lenient mode); `trailingBytes` (when present) is any non-empty content after IEA - preserved verbatim so a consumer can inspect or re-emit it. #### Example ```ts import { parseX12 } from "@cosyte/x12"; const ix = parseX12(raw); for (const w of ix.warnings) console.warn(w.code, w.position); ``` #### Properties ##### delimiters > `readonly` **delimiters**: [`Delimiters`](#delimiters) ##### groups > `readonly` **groups**: readonly [`X12FunctionalGroup`](#x12functionalgroup)[] ##### iea > `readonly` **iea**: [`IeaSegment`](#ieasegment) \| `undefined` ##### isa > `readonly` **isa**: [`IsaSegment`](#isasegment) ##### orphanSegments > `readonly` **orphanSegments**: readonly [`X12OrphanSegment`](#x12orphansegment)[] Segments the envelope grammar could not place, in input order - almost always because they fell outside every ST..SE transaction set, plus the `TA1`-inside-a-group case, which is lifted out of the transaction it arrived in. Empty for a well-formed interchange. See [X12OrphanSegment](#x12orphansegment). ##### profile? > `readonly` `optional` **profile?**: [`X12Profile`](#x12profile) The trading-partner [X12Profile](#x12profile) in effect for this parse, if any - either passed explicitly via `options.profile` or resolved from the process-scoped default. Present only when a profile applied; used for attribution and as the partition basis for `partitionWarnings`. ##### ta1Segments > `readonly` **ta1Segments**: readonly [`Ta1Segment`](#ta1segment)[] ##### trailingBytes? > `readonly` `optional` **trailingBytes?**: `string` ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12LineAdjudication Decoded SVD + adjacent CAS / DTP - Line Adjudication Information (Loop 2430). Captures another payer's prior adjudication of THIS line so the downstream payer has the COB context. The `adjustments` re-use the remit `X12RemitAdjustment` shape since the CAS semantics are identical to those on the 835. `procedureCode` is SVD-03-2 (verbatim) - the adjudicated procedure code as the other payer recorded it. May differ from the line's SV procedure code if the other payer remapped. #### Example ```ts import type { X12LineAdjudication } from "@cosyte/x12"; declare const a: X12LineAdjudication; a.otherPayerId; // "84320" (the other payer's id) a.amountPaid?.toString(); // "50.00", or undefined if none decoded a.procedureCode; // "99213" a.adjustments[0]?.groupCode;// "CO" a.dateAdjudicated; // "20260520" CCYYMMDD verbatim ``` #### Properties ##### adjustments > `readonly` **adjustments**: readonly [`X12RemitAdjustment`](#x12remitadjustment)[] ##### amountPaid > `readonly` **amountPaid**: [`X12Decimal`](#x12decimal) \| `undefined` SVD-02, the amount the other payer paid on this line. `undefined` where this library decoded no value from that element - absent, empty, or bytes that do not decode (which also emits `X12_UNPARSEABLE_DECIMAL`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from a payer that adjudicated the line to nothing. ##### dateAdjudicated > `readonly` **dateAdjudicated**: `string` \| `undefined` ##### otherPayerId > `readonly` **otherPayerId**: `string` ##### paidUnits > `readonly` **paidUnits**: [`X12Decimal`](#x12decimal) \| `undefined` ##### procedureCode > `readonly` **procedureCode**: `string` \| `undefined` ##### procedureQualifier > `readonly` **procedureQualifier**: `string` \| `undefined` *** ### X12LineDrug Decoded LIN + CTP - Drug Identification (837P Loop 2410). Surfaces the NDC and the optional dispensed-quantity + UCUM unit. `qualifier` = `N4` NDC (overwhelmingly common), `EN` EAN/UCC-13, `HI` HIBC. #### Example ```ts import type { X12LineDrug } from "@cosyte/x12"; declare const d: X12LineDrug; d.qualifier; // "N4" d.code; // verbatim NDC d.quantity?.toString(); // "1.50" d.unitOfMeasure; // "ML" UCUM milliliter ``` #### Properties ##### code > `readonly` **code**: `string` ##### qualifier > `readonly` **qualifier**: `string` ##### quantity > `readonly` **quantity**: [`X12Decimal`](#x12decimal) \| `undefined` ##### unitOfMeasure > `readonly` **unitOfMeasure**: `string` \| `undefined` *** ### X12OrphanSegment A segment the envelope grammar has no place for, captured verbatim rather than discarded. Every segment recorded here also raised exactly one `X12_UNEXPECTED_SEGMENT` warning whose `position.segmentIndex` equals [segmentIndex](#segmentindex-1), and `context` is that warning's library-owned discriminant, so the two surfaces can be joined without string matching. These are structural anomalies, not a normal shape: a stray segment between `GE` and `IEA`, a body segment between an `SE` and its group's `GE`, a body segment between `GS` and the first `ST`, an `ST` with no open group, an `SE` closing nothing, a `GE` closing nothing, or a `TA1` inside an open functional group. The parser cannot place any of them in the typed tree, but it no longer drops them either, so a segment that used to vanish is now readable here. **Most of these sit outside every ST..SE transaction set, but the `TA1` case does not.** `TA1` is envelope-level by spec, so a `TA1` anywhere inside an open group is routed here even when it arrives BETWEEN an `ST` and its `SE` - in which case it is lifted out of that transaction's `segments` / `rawSegments` and appears only on this array. That is long-standing behaviour (it predates this array; the segment simply used to be discarded), and it is the one case where `ix.groups` is not the whole typed model. **`serializeX12` re-emits these, at [anchor](#anchor)**, so an orphan and its `X12_UNEXPECTED_SEGMENT` warning both survive a round trip. Placement is by the structural anchor and NEVER by [segmentIndex](#segmentindex-1): that index is an index into the INPUT stream, and the emit is not in input order (it hoists `ta1Segments` ahead of the groups and skips the zero-length segment a doubled terminator produces), so replaying by index splices an orphan into whatever occupies that slot - measurably, into an 835's `ST..SE` body, where a re-parse reports nothing at all. Use [segmentIndex](#segmentindex-1) to join back to the warning, not to place the segment. See `KNOWN-LIMITATIONS.md`. Two things are NOT recorded here. A doubled segment terminator delimits a zero-length segment carrying no elements, so there is nothing to retain. A segment whose first element is empty (`*A*B~`) has no id for the envelope walker to dispatch on and is skipped without a warning; that is long-standing behaviour this array does not change. **This is document content, so treat it as PHI.** Unlike an `X12ParseWarning`, whose `message` is a lookup into a frozen registry and whose metadata is positional only, an orphan carries the sender's bytes verbatim, exactly as `X12TransactionSet.rawSegments` and `isa.raw` do. A segment outside a transaction is not required to be PHI-free, so do NOT log this array wholesale when triaging; log `context` and `segmentIndex`, which name the structural rule and the location without echoing content. #### Example ```ts import { parseX12 } from "@cosyte/x12"; const ix = parseX12(raw); for (const o of ix.orphanSegments) { console.warn(o.context, o.segmentIndex, o.segment.id); } ``` #### Properties ##### anchor > `readonly` **anchor**: [`X12OrphanAnchor`](#x12orphananchor) Where the segment sat in the typed tree - the slot `serializeX12` puts it back into. See [X12OrphanAnchor](#x12orphananchor) for why a structural anchor and not [segmentIndex](#segmentindex-1) is what makes re-emission sound. ##### context > `readonly` **context**: [`X12UnexpectedSegmentContext`](#x12unexpectedsegmentcontext) Which structural rule the segment broke. ##### raw > `readonly` **raw**: `string` The verbatim segment text, segment terminator stripped. ##### segment > `readonly` **segment**: [`X12Segment`](#x12segment) The decoded segment (id + 1-indexed elements), as for any body segment. ##### segmentIndex > `readonly` **segmentIndex**: `number` Global segment index in the post-ISA stream (ISA itself is index 0) - identical to the `position.segmentIndex` of the segment's `X12_UNEXPECTED_SEGMENT` warning. *** ### X12OtherSubscriber Decoded Loop 2320 - Other Subscriber Information. Captures the SBR-01 payer responsibility code and the associated other-subscriber / other- payer NM1 entities; this reader records the surface so a consumer knows COB exists. Detailed CAS / OI / MOA breakdown inside Loop 2320 is deferred to the profile system (it tracks one payer's specific adjudication and is rarely needed for outbound claim creation). #### Example ```ts import type { X12OtherSubscriber } from "@cosyte/x12"; declare const o: X12OtherSubscriber; o.payerResponsibilityCode; // "S" secondary o.otherSubscriber?.name; // verbatim o.otherPayer?.name; // verbatim ``` #### Properties ##### claimFilingIndicator > `readonly` **claimFilingIndicator**: `string` \| `undefined` ##### individualRelationshipCode > `readonly` **individualRelationshipCode**: `string` \| `undefined` ##### otherPayer > `readonly` **otherPayer**: [`X12ClaimEntity`](#x12claimentity) \| `undefined` ##### otherSubscriber > `readonly` **otherSubscriber**: [`X12ClaimEntity`](#x12claimentity) \| `undefined` ##### payerResponsibilityCode > `readonly` **payerResponsibilityCode**: `string` *** ### X12ParseOptions Options accepted by `parseX12` to tune lenient/strict behaviour. Every field is optional; `parseX12(raw, {})` is valid and produces the library defaults. #### Remarks With `exactOptionalPropertyTypes: true`, callers cannot pass `{ strict: undefined }` - either omit the key or pass a boolean. #### Example ```ts import { parseX12, type X12ParseOptions } from "@cosyte/x12"; const opts: X12ParseOptions = { strict: true, onWarning: (w) => console.warn(w.code), }; parseX12(raw, opts); ``` #### Properties ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) ##### profile? > `readonly` `optional` **profile?**: [`X12Profile`](#x12profile) \| `null` A trading-partner [X12Profile](#x12profile) to attach to the result. An explicit profile ALWAYS wins over any process-scoped default; pass `null` to opt out of the default for a single call. When omitted, `parseX12` consults `getDefaultProfile()`. The profile is attached as `ix.profile` for attribution and consumed by `partitionWarnings`; it does not alter the lenient parse itself (v1 profiles are descriptive - see the profile subsystem docs). ##### strict? > `readonly` `optional` **strict?**: `boolean` *** ### X12ParseWarning Data shape for every Tier-2 warning emitted by the parser. Warnings are plain data (distinct from `X12ParseError`, which is a thrown `Error` subclass) so they can be safely accumulated on `X12Interchange.warnings` and passed to `onWarning` callbacks. `message` is always a member of [ALL\_WARNING\_MESSAGES](#all_warning_messages): it names the deviation, never the bytes that caused it. `position` says where to look and the bytes stay on the model. A warning has no `snippet`. #### Example ```ts import type { X12ParseWarning } from "@cosyte/x12"; declare const w: X12ParseWarning; w.code; // "X12_PRE_005010" w.position; // { segmentIndex: 0, interchangeIndex: 0, elementIndex: 12 } ``` #### Properties ##### code > `readonly` **code**: [`X12WarningCode`](#x12warningcode) ##### message > `readonly` **message**: `string` ##### position > `readonly` **position**: [`X12Position`](#x12position) *** ### X12Position Positional context attached to every warning and fatal error. Fields are 1-indexed against the X12 spec convention (interchange first, then group, then transaction, then segment, then element within that segment, then component within that element). All fields past `segmentIndex` are optional - for a top-level fatal like `X12_EMPTY_INPUT` only `segmentIndex: 0` is populated; for a per-element warning deep inside a transaction every field may be set. #### Remarks With `exactOptionalPropertyTypes: true`, do not pass `interchangeIndex: undefined` explicitly - omit the key instead. #### Example ```ts import type { X12Position } from "@cosyte/x12"; const pos: X12Position = { segmentIndex: 0, interchangeIndex: 0 }; ``` #### Properties ##### componentIndex? > `readonly` `optional` **componentIndex?**: `number` ##### elementIndex? > `readonly` `optional` **elementIndex?**: `number` ##### groupIndex? > `readonly` `optional` **groupIndex?**: `number` ##### interchangeIndex? > `readonly` `optional` **interchangeIndex?**: `number` ##### repetitionIndex? > `readonly` `optional` **repetitionIndex?**: `number` ##### segmentIndex > `readonly` **segmentIndex**: `number` ##### transactionIndex? > `readonly` `optional` **transactionIndex?**: `number` *** ### X12PremiumAddress Decoded N3 + N4 address block attached to a party. `lines` is the N3 address lines (1-2 entries); `city` / `state` / `postalCode` come from N4. All verbatim - no normalization. #### Example ```ts import type { X12PremiumAddress } from "@cosyte/x12"; declare const a: X12PremiumAddress; a.lines[0]; // "500 CORPORATE BLVD" a.city; // "COLUMBUS" ``` #### Properties ##### city > `readonly` **city**: `string` \| `undefined` ##### countryCode > `readonly` **countryCode**: `string` \| `undefined` ##### lines > `readonly` **lines**: readonly `string`[] ##### postalCode > `readonly` **postalCode**: `string` \| `undefined` ##### state > `readonly` **state**: `string` \| `undefined` *** ### X12PremiumAdjustment Decoded ADX - Adjustment to a premium remittance. `amount` is ADX-01 (signed monetary adjustment); `reasonCode` is ADX-02 (adjustment reason - `"52"` credit memo, `"53"` debit memo, …); the optional `referenceQualifier` / `referenceId` (ADX-03 / ADX-04) tie the adjustment to a prior item. #### Example ```ts import type { X12PremiumAdjustment } from "@cosyte/x12"; declare const a: X12PremiumAdjustment; a.amount.toString(); // "-25.00" a.reasonCode; // "53" ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) ##### reasonCode > `readonly` **reasonCode**: `string` ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` ##### referenceQualifier > `readonly` **referenceQualifier**: `string` \| `undefined` *** ### X12PremiumDate Decoded DTM date attached to the header or a remittance loop. `qualifier` is the date/time qualifier (DTM-01); `value` is the verbatim CCYYMMDD (DTM-02). No normalization. #### Example ```ts import type { X12PremiumDate } from "@cosyte/x12"; declare const d: X12PremiumDate; d.qualifier; // "582" (report period) d.value; // "20260601" ``` #### Properties ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12PremiumEntity Decoded ENT - Entity (organization-summary remittance). `assignedNumber` is ENT-01; the entity is identified by ENT-02 (entity id code) plus the ENT-03 / ENT-04 qualifier + id. All verbatim. #### Example ```ts import type { X12PremiumEntity } from "@cosyte/x12"; declare const e: X12PremiumEntity; e.assignedNumber; // "1" e.entityIdentifierCode; // "2J" (correspondent) e.idCode; // "GRP-0001" ``` #### Properties ##### assignedNumber > `readonly` **assignedNumber**: `string` \| `undefined` ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` *** ### X12PremiumOpenItem Decoded RMR - Remittance Advice Accounts Receivable Open Item Reference. The premium-line unit: a policy / invoice reference plus the amount paid and (optionally) the amount due. `qualifier` is RMR-01 (reference id qualifier - `"11"` account number, `"IK"` invoice, `"AZ"` health-insurance policy number); `referenceId` is RMR-02; `amountPaid` is RMR-04; `amountDue` is RMR-05. #### Example ```ts import type { X12PremiumOpenItem } from "@cosyte/x12"; declare const o: X12PremiumOpenItem; o.qualifier; // "AZ" o.referenceId; // "POL-0001" o.amountPaid?.toString(); // "250.00", or undefined if none decoded ``` #### Properties ##### amountDue > `readonly` **amountDue**: [`X12Decimal`](#x12decimal) \| `undefined` ##### amountPaid > `readonly` **amountPaid**: [`X12Decimal`](#x12decimal) \| `undefined` RMR-04, the amount paid against this open item. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state. ##### paymentActionCode > `readonly` **paymentActionCode**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### referenceId > `readonly` **referenceId**: `string` *** ### X12PremiumParty Decoded Loop 1000A (Premium Receiver, `N1*PE`) or Loop 1000B (Premium Payer / Remitter, `N1*PR` / `N1*RM`) party. Uniform shape so a consumer can read both by role. Names + addresses here are PII (organizational), not PHI in the §164.514 sense - member identity lives on the per-member remittance detail. #### Example ```ts import type { X12PremiumParty } from "@cosyte/x12"; declare const payer: X12PremiumParty; payer.name; // "EMPLOYER CO" payer.idCode; // "FEIN123" (verbatim) ``` #### Properties ##### address > `readonly` **address**: [`X12PremiumAddress`](#x12premiumaddress) \| `undefined` ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### name > `readonly` **name**: `string` ##### references > `readonly` **references**: readonly [`X12PremiumReference`](#x12premiumreference)[] *** ### X12PremiumPaymentHeader Decoded BPR - Financial Information / payment header (identical segment shape to the 835). `totalPremiumAmount` is BPR-02, the aggregate premium the bank actually moved; `method` is BPR-04 (`ACH` / `CHK` / `NON` / …). #### Example ```ts import type { X12PremiumPaymentHeader } from "@cosyte/x12"; declare const p: X12PremiumPaymentHeader; p.totalPremiumAmount?.toString(); // "12500.00", or undefined if none decoded p.method; // "ACH" p.paymentDate; // "20260601" (CCYYMMDD, verbatim) ``` #### Properties ##### creditDebitFlag > `readonly` **creditDebitFlag**: `string` BPR-03 credit/debit flag. Spec-defined `"C"` (credit) or `"D"` (debit); typed `string` to preserve any verbatim non-spec value from a quirky sender. ##### method > `readonly` **method**: `string` ##### paymentDate > `readonly` **paymentDate**: `string` ##### paymentFormatCode > `readonly` **paymentFormatCode**: `string` \| `undefined` ##### totalPremiumAmount > `readonly` **totalPremiumAmount**: [`X12Decimal`](#x12decimal) \| `undefined` BPR-02, the aggregate premium moved. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state. ##### transactionHandlingCode > `readonly` **transactionHandlingCode**: `string` *** ### X12PremiumPayments The top-level result returned by "./get-820.js".get820Payments. Carries the payment header (BPR), reassociation traces (TRN), the premium receiver + remitter parties (Loop 1000A / 1000B), every organization / individual remittance detail loop (Loop 2000 → RMR / ADX), and every warning surfaced during the walk. #### Example ```ts import { parseX12, get820Payments } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "820"); if (tx !== undefined) { const prem = get820Payments(ix.delimiters, tx); prem.payment.totalPremiumAmount.toString(); for (const r of prem.remittances) { r.openItems[0]?.amountPaid.toString(); } } ``` #### Properties ##### payment > `readonly` **payment**: [`X12PremiumPaymentHeader`](#x12premiumpaymentheader) ##### receiver > `readonly` **receiver**: [`X12PremiumParty`](#x12premiumparty) \| `undefined` ##### remittances > `readonly` **remittances**: readonly [`X12PremiumRemittance`](#x12premiumremittance)[] ##### remitter > `readonly` **remitter**: [`X12PremiumParty`](#x12premiumparty) \| `undefined` ##### traces > `readonly` **traces**: readonly [`X12PremiumTrace`](#x12premiumtrace)[] ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12PremiumPerson Decoded NM1 individual (member) inside an individual remittance loop. PHI surface: every field carries PHI. #### Example ```ts import type { X12PremiumPerson } from "@cosyte/x12"; declare const p: X12PremiumPerson; p.entityIdentifierCode; // "IL" insured p.lastName; // verbatim p.idQualifier; // "34" SSN / "ZZ" mutually defined p.idCode; // verbatim member id ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### firstName > `readonly` **firstName**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### lastName > `readonly` **lastName**: `string` \| `undefined` ##### middleName > `readonly` **middleName**: `string` \| `undefined` ##### suffix > `readonly` **suffix**: `string` \| `undefined` *** ### X12PremiumReference Decoded REF segment - supplemental identifier (master policy number, plan id, version). `qualifier` is the X12 reference-identification qualifier; `value` is the verbatim id. #### Example ```ts import type { X12PremiumReference } from "@cosyte/x12"; declare const r: X12PremiumReference; r.qualifier; // "38" (master policy number) r.value; // "POL-0001" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12PremiumRemittance Decoded Loop 2000 remittance entry - one organization-summary (`ENT`) or individual (`NM1`) remittance. Carries the open-item references (RMR), the adjustments (ADX), and any REF / DTM context. The entity / individual name fields carry PII / PHI (member name + id) - surfaced verbatim, never echoed in a warning, never normalized. #### Example ```ts import type { X12PremiumRemittance } from "@cosyte/x12"; declare const r: X12PremiumRemittance; r.individual?.lastName; // verbatim member surname r.openItems[0]?.referenceId; // policy / invoice number r.openItems[0]?.amountPaid.toString(); // "250.00" ``` #### Properties ##### adjustments > `readonly` **adjustments**: readonly [`X12PremiumAdjustment`](#x12premiumadjustment)[] ##### dates > `readonly` **dates**: readonly [`X12PremiumDate`](#x12premiumdate)[] ##### entity > `readonly` **entity**: [`X12PremiumEntity`](#x12premiumentity) \| `undefined` ##### individual > `readonly` **individual**: [`X12PremiumPerson`](#x12premiumperson) \| `undefined` ##### openItems > `readonly` **openItems**: readonly [`X12PremiumOpenItem`](#x12premiumopenitem)[] ##### references > `readonly` **references**: readonly [`X12PremiumReference`](#x12premiumreference)[] *** ### X12PremiumTrace Decoded TRN - Reassociation Trace Number. Pairs the 820 to the originating ACH / check artifact so the receiver can reconcile the premium deposit. `referenceId` is the trace number a bank statement / ACH addenda will carry. #### Example ```ts import type { X12PremiumTrace } from "@cosyte/x12"; declare const t: X12PremiumTrace; t.traceTypeCode; // "1" - Current Transaction Trace Numbers t.referenceId; // e.g. "PREM-202606" (verbatim) ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` ##### originatingCompanySupplementalCode > `readonly` **originatingCompanySupplementalCode**: `string` \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` ##### traceTypeCode > `readonly` **traceTypeCode**: `string` *** ### X12Profile A readonly, frozen profile produced by `defineProfile()`. Mirrors the locked hl7 `Profile` shape (name / description / lineage) plus X12's `quirks` axis and a structured `describe()`. #### Example ```ts import { parseX12, profiles } from "@cosyte/x12"; const ix = parseX12(raw, { profile: profiles.availity }); ix.profile?.name; // "availity" ix.profile?.describe().adds.length; ``` #### Properties ##### describe > `readonly` **describe**: () => [`X12ProfileDescription`](#x12profiledescription-1) ###### Returns [`X12ProfileDescription`](#x12profiledescription-1) ##### description? > `readonly` `optional` **description?**: `string` ##### lineage > `readonly` **lineage**: readonly `string`[] ##### name > `readonly` **name**: `string` ##### quirks > `readonly` **quirks**: readonly [`X12ProfileQuirk`](#x12profilequirk)[] *** ### X12ProfileConformancePartition The rendered quirks of a profile, grouped by their conformance state. Every quirk appears in exactly one of the three, and the three together are the profile's whole quirk set, so a consumer can ask "does this profile record anything a partner may not lawfully require?" without walking the effect buckets. A profile with no quirks makes no conformance claim of any kind: all three are empty. #### Example ```ts import { profiles } from "@cosyte/x12"; const { notPermitted } = profiles.availity.describe().conformance; notPermitted.map((q) => q.id); // quirks 45 CFR 162.915 forbids requiring ``` #### Properties ##### notPermitted > `readonly` **notPermitted**: readonly [`X12ClassifiedQuirk`](#x12classifiedquirk)[] Quirks recorded as partner deviations 162.915 forbids requiring. ##### permitted > `readonly` **permitted**: readonly [`X12ClassifiedQuirk`](#x12classifiedquirk)[] Quirks recorded as ones a partner MAY lawfully require. ##### undetermined > `readonly` **undetermined**: readonly [`X12ClassifiedQuirk`](#x12classifiedquirk)[] Quirks with no recorded judgement. NOT a claim in either direction. *** ### X12ProfileDescription Structured `describe()` output - the "what this profile relaxes / adds / requires" record published with the package. Returned as DATA (not a formatted string, unlike hl7) so downstream tooling - docs generators, the `pathways` engine - can consume it programmatically. Each bucketed quirk carries the conformance state it resolved to, so a consumer reading a described deviation also reads whether a partner could lawfully have required it. `requires` is a record of what a partner MANDATES and never a claim that the standard supports the mandate: that question is answered by `conformance` alone, and its default answer is "undetermined". #### Example ```ts import { profiles } from "@cosyte/x12"; const d = profiles.availity.describe(); d.adds.map((q) => q.id); // ["payer-loop-ref-2u", "service-line-ref-f8"] d.adds.map((q) => q.conformance); // ["undetermined", "undetermined"] d.expectedWarnings; // readonly X12WarningCode[] ``` #### Properties ##### adds > `readonly` **adds**: readonly [`X12ClassifiedQuirk`](#x12classifiedquirk)[] ##### conformance > `readonly` **conformance**: [`X12ProfileConformancePartition`](#x12profileconformancepartition) The same quirks, grouped by conformance state instead of by effect. See [X12ProfileConformancePartition](#x12profileconformancepartition); the states themselves are documented on [X12ProfileConformance](#x12profileconformance). ##### description? > `readonly` `optional` **description?**: `string` ##### expectedWarnings > `readonly` **expectedWarnings**: readonly [`X12WarningCode`](#x12warningcode)[] Sorted, de-duplicated union of every quirk's `expectedWarnings`. ##### lineage > `readonly` **lineage**: readonly `string`[] ##### name > `readonly` **name**: `string` ##### relaxes > `readonly` **relaxes**: readonly [`X12ClassifiedQuirk`](#x12classifiedquirk)[] ##### requires > `readonly` **requires**: readonly [`X12ClassifiedQuirk`](#x12classifiedquirk)[] *** ### X12ProfileQuirk A single trading-partner deviation captured by a profile. Every quirk is fixture-grounded: `fixture` points at a real Tier-2 corpus file that demonstrates the deviation, and `sourceCategory` records where the quirk was observed. This is the locked hard rule - a quirk without a demonstrating fixture is forbidden, enforced both by this required field and by the accuracy test. #### Example ```ts import type { X12ProfileQuirk } from "@cosyte/x12"; const quirk: X12ProfileQuirk = { id: "payer-loop-ref-2u", effect: "adds", summary: "Payer Loop 1000A carries a REF*2U additional payer identifier.", fixture: "remit/835-availity-quirk.edi", sourceCategory: "Availity 835 ERA companion guide - payer-loop REF", }; ``` #### Extended by - [`X12ClassifiedQuirk`](#x12classifiedquirk) #### Properties ##### conformance? > `readonly` `optional` **conformance?**: [`X12ProfileConformance`](#x12profileconformance) OPTIONAL recorded judgement of whether a trading partner may lawfully require this deviation under 45 CFR 162.915. Omit it and the quirk renders as [X12ProfileConformance](#x12profileconformance) `"undetermined"` - the fail-safe default - so a profile written before this field existed keeps defining and describing exactly as it did, and never gains a claim nobody made. ##### effect > `readonly` **effect**: [`X12ProfileEffect`](#x12profileeffect) Which `describe()` bucket this quirk renders into. ##### expectedWarnings? > `readonly` `optional` **expectedWarnings?**: readonly [`X12WarningCode`](#x12warningcode)[] Warning codes this quirk leads a consumer to EXPECT when the deviation is present. Drives `partitionWarnings`. Often empty: the lenient parser absorbs most corpus deviations with zero warnings, and that "lossless, no warning" outcome is itself the documented behavior. ##### fixture > `readonly` **fixture**: `string` Path to the Tier-2 fixture demonstrating the deviation, relative to `test/fixtures/` (e.g. `"remit/835-availity-quirk.edi"`). REQUIRED - the locked hard rule. The accuracy test parses this file and asserts the claimed deviation is present. ##### id > `readonly` **id**: `string` Stable, kebab-case identifier - unique within a profile's quirk set. ##### sourceCategory > `readonly` **sourceCategory**: `string` Where the deviation was observed (companion guide / corpus category). ##### summary > `readonly` **summary**: `string` One-line human summary. NEVER contains PHI - describes structure only. *** ### X12ProfileSpec Input accepted by `defineProfile()`. Every field except `name` is optional; `extends` composes parent profiles (lineage + quirks merge) the same way hl7's `extends` does. #### Example ```ts import { defineProfile, profiles, type X12ProfileSpec } from "@cosyte/x12"; const spec: X12ProfileSpec = { name: "my-bcbs-regional", extends: profiles.bcbsCommon, quirks: [ { id: "service-line-ref-f8", effect: "adds", summary: "Service line carries a REF*F8 original-reference identifier.", fixture: "remit/835-availity-quirk.edi", sourceCategory: "regional BCBS 835 companion guide", }, ], }; const profile = defineProfile(spec); ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` ##### extends? > `readonly` `optional` **extends?**: [`X12Profile`](#x12profile) \| readonly [`X12Profile`](#x12profile)[] ##### name > `readonly` **name**: `string` ##### quirks? > `readonly` `optional` **quirks?**: readonly [`X12ProfileQuirk`](#x12profilequirk)[] *** ### X12RemitAddress Decoded N3 + N4 address block attached to a party. `lines` is the N3 address lines (1-2 entries); `city`/`state`/`postalCode` come from N4. All fields verbatim - no normalization (no proper-casing, no postal-code canonicalization). #### Example ```ts import type { X12RemitAddress } from "@cosyte/x12"; declare const a: X12RemitAddress; a.lines[0]; // "123 PAYER WAY" a.city; // "ANYTOWN" a.state; // "OH" a.postalCode; // "44113" ``` #### Properties ##### city > `readonly` **city**: `string` \| `undefined` ##### countryCode > `readonly` **countryCode**: `string` \| `undefined` ##### lines > `readonly` **lines**: readonly `string`[] ##### postalCode > `readonly` **postalCode**: `string` \| `undefined` ##### state > `readonly` **state**: `string` \| `undefined` *** ### X12RemitAdjustment Decoded CAS adjustment. One CAS segment carries up to 6 adjustment triples (reason code + amount + optional quantity) all under the same `groupCode` (CAS-01). The walker flattens these so each [X12RemitAdjustment](#x12remitadjustment) is ONE adjustment, not one segment. **`groupCode` is the safety primitive** - see "../../code-lists/cagc.js".CLAIM\_ADJUSTMENT\_GROUP\_CODES. `CO` = provider write-off; `PR` = patient owes; `OA` / `PI` = other / payer edit. `reasonCode` is the CARC value (verbatim); `reasonDescription` is from the bundled snapshot or `undefined` if outside the subset. #### Example ```ts import type { X12RemitAdjustment } from "@cosyte/x12"; declare const a: X12RemitAdjustment; a.groupCode; // "PR" (patient responsibility) a.reasonCode; // "1" (deductible) a.reasonDescription; // "Deductible Amount" a.amount?.toString(); // "50.00", or undefined if none decoded ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) \| `undefined` The CAS adjustment amount for this triple. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. ##### groupCode > `readonly` **groupCode**: `string` ##### quantity > `readonly` **quantity**: [`X12Decimal`](#x12decimal) \| `undefined` ##### reasonCode > `readonly` **reasonCode**: `string` ##### reasonDescription > `readonly` **reasonDescription**: `string` \| `undefined` *** ### X12RemitAmount Decoded AMT segment - supplemental claim or service amount (allowed charge, late filing penalty, capitation payment, etc.). `qualifier` names which amount (`AU` coverage amount, `B6` allowed actual, …). Surfaced verbatim; never folded into the balance invariant (which checks only CLP / CAS / PLB / SVC fields). #### Example ```ts import type { X12RemitAmount } from "@cosyte/x12"; declare const a: X12RemitAmount; a.qualifier; // "B6" a.amount.toString(); // "450.00" ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) ##### qualifier > `readonly` **qualifier**: `string` *** ### X12RemitClaim Decoded Loop 2100 - Claim Payment Information. The clinical-claim unit: one provider-billed claim adjudicated by the payer. **Carries the per-claim balance invariant** (`totalPaymentAmount + patientResponsibilityAmount + Σ(claim-level adjustments) === totalChargeAmount`); a mismatch fires `X12_835_REMIT_BALANCE_MISMATCH` and is NEVER silently rebalanced. PHI surface: `patientControlNumber` (provider's account number), `payerClaimControlNumber`, the patient name on NM1*QC, and member ID on NM1*IL all carry PHI; the parser surfaces them verbatim, never echoes them in warnings, and never normalizes. #### Example ```ts import type { X12RemitClaim } from "@cosyte/x12"; declare const c: X12RemitClaim; c.patientControlNumber; // "PT-ACCT-001" c.totalChargeAmount?.toString(); // "500.00", or undefined if none decoded c.totalPaymentAmount?.toString(); // "450.00", or undefined if none decoded c.claimStatusCode; // "1" - Processed as Primary c.serviceLines.length; // count of SVC loops ``` #### Properties ##### adjustments > `readonly` **adjustments**: readonly [`X12RemitAdjustment`](#x12remitadjustment)[] ##### amounts > `readonly` **amounts**: readonly [`X12RemitAmount`](#x12remitamount)[] ##### claimFilingIndicatorCode > `readonly` **claimFilingIndicatorCode**: `string` \| `undefined` ##### claimFrequencyCode > `readonly` **claimFrequencyCode**: `string` \| `undefined` ##### claimStatusCode > `readonly` **claimStatusCode**: `string` ##### claimStatusDescription > `readonly` **claimStatusDescription**: `string` \| `undefined` ##### correctedPatient > `readonly` **correctedPatient**: [`X12RemitPerson`](#x12remitperson) \| `undefined` ##### facilityTypeCode > `readonly` **facilityTypeCode**: `string` \| `undefined` ##### patient > `readonly` **patient**: [`X12RemitPerson`](#x12remitperson) \| `undefined` ##### patientControlNumber > `readonly` **patientControlNumber**: `string` ##### patientResponsibilityAmount > `readonly` **patientResponsibilityAmount**: [`X12Decimal`](#x12decimal) \| `undefined` CLP-05, the patient responsibility amount. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. ##### payerClaimControlNumber > `readonly` **payerClaimControlNumber**: `string` \| `undefined` ##### references > `readonly` **references**: readonly [`X12RemitReference`](#x12remitreference)[] ##### remarks > `readonly` **remarks**: readonly [`X12RemitRemark`](#x12remitremark)[] ##### renderingProvider > `readonly` **renderingProvider**: [`X12RemitProvider`](#x12remitprovider) \| `undefined` ##### serviceLines > `readonly` **serviceLines**: readonly [`X12RemitServiceLine`](#x12remitserviceline)[] ##### servicePeriodEnd > `readonly` **servicePeriodEnd**: `string` \| `undefined` ##### servicePeriodStart > `readonly` **servicePeriodStart**: `string` \| `undefined` ##### serviceProvider > `readonly` **serviceProvider**: [`X12RemitProvider`](#x12remitprovider) \| `undefined` ##### subscriber > `readonly` **subscriber**: [`X12RemitPerson`](#x12remitperson) \| `undefined` ##### totalChargeAmount > `readonly` **totalChargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` CLP-03, the total submitted charge. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. ##### totalPaymentAmount > `readonly` **totalPaymentAmount**: [`X12Decimal`](#x12decimal) \| `undefined` CLP-04, the total amount paid on the claim. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. *** ### X12RemitContact Decoded PER contact segment. `contactFunctionCode` = `"BL"` (Technical), `"CX"` (Payers' Claim Office), etc.; each contact may carry up to 3 communication channels (TE/EX/EM/FX). #### Example ```ts import type { X12RemitContact } from "@cosyte/x12"; declare const c: X12RemitContact; c.contactFunctionCode; // "BL" c.name; // "JANE COORDINATOR" c.communications[0]?.qualifier; // "TE" c.communications[0]?.value; // "5551234567" ``` #### Properties ##### communications > `readonly` **communications**: readonly `object`[] ##### contactFunctionCode > `readonly` **contactFunctionCode**: `string` ##### name > `readonly` **name**: `string` \| `undefined` *** ### X12RemitParty Decoded Loop 1000A (Payer Identification) or Loop 1000B (Payee Identification) party. The shape is uniform across both loops so a consumer can iterate by role. PHI surface here: payer/payee names + addresses + contact info are PII (payee may be an individual provider) but NOT PHI in the §164.514 sense - patient identity lives on the per-claim level. #### Example ```ts import type { X12RemitParty } from "@cosyte/x12"; declare const payer: X12RemitParty; payer.name; // "PAYER NAME" payer.idCode; // "12345" (NAIC / NPI / payer ID, verbatim) ``` #### Properties ##### additionalIdentifiers > `readonly` **additionalIdentifiers**: readonly [`X12RemitReference`](#x12remitreference)[] ##### address > `readonly` **address**: [`X12RemitAddress`](#x12remitaddress) \| `undefined` ##### contacts > `readonly` **contacts**: readonly [`X12RemitContact`](#x12remitcontact)[] ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### name > `readonly` **name**: `string` *** ### X12RemitPaymentHeader Decoded BPR - Financial Information / payment header. The PAYMENT MOVEMENT primitive: actual payment amount, credit/debit flag, method (`ACH`/`CHK`/`NON`/`BOP`/`FWT`), payment date. **`totalActualPayment` is the sum the bank actually moved** - `Σ(claim CLP-04) + Σ(PLB adjustments) === BPR-02` is the top-level balance invariant. #### Example ```ts import type { X12RemitPaymentHeader } from "@cosyte/x12"; declare const p: X12RemitPaymentHeader; p.totalActualPayment?.toString(); // "945.00", or undefined if none decoded p.method; // "ACH" p.paymentDate; // "20260601" (CCYYMMDD, verbatim) ``` #### Properties ##### creditDebitFlag > `readonly` **creditDebitFlag**: `string` BPR-03 credit/debit flag. Spec-defined values: `"C"` (credit - money to provider, the normal case) or `"D"` (debit - refund / chargeback, uncommon). The field is typed as `string` to preserve verbatim any non-spec value from a quirky payer; consumers branching on it should compare against the literals. ##### method > `readonly` **method**: `string` ##### paymentDate > `readonly` **paymentDate**: `string` ##### paymentFormatCode > `readonly` **paymentFormatCode**: `string` \| `undefined` ##### totalActualPayment > `readonly` **totalActualPayment**: [`X12Decimal`](#x12decimal) \| `undefined` BPR-02, the amount the bank actually moved. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. ##### transactionHandlingCode > `readonly` **transactionHandlingCode**: `string` *** ### X12RemitPerson Decoded NM1 person - patient, subscriber, or corrected patient. The `idQualifier` distinguishes member ID (`MI`), Social Security (`34` - rare, regulated), payer ID, etc. PHI surface: every field on a person model carries PHI. #### Example ```ts import type { X12RemitPerson } from "@cosyte/x12"; declare const p: X12RemitPerson; p.entityIdentifierCode; // "QC" patient / "IL" insured / "74" corrected patient p.lastName; // verbatim p.idQualifier; // "MI" p.idCode; // "MEMBER123" ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### firstName > `readonly` **firstName**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### lastName > `readonly` **lastName**: `string` \| `undefined` ##### middleName > `readonly` **middleName**: `string` \| `undefined` ##### suffix > `readonly` **suffix**: `string` \| `undefined` *** ### X12RemitProvider Decoded NM1 provider - service provider (`82`), rendering provider (`82` in some contexts), crossover carrier (`TT`), other payer (`PR` / `GB`), etc. Same shape as a person but the qualifier semantics are organizational. #### Example ```ts import type { X12RemitProvider } from "@cosyte/x12"; declare const r: X12RemitProvider; r.entityIdentifierCode; // "82" service provider r.name; // "RENDERING PROVIDER INC" r.idQualifier; // "XX" (NPI) r.idCode; // "1234567890" ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### name > `readonly` **name**: `string` \| `undefined` *** ### X12RemitProviderAdjustment Decoded PLB - Provider-Level Adjustment. Off-claim adjustments (recoupments, interest, capitation, write-offs) that move money at the provider level, not the claim level. Each PLB segment carries up to 6 adjustment triples (reason + amount); the walker flattens these so each [X12RemitProviderAdjustment](#x12remitprovideradjustment) is one adjustment. **Sign convention:** a POSITIVE PLB amount REDUCES the provider's payment (recoupment / take-back); a NEGATIVE PLB amount ADDS to the payment (interest / advance payment). The 835 balance invariant `Σ(claim CLP-04) + Σ(PLB amounts) === BPR-02` works because PLB amounts already carry the correct sign. `reasonCode` is the **composite** PLB reason code (the qualifier + optional reference together, e.g. `WO:123456` for "withholding for claim 123456"); `subCode` carries the optional second component. #### Example ```ts import type { X12RemitProviderAdjustment } from "@cosyte/x12"; declare const p: X12RemitProviderAdjustment; p.providerId; // "1234567890" (NPI) p.fiscalPeriodDate; // "20261231" p.reasonCode; // "WO" (withholding) p.subCode; // "123456" (related-claim reference) p.amount?.toString();// "50.00", or undefined if none decoded ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) \| `undefined` The PLB adjustment amount, with its raw EDI sign. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. ##### fiscalPeriodDate > `readonly` **fiscalPeriodDate**: `string` ##### providerId > `readonly` **providerId**: `string` ##### reasonCode > `readonly` **reasonCode**: `string` ##### subCode > `readonly` **subCode**: `string` \| `undefined` *** ### X12RemitReference Decoded REF segment - additional identifier (Tax ID, payer ID, supplemental). `qualifier` is the X12 reference-identification qualifier (`"2U"` payer ID, `"TJ"` Tax ID, `"EV"` participant receiver, `"D9"` claim number, …); `value` is the verbatim ID. Used on payer / payee loops and on per-claim contexts. #### Example ```ts import type { X12RemitReference } from "@cosyte/x12"; declare const r: X12RemitReference; r.qualifier; // "TJ" r.value; // "123456789" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12RemitRemark Decoded remark segment (LQ at claim or service level). `system` is the LQ-01 industry code system (`"HE"` healthcare remark codes / RARC, `"RX"` reject-reason, etc.); `code` is the verbatim value; `description` comes from the bundled RARC snapshot for `HE` codes, `undefined` otherwise. Unknown HE codes also emit `X12_UNKNOWN_RARC`. #### Example ```ts import type { X12RemitRemark } from "@cosyte/x12"; declare const r: X12RemitRemark; r.system; // "HE" r.code; // "N4" r.description; // "Missing/incomplete/invalid prior insurance carrier(s) EOB." ``` #### Properties ##### code > `readonly` **code**: `string` ##### description > `readonly` **description**: `string` \| `undefined` ##### system > `readonly` **system**: `string` *** ### X12RemitServiceLine Decoded Loop 2110 - Service Payment Information. One per service line adjudicated. Line-level CAS adjustments roll up into the claim-level balance invariant (`Σ(SVC paid) + Σ(line CAS) === CLP-04`); a mismatch fires `X12_835_REMIT_BALANCE_MISMATCH` on the parent claim. `productServiceIdQualifier` is the SVC-01-1 procedure-code system (`HC` HCPCS/CPT, `AD` ADA dental, `N4` NDC, `WK` Advanced Billing Concepts, `IV` HIPPS rate code, …); `productServiceId` is the verbatim code. The qualifier governs interpretation - **misreading it picks the wrong code system** and corrupts the clinical context. X221A1 is reported to default an absent SVC-05 to one (secondhand, via X12's RFI #2163 - this library has not read the TR3). This reader does **not** apply that default, because fabricating a count the sender did not send is inventing data. Apply it yourself if you want it. **`undefined` on either quantity means "not decoded", not "absent".** The element may have been missing, or present and unparseable as a decimal - the two are not distinguished here and neither raises a warning. Read the verbatim element off `tx.segments` if you need to tell them apart. #### Example ```ts import type { X12RemitServiceLine } from "@cosyte/x12"; declare const s: X12RemitServiceLine; s.productServiceIdQualifier; // "HC" s.productServiceId; // "99213" s.chargeAmount?.toString(); // "150.00", or undefined if none decoded s.paymentAmount?.toString(); // "135.00", or undefined if none decoded ``` #### Properties ##### adjustments > `readonly` **adjustments**: readonly [`X12RemitAdjustment`](#x12remitadjustment)[] ##### amounts > `readonly` **amounts**: readonly [`X12RemitAmount`](#x12remitamount)[] ##### chargeAmount > `readonly` **chargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-02, the line's submitted charge. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. ##### modifiers > `readonly` **modifiers**: readonly `string`[] ##### originalServiceId > `readonly` **originalServiceId**: `string` \| `undefined` ##### originalServiceIdQualifier > `readonly` **originalServiceIdQualifier**: `string` \| `undefined` ##### originalUnitsOfService > `readonly` **originalUnitsOfService**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-07 - Original Units of Service Count: the units as SUBMITTED. Sent only when they differ from the paid count in SVC-05, so `undefined` here does NOT mean zero submitted units - it means "same as paid". ##### paidUnitsOfService > `readonly` **paidUnitsOfService**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-05 - Units of Service Paid Count: what the payer adjudicated. ##### paymentAmount > `readonly` **paymentAmount**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-03, the amount paid on the line. `undefined` where this library decoded no value from that element - it was absent or empty, or it held bytes that do not decode as a decimal (which also emits `X12_UNPARSEABLE_DECIMAL` at its `elementIndex`). Through `0.0.12` both cases read `X12Decimal.ZERO`, indistinguishable from an amount of zero the sender did state, and the balance invariants summed that fabricated zero. ##### productServiceId > `readonly` **productServiceId**: `string` ##### productServiceIdQualifier > `readonly` **productServiceIdQualifier**: `string` ##### references > `readonly` **references**: readonly [`X12RemitReference`](#x12remitreference)[] ##### remarks > `readonly` **remarks**: readonly [`X12RemitRemark`](#x12remitremark)[] ##### revenueCode > `readonly` **revenueCode**: `string` \| `undefined` SVC-04 - NUBC revenue code. Absent on a professional line. ##### serviceDateEnd > `readonly` **serviceDateEnd**: `string` \| `undefined` ##### serviceDateStart > `readonly` **serviceDateStart**: `string` \| `undefined` *** ### X12Remittance The top-level result returned by "./get-835.js".get835. Carries the payment header (BPR), the trace (TRN), payer + payee identification (Loop 1000A/1000B), every claim payment loop (Loop 2100 → Loop 2110), provider-level adjustments (PLB), and every warning surfaced during the walk - including the safety-critical "../../parser/warnings.js".WARNING\_CODES.X12\_835\_REMIT\_BALANCE\_MISMATCH. #### Example ```ts import { parseX12, get835 } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835"); if (tx !== undefined) { const remit = get835(ix.delimiters, tx); remit.payment.totalActualPayment.toString(); for (const claim of remit.claims) { claim.totalChargeAmount.toString(); claim.totalPaymentAmount.toString(); } } ``` #### Properties ##### claims > `readonly` **claims**: readonly [`X12RemitClaim`](#x12remitclaim)[] ##### payee > `readonly` **payee**: [`X12RemitParty`](#x12remitparty) \| `undefined` ##### payer > `readonly` **payer**: [`X12RemitParty`](#x12remitparty) \| `undefined` ##### payment > `readonly` **payment**: [`X12RemitPaymentHeader`](#x12remitpaymentheader) ##### providerAdjustments > `readonly` **providerAdjustments**: readonly [`X12RemitProviderAdjustment`](#x12remitprovideradjustment)[] ##### traces > `readonly` **traces**: readonly [`X12RemitTrace`](#x12remittrace)[] ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12RemitTrace Decoded TRN - Reassociation Trace Number. Pairs the 835 to the originating payer's payment artifact (ACH trace, check number) so a cash-poster can reconcile. `referenceId` is the trace number a bank statement / ACH addenda will carry; `originatingCompanyId` is the payer's CMS-assigned (or proprietary) routing identifier. #### Example ```ts import type { X12RemitTrace } from "@cosyte/x12"; declare const t: X12RemitTrace; t.traceTypeCode; // "1" - Current Transaction Trace Numbers t.referenceId; // e.g. "12345" (verbatim) t.originatingCompanyId; // e.g. "1512345678" ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` ##### originatingCompanySupplementalCode > `readonly` **originatingCompanySupplementalCode**: `string` \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` ##### traceTypeCode > `readonly` **traceTypeCode**: `string` *** ### X12ReviewDecision The HCR Health Care Services Review decision (response only, Loop 2000E / 2000F). **`actionCode` (HCR-01) is the certification outcome and is preserved verbatim** - the parser never infers an outcome. `reviewIdentificationNumber` (HCR-02) is the authorization / certification number a provider quotes back to the payer. #### Example ```ts import type { X12ReviewDecision } from "@cosyte/x12"; declare const d: X12ReviewDecision; d.actionCode; // "A1" (certified in total) d.reviewIdentificationNumber; // "AUTH123456" d.reasonCode; // "0" (HCR-03, free-form decision reason) ``` #### Properties ##### actionCode > `readonly` **actionCode**: `string` ##### reasonCode > `readonly` **reasonCode**: `string` \| `undefined` ##### reviewIdentificationNumber > `readonly` **reviewIdentificationNumber**: `string` \| `undefined` ##### secondSurgicalOpinionCode > `readonly` **secondSurgicalOpinionCode**: `string` \| `undefined` *** ### X12Segment Immutable decoded X12 segment. `elements` is 1-indexed: `elements[0]` is the segment id placeholder (matching "./types.js".IsaSegment, `GsSegment`, etc. - every typed segment in the envelope follows the same 1-indexed shape so consumers learn one rule and never have to recall whether a particular accessor offsets by one). `raw` preserves the exact segment text from input (terminator stripped) so a byte-exact round-trip survives even when downstream stages mutate the model. Element values are stored RAW (pre-`?`-unescape). Reads via [getSegmentValue](#getsegmentvalue) apply [unescapeRelease](#unescaperelease) on demand - this keeps round-trip byte-exact while still letting helpers receive spec-compliant logical values. #### Example ```ts import type { X12Segment } from "@cosyte/x12"; declare const seg: X12Segment; seg.id; // "NM1" seg.elements[3]; // raw text of NM1-03 (post-element-split, pre-?-unescape) ``` #### Properties ##### elements > `readonly` **elements**: readonly `string`[] ##### id > `readonly` **id**: `string` ##### raw > `readonly` **raw**: `string` *** ### X12ServiceLineStatus One service-line status (Loop 2220). Triggered by an SVC; carries the procedure / revenue identification, line amounts, and its own STC statuses + REF / DTP. #### Example ```ts import type { X12ServiceLineStatus } from "@cosyte/x12"; declare const l: X12ServiceLineStatus; l.procedureCode; // "99213" l.lineChargeAmount?.toString(); // "150.00" l.statuses[0]?.statuses[0]?.statusCode; // "20" ``` #### Properties ##### dates > `readonly` **dates**: readonly [`X12StatusDate`](#x12statusdate)[] ##### lineChargeAmount > `readonly` **lineChargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` ##### linePaymentAmount > `readonly` **linePaymentAmount**: [`X12Decimal`](#x12decimal) \| `undefined` ##### modifiers > `readonly` **modifiers**: readonly `string`[] ##### procedureCode > `readonly` **procedureCode**: `string` \| `undefined` ##### references > `readonly` **references**: readonly [`X12StatusReference`](#x12statusreference)[] ##### revenueCode > `readonly` **revenueCode**: `string` \| `undefined` ##### serviceIdQualifier > `readonly` **serviceIdQualifier**: `string` \| `undefined` ##### statuses > `readonly` **statuses**: readonly [`X12StatusInfo`](#x12statusinfo)[] ##### unitsOfService > `readonly` **unitsOfService**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-07, data element 380 (Quantity). Named "Units of Service Count" by TR3 `005010X212` and "Original Units of Service Count" by `005010X214`; the same element in the same position either way, so one field serves both and the walker never has to know which TR3 it is reading. SVC-05 is NOT USED in both TR3s, so unlike the 835 there is no second, paid quantity to tell this one apart from. `undefined` means **not decoded**, not "the sender sent zero": an absent element and a present one that is not an X12 R-type decimal share it, and only the latter raises `X12_UNPARSEABLE_DECIMAL` at `position.elementIndex` 7. *** ### X12ServiceReview One services-review item - a patient-event HL (`EV`, Loop 2000E) or a service HL (`SS`, Loop 2000F). Carries the `UM` review information, the optional `HCR` decision (response only), echoed `TRN` traces, `HI` diagnoses, attached provider NM1s, and the supplemental REF/DTP/MSG. #### Example ```ts import type { X12ServiceReview } from "@cosyte/x12"; declare const r: X12ServiceReview; r.requestCategoryCode; // "HS" (health services review) / "AR" (admission review) r.certificationTypeCode; // "I" (initial) / "R" (renewal) r.decision?.actionCode; // "A1" (certified) - response only ``` #### Properties ##### certificationTypeCode > `readonly` **certificationTypeCode**: `string` \| `undefined` ##### dates > `readonly` **dates**: readonly [`X12AuthDate`](#x12authdate)[] ##### decision > `readonly` **decision**: [`X12ReviewDecision`](#x12reviewdecision) \| `undefined` ##### diagnoses > `readonly` **diagnoses**: readonly [`X12AuthDiagnosis`](#x12authdiagnosis)[] ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) \| `undefined` ##### levelOfServiceCode > `readonly` **levelOfServiceCode**: `string` \| `undefined` ##### messages > `readonly` **messages**: readonly `string`[] ##### providers > `readonly` **providers**: readonly [`X12AuthEntity`](#x12authentity)[] ##### references > `readonly` **references**: readonly [`X12AuthReference`](#x12authreference)[] ##### requestCategoryCode > `readonly` **requestCategoryCode**: `string` \| `undefined` ##### serviceTypeCode > `readonly` **serviceTypeCode**: `string` \| `undefined` ##### traces > `readonly` **traces**: readonly [`X12AuthTrace`](#x12authtrace)[] *** ### X12ServicesReview Top-level result of "./get-278.js".get278Request / "./get-278.js".get278Response. Carries the BHT header, the four named HL parties (UMO, requester, subscriber, dependent) resolved from the hierarchy, every per-event / per-service review item, the verbatim HL tree, and the warnings surfaced during the walk. #### Example ```ts import { parseX12, get278Response } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "278"); if (tx !== undefined) { const review = get278Response(ix.delimiters, tx); review?.reviews[0]?.decision?.actionCode; // "A1" (certified) } ``` #### Properties ##### dependent > `readonly` **dependent**: [`X12AuthMember`](#x12authmember) \| `undefined` ##### direction > `readonly` **direction**: `"request"` \| `"response"` ##### header > `readonly` **header**: [`X12AuthHeader`](#x12authheader) ##### hierarchies > `readonly` **hierarchies**: readonly [`X12Hl`](#x12hl)[] ##### implementationConventionReference > `readonly` **implementationConventionReference**: `string` \| `undefined` ##### requester > `readonly` **requester**: [`X12AuthEntity`](#x12authentity) \| `undefined` ##### reviews > `readonly` **reviews**: readonly [`X12ServiceReview`](#x12servicereview)[] ##### subscriber > `readonly` **subscriber**: [`X12AuthMember`](#x12authmember) \| `undefined` ##### utilizationManagementOrganization > `readonly` **utilizationManagementOrganization**: [`X12AuthEntity`](#x12authentity) \| `undefined` ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12StatusCode One Health Care Claim Status composite (C043, STC-01 / STC-10 / STC-11). Pairs a **CSCC** (Claim Status Category Code, X12 source 507) with a **CSC** (Claim Status Code, X12 source 508) and the responsible entity. The verbatim codes are always preserved; descriptions resolve from the bundled snapshots (or `undefined` outside the subset). #### Example ```ts import type { X12StatusCode } from "@cosyte/x12"; declare const c: X12StatusCode; c.categoryCode; // "A7" (rejected/invalid) c.statusCode; // "21" (Missing or invalid information) c.statusDescription; // "Missing or invalid information." c.entityCode; // "85" (billing provider) ``` #### Properties ##### categoryCode > `readonly` **categoryCode**: `string` ##### categoryDescription > `readonly` **categoryDescription**: `string` \| `undefined` ##### entityCode > `readonly` **entityCode**: `string` \| `undefined` ##### statusCode > `readonly` **statusCode**: `string` ##### statusDescription > `readonly` **statusDescription**: `string` \| `undefined` *** ### X12StatusDate A DTP date / date-range on a claim or service-line status (e.g. DTP*472 service date, DTP*050 received date). #### Example ```ts import type { X12StatusDate } from "@cosyte/x12"; declare const d: X12StatusDate; d.qualifier; // "472" d.formatQualifier; // "RD8" d.value; // "20260601-20260601" ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12StatusEntity A non-person entity decoded from an NM1 - the payer (Loop 2100A), information receiver (2100B), or service provider (2100C). #### Example ```ts import type { X12StatusEntity } from "@cosyte/x12"; declare const e: X12StatusEntity; e.entityIdentifierCode; // "PR" / "41" / "1P" e.name; // "MEDPAY INSURANCE" e.idCode; // "00123" ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### name > `readonly` **name**: `string` *** ### X12StatusInfo One decoded STC segment - the headline status fields plus the up-to-three [X12StatusCode](#x12statuscode) composites (STC-01, STC-10, STC-11). A single claim (or service line) can carry multiple STC segments; each becomes one `X12StatusInfo`. #### Example ```ts import type { X12StatusInfo } from "@cosyte/x12"; declare const s: X12StatusInfo; s.statusEffectiveDate; // "20260627" s.totalChargeAmount?.toString(); // "150.00" s.statuses[0]?.statusCode; // "20" ``` #### Properties ##### actionCode > `readonly` **actionCode**: `string` \| `undefined` ##### adjudicationDate > `readonly` **adjudicationDate**: `string` \| `undefined` ##### message > `readonly` **message**: `string` \| `undefined` ##### paymentAmount > `readonly` **paymentAmount**: [`X12Decimal`](#x12decimal) \| `undefined` ##### statusEffectiveDate > `readonly` **statusEffectiveDate**: `string` \| `undefined` ##### statuses > `readonly` **statuses**: readonly [`X12StatusCode`](#x12statuscode)[] ##### totalChargeAmount > `readonly` **totalChargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` *** ### X12StatusInquiry Top-level result of "./get-276.js".get276StatusInquiry. Carries the transaction header, the information-source roots of the transmitted hierarchy, every declared HL verbatim, and every warning raised while walking. #### Example ```ts import { parseX12, get276StatusInquiry } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "276"); if (tx !== undefined) { const inquiry = get276StatusInquiry(ix.delimiters, tx); const sub = inquiry?.informationSources[0]?.receivers[0]?.providers[0]?.subscribers[0]; sub?.claims[0]?.trace?.referenceId; // the trace a 277 echoes back } ``` #### Properties ##### header > `readonly` **header**: [`X12StatusInquiryHeader`](#x12statusinquiryheader-1) \| `undefined` ##### hierarchies > `readonly` **hierarchies**: readonly [`X12Hl`](#x12hl)[] ##### informationSources > `readonly` **informationSources**: readonly [`X12StatusInquirySource`](#x12statusinquirysource)[] ##### warnings > `readonly` **warnings**: readonly [`X12ParseWarning`](#x12parsewarning)[] *** ### X12StatusInquiryAmount An AMT amount row on a claim (e.g. `AMT*T3` total submitted charges). A row and not a slot: an AMT reaching the reader with no decodable amount builds no row at all and reports the loss, so an empty `amounts` list can be told apart from an amount this reader could build no row from. #### Example ```ts import type { X12StatusInquiryAmount } from "@cosyte/x12"; declare const a: X12StatusInquiryAmount; a.qualifier; // "T3" a.amount.toString(); // "150.00" ``` #### Properties ##### amount > `readonly` **amount**: [`X12Decimal`](#x12decimal) AMT-02 - the monetary amount, NEVER a JavaScript `number`. ##### qualifier > `readonly` **qualifier**: `string` AMT-01 - amount qualifier code. *** ### X12StatusInquiryClaim One claim the submitter is asking about (Loop 2200D / 2200E), opened by the TRN that carries its trace. Carries the identifiers, submitted amounts, dates and service lines transmitted under that trace. #### Example ```ts import type { X12StatusInquiryClaim } from "@cosyte/x12"; declare const c: X12StatusInquiryClaim; c.trace?.referenceId; // "STATUS20260601001" c.references[0]?.qualifier; // "1K" (payer claim control number) c.serviceLines.length; // 1 ``` #### Properties ##### amounts > `readonly` **amounts**: readonly [`X12StatusInquiryAmount`](#x12statusinquiryamount)[] AMT amount rows transmitted under this claim. ##### dates > `readonly` **dates**: readonly [`X12StatusInquiryDate`](#x12statusinquirydate)[] DTP date rows transmitted under this claim. ##### references > `readonly` **references**: readonly [`X12StatusInquiryReference`](#x12statusinquiryreference)[] REF identifiers transmitted under this claim. ##### serviceLines > `readonly` **serviceLines**: readonly [`X12StatusInquiryServiceLine`](#x12statusinquiryserviceline)[] Loop 2210 service lines transmitted under this claim. ##### trace > `readonly` **trace**: [`X12StatusInquiryTrace`](#x12statusinquirytrace) \| `undefined` TRN - the trace the answering 277 echoes back verbatim. *** ### X12StatusInquiryDate A DTP date or date range on a claim or a service line. `formatQualifier` (DTP-02) is the element that says WHICH: `D8` is a single `CCYYMMDD` date and `RD8` a `CCYYMMDD-CCYYMMDD` range. It is preserved verbatim beside the value, so a consumer never has to infer which one a `value` holds. #### Example ```ts import type { X12StatusInquiryDate } from "@cosyte/x12"; declare const d: X12StatusInquiryDate; d.qualifier; // "472" (service date) d.formatQualifier; // "D8" or "RD8" d.value; // "20260520" ``` #### Properties ##### formatQualifier > `readonly` **formatQualifier**: `string` DTP-02 - date/time period format qualifier (`D8` single, `RD8` range). ##### qualifier > `readonly` **qualifier**: `string` DTP-01 - date/time qualifier. ##### value > `readonly` **value**: `string` DTP-03 - the date or range, in the DTP-02 format. *** ### X12StatusInquiryDependent One dependent (Loop 2000E / 2100E / 2200E) - a patient who cannot be identified as a subscriber in their own right. HL-03 is `23`. Carries its OWN name, demographics and claims and is never merged onto the subscriber it hangs under. #### Example ```ts import type { X12StatusInquiryDependent } from "@cosyte/x12"; declare const d: X12StatusInquiryDependent; d.name?.firstName; // "BABY" d.claims.length; // the dependent's own claims ``` #### Properties ##### claims > `readonly` **claims**: readonly [`X12StatusInquiryClaim`](#x12statusinquiryclaim)[] ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### name > `readonly` **name**: [`X12StatusInquiryName`](#x12statusinquiryname) \| `undefined` *** ### X12StatusInquiryHeader The BHT beginning-of-hierarchical-transaction header. `purposeCode` (BHT-02) is `13` on a request; `referenceId` (BHT-03) is the submitter's own identifier for the request. #### Example ```ts import type { X12StatusInquiryHeader } from "@cosyte/x12"; declare const h: X12StatusInquiryHeader; h.hierarchicalStructureCode; // "0010" h.purposeCode; // "13" ``` #### Properties ##### date > `readonly` **date**: `string` \| `undefined` BHT-04 - transaction creation date, CCYYMMDD. ##### hierarchicalStructureCode > `readonly` **hierarchicalStructureCode**: `string` BHT-01 - hierarchical structure code. ##### purposeCode > `readonly` **purposeCode**: `string` BHT-02 - transaction set purpose code. ##### referenceId > `readonly` **referenceId**: `string` \| `undefined` BHT-03 - submitter transaction identifier. ##### time > `readonly` **time**: `string` \| `undefined` BHT-05 - transaction creation time. *** ### X12StatusInquiryName An NM1 name loop, plus the DMG demographics that follow it. ONE type covers every level, because NM1 is one segment: NM1-03 is "name last or organization name", so a payer fills it and a member fills it, and splitting the type by level would mean this reader deciding which kind of party a level holds. `entityTypeQualifier` (NM1-02) is the sender's own statement of that (`1` person, `2` non-person) and is preserved verbatim. **No postal address is surfaced here, deliberately.** The response reader beside this one surfaces none either, so the two directions of this family agree; an N3 or N4 a sender transmits stays verbatim on `tx.segments`. #### Example ```ts import type { X12StatusInquiryName } from "@cosyte/x12"; declare const n: X12StatusInquiryName; n.entityIdentifierCode; // "IL" subscriber / "PR" payer / "1P" provider n.lastNameOrOrganizationName; // "DOE" or "MEDPAY INSURANCE" n.dateOfBirth; // "19850515" (DMG-02, CCYYMMDD) ``` #### Properties ##### dateOfBirth > `readonly` **dateOfBirth**: `string` \| `undefined` DMG-02 - date of birth. ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` NM1-01 - entity identifier code. ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` NM1-02 - entity type qualifier (`1` person, `2` non-person). ##### firstName > `readonly` **firstName**: `string` \| `undefined` NM1-04 - first name. ##### genderCode > `readonly` **genderCode**: `string` \| `undefined` DMG-03 - gender code. ##### idCode > `readonly` **idCode**: `string` \| `undefined` NM1-09 - identification code. ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` NM1-08 - identification code qualifier. ##### lastNameOrOrganizationName > `readonly` **lastNameOrOrganizationName**: `string` \| `undefined` NM1-03 - last name, or the organization name for a non-person. ##### middleName > `readonly` **middleName**: `string` \| `undefined` NM1-05 - middle name. ##### suffix > `readonly` **suffix**: `string` \| `undefined` NM1-07 - name suffix. *** ### X12StatusInquiryProcedure The SVC-01 composite medical procedure identifier, exposed as its SEPARATED components. It is never handed back as one joined string: the component separator is framing, so joining it into a value would make two documents that differ only in their declared delimiters decode to different models. #### Example ```ts import type { X12StatusInquiryProcedure } from "@cosyte/x12"; declare const p: X12StatusInquiryProcedure; p.qualifier; // "HC" (SVC-01-1, product/service id qualifier) p.code; // "99213" (SVC-01-2) p.modifiers[0]; // "25" (SVC-01-3 onward) ``` #### Properties ##### code > `readonly` **code**: `string` \| `undefined` SVC-01-2 - the procedure code. ##### description > `readonly` **description**: `string` \| `undefined` SVC-01-7 - procedure description. ##### modifiers > `readonly` **modifiers**: readonly `string`[] SVC-01-3 through SVC-01-6 - procedure modifiers, in transmitted order. ##### qualifier > `readonly` **qualifier**: `string` SVC-01-1 - product or service id qualifier. *** ### X12StatusInquiryProvider One service provider (Loop 2000C / 2100C) - the provider whose claim is being asked about. HL-03 is `19` and its HL-02 names an information receiver. This level is the one the 270's spine does NOT have, and it is why the claim-status pair carries five levels where the eligibility pair carries four. #### Example ```ts import type { X12StatusInquiryProvider } from "@cosyte/x12"; declare const p: X12StatusInquiryProvider; p.name?.idCode; // the provider's NPI as transmitted p.subscribers.length; // 1 ``` #### Properties ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### name > `readonly` **name**: [`X12StatusInquiryName`](#x12statusinquiryname) \| `undefined` ##### subscribers > `readonly` **subscribers**: readonly [`X12StatusInquirySubscriber`](#x12statusinquirysubscriber)[] *** ### X12StatusInquiryReceiver One information receiver (Loop 2000B / 2100B) - the party the answer goes back to. HL-03 is `21` and its HL-02 names an information source. #### Example ```ts import type { X12StatusInquiryReceiver } from "@cosyte/x12"; declare const r: X12StatusInquiryReceiver; r.name?.idCode; // the receiver's identifier r.providers.length; // 1 ``` #### Properties ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### name > `readonly` **name**: [`X12StatusInquiryName`](#x12statusinquiryname) \| `undefined` ##### providers > `readonly` **providers**: readonly [`X12StatusInquiryProvider`](#x12statusinquiryprovider)[] *** ### X12StatusInquiryReference A REF supplemental identifier on a claim or on a service line (e.g. `REF*1K` payer claim control number, `REF*BLT` bill type, `REF*FJ` line item control number). #### Example ```ts import type { X12StatusInquiryReference } from "@cosyte/x12"; declare const r: X12StatusInquiryReference; r.qualifier; // "1K" r.value; // "PCN0001" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` REF-03 - description. ##### qualifier > `readonly` **qualifier**: `string` REF-01 - reference identification qualifier. ##### value > `readonly` **value**: `string` REF-02 - reference identification. *** ### X12StatusInquiryServiceLine One service line the request asks about (Loop 2210), opened by an SVC. **Which SVC elements this reader surfaces is a property of the READ, stated as one.** It surfaces SVC-01 as its separated components, SVC-02 as the line charge, SVC-04 as the revenue code and SVC-07 as the units of service count - the same four the 277 reader beside it surfaces for the same segment. SVC-03, SVC-05 and SVC-06 are left unread: this is the REQUEST direction, where a submitter states what it billed rather than what was paid, and a slot named for a payment on a question nobody has answered yet would invite a consumer to read one. A sender that transmits them keeps them verbatim on `tx.segments`. #### Example ```ts import type { X12StatusInquiryServiceLine } from "@cosyte/x12"; declare const l: X12StatusInquiryServiceLine; l.procedure?.code; // "99213" l.lineChargeAmount?.toString(); // "150.00" l.unitsOfService?.toString(); // "1" ``` #### Properties ##### dates > `readonly` **dates**: readonly [`X12StatusInquiryDate`](#x12statusinquirydate)[] DTP date rows transmitted under this service line. ##### lineChargeAmount > `readonly` **lineChargeAmount**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-02 - line item charge amount. ##### procedure > `readonly` **procedure**: [`X12StatusInquiryProcedure`](#x12statusinquiryprocedure) \| `undefined` SVC-01 - the billed procedure, as its separated components. ##### references > `readonly` **references**: readonly [`X12StatusInquiryReference`](#x12statusinquiryreference)[] REF identifiers transmitted under this service line. ##### revenueCode > `readonly` **revenueCode**: `string` \| `undefined` SVC-04 - revenue code. ##### unitsOfService > `readonly` **unitsOfService**: [`X12Decimal`](#x12decimal) \| `undefined` SVC-07 - units of service count. *** ### X12StatusInquirySource One information source (Loop 2000A / 2100A) - the payer the request is put to. The root of a transmitted hierarchy: HL-03 is `20` and HL-02 is absent. #### Example ```ts import type { X12StatusInquirySource } from "@cosyte/x12"; declare const s: X12StatusInquirySource; s.name?.lastNameOrOrganizationName; // "MEDPAY INSURANCE" s.receivers.length; // 1 ``` #### Properties ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### name > `readonly` **name**: [`X12StatusInquiryName`](#x12statusinquiryname) \| `undefined` ##### receivers > `readonly` **receivers**: readonly [`X12StatusInquiryReceiver`](#x12statusinquiryreceiver)[] *** ### X12StatusInquirySubscriber One subscriber (Loop 2000D / 2100D / 2200D) - the member the claim was filed under, or the member a dependent hangs under. HL-03 is `22`. #### Example ```ts import type { X12StatusInquirySubscriber } from "@cosyte/x12"; declare const s: X12StatusInquirySubscriber; s.name?.idCode; // member identifier as transmitted s.claims[0]?.trace?.referenceId; // the trace a 277 echoes back s.dependents.length; // 0 ``` #### Properties ##### claims > `readonly` **claims**: readonly [`X12StatusInquiryClaim`](#x12statusinquiryclaim)[] ##### dependents > `readonly` **dependents**: readonly [`X12StatusInquiryDependent`](#x12statusinquirydependent)[] ##### hierarchy > `readonly` **hierarchy**: [`X12Hl`](#x12hl) ##### name > `readonly` **name**: [`X12StatusInquiryName`](#x12statusinquiryname) \| `undefined` *** ### X12StatusInquiryTrace A reassociation trace (TRN) transmitted with the request. **TRN-02 is the value the answering 277 must echo verbatim**, which is what lets the submitter match the answer to this question. #### Example ```ts import type { X12StatusInquiryTrace } from "@cosyte/x12"; declare const t: X12StatusInquiryTrace; t.traceTypeCode; // "1" (current transaction trace numbers) t.referenceId; // "STATUS20260601001" ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` TRN-03 - originating company identifier. ##### referenceId > `readonly` **referenceId**: `string` TRN-02 - the trace number a 277 echoes back. ##### supplementalReferenceId > `readonly` **supplementalReferenceId**: `string` \| `undefined` TRN-04 - supplemental reference identifier. ##### traceTypeCode > `readonly` **traceTypeCode**: `string` TRN-01 - trace type code. *** ### X12StatusMember A person (subscriber Loop 2100D / dependent Loop 2100E) decoded from an NM1. `idCode` is the member identifier (NM1-09) - synthetic-only in fixtures. #### Example ```ts import type { X12StatusMember } from "@cosyte/x12"; declare const m: X12StatusMember; m.lastName; // "DOE" m.idCode; // "MBR0001" ``` #### Properties ##### entityIdentifierCode > `readonly` **entityIdentifierCode**: `string` ##### entityTypeQualifier > `readonly` **entityTypeQualifier**: `string` ##### firstName > `readonly` **firstName**: `string` \| `undefined` ##### idCode > `readonly` **idCode**: `string` \| `undefined` ##### idQualifier > `readonly` **idQualifier**: `string` \| `undefined` ##### lastName > `readonly` **lastName**: `string` \| `undefined` ##### middleName > `readonly` **middleName**: `string` \| `undefined` ##### suffix > `readonly` **suffix**: `string` \| `undefined` *** ### X12StatusReference A REF supplemental identifier on a claim or service-line status (e.g. REF*1K payer claim control number, REF*BLT bill type). #### Example ```ts import type { X12StatusReference } from "@cosyte/x12"; declare const r: X12StatusReference; r.qualifier; // "1K" r.value; // "PCN0001" ``` #### Properties ##### description > `readonly` **description**: `string` \| `undefined` ##### qualifier > `readonly` **qualifier**: `string` ##### value > `readonly` **value**: `string` *** ### X12StatusTrace A reassociation trace (TRN). For a 277 claim status, **`referenceId` (TRN-02) echoes the requesting 276's trace number verbatim** so the provider can re-associate the answer. The walker never mutates it. #### Example ```ts import type { X12StatusTrace } from "@cosyte/x12"; declare const t: X12StatusTrace; t.traceTypeCode; // "2" t.referenceId; // "CLAIM20260627001" ``` #### Properties ##### originatingCompanyId > `readonly` **originatingCompanyId**: `string` \| `undefined` ##### referenceId > `readonly` **referenceId**: `string` ##### supplementalReferenceId > `readonly` **supplementalReferenceId**: `string` \| `undefined` ##### traceTypeCode > `readonly` **traceTypeCode**: `string` *** ### X12SubscriberInfo Decoded SBR - Subscriber Information (Loop 2000B's primary trigger). Or decoded PAT - Patient Information when the patient is the subscriber. - `payerResponsibilityCode` (SBR-01 / X12 1138): `P` Primary, `S` Secondary, `T` Tertiary, etc. Drives COB ordering. - `individualRelationshipCode` (SBR-02 / PAT-01 / X12 1069): `18` Self, `01` Spouse, `19` Child, `G8` Other Insured. - `groupNumber` (SBR-03), `groupName` (SBR-04). - `claimFilingIndicator` (SBR-09 / X12 1032): `MC` Medicaid, `MB` Medicare Part B, `BL` BlueCross/BlueShield, `CI` Commercial, etc. #### Example ```ts import type { X12SubscriberInfo } from "@cosyte/x12"; declare const s: X12SubscriberInfo; s.payerResponsibilityCode; // "P" s.individualRelationshipCode; // "18" self s.claimFilingIndicator; // "MB" ``` #### Properties ##### claimFilingIndicator > `readonly` **claimFilingIndicator**: `string` \| `undefined` ##### groupName > `readonly` **groupName**: `string` \| `undefined` ##### groupNumber > `readonly` **groupNumber**: `string` \| `undefined` ##### individualRelationshipCode > `readonly` **individualRelationshipCode**: `string` \| `undefined` ##### payerResponsibilityCode > `readonly` **payerResponsibilityCode**: `string` \| `undefined` *** ### X12ToothInformation Decoded TOO - Tooth Information (837D Loop 2400). `qualifier` is the tooth-numbering code list (`JP` ADA Universal Tooth Numbering, `JO` ANSI / ISO 3950 / FDI). `toothCode` is the verbatim tooth identifier; `surfaces` are the per-surface codes (`M` mesial, `O` occlusal, `D` distal, etc.) from TOO-03's composite components. #### Example ```ts import type { X12ToothInformation } from "@cosyte/x12"; declare const t: X12ToothInformation; t.qualifier; // "JP" t.toothCode; // "14" t.surfaces; // ["O"] ``` #### Properties ##### qualifier > `readonly` **qualifier**: `string` ##### surfaces > `readonly` **surfaces**: readonly `string`[] ##### toothCode > `readonly` **toothCode**: `string` *** ### X12Tr3Conformance One conformance row: the identifier this package implements for one transaction (and, where a transaction has more than one document or more than one shape, one variant of it), the directions it is implemented in, and the identifiers 45 CFR 162.920 names for it. `transaction` is the transaction set number as it appears in this package's own export names, so `get835` and `build837D` and `parse999` and `buildTA1` are reachable from a row without a second hand-maintained list. `directions` states what is implemented and NEVER implies the other direction: a transaction this package only reads carries `["read"]` alone. `tr3` is `null` only where no implementation guide identifier names the document at all, and `note` then says why. #### Example ```ts import { X12_TR3_CONFORMANCE, type X12Tr3Conformance } from "@cosyte/x12"; const row: X12Tr3Conformance | undefined = X12_TR3_CONFORMANCE.find( (r) => r.transaction === "837" && r.variant === "I", ); row?.tr3; // "005010X223A3" row?.adoption; // "errata-in-practice" row?.cfrAdopted; // ["005010X223", "005010X223A1"] ``` #### Properties ##### adoption > `readonly` **adoption**: [`X12Tr3Adoption`](#x12tr3adoption) How `tr3` stands against 45 CFR 162.920. ##### cfrAdopted > `readonly` **cfrAdopted**: readonly `string`[] Identifiers 45 CFR 162.920 names for this transaction; empty iff `not-adopted`. ##### directions > `readonly` **directions**: readonly [`X12Tr3Direction`](#x12tr3direction)[] The directions actually implemented. Never empty. ##### note > `readonly` **note**: `string` \| `null` Why this row is not the simple case, where it is not. Never an empty string. ##### title > `readonly` **title**: `string` Human-readable document title. Never empty. ##### tr3 > `readonly` **tr3**: `string` \| `null` The identifier this package implements, errata suffix included. ##### transaction > `readonly` **transaction**: `string` Transaction set number, spelled as this package's export names spell it. ##### variant > `readonly` **variant**: `string` \| `null` The variant within that transaction, or `null` where there is only one. *** ### X12TransactionSet A single ST..SE transaction set inside a functional group. The parser decodes every body segment via "./segment.js".decodeSegment so `segments` carries typed [X12Segment](#x12segment) entries (ST through SE, inclusive). `rawSegments` mirrors the same list as the verbatim raw segment strings (terminator stripped) so a byte-exact round-trip survives any downstream consumer that needs to re-emit the source. `elements` on the ST and SE segments themselves IS decoded at envelope time so envelope invariants can be checked (ST-02 ↔ SE-02 control-number reconciliation, SE-01 segment count). **Read "decoded" as element-SPLIT and nothing more: the strings are PRE-`?`-unescape**, and a reader that publishes one to a consumer unescapes it first. #### Example ```ts import type { X12TransactionSet } from "@cosyte/x12"; declare const tx: X12TransactionSet; tx.st.elements[1]; // ST-01 - transaction set ID (e.g. "835") tx.segments[1]?.id; // first body segment id tx.rawSegments[1]; // first body segment raw text ``` #### Properties ##### gs? > `readonly` `optional` **gs?**: [`GsSegment`](#gssegment) The GS header of the functional group the envelope walker framed this transaction set in, the same object as that group's `gs`. It is carried here so a typed reader called with `(delimiters, tx)` can read GS-08 where ST-03 is absent or empty. **Element values are stored RAW, pre-`?`-unescape**, like every envelope element, so an element is the framed byte text of its slot and not necessarily the value the sender stated. Optional: a transaction set assembled by hand may carry none, and every typed reader treats that exactly as a group whose GS-08 is absent. ##### rawSegments > `readonly` **rawSegments**: readonly `string`[] ##### se > `readonly` **se**: \{ `elements`: readonly `string`[]; `raw`: `string`; \} \| `undefined` ##### segments > `readonly` **segments**: readonly [`X12Segment`](#x12segment)[] ##### st > `readonly` **st**: `object` ###### elements > `readonly` **elements**: readonly `string`[] ###### raw > `readonly` **raw**: `string` *** ### X12WarningPartition The result of [partitionWarnings](#partitionwarnings): warnings split into those a profile leads you to EXPECT and those it does not. #### Example ```ts import type { X12WarningPartition } from "@cosyte/x12"; declare const p: X12WarningPartition; p.unexpected.length; // alert only on these ``` #### Properties ##### expected > `readonly` **expected**: readonly [`X12ParseWarning`](#x12parsewarning)[] ##### unexpected > `readonly` **unexpected**: readonly [`X12ParseWarning`](#x12parsewarning)[] ## Type Aliases ### AckBuildErrorCode > **AckBuildErrorCode** = *typeof* [`ACK_BUILD_ERROR_CODES`](#ack_build_error_codes)\[keyof *typeof* [`ACK_BUILD_ERROR_CODES`](#ack_build_error_codes)\] String-literal union over [ACK\_BUILD\_ERROR\_CODES](#ack_build_error_codes). Used as `AckBuildError.code`. *** ### Attachment275BuildErrorCode > **Attachment275BuildErrorCode** = *typeof* [`ATTACHMENT_275_BUILD_ERROR_CODES`](#attachment_275_build_error_codes)\[keyof *typeof* [`ATTACHMENT_275_BUILD_ERROR_CODES`](#attachment_275_build_error_codes)\] String-literal union over [ATTACHMENT\_275\_BUILD\_ERROR\_CODES](#attachment_275_build_error_codes). #### Example ```ts import type { Attachment275BuildErrorCode } from "@cosyte/x12"; const code: Attachment275BuildErrorCode = "X12_275_BUILD_NO_ATTACHMENT"; ``` *** ### Build837ServiceLineSpec > **Build837ServiceLineSpec** = [`Build837ServiceLineProfessionalSpec`](#build837servicelineprofessionalspec) \| [`Build837ServiceLineInstitutionalSpec`](#build837servicelineinstitutionalspec) \| [`Build837ServiceLineDentalSpec`](#build837servicelinedentalspec) Service-line spec - discriminated union keyed by `variant`. Every line in a `build837P` spec must be `"P"`, etc.; a mismatch is REFUSED. #### Example ```ts import type { Build837ServiceLineSpec } from "@cosyte/x12"; declare const sl: Build837ServiceLineSpec; if (sl.variant === "P") sl.diagnosisPointers; ``` *** ### Claim837BuildErrorCode > **Claim837BuildErrorCode** = *typeof* [`CLAIM_837_BUILD_ERROR_CODES`](#claim_837_build_error_codes)\[keyof *typeof* [`CLAIM_837_BUILD_ERROR_CODES`](#claim_837_build_error_codes)\] String-literal union over [CLAIM\_837\_BUILD\_ERROR\_CODES](#claim_837_build_error_codes). Used as [Claim837BuildError](#claim837builderror).`code`. *** ### ClaimAdjustmentGroupCode > **ClaimAdjustmentGroupCode** = *typeof* [`CLAIM_ADJUSTMENT_GROUP_CODES`](#claim_adjustment_group_codes)\[keyof *typeof* [`CLAIM_ADJUSTMENT_GROUP_CODES`](#claim_adjustment_group_codes)\] Discriminant type for a known Claim Adjustment Group Code. Used to narrow the `group` field on a parsed claim adjustment; unknown inbound values keep the verbatim string (NEVER coerced) and consumers can distinguish via `isClaimAdjustmentGroupCode`. #### Example ```ts import type { ClaimAdjustmentGroupCode } from "@cosyte/x12"; const code: ClaimAdjustmentGroupCode = "PR"; ``` *** ### ClaimStatus276BuildErrorCode > **ClaimStatus276BuildErrorCode** = *typeof* [`CLAIM_STATUS_276_BUILD_ERROR_CODES`](#claim_status_276_build_error_codes)\[keyof *typeof* [`CLAIM_STATUS_276_BUILD_ERROR_CODES`](#claim_status_276_build_error_codes)\] String-literal union over [CLAIM\_STATUS\_276\_BUILD\_ERROR\_CODES](#claim_status_276_build_error_codes). Used as [ClaimStatus276BuildError](#claimstatus276builderror).`code`. *** ### ClaimStatus277BuildErrorCode > **ClaimStatus277BuildErrorCode** = *typeof* [`CLAIM_STATUS_277_BUILD_ERROR_CODES`](#claim_status_277_build_error_codes)\[keyof *typeof* [`CLAIM_STATUS_277_BUILD_ERROR_CODES`](#claim_status_277_build_error_codes)\] String-literal union over [CLAIM\_STATUS\_277\_BUILD\_ERROR\_CODES](#claim_status_277_build_error_codes). Used as [ClaimStatus277BuildError](#claimstatus277builderror).`code`. *** ### CodeListCompleteness > **CodeListCompleteness** = `"complete-published-list"` \| `"cited-subset"` Whether a bundled snapshot is the whole published list or a cited part of it. It is what tells a code the lookup does not know from a code the publisher never issued: outside a `"cited-subset"` the absence means nothing, and outside a `"complete-published-list"` it means the code is not in the list at all. #### Example ```ts import { SERVICE_TYPE_CODES } from "@cosyte/x12"; SERVICE_TYPE_CODES.meta.completeness; // "cited-subset" ``` *** ### CodeListRedistributionStatus > **CodeListRedistributionStatus** = `"permitted"` \| `"licence-required"` \| `"not-established"` What the recorded evidence says about redistributing a bundled list's descriptions. - `"permitted"` - the maintainer publishes the list without a licence requirement, so the descriptions may be redistributed. - `"licence-required"` - the maintainer requires a purchased licence or express permission before the descriptions may be reproduced. - `"not-established"` - the sources obtained for this package do not settle it. This is a real answer and is treated as NOT redistributable. #### Example ```ts import { CARC, RARC } from "@cosyte/x12"; CARC.meta.redistribution?.status; // "licence-required" RARC.meta.redistribution?.status; // "permitted" ``` *** ### CodeValidity > **CodeValidity** = *typeof* [`CODE_VALIDITY`](#code_validity)\[keyof *typeof* [`CODE_VALIDITY`](#code_validity)\] String-literal union over [CODE\_VALIDITY](#code_validity). Used as [CodeValidityResult](#codevalidityresult).`validity`. *** ### CodeValidityReason > **CodeValidityReason** = *typeof* [`CODE_VALIDITY_REASONS`](#code_validity_reasons)\[keyof *typeof* [`CODE_VALIDITY_REASONS`](#code_validity_reasons)\] String-literal union over [CODE\_VALIDITY\_REASONS](#code_validity_reasons). Used as [CodeValidityResult](#codevalidityresult).`reason`. *** ### CoreBusinessScenario > **CoreBusinessScenario** = *typeof* [`CORE_BUSINESS_SCENARIOS`](#core_business_scenarios)\[keyof *typeof* [`CORE_BUSINESS_SCENARIOS`](#core_business_scenarios)\] One of the four CORE-defined business scenario identifiers in [CORE\_BUSINESS\_SCENARIOS](#core_business_scenarios). #### Example ```ts import type { CoreBusinessScenario } from "@cosyte/x12"; const scenario: CoreBusinessScenario = "scenario-4"; ``` *** ### CoreCodeCombinationErrorCode > **CoreCodeCombinationErrorCode** = *typeof* [`CORE_CODE_COMBINATION_ERROR_CODES`](#core_code_combination_error_codes)\[keyof *typeof* [`CORE_CODE_COMBINATION_ERROR_CODES`](#core_code_combination_error_codes)\] String-literal union over [CORE\_CODE\_COMBINATION\_ERROR\_CODES](#core_code_combination_error_codes). Used as [CoreCodeCombinationTableError](#corecodecombinationtableerror).`code`. #### Example ```ts import type { CoreCodeCombinationErrorCode } from "@cosyte/x12"; const code: CoreCodeCombinationErrorCode = "X12_CORE_COMBINATION_TABLE_INVALID"; ``` *** ### CoreCodeCombinationOutcome > **CoreCodeCombinationOutcome** = *typeof* [`CORE_CODE_COMBINATION_OUTCOMES`](#core_code_combination_outcomes)\[keyof *typeof* [`CORE_CODE_COMBINATION_OUTCOMES`](#core_code_combination_outcomes)\] String-literal union over [CORE\_CODE\_COMBINATION\_OUTCOMES](#core_code_combination_outcomes). #### Example ```ts import type { CoreCodeCombinationOutcome } from "@cosyte/x12"; const outcome: CoreCodeCombinationOutcome = "unevaluated"; ``` *** ### CoreCodeCombinationResult > **CoreCodeCombinationResult** = \{ `outcome`: *typeof* [`IN_TABLE`](#in_table); `tableVersion`: `string`; \} \| \{ `outcome`: *typeof* [`NOT_IN_TABLE`](#not_in_table); `tableVersion`: `string`; \} \| \{ `outcome`: *typeof* [`UNEVALUATED`](#unevaluated); `reason`: [`CoreCodeCombinationUnevaluatedReason`](#corecodecombinationunevaluatedreason); `tableVersion`: `string` \| `undefined`; \} The answer from [checkCoreCodeCombination](#checkcorecodecombination). Switch on `outcome`. `tableVersion` is the supplied table's `version`, exactly as supplied; it is `undefined` only when no table was supplied. An `unevaluated` answer also names its `reason`. #### Example ```ts import type { CoreCodeCombinationResult } from "@cosyte/x12"; declare const result: CoreCodeCombinationResult; switch (result.outcome) { case "in-table": result.tableVersion; // the label you supplied break; case "not-in-table": break; case "unevaluated": result.reason; // e.g. "no-table" break; } ``` *** ### CoreCodeCombinationTableField > **CoreCodeCombinationTableField** = `"table"` \| `"version"` \| `"rows"` \| `"row"` \| `"scenario"` \| `"groupCode"` \| `"reasonCode"` \| `"remarkCode"` Which part of a combination table a [CoreCodeCombinationTableError](#corecodecombinationtableerror) refused. `table` is the argument itself (not an object), `version` its label, `rows` its row list, `row` one entry of that list (not an object), and the last four are fields of one row. #### Example ```ts import type { CoreCodeCombinationTableField } from "@cosyte/x12"; const field: CoreCodeCombinationTableField = "reasonCode"; ``` *** ### CoreCodeCombinationUnevaluatedReason > **CoreCodeCombinationUnevaluatedReason** = *typeof* [`CORE_CODE_COMBINATION_UNEVALUATED_REASONS`](#core_code_combination_unevaluated_reasons)\[keyof *typeof* [`CORE_CODE_COMBINATION_UNEVALUATED_REASONS`](#core_code_combination_unevaluated_reasons)\] String-literal union over [CORE\_CODE\_COMBINATION\_UNEVALUATED\_REASONS](#core_code_combination_unevaluated_reasons). #### Example ```ts import type { CoreCodeCombinationUnevaluatedReason } from "@cosyte/x12"; const reason: CoreCodeCombinationUnevaluatedReason = "no-rows-for-scenario"; ``` *** ### Eligibility270BuildErrorCode > **Eligibility270BuildErrorCode** = *typeof* [`ELIGIBILITY_270_BUILD_ERROR_CODES`](#eligibility_270_build_error_codes)\[keyof *typeof* [`ELIGIBILITY_270_BUILD_ERROR_CODES`](#eligibility_270_build_error_codes)\] String-literal union over [ELIGIBILITY\_270\_BUILD\_ERROR\_CODES](#eligibility_270_build_error_codes). Used as [Eligibility270BuildError](#eligibility270builderror).`code`. *** ### Eligibility271BuildErrorCode > **Eligibility271BuildErrorCode** = *typeof* [`ELIGIBILITY_271_BUILD_ERROR_CODES`](#eligibility_271_build_error_codes)\[keyof *typeof* [`ELIGIBILITY_271_BUILD_ERROR_CODES`](#eligibility_271_build_error_codes)\] String-literal union over [ELIGIBILITY\_271\_BUILD\_ERROR\_CODES](#eligibility_271_build_error_codes). Used as [Eligibility271BuildError](#eligibility271builderror).`code`. *** ### Enrollment834BuildErrorCode > **Enrollment834BuildErrorCode** = *typeof* [`ENROLLMENT_834_BUILD_ERROR_CODES`](#enrollment_834_build_error_codes)\[keyof *typeof* [`ENROLLMENT_834_BUILD_ERROR_CODES`](#enrollment_834_build_error_codes)\] String-literal union over [ENROLLMENT\_834\_BUILD\_ERROR\_CODES](#enrollment_834_build_error_codes). Used as [Enrollment834BuildError](#enrollment834builderror).`code`. *** ### Ik304Code > **Ik304Code** = *typeof* [`IK3_SYNTAX_ERROR_CODES`](#ik3_syntax_error_codes)\[keyof *typeof* [`IK3_SYNTAX_ERROR_CODES`](#ik3_syntax_error_codes)\] String-literal union over [IK3\_SYNTAX\_ERROR\_CODES](#ik3_syntax_error_codes). Used as the type of `IK3-04` on the ack-spec/parsed-ack models. *** ### Ik403Code > **Ik403Code** = *typeof* [`IK4_SYNTAX_ERROR_CODES`](#ik4_syntax_error_codes)\[keyof *typeof* [`IK4_SYNTAX_ERROR_CODES`](#ik4_syntax_error_codes)\] String-literal union over [IK4\_SYNTAX\_ERROR\_CODES](#ik4_syntax_error_codes). Used as the type of `IK4-03` on the ack-spec/parsed-ack models. *** ### LoopMax > **LoopMax** = `number` \| `">1"` Repetition count for a segment or child loop inside its parent. `">1"` means many (TR3 typically writes `>1` to mean "no cap"). A finite max surfaces as the numeric value. This module stores the value; the loop walker uses it for over-limit warnings. #### Example ```ts import type { LoopMax } from "@cosyte/x12"; const single: LoopMax = 1; const many: LoopMax = ">1"; ``` *** ### LoopUsage > **LoopUsage** = `"required"` \| `"situational"` \| `"optional"` Cardinality of a segment or child loop inside its parent. Mirrors TR3 spelling so the spec reads like the implementation guide: - `required` - the segment/loop MUST appear at least once. - `situational` - the segment/loop MAY appear when its situational rule triggers; the parser does not enforce the rule itself but loop-walker warnings reference it. - `optional` - the segment/loop MAY appear in any usage. #### Example ```ts import type { LoopUsage } from "@cosyte/x12"; const usage: LoopUsage = "required"; ``` *** ### OnWarningCallback > **OnWarningCallback** = (`warning`) => `void` Callback invoked inline each time the parser emits a Tier-2 warning. Always fires BEFORE the warning is appended to `X12Interchange.warnings` so consumers observe warnings in the same order the parser discovered them. #### Parameters ##### warning [`X12ParseWarning`](#x12parsewarning) #### Returns `void` #### Example ```ts import { parseX12, type OnWarningCallback } from "@cosyte/x12"; const onWarning: OnWarningCallback = (w) => { console.warn(w.code, w.message); }; parseX12(raw, { onWarning }); ``` *** ### Premium820BuildErrorCode > **Premium820BuildErrorCode** = *typeof* [`PREMIUM_820_BUILD_ERROR_CODES`](#premium_820_build_error_codes)\[keyof *typeof* [`PREMIUM_820_BUILD_ERROR_CODES`](#premium_820_build_error_codes)\] String-literal union over [PREMIUM\_820\_BUILD\_ERROR\_CODES](#premium_820_build_error_codes). Used as [Premium820BuildError](#premium820builderror).`code`. *** ### Remit835BuildErrorCode > **Remit835BuildErrorCode** = *typeof* [`REMIT_835_BUILD_ERROR_CODES`](#remit_835_build_error_codes)\[keyof *typeof* [`REMIT_835_BUILD_ERROR_CODES`](#remit_835_build_error_codes)\] String-literal union over [REMIT\_835\_BUILD\_ERROR\_CODES](#remit_835_build_error_codes). Used as [Remit835BuildError](#remit835builderror).`code`. *** ### Rfai277BuildErrorCode > **Rfai277BuildErrorCode** = *typeof* [`RFAI_277_BUILD_ERROR_CODES`](#rfai_277_build_error_codes)\[keyof *typeof* [`RFAI_277_BUILD_ERROR_CODES`](#rfai_277_build_error_codes)\] String-literal union over [RFAI\_277\_BUILD\_ERROR\_CODES](#rfai_277_build_error_codes). #### Example ```ts import type { Rfai277BuildErrorCode } from "@cosyte/x12"; const code: Rfai277BuildErrorCode = "X12_277_RFAI_BUILD_NO_LEVEL"; ``` *** ### SegmentSpec > **SegmentSpec** = readonly `string`[] A single body segment as a raw element array: `[segmentId, ...elements]`, 1-indexed against the X12 spec convention once placed (i.e. `spec[1]` is element 1). Element values are LOGICAL - the builder applies the `?`-release-character escape on emit so any active delimiter inside a value survives. The segment id (`spec[0]`) is emitted verbatim. ST and SE are NOT included here - the builder synthesizes them from [TransactionSetSpec](#transactionsetspec). #### Example ```ts const nm1: SegmentSpec = ["NM1", "IL", "1", "DOE", "JANE"]; ``` *** ### ServicesReview278BuildErrorCode > **ServicesReview278BuildErrorCode** = *typeof* [`AUTH_278_BUILD_ERROR_CODES`](#auth_278_build_error_codes)\[keyof *typeof* [`AUTH_278_BUILD_ERROR_CODES`](#auth_278_build_error_codes)\] String-literal union over [AUTH\_278\_BUILD\_ERROR\_CODES](#auth_278_build_error_codes). Used as [ServicesReview278BuildError](#servicesreview278builderror).`code`. *** ### Ta1AckCode > **Ta1AckCode** = *typeof* [`TA1_ACK_CODES`](#ta1_ack_codes)\[keyof *typeof* [`TA1_ACK_CODES`](#ta1_ack_codes)\] String-literal union over [TA1\_ACK\_CODES](#ta1_ack_codes). *** ### Ta1NoteCode > **Ta1NoteCode** = *typeof* [`TA1_NOTE_CODES`](#ta1_note_codes)\[keyof *typeof* [`TA1_NOTE_CODES`](#ta1_note_codes)\] String-literal union over [TA1\_NOTE\_CODES](#ta1_note_codes) - the standard-issued note codes `000` to `028`. Real-world inbound TA1 may carry a value past `028` (some revisions extend the list); `parseTA1` exposes the un-narrowed string on `noteCodeRaw` in that case, so the value survives even when the union cannot statically type it. **It is post-`?`-unescape as of a later release, and a clause here calling it VERBATIM is deleted rather than reworded** - `raw.elements[5]` is the byte surface. *** ### X12\_837ServiceLine > **X12\_837ServiceLine** = [`X12_837ServiceLineProfessional`](#x12_837servicelineprofessional) \| [`X12_837ServiceLineInstitutional`](#x12_837servicelineinstitutional) \| [`X12_837ServiceLineDental`](#x12_837servicelinedental) Service-line discriminated union - one variant per TR3. The walker picks the variant from the segment id (`SV1` → P, `SV2` → I, `SV3` → D). When an `LX` opens a service line with no SVx segment that follows before the next LX / SE, the line is dropped (an `X12_UNEXPECTED_SEGMENT` warning will have already fired if the body content was structurally impossible). #### Example ```ts import type { X12_837ServiceLine } from "@cosyte/x12"; declare const sl: X12_837ServiceLine; switch (sl.variant) { case "P": sl.procedureCode; sl.diagnosisPointers; break; case "I": sl.revenueCode; break; case "D": sl.toothInformation; break; } ``` *** ### X12AaaConditionLevel > **X12AaaConditionLevel** = `Exclude`\<[`X12AaaLevelContext`](#x12aaalevelcontext), *typeof* [`UNATTACHED`](#unattached)\> String-literal union over [AAA\_CONDITION\_LEVELS](#aaa_condition_levels): the warning discriminant minus its unknown-level member. #### Example ```ts import type { X12AaaConditionLevel } from "@cosyte/x12"; const level: X12AaaConditionLevel = "dependent"; ``` *** ### X12AaaLevelContext > **X12AaaLevelContext** = *typeof* [`AAA_LEVEL_CONTEXTS`](#aaa_level_contexts)\[keyof *typeof* [`AAA_LEVEL_CONTEXTS`](#aaa_level_contexts)\] String-literal union over [AAA\_LEVEL\_CONTEXTS](#aaa_level_contexts). #### Example ```ts import type { X12AaaLevelContext } from "@cosyte/x12"; const level: X12AaaLevelContext = "dependent"; ``` *** ### X12AckDispositionCode > **X12AckDispositionCode** = *typeof* [`X12_ACK_DISPOSITION_CODES`](#x12_ack_disposition_codes)\[keyof *typeof* [`X12_ACK_DISPOSITION_CODES`](#x12_ack_disposition_codes)\] String-literal union over [X12\_ACK\_DISPOSITION\_CODES](#x12_ack_disposition_codes). Used as the type of `AK9-01`, `IK5-01`, and the `disposition` field on the ack-spec/parsed-ack models. #### Example ```ts import type { X12AckDispositionCode } from "@cosyte/x12"; function isReject(code: X12AckDispositionCode): boolean { return code === "R" || code === "M" || code === "W" || code === "X"; } ``` *** ### X12BalanceInvariant > **X12BalanceInvariant** = *typeof* [`BALANCE_INVARIANTS`](#balance_invariants)\[keyof *typeof* [`BALANCE_INVARIANTS`](#balance_invariants)\] String-literal union over [BALANCE\_INVARIANTS](#balance_invariants). #### Example ```ts import type { X12BalanceInvariant } from "@cosyte/x12"; const which: X12BalanceInvariant = "service-line"; ``` *** ### X12BuildErrorCode > **X12BuildErrorCode** = *typeof* [`X12_BUILD_ERROR_CODES`](#x12_build_error_codes)\[keyof *typeof* [`X12_BUILD_ERROR_CODES`](#x12_build_error_codes)\] String-literal union over [X12\_BUILD\_ERROR\_CODES](#x12_build_error_codes). Used as [X12BuildError](#x12builderror).`code`. *** ### X12Claim837Variant > **X12Claim837Variant** = `"P"` \| `"I"` \| `"D"` \| `"unknown"` 837 variant - discriminator for the service-line union and for any variant-specific helper logic. `"unknown"` covers transactions where neither ST-03 nor a service-line segment id resolved the variant. #### Example ```ts import type { X12Claim837Variant } from "@cosyte/x12"; const v: X12Claim837Variant = "P"; ``` *** ### X12CodeListErrorCode > **X12CodeListErrorCode** = *typeof* [`X12_CODE_LIST_ERROR_CODES`](#x12_code_list_error_codes)\[keyof *typeof* [`X12_CODE_LIST_ERROR_CODES`](#x12_code_list_error_codes)\] String-literal union over [X12\_CODE\_LIST\_ERROR\_CODES](#x12_code_list_error_codes). Used as [X12CodeListError](#x12codelisterror).`code`. *** ### X12ControlNumberPair > **X12ControlNumberPair** = *typeof* [`CONTROL_NUMBER_PAIRS`](#control_number_pairs)\[keyof *typeof* [`CONTROL_NUMBER_PAIRS`](#control_number_pairs)\] String-literal union over [CONTROL\_NUMBER\_PAIRS](#control_number_pairs). #### Example ```ts import type { X12ControlNumberPair } from "@cosyte/x12"; const pair: X12ControlNumberPair = "group"; ``` *** ### X12DecimalStatus > **X12DecimalStatus** = `"decoded"` \| `"absent"` \| `"unparseable"` Why a decimal read produced no [X12Decimal](#x12decimal), or that it produced one. The three states are spec-distinct and were previously collapsed onto a single `undefined`: - `"decoded"` - the element matched the shape "../decimal.js".X12Decimal decodes, `[+-]?digits(.digits?)?`. - `"absent"` - the element was missing or empty. The sender said nothing. - `"unparseable"` - the element held bytes outside that shape. The sender said something and this library could not read it, which is a materially different fact from `"absent"`. **That shape is what this library reads; no clause of X12.6 is cited for it, so do not read this as an assertion about what type R does and does not permit.** #### Example ```ts import type { X12DecimalStatus } from "@cosyte/x12"; const status: X12DecimalStatus = "unparseable"; ``` *** ### X12FatalCode > **X12FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] Discriminant type for `X12ParseError.code`. Narrowing a caught error by this code lets consumers write exhaustive `switch` blocks (enabled by the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free comparison against the `FATAL_CODES` registry. #### Example ```ts import type { X12FatalCode } from "@cosyte/x12"; function describe(code: X12FatalCode): string { switch (code) { case "X12_EMPTY_INPUT": return "input was empty"; case "X12_NO_ISA_HEADER": return "missing ISA"; case "X12_ISA_TOO_SHORT": return "ISA truncated"; case "X12_INVALID_DELIMITERS": return "bad ISA delimiters"; } } ``` *** ### X12HiCategory > **X12HiCategory** = `"diagnosis"` \| `"procedure"` \| `"external-cause"` \| `"principal-diagnosis"` \| `"admitting-diagnosis"` \| `"patient-reason-for-visit"` \| `"drg"` \| `"condition"` \| `"occurrence"` \| `"occurrence-span"` \| `"value"` \| `"treatment"` \| `"unknown"` Category of an HI qualifier - separates diagnoses from procedures from the institutional NUBC code families. Consumers branching on the category get exhaustiveness; the variant-specific helpers (`getDiagnoses` / `getProcedures`) filter on it. #### Example ```ts import type { X12HiCategory } from "@cosyte/x12"; const c: X12HiCategory = "diagnosis"; ``` *** ### X12HiCodeSystem > **X12HiCodeSystem** = `"ICD-10-CM"` \| `"ICD-10-PCS"` \| `"ICD-9-CM"` \| `"ICD-9-PCS"` \| `"DRG"` \| `"NDC"` \| `"NUBC-CONDITION"` \| `"NUBC-OCCURRENCE"` \| `"NUBC-OCCURRENCE-SPAN"` \| `"NUBC-VALUE"` \| `"NUBC-PATIENT-REASON"` \| `"unknown"` Discriminant for the code system referenced by a single HI composite. The `"unknown"` member is the catch-all for qualifiers outside the TR3-cited set - verbatim code is preserved on the parsed structure and an `X12_UNKNOWN_HI_QUALIFIER` warning is emitted. #### Example ```ts import type { X12HiCodeSystem } from "@cosyte/x12"; const sys: X12HiCodeSystem = "ICD-10-CM"; ``` *** ### X12HiQualifier > **X12HiQualifier** = keyof *typeof* [`HI_QUALIFIERS`](#hi_qualifiers) Stable string-literal union of every known HI qualifier the parser recognizes. Inferred from [HI\_QUALIFIERS](#hi_qualifiers) keys. #### Example ```ts import type { X12HiQualifier } from "@cosyte/x12"; const q: X12HiQualifier = "ABK"; ``` *** ### X12OrphanAnchor > **X12OrphanAnchor** = \{ `groupIndex`: `number`; `kind`: `"interchange"`; \} \| \{ `groupIndex`: `number`; `kind`: `"group"`; `transactionIndex`: `number`; \} \| \{ `groupIndex`: `number`; `kind`: `"transaction"`; `segmentOffset`: `number`; `transactionIndex`: `number`; \} Where an orphan sat in the STRUCTURE of the interchange, as opposed to where it sat in the input byte stream. This is what lets `serializeX12` put an orphan back without guessing. `X12OrphanSegment.segmentIndex` cannot do that job. It indexes the INPUT stream, and the emit is not in input order: `ta1Segments` are hoisted ahead of the groups, and a doubled terminator's zero-length segment occupies an input index that is never emitted. Either one shifts the output's indices away from the input's, so replaying by index splices the orphan into whatever occupies that slot - measurably, into an 835's `ST..SE` body, where a re-parse reports nothing. An anchor names a slot in the typed tree instead, which survives both reorderings because it does not mention bytes. Three kinds, one per structural level: - **`"interchange"`** - between the ISA and the IEA but outside every functional group. `groupIndex` is the number of groups that had already closed, so the orphan is emitted immediately before `ix.groups[groupIndex]` (or before the IEA when `groupIndex === ix.groups.length`). - **`"group"`** - inside `ix.groups[groupIndex]` but outside every transaction set in it. `transactionIndex` is the number of transactions that had already closed, so the orphan is emitted immediately before that group's `transactions[transactionIndex]` (or before its `GE` when `transactionIndex === transactions.length`). - **`"transaction"`** - inside an open `ST..SE`, which only a `TA1` can be, since every other segment arriving there is body content. `segmentOffset` is the number of `rawSegments` already collected, so the orphan is emitted immediately before `rawSegments[segmentOffset]`. It is never `0` (the `ST` is always `rawSegments[0]`) and never exceeds `rawSegments.length`. Such an orphan is written back BETWEEN the `ST` and the `SE`, so `serializeX12` counts it toward SE-01 in spec-clean mode even though it is not on `tx.rawSegments` - it is a segment of that transaction set per X12.6. **An anchor is a POSITION, so reshaping the model invalidates it.** The indices address `ix.groups`, that group's `transactions`, and that transaction's `rawSegments` as they stand on the interchange you pass to `serializeX12`. Filter or reorder any of those and an orphan's anchor may still resolve while naming a different slot, and it will be emitted there with no warning. An anchor that resolves to nothing is emitted at interchange level before the IEA rather than dropped. Re-parse rather than hand-edit if you need anchors to stay meaningful. #### Union Members ##### Type Literal \{ `groupIndex`: `number`; `kind`: `"interchange"`; \} ###### groupIndex > `readonly` **groupIndex**: `number` How many functional groups had closed before this segment arrived. ###### kind > `readonly` **kind**: `"interchange"` *** ##### Type Literal \{ `groupIndex`: `number`; `kind`: `"group"`; `transactionIndex`: `number`; \} ###### groupIndex > `readonly` **groupIndex**: `number` Index into `ix.groups` of the group that was open. ###### kind > `readonly` **kind**: `"group"` ###### transactionIndex > `readonly` **transactionIndex**: `number` How many transaction sets in that group had closed. *** ##### Type Literal \{ `groupIndex`: `number`; `kind`: `"transaction"`; `segmentOffset`: `number`; `transactionIndex`: `number`; \} ###### groupIndex > `readonly` **groupIndex**: `number` Index into `ix.groups` of the group that was open. ###### kind > `readonly` **kind**: `"transaction"` ###### segmentOffset > `readonly` **segmentOffset**: `number` How many of that set's `rawSegments` had been collected. ###### transactionIndex > `readonly` **transactionIndex**: `number` Index into that group's `transactions` of the set that was open. #### Example ```ts import { parseX12 } from "@cosyte/x12"; const ix = parseX12(raw); for (const o of ix.orphanSegments) { if (o.anchor.kind === "group") console.warn(o.anchor.groupIndex); } ``` *** ### X12OrphanAnchorKind > **X12OrphanAnchorKind** = [`X12OrphanAnchor`](#x12orphananchor)\[`"kind"`\] String-literal union over the `kind` discriminant of [X12OrphanAnchor](#x12orphananchor). #### Example ```ts import type { X12OrphanAnchorKind } from "@cosyte/x12"; const kind: X12OrphanAnchorKind = "transaction"; ``` *** ### X12ProfileConformance > **X12ProfileConformance** = `"permitted"` \| `"not-permitted"` \| `"undetermined"` Whether the deviation a quirk describes is one a trading partner may LAWFULLY require. The axis is 45 CFR 162.915, which forbids a covered entity from entering a trading partner agreement that would change the definition, data condition or use of a data element or segment in a standard, add data elements or segments to the maximum defined data set, use any code or data element marked "not used" in (or absent from) the standard's implementation specification, or change the meaning or intent of that implementation specification. The three states, and what each one does and does not claim: - `permitted` - a recorded judgement that none of 162.915(a) to (d) is engaged, so a partner may lawfully require the deviation. It is a statement about the RULE, never a promise that your partner's guide is correct in any other respect. - `not-permitted` - a recorded judgement that the deviation is one 162.915 forbids a partner from requiring. The profile still describes it, because describing what a partner SENDS is exactly what a profile is for; the classification says the partner is deviating from the adopted standard rather than that the standard supports the requirement. - `undetermined` - NO judgement was recorded, or the recorded value was not one of the two above. This is the fail-safe default, and it is the value you get for any quirk that says nothing. Read it as "this library makes no claim", never as "permitted". **Every one of these is a recorded human judgement, not a check this library performed.** In particular, whether an element is marked "not used" in an adopted implementation specification is not machine-checkable here: 45 CFR 162.920(a) states a fee is charged for those specifications, and none is bundled with this package. A classification that rests on such a marking rests on someone having read the guide and written the answer down. Treat it as documentation, and confirm it against your own copy of the guide before you rely on it. `KNOWN-LIMITATIONS.md` carries the same caveat for consumers who never open this type. #### Example ```ts import type { X12ProfileConformance } from "@cosyte/x12"; const c: X12ProfileConformance = "undetermined"; ``` *** ### X12ProfileEffect > **X12ProfileEffect** = `"relaxes"` \| `"adds"` \| `"requires"` The bucket a quirk falls into when rendered by [X12Profile.describe](#describe). Mirrors the roadmap's "what this profile relaxes / adds / requires" framing. - `relaxes` - the partner tolerates / emits a structural variation the strict 005010 baseline would flag (e.g. an alternate component delimiter). - `adds` - the partner emits extra spec-optional content (e.g. additional REF segments) a generic consumer might not expect. - `requires` - the partner mandates a normally-situational element be present. #### Example ```ts import type { X12ProfileEffect } from "@cosyte/x12"; const effect: X12ProfileEffect = "adds"; ``` *** ### X12RequiredLoop > **X12RequiredLoop** = *typeof* [`REQUIRED_LOOPS`](#required_loops)\[keyof *typeof* [`REQUIRED_LOOPS`](#required_loops)\] String-literal union over [REQUIRED\_LOOPS](#required_loops). #### Example ```ts import type { X12RequiredLoop } from "@cosyte/x12"; const loop: X12RequiredLoop = "2010BB"; ``` *** ### X12Tr3Adoption > **X12Tr3Adoption** = `"incorporated-by-reference"` \| `"errata-in-practice"` \| `"not-adopted"` How the identifier a row names stands against 45 CFR 162.920. - `incorporated-by-reference`: that section names this exact identifier for this transaction. - `errata-in-practice`: an errata revision the industry implements, which that section does not name for this transaction. The base identifier it DOES name is carried alongside, on `cfrAdopted`. - `not-adopted`: the section names no identifier at all for this transaction, so there is nothing to be adopted or superseded. #### Example ```ts import { X12_TR3_CONFORMANCE, type X12Tr3Adoption } from "@cosyte/x12"; const strict: X12Tr3Adoption = "incorporated-by-reference"; X12_TR3_CONFORMANCE.filter((row) => row.adoption === strict).map((row) => row.transaction); // ["275", "276", "277", "277", "278", "278", "820"] ``` *** ### X12Tr3Direction > **X12Tr3Direction** = `"read"` \| `"build"` A direction a transaction is implemented in: `read` when the package decodes it into a typed model, `build` when the package emits it from one. #### Example ```ts import { X12_TR3_CONFORMANCE, type X12Tr3Direction } from "@cosyte/x12"; const wanted: X12Tr3Direction = "build"; const emitted = X12_TR3_CONFORMANCE.filter((row) => row.directions.includes(wanted)); emitted.length; // rows this package can produce documents for ``` *** ### X12UnexpectedSegmentContext > **X12UnexpectedSegmentContext** = *typeof* [`UNEXPECTED_SEGMENT_CONTEXTS`](#unexpected_segment_contexts)\[keyof *typeof* [`UNEXPECTED_SEGMENT_CONTEXTS`](#unexpected_segment_contexts)\] String-literal union over [UNEXPECTED\_SEGMENT\_CONTEXTS](#unexpected_segment_contexts). #### Example ```ts import type { X12UnexpectedSegmentContext } from "@cosyte/x12"; const ctx: X12UnexpectedSegmentContext = "se-without-st"; ``` *** ### X12WarningCode > **X12WarningCode** = *typeof* [`WARNING_CODES`](#warning_codes)\[keyof *typeof* [`WARNING_CODES`](#warning_codes)\] Discriminant type for `X12ParseWarning.code`. Narrowing a warning by this code lets consumers write exhaustive `switch` blocks and guarantees a typo-free comparison against the `WARNING_CODES` registry. #### Example ```ts import type { X12ParseWarning, X12WarningCode } from "@cosyte/x12"; function describe(w: X12ParseWarning): string { const code: X12WarningCode = w.code; switch (code) { case "X12_PRE_005010": return "pre-005010 sender"; default: return `warning: ${code}`; } } ``` ## Variables ### AAA\_CONDITION\_LEVELS > `const` **AAA\_CONDITION\_LEVELS**: `Readonly`\<\{ `DEPENDENT`: `"dependent"`; `INFORMATION_RECEIVER`: `"information-receiver"`; `INFORMATION_SOURCE`: `"information-source"`; `SUBSCRIBER`: `"subscriber"`; \}\> The four hierarchical levels a 271 AAA request-validation segment can be surfaced against. Derived from the library-owned warning discriminant so the model and the diagnostics can never drift apart on what a level is called; the discriminant's fifth member covers an AAA whose level is unknown, and on the model that case is `undefined` rather than a fifth name. #### Example ```ts import { AAA_CONDITION_LEVELS } from "@cosyte/x12"; AAA_CONDITION_LEVELS.SUBSCRIBER; // "subscriber" ``` *** ### AAA\_FOLLOW\_UP\_ACTION\_CODES > `const` **AAA\_FOLLOW\_UP\_ACTION\_CODES**: [`AaaCodeListSnapshot`](#aaacodelistsnapshot) Bundled **AAA-04 Follow-up Action Code** snapshot (X12 data element 889), for the 271 request-validation surface. Ships EMPTY on the same terms as [AAA\_REJECT\_REASON\_CODES](#aaa_reject_reason_codes); every inbound follow-up action code is echoed verbatim with its description absent. #### Example ```ts import { AAA_FOLLOW_UP_ACTION_CODES } from "@cosyte/x12"; AAA_FOLLOW_UP_ACTION_CODES.meta.id; // "AAA-FOLLOW-UP-ACTION" ``` *** ### AAA\_LEVEL\_CONTEXTS > `const` **AAA\_LEVEL\_CONTEXTS**: `object` Which hierarchical level a 271 AAA request-validation segment was read under. A closed, library-owned discriminant: the four level names are the reader's own vocabulary, NOT bytes taken out of the document, so a diagnostic can say where the payer rejected without echoing anything the sender sent. `UNATTACHED` covers the AAA whose level this reader could not determine: one transmitted before any hierarchical level opened, and one under a level whose HL-03 is none of the four this surface names. It says the level is unknown; it never guesses one. #### Type Declaration ##### DEPENDENT > `readonly` **DEPENDENT**: `"dependent"` = `"dependent"` Loop 2000D, the dependent. ##### INFORMATION\_RECEIVER > `readonly` **INFORMATION\_RECEIVER**: `"information-receiver"` = `"information-receiver"` Loop 2000B, the information receiver (provider). ##### INFORMATION\_SOURCE > `readonly` **INFORMATION\_SOURCE**: `"information-source"` = `"information-source"` Loop 2000A, the information source (payer). ##### SUBSCRIBER > `readonly` **SUBSCRIBER**: `"subscriber"` = `"subscriber"` Loop 2000C, the subscriber. ##### UNATTACHED > `readonly` **UNATTACHED**: `"unattached"` = `"unattached"` No hierarchical level of a named kind encloses the segment. #### Example ```ts import { aaaRejectReasonAbsent, AAA_LEVEL_CONTEXTS } from "@cosyte/x12"; const w = aaaRejectReasonAbsent({ segmentIndex: 7 }, AAA_LEVEL_CONTEXTS.SUBSCRIBER); ``` *** ### AAA\_REJECT\_REASON\_CODES > `const` **AAA\_REJECT\_REASON\_CODES**: [`AaaCodeListSnapshot`](#aaacodelistsnapshot) Bundled **AAA-03 Reject Reason Code** snapshot (X12 data element 901), for the 271 request-validation surface. Ships EMPTY: no description source was obtained, and the recorded redistribution terms do not permit bundling. Every inbound reject reason code is echoed verbatim on the typed result with its description absent. #### Example ```ts import { AAA_REJECT_REASON_CODES } from "@cosyte/x12"; AAA_REJECT_REASON_CODES.meta.id; // "AAA-REJECT-REASON" Object.keys(AAA_REJECT_REASON_CODES.codes).length; // 0 ``` *** ### ACK\_BUILD\_ERROR\_CODES > `const` **ACK\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every `AckBuildError` thrown by the acknowledgment builders. Locked here so consumers can narrow on `err.code` exhaustively. Additions-only thereafter; renaming any of these codes is a breaking change. - `X12_ACK_INVALID_DISPOSITION` - A disposition was supplied that does not match "./codes.js".X12AckDispositionCode (or "./codes.js".Ta1AckCode for `buildTA1`). - `X12_ACK_INVALID_SPEC` - A spec field violated a structural constraint the builder cannot recover from (e.g., an ISA-13 control number longer than the spec's 9-char limit). - `X12_ACK_ACCEPT_WITH_ERRORS` - An `A` disposition was supplied alongside non-empty per-transaction errors or a transaction-set whose own disposition is not `A`. Refused - accept must mean accept. Use `E` for "accepted, errors noted" or `R` for "rejected" instead. - `X12_ACK_COUNT_MISMATCH` - The functional-level `numberOfTransactionSets` / `numberReceived` / `numberAccepted` are internally inconsistent (e.g., accepted > received) or do not match the supplied `transactionResponses` list. - `X12_TA1_ACCEPT_WITH_NOTE` - A TA1 `A` ack code was supplied with a note code other than `000` (no error). Refused for the same safety reason: an accept cannot cite a non-zero note. #### Type Declaration ##### X12\_ACK\_ACCEPT\_WITH\_ERRORS > `readonly` **X12\_ACK\_ACCEPT\_WITH\_ERRORS**: `"X12_ACK_ACCEPT_WITH_ERRORS"` = `"X12_ACK_ACCEPT_WITH_ERRORS"` ##### X12\_ACK\_COUNT\_MISMATCH > `readonly` **X12\_ACK\_COUNT\_MISMATCH**: `"X12_ACK_COUNT_MISMATCH"` = `"X12_ACK_COUNT_MISMATCH"` ##### X12\_ACK\_INVALID\_DISPOSITION > `readonly` **X12\_ACK\_INVALID\_DISPOSITION**: `"X12_ACK_INVALID_DISPOSITION"` = `"X12_ACK_INVALID_DISPOSITION"` ##### X12\_ACK\_INVALID\_SPEC > `readonly` **X12\_ACK\_INVALID\_SPEC**: `"X12_ACK_INVALID_SPEC"` = `"X12_ACK_INVALID_SPEC"` ##### X12\_TA1\_ACCEPT\_WITH\_NOTE > `readonly` **X12\_TA1\_ACCEPT\_WITH\_NOTE**: `"X12_TA1_ACCEPT_WITH_NOTE"` = `"X12_TA1_ACCEPT_WITH_NOTE"` #### Example ```ts import { ACK_BUILD_ERROR_CODES, AckBuildError } from "@cosyte/x12"; try { buildSomeAck(); } catch (err) { if (err instanceof AckBuildError && err.code === ACK_BUILD_ERROR_CODES.X12_ACK_ACCEPT_WITH_ERRORS) { // application bug - never silently accept with errors } } ``` *** ### ALL\_WARNING\_MESSAGES > `const` **ALL\_WARNING\_MESSAGES**: `ReadonlySet`\<`string`\> Every message string this library can put on a warning. Exported so a consumer, or a conformance gate, can assert set membership: if `ALL_WARNING_MESSAGES.has(w.message)` is ever false, something interpolated document bytes into a diagnostic. #### Example ```ts import { parseX12, ALL_WARNING_MESSAGES } from "@cosyte/x12"; const ix = parseX12(raw); ix.warnings.every((w) => ALL_WARNING_MESSAGES.has(w.message)); // true, always ``` *** ### ATTACHMENT\_275\_BUILD\_ERROR\_CODES > `const` **ATTACHMENT\_275\_BUILD\_ERROR\_CODES**: `object` Stable codes for every [Attachment275BuildError](#attachment275builderror). Additions only. - `X12_275_BUILD_NO_ATTACHMENT`: no line carries an attachment. - `X12_275_BUILD_EMPTY_DATA`: an attachment's data holds no octet. - `X12_275_BUILD_FILTER_CODE_INVALID`: an attachment's filter code is not exactly three characters. - `X12_275_BUILD_DATA_NOT_OCTETS`: an attachment's data holds a character above U+00FF, which is not one octet, so no true BDS-02 could be written. - `X12_275_BUILD_INVALID_SPEC`: the spec is not shaped like one: a value that is not a string, data that is neither a string nor a `Uint8Array`, a list that is not an array, an unusable delimiter set, or an empty or over-long control number. #### Type Declaration ##### X12\_275\_BUILD\_DATA\_NOT\_OCTETS > `readonly` **X12\_275\_BUILD\_DATA\_NOT\_OCTETS**: `"X12_275_BUILD_DATA_NOT_OCTETS"` = `"X12_275_BUILD_DATA_NOT_OCTETS"` ##### X12\_275\_BUILD\_EMPTY\_DATA > `readonly` **X12\_275\_BUILD\_EMPTY\_DATA**: `"X12_275_BUILD_EMPTY_DATA"` = `"X12_275_BUILD_EMPTY_DATA"` ##### X12\_275\_BUILD\_FILTER\_CODE\_INVALID > `readonly` **X12\_275\_BUILD\_FILTER\_CODE\_INVALID**: `"X12_275_BUILD_FILTER_CODE_INVALID"` = `"X12_275_BUILD_FILTER_CODE_INVALID"` ##### X12\_275\_BUILD\_INVALID\_SPEC > `readonly` **X12\_275\_BUILD\_INVALID\_SPEC**: `"X12_275_BUILD_INVALID_SPEC"` = `"X12_275_BUILD_INVALID_SPEC"` ##### X12\_275\_BUILD\_NO\_ATTACHMENT > `readonly` **X12\_275\_BUILD\_NO\_ATTACHMENT**: `"X12_275_BUILD_NO_ATTACHMENT"` = `"X12_275_BUILD_NO_ATTACHMENT"` #### Example ```ts import { ATTACHMENT_275_BUILD_ERROR_CODES, Attachment275BuildError } from "@cosyte/x12"; try { // build275(spec); } catch (err) { if (err instanceof Attachment275BuildError && err.code === ATTACHMENT_275_BUILD_ERROR_CODES.X12_275_BUILD_EMPTY_DATA) { // an attachment carries at least one octet } } ``` *** ### AUTH\_278\_BUILD\_ERROR\_CODES > `const` **AUTH\_278\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [ServicesReview278BuildError](#servicesreview278builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_278_BUILD_INVALID_HIERARCHY` - the nested tree cannot form a valid 278 HL spine (a subscriber with neither a review nor a dependent; a dependent with no review). The message carries structural indices + counts only - never a member id / name. - `X12_278_BUILD_INVALID_SPEC` - a non-hierarchy precondition failed (a review whose HL-03 `levelCode` is outside `EV` / `SS`; a review with no `requestCategoryCode`; a request spec carrying an `HCR` certification decision - `HCR` is response-only; a response review with a decision whose `actionCode` is empty; an over-long ISA-13 control number). The message carries structural indices, counts and X12 control codes - never a member id / name. #### Type Declaration ##### X12\_278\_BUILD\_INVALID\_HIERARCHY > `readonly` **X12\_278\_BUILD\_INVALID\_HIERARCHY**: `"X12_278_BUILD_INVALID_HIERARCHY"` = `"X12_278_BUILD_INVALID_HIERARCHY"` ##### X12\_278\_BUILD\_INVALID\_SPEC > `readonly` **X12\_278\_BUILD\_INVALID\_SPEC**: `"X12_278_BUILD_INVALID_SPEC"` = `"X12_278_BUILD_INVALID_SPEC"` #### Example ```ts import { AUTH_278_BUILD_ERROR_CODES, ServicesReview278BuildError, build278Response } from "@cosyte/x12"; try { build278Response(spec); } catch (err) { if ( err instanceof ServicesReview278BuildError && err.code === AUTH_278_BUILD_ERROR_CODES.X12_278_BUILD_INVALID_HIERARCHY ) { // the hierarchy is impossible - fix the tree, do not emit } } ``` *** ### AUTH\_278\_LOOP\_2000A > `const` **AUTH\_278\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 278 Loop 2000A - Utilization Management Organization (UMO) HL. Triggered by `HL` (`HL-03 = "20"`) - the top of the hierarchy, no parent. Carries the UMO name NM1. #### Example ```ts import { AUTH_278_LOOP_2000A } from "@cosyte/x12"; AUTH_278_LOOP_2000A.trigger; // "HL" ``` *** ### AUTH\_278\_LOOP\_2000B > `const` **AUTH\_278\_LOOP\_2000B**: [`LoopSpec`](#loopspec) 278 Loop 2000B - Requester HL. Triggered by `HL` (`HL-03 = "21"`). The requesting provider / facility. #### Example ```ts import { AUTH_278_LOOP_2000B } from "@cosyte/x12"; AUTH_278_LOOP_2000B.id; // "2000B" ``` *** ### AUTH\_278\_LOOP\_2000C > `const` **AUTH\_278\_LOOP\_2000C**: [`LoopSpec`](#loopspec) 278 Loop 2000C - Subscriber HL. Triggered by `HL` (`HL-03 = "22"`). Carries the subscriber name NM1 + DMG demographics. #### Example ```ts import { AUTH_278_LOOP_2000C } from "@cosyte/x12"; AUTH_278_LOOP_2000C.id; // "2000C" ``` *** ### AUTH\_278\_LOOP\_2000D > `const` **AUTH\_278\_LOOP\_2000D**: [`LoopSpec`](#loopspec) 278 Loop 2000D - Dependent HL. Triggered by `HL` (`HL-03 = "23"`). Carries the dependent name NM1 + DMG demographics. #### Example ```ts import { AUTH_278_LOOP_2000D } from "@cosyte/x12"; AUTH_278_LOOP_2000D.id; // "2000D" ``` *** ### AUTH\_278\_LOOP\_2000E > `const` **AUTH\_278\_LOOP\_2000E**: [`LoopSpec`](#loopspec) 278 Loop 2000E - Patient Event HL. Triggered by `HL` (`HL-03 = "EV"`). Anchors the `UM` services-review information, the `HCR` decision (response only), `HI` diagnoses, the echoed `TRN`, and the service-provider NM1s. Nests [AUTH\_278\_LOOP\_2000F](#auth_278_loop_2000f). #### Example ```ts import { AUTH_278_LOOP_2000E } from "@cosyte/x12"; AUTH_278_LOOP_2000E.children[0]?.id; // "2000F" ``` *** ### AUTH\_278\_LOOP\_2000F > `const` **AUTH\_278\_LOOP\_2000F**: [`LoopSpec`](#loopspec) 278 Loop 2000F - Service HL. Triggered by `HL` (`HL-03 = "SS"`). Carries a service-level `UM` review and, in a response, its `HCR` decision. #### Example ```ts import { AUTH_278_LOOP_2000F } from "@cosyte/x12"; AUTH_278_LOOP_2000F.trigger; // "HL" ``` *** ### BALANCE\_INVARIANTS > `const` **BALANCE\_INVARIANTS**: `object` Which 835 balance invariant failed. A library-owned discriminant naming the TR3 equation. The amounts on either side stay on the model as `X12Decimal` and are never rendered into the message: an EDI amount is a consumer-controlled element like any other, and a 300,000-digit "amount" would otherwise become a 300,000-byte diagnostic. #### Type Declaration ##### CLAIM > `readonly` **CLAIM**: `"claim"` = `"claim"` `CLP-04 + Σ(claim CAS + line CAS) == CLP-03`. ##### REMIT\_TOTAL > `readonly` **REMIT\_TOTAL**: `"remit-total"` = `"remit-total"` `Σ(CLP-04) - Σ(PLB amounts) == BPR-02`. ##### SERVICE\_LINE > `readonly` **SERVICE\_LINE**: `"service-line"` = `"service-line"` `SVC-03 + Σ(line CAS) == SVC-02`. #### Example ```ts import { remitBalanceMismatch, BALANCE_INVARIANTS } from "@cosyte/x12"; const w = remitBalanceMismatch({ segmentIndex: 12 }, BALANCE_INVARIANTS.CLAIM); ``` *** ### BUILD\_REFUSAL\_VALUE\_MAX\_LENGTH > `const` **BUILD\_REFUSAL\_VALUE\_MAX\_LENGTH**: `63` = `63` How many characters of a caller-supplied value survive into a refusal message, whether it comes from a `build*` function or from `defineProfile()`. Set to mirror the parser's own `SNIPPET_MAX_INPUT` (63), so the two bounded copies in this library agree rather than each picking a number. Comfortably wider than every slot it guards: an ISA-13 / IEA-02 control number is 9, a TA1-05 note code 3, an ST-01 transaction set id 3, an 834 maintenance type 2-3, an 837 service-line variant 1. #### Example ```ts import { BUILD_REFUSAL_VALUE_MAX_LENGTH } from "@cosyte/x12"; BUILD_REFUSAL_VALUE_MAX_LENGTH; // 63 ``` *** ### BUILD\_REFUSAL\_VALUE\_MAX\_RENDERED > `const` **BUILD\_REFUSAL\_VALUE\_MAX\_RENDERED**: `number` Hard ceiling, in characters, on the fragment [renderCallerValue](#rendercallervalue) returns: [BUILD\_REFUSAL\_VALUE\_MAX\_LENGTH](#build_refusal_value_max_length) surviving characters, two quotes, one ellipsis, and the ` (N characters)` suffix at its widest. renderCallerJson is held to the same ceiling and comes in two characters under it, because JSON supplies its own quotes. **This is the bound on the FRAGMENT, not on the message.** A refusal message is this plus the site's own fixed template text, which differs per site, so the message is bounded by a constant but not by *this* constant. Measured on this tree: a 120,000-character control number gives an 86-character fragment and a **150-character** `X12BuildError.message` from `buildInterchange`. Do not quote the ceiling as though it were a message length - an earlier revision of the docs did exactly that, reporting "now 90 bytes" for a message that is 150. The suite asserts the fragment against this constant and each message against its own site. #### Example ```ts import { BUILD_REFUSAL_VALUE_MAX_RENDERED } from "@cosyte/x12"; BUILD_REFUSAL_VALUE_MAX_RENDERED; // 90 ``` *** ### CARC > `const` **CARC**: [`DatedCodeListSnapshot`](#datedcodelistsnapshot) Bundled CARC snapshot. `meta.publishedDate` is the WPC publication date this subset reflects; `meta.snapshotDate` is when cosyte captured it, and `meta.datesCapturedAt` / `meta.datesSource` say separately when the per-code validity dates were read and from which maintainer page. The `codes` and `dates` maps are frozen - use the [lookupCarc](#lookupcarc) helper for the ergonomic `{ code, description }` shape consumed by the 835 helper. `meta.maintainingOrganization` and `meta.redistribution` carry the answer a consumer needs before displaying, caching or re-publishing a description: this list is X12's and its descriptions require a purchased licence, so the bundled snapshot stays exactly the cited part it has always been and the record names the licensor to approach. #### Example ```ts import { CARC } from "@cosyte/x12"; CARC.meta.snapshotDate; // "2026-06-27" CARC.meta.datesCapturedAt; // "2026-08-28" CARC.codes["45"]; // "Charge exceeds fee schedule..." CARC.dates["45"]?.start; // "1995-01-01" Object.keys(CARC.codes).length; // count of bundled codes CARC.meta.maintainingOrganization; // "ASC X12" CARC.meta.redistribution?.status; // "licence-required" ``` *** ### checkCarcValidity > `const` **checkCarcValidity**: (`code`, `documentDate`) => [`CodeValidityResult`](#codevalidityresult) Report whether a CARC code was valid on the day a document was produced, rather than only whether this package bundles it. Three answers, never two. `valid` and `not-valid` are both claims backed by a date the maintainer published; `indeterminate` says the shipped data cannot decide, and carries the reason - the code is outside the bundled subset, or it is inside it with no published start date. A code is NEVER reported valid for want of evidence, which is the whole asymmetry here: a retired code read as current is a payer credited with an adjustment reason it was not entitled to use. `documentDate` is a calendar day in `YYYY-MM-DD` or `CCYYMMDD` form. Anything else, a JavaScript `Date` included, is refused with an "./errors.js".X12CodeListError and yields no answer at all. #### Parameters ##### code `string` ##### documentDate `string` #### Returns [`CodeValidityResult`](#codevalidityresult) #### Example ```ts import { checkCarcValidity } from "@cosyte/x12"; checkCarcValidity("1", "2026-06-27").validity; // "valid" checkCarcValidity("15", "2018-04-30").validity; // "valid" checkCarcValidity("15", "2018-05-01").validity; // "not-valid" (stopped that day) checkCarcValidity("9999", "20260627").validity; // "indeterminate" checkCarcValidity("9999", "20260627").code; // "9999" (echoed verbatim) ``` *** ### checkRarcValidity > `const` **checkRarcValidity**: (`code`, `documentDate`) => [`CodeValidityResult`](#codevalidityresult) Report whether a RARC code was valid on the day a document was produced. Same three-state answer and the same fail-safe direction as "./carc.js".checkCarcValidity: never `valid` for want of evidence, and an `indeterminate` answer always names its reason. `documentDate` is a calendar day in `YYYY-MM-DD` or `CCYYMMDD` form; anything else is refused with an "./errors.js".X12CodeListError. #### Parameters ##### code `string` ##### documentDate `string` #### Returns [`CodeValidityResult`](#codevalidityresult) #### Example ```ts import { checkRarcValidity } from "@cosyte/x12"; checkRarcValidity("N4", "2026-06-27").validity; // "valid" checkRarcValidity("N4", "1999-12-31").validity; // "not-valid" (before its start) checkRarcValidity("ZZZZ", "2026-06-27").reason; // "code-not-in-bundled-subset" ``` *** ### CLAIM\_837\_BUILD\_ERROR\_CODES > `const` **CLAIM\_837\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [Claim837BuildError](#claim837builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_837_BUILD_INVALID_HIERARCHY` - the billing-provider → subscriber → (claims | patient) tree cannot form a valid HL spine: no billing providers, a billing provider with no subscribers, a subscriber with neither direct claims nor dependent patients, or a dependent patient with no claims. - `X12_837_BUILD_INVALID_SPEC` - a non-hierarchy structural precondition failed: an empty `claimId`, a claim with no service lines, a service line whose `variant` does not match the builder, an empty procedure / revenue code, or an over-length control number. #### Type Declaration ##### X12\_837\_BUILD\_INVALID\_HIERARCHY > `readonly` **X12\_837\_BUILD\_INVALID\_HIERARCHY**: `"X12_837_BUILD_INVALID_HIERARCHY"` = `"X12_837_BUILD_INVALID_HIERARCHY"` ##### X12\_837\_BUILD\_INVALID\_SPEC > `readonly` **X12\_837\_BUILD\_INVALID\_SPEC**: `"X12_837_BUILD_INVALID_SPEC"` = `"X12_837_BUILD_INVALID_SPEC"` #### Example ```ts import { CLAIM_837_BUILD_ERROR_CODES, Claim837BuildError, build837P } from "@cosyte/x12"; try { build837P(spec); } catch (err) { if ( err instanceof Claim837BuildError && err.code === CLAIM_837_BUILD_ERROR_CODES.X12_837_BUILD_INVALID_HIERARCHY ) { // the HL spine is impossible - fix the tree, do not emit } } ``` *** ### CLAIM\_837\_LOOP\_1000A > `const` **CLAIM\_837\_LOOP\_1000A**: [`LoopSpec`](#loopspec) 837 Loop 1000A - Submitter Name. Same across variants. #### Example ```ts import { CLAIM_837_LOOP_1000A } from "@cosyte/x12"; CLAIM_837_LOOP_1000A.id; // "1000A" ``` *** ### CLAIM\_837\_LOOP\_1000B > `const` **CLAIM\_837\_LOOP\_1000B**: [`LoopSpec`](#loopspec) 837 Loop 1000B - Receiver Name. Same across variants. #### Example ```ts import { CLAIM_837_LOOP_1000B } from "@cosyte/x12"; CLAIM_837_LOOP_1000B.id; // "1000B" ``` *** ### CLAIM\_837\_LOOP\_2010AA > `const` **CLAIM\_837\_LOOP\_2010AA**: [`LoopSpec`](#loopspec) 837 Loop 2010AA - Billing Provider Name. Triggered by `NM1` with `NM1-01 = "85"`. The trigger qualifier value is enforced in the walker (the loop spec keys off the segment id, not the element value). #### Example ```ts import { CLAIM_837_LOOP_2010AA } from "@cosyte/x12"; CLAIM_837_LOOP_2010AA.trigger; // "NM1" ``` *** ### CLAIM\_837\_LOOP\_2010BA > `const` **CLAIM\_837\_LOOP\_2010BA**: [`LoopSpec`](#loopspec) 837 Loop 2010BA - Subscriber Name (NM1*IL). The subscriber identity + member id; address is situational. #### Example ```ts import { CLAIM_837_LOOP_2010BA } from "@cosyte/x12"; CLAIM_837_LOOP_2010BA.id; // "2010BA" ``` *** ### CLAIM\_837\_LOOP\_2010BB > `const` **CLAIM\_837\_LOOP\_2010BB**: [`LoopSpec`](#loopspec) 837 Loop 2010BB - Payer Name (NM1*PR). Identifies the payer the provider is billing for this hierarchy. #### Example ```ts import { CLAIM_837_LOOP_2010BB } from "@cosyte/x12"; CLAIM_837_LOOP_2010BB.id; // "2010BB" ``` *** ### CLAIM\_837\_LOOP\_2010CA > `const` **CLAIM\_837\_LOOP\_2010CA**: [`LoopSpec`](#loopspec) 837 Loop 2010CA - Patient Name (NM1*QC). Triggered only when Loop 2000C (patient HL) is present (patient ≠ subscriber). #### Example ```ts import { CLAIM_837_LOOP_2010CA } from "@cosyte/x12"; CLAIM_837_LOOP_2010CA.id; // "2010CA" ``` *** ### CLAIM\_837\_LOOP\_2430 > `const` **CLAIM\_837\_LOOP\_2430**: [`LoopSpec`](#loopspec) 837 Loop 2430 - Line Adjudication Information. Triggered by `SVD`. Captures a prior payer's adjudication of this line for COB. #### Example ```ts import { CLAIM_837_LOOP_2430 } from "@cosyte/x12"; CLAIM_837_LOOP_2430.trigger; // "SVD" ``` *** ### CLAIM\_837D\_LOOP\_2000A > `const` **CLAIM\_837D\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 837D Loop 2000A - Billing Provider Hierarchical Level (dental). #### Example ```ts import { CLAIM_837D_LOOP_2000A } from "@cosyte/x12"; CLAIM_837D_LOOP_2000A.id; // "2000A" ``` *** ### CLAIM\_837D\_LOOP\_2300 > `const` **CLAIM\_837D\_LOOP\_2300**: [`LoopSpec`](#loopspec) 837D Loop 2300 - Claim Information (dental). Triggered by `CLM`. Nests [CLAIM\_837D\_LOOP\_2400](#claim_837d_loop_2400) (SV3-led service lines). #### Example ```ts import { CLAIM_837D_LOOP_2300 } from "@cosyte/x12"; CLAIM_837D_LOOP_2300.trigger; // "CLM" ``` *** ### CLAIM\_837D\_LOOP\_2400 > `const` **CLAIM\_837D\_LOOP\_2400**: [`LoopSpec`](#loopspec) 837D Loop 2400 - Service Line (dental). Body led by `SV3`; per-line `TOO` segments capture tooth + surface detail. #### Example ```ts import { CLAIM_837D_LOOP_2400 } from "@cosyte/x12"; CLAIM_837D_LOOP_2400.trigger; // "LX" ``` *** ### CLAIM\_837I\_LOOP\_2000A > `const` **CLAIM\_837I\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 837I Loop 2000A - Billing Provider Hierarchical Level (institutional). #### Example ```ts import { CLAIM_837I_LOOP_2000A } from "@cosyte/x12"; CLAIM_837I_LOOP_2000A.id; // "2000A" ``` *** ### CLAIM\_837I\_LOOP\_2300 > `const` **CLAIM\_837I\_LOOP\_2300**: [`LoopSpec`](#loopspec) 837I Loop 2300 - Claim Information (institutional). Triggered by `CLM`. Nests [CLAIM\_837I\_LOOP\_2400](#claim_837i_loop_2400) (SV2-led service lines). #### Example ```ts import { CLAIM_837I_LOOP_2300 } from "@cosyte/x12"; CLAIM_837I_LOOP_2300.trigger; // "CLM" ``` *** ### CLAIM\_837I\_LOOP\_2400 > `const` **CLAIM\_837I\_LOOP\_2400**: [`LoopSpec`](#loopspec) 837I Loop 2400 - Service Line (institutional). Body led by `SV2` (revenue code + HCPCS). #### Example ```ts import { CLAIM_837I_LOOP_2400 } from "@cosyte/x12"; CLAIM_837I_LOOP_2400.trigger; // "LX" ``` *** ### CLAIM\_837P\_LOOP\_2000A > `const` **CLAIM\_837P\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 837P Loop 2000A - Billing Provider Hierarchical Level (professional). Top of the HL tree for professional claims. Nests Loop 2010AA (Billing Provider Name) and Loop 2000B (Subscriber HL). #### Example ```ts import { CLAIM_837P_LOOP_2000A } from "@cosyte/x12"; CLAIM_837P_LOOP_2000A.id; // "2000A" ``` *** ### CLAIM\_837P\_LOOP\_2300 > `const` **CLAIM\_837P\_LOOP\_2300**: [`LoopSpec`](#loopspec) 837P Loop 2300 - Claim Information (professional). Triggered by `CLM`. Nests [CLAIM\_837P\_LOOP\_2400](#claim_837p_loop_2400) (SV1-led service lines). #### Example ```ts import { CLAIM_837P_LOOP_2300 } from "@cosyte/x12"; CLAIM_837P_LOOP_2300.trigger; // "CLM" ``` *** ### CLAIM\_837P\_LOOP\_2400 > `const` **CLAIM\_837P\_LOOP\_2400**: [`LoopSpec`](#loopspec) 837P Loop 2400 - Service Line (professional). Triggered by `LX`, body led by `SV1`. Nests Loop 2410 (drug) and Loop 2430 (line adjudication). #### Example ```ts import { CLAIM_837P_LOOP_2400 } from "@cosyte/x12"; CLAIM_837P_LOOP_2400.trigger; // "LX" ``` *** ### CLAIM\_837P\_LOOP\_2410 > `const` **CLAIM\_837P\_LOOP\_2410**: [`LoopSpec`](#loopspec) 837P Loop 2410 - Drug Identification. Triggered by `LIN` inside the 2400 service-line loop. Carries the NDC + dispensed quantity for a professional pharmacy / injectable claim line. #### Example ```ts import { CLAIM_837P_LOOP_2410 } from "@cosyte/x12"; CLAIM_837P_LOOP_2410.trigger; // "LIN" ``` *** ### CLAIM\_ADJUSTMENT\_GROUP\_CODES > `const` **CLAIM\_ADJUSTMENT\_GROUP\_CODES**: `object` The 4 spec-fixed Claim Adjustment Group Codes. Frozen literal union via `as const` so consumers compare `code === CLAIM_ADJUSTMENT_GROUP_CODES.PR` and TypeScript narrows exhaustively. - `CO` - **Contractual Obligation.** The provider's contracted write-off (e.g. payer fee schedule below billed charges). **The PROVIDER eats this.** Posting `CO` to patient responsibility is the wrong-party bug. - `PR` - **Patient Responsibility.** Patient deductible / coinsurance / copay / non-covered. **The PATIENT owes this.** This is the patient-statement total. - `OA` - **Other Adjustment.** Used when neither CO nor PR fits - typically prior-payer payments, COB adjustments, withholding for refund. Use is fact-pattern-specific. - `PI` - **Payer Initiated Reductions.** Payer-side reduction the provider may not dispute under the contract - e.g. bundling edits, prepayment review reductions. #### Type Declaration ##### CO > `readonly` **CO**: `"CO"` = `"CO"` ##### OA > `readonly` **OA**: `"OA"` = `"OA"` ##### PI > `readonly` **PI**: `"PI"` = `"PI"` ##### PR > `readonly` **PR**: `"PR"` = `"PR"` #### Example ```ts import { CLAIM_ADJUSTMENT_GROUP_CODES } from "@cosyte/x12"; function bucketFor(group: string): "provider" | "patient" | "other" | "payer-edit" | "unknown" { switch (group) { case CLAIM_ADJUSTMENT_GROUP_CODES.CO: return "provider"; case CLAIM_ADJUSTMENT_GROUP_CODES.PR: return "patient"; case CLAIM_ADJUSTMENT_GROUP_CODES.OA: return "other"; case CLAIM_ADJUSTMENT_GROUP_CODES.PI: return "payer-edit"; default: return "unknown"; } } ``` *** ### CLAIM\_STATUS\_276\_BUILD\_ERROR\_CODES > `const` **CLAIM\_STATUS\_276\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [ClaimStatus276BuildError](#claimstatus276builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_276_BUILD_INVALID_HIERARCHY` - the nested tree cannot form a valid 276 HL spine (no information sources, a source with no receiver, a receiver with no service provider, a provider with no subscriber), or a list slot was handed something that is not a list. The message carries structural indices and counts only, never a member id, a name, a trace value or a claim number. - `X12_276_BUILD_INVALID_SPEC` - a non-hierarchy precondition failed: an over-long interchange control number, an empty control number, a non-string element value, or a level the builder cannot emit spec-clean (a level with no name loop, a level that asks about no claim, or a claim that asks nothing). #### Type Declaration ##### X12\_276\_BUILD\_INVALID\_HIERARCHY > `readonly` **X12\_276\_BUILD\_INVALID\_HIERARCHY**: `"X12_276_BUILD_INVALID_HIERARCHY"` = `"X12_276_BUILD_INVALID_HIERARCHY"` ##### X12\_276\_BUILD\_INVALID\_SPEC > `readonly` **X12\_276\_BUILD\_INVALID\_SPEC**: `"X12_276_BUILD_INVALID_SPEC"` = `"X12_276_BUILD_INVALID_SPEC"` #### Example ```ts import { CLAIM_STATUS_276_BUILD_ERROR_CODES, ClaimStatus276BuildError, build276 } from "@cosyte/x12"; try { build276(spec); } catch (err) { if ( err instanceof ClaimStatus276BuildError && err.code === CLAIM_STATUS_276_BUILD_ERROR_CODES.X12_276_BUILD_INVALID_HIERARCHY ) { // the hierarchy is impossible - fix the tree, do not emit } } ``` *** ### CLAIM\_STATUS\_277\_BUILD\_ERROR\_CODES > `const` **CLAIM\_STATUS\_277\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [ClaimStatus277BuildError](#claimstatus277builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_277_BUILD_INVALID_HIERARCHY` - the nested tree cannot form a valid 277 HL spine (no sources, a source with no receiver, a receiver with no provider, a provider with no subscriber, a subscriber with neither claims nor dependents, a dependent with no claim). The message carries structural indices + counts only - never a member id / name. - `X12_277_BUILD_INVALID_SPEC` - a non-hierarchy precondition failed (a claim that would not materialize on read - no trace, no statuses, no service lines; a status whose first composite has no category code; a service line with no SVC-07 units of service count, which `005010X212` makes REQUIRED and `005010X214` does not, so this arm fires for `build277` and never for `build277CA`; an over-long ISA-13 control number). #### Type Declaration ##### X12\_277\_BUILD\_INVALID\_HIERARCHY > `readonly` **X12\_277\_BUILD\_INVALID\_HIERARCHY**: `"X12_277_BUILD_INVALID_HIERARCHY"` = `"X12_277_BUILD_INVALID_HIERARCHY"` ##### X12\_277\_BUILD\_INVALID\_SPEC > `readonly` **X12\_277\_BUILD\_INVALID\_SPEC**: `"X12_277_BUILD_INVALID_SPEC"` = `"X12_277_BUILD_INVALID_SPEC"` #### Example ```ts import { CLAIM_STATUS_277_BUILD_ERROR_CODES, ClaimStatus277BuildError, build277 } from "@cosyte/x12"; try { build277(spec); } catch (err) { if ( err instanceof ClaimStatus277BuildError && err.code === CLAIM_STATUS_277_BUILD_ERROR_CODES.X12_277_BUILD_INVALID_HIERARCHY ) { // the hierarchy is impossible - fix the tree, do not emit } } ``` *** ### CLAIM\_STATUS\_CATEGORY\_CODES > `const` **CLAIM\_STATUS\_CATEGORY\_CODES**: [`CodeListSnapshot`](#codelistsnapshot) Bundled Claim Status Category Code (CSCC) snapshot. Used by the 277 and 277CA helpers to surface a human-readable category alongside each verbatim CSCC parsed from an STC composite. #### Example ```ts import { CLAIM_STATUS_CATEGORY_CODES } from "@cosyte/x12"; CLAIM_STATUS_CATEGORY_CODES.codes["A2"]; // "Acknowledgement/Acceptance into adjudication system" CLAIM_STATUS_CATEGORY_CODES.codes["F2"]; // "Finalized/Denial" CLAIM_STATUS_CATEGORY_CODES.meta.redistribution?.status; // "licence-required" ``` *** ### CLAIM\_STATUS\_CODES > `const` **CLAIM\_STATUS\_CODES**: [`CodeListSnapshot`](#codelistsnapshot) Bundled Claim Status Code (CSC) snapshot. Used by the 277 and 277CA helpers to surface a human-readable status alongside each verbatim CSC parsed from an STC composite. #### Example ```ts import { CLAIM_STATUS_CODES } from "@cosyte/x12"; CLAIM_STATUS_CODES.codes["20"]; // "Accepted for processing." CLAIM_STATUS_CODES.codes["21"]; // "Missing or invalid information." CLAIM_STATUS_CODES.meta.redistribution?.status; // "licence-required" ``` *** ### CLP\_STATUS > `const` **CLP\_STATUS**: [`CodeListSnapshot`](#codelistsnapshot) Bundled CLP-02 (Claim Status) snapshot. Used by the 835 helper to surface a human-readable description alongside the verbatim disposition code. #### Example ```ts import { CLP_STATUS } from "@cosyte/x12"; CLP_STATUS.codes["1"]; // "Processed as Primary" CLP_STATUS.codes["4"]; // "Denied" CLP_STATUS.meta.redistribution?.status; // "not-established" ``` *** ### CODE\_VALIDITY > `const` **CODE\_VALIDITY**: `object` The three answers to "was this code valid on that day". `not-valid` and `indeterminate` are deliberately different answers: the first is a claim this package can support from a published date, the second says the shipped data cannot decide. A code is never reported `valid` for want of evidence. #### Type Declaration ##### INDETERMINATE > `readonly` **INDETERMINATE**: `"indeterminate"` = `"indeterminate"` ##### NOT\_VALID > `readonly` **NOT\_VALID**: `"not-valid"` = `"not-valid"` ##### VALID > `readonly` **VALID**: `"valid"` = `"valid"` #### Example ```ts import { CODE_VALIDITY, checkCarcValidity } from "@cosyte/x12"; checkCarcValidity("15", "2026-06-27").validity === CODE_VALIDITY.NOT_VALID; // true ``` *** ### CODE\_VALIDITY\_REASONS > `const` **CODE\_VALIDITY\_REASONS**: `object` Why a validity answer came back `indeterminate`. Locked here so a consumer can branch on the reason exhaustively; additions-only thereafter. - `code-not-in-bundled-subset` - the code is not one this package bundles, so nothing is known about it. The inbound code is still echoed verbatim. - `no-published-start-date` - the code IS bundled but the maintainer publishes no start date for it, so no interval exists to test the day against. #### Type Declaration ##### CODE\_NOT\_IN\_BUNDLED\_SUBSET > `readonly` **CODE\_NOT\_IN\_BUNDLED\_SUBSET**: `"code-not-in-bundled-subset"` = `"code-not-in-bundled-subset"` ##### NO\_PUBLISHED\_START\_DATE > `readonly` **NO\_PUBLISHED\_START\_DATE**: `"no-published-start-date"` = `"no-published-start-date"` #### Example ```ts import { CODE_VALIDITY_REASONS, checkCarcValidity } from "@cosyte/x12"; checkCarcValidity("9999", "2026-06-27").reason; // CODE_VALIDITY_REASONS.CODE_NOT_IN_BUNDLED_SUBSET ``` *** ### CONTROL\_NUMBER\_PAIRS > `const` **CONTROL\_NUMBER\_PAIRS**: `object` Which header/trailer control-number pair disagreed. A library-owned discriminant, NOT a value read out of the document: the useful part of the diagnostic is that the two disagree and where each one sits, never what either one says. #### Type Declaration ##### GROUP > `readonly` **GROUP**: `"group"` = `"group"` GS-06 against GE-02. ##### INTERCHANGE > `readonly` **INTERCHANGE**: `"interchange"` = `"interchange"` ISA-13 against IEA-02. ##### TRANSACTION > `readonly` **TRANSACTION**: `"transaction"` = `"transaction"` ST-02 against SE-02. #### Example ```ts import { controlNumberMismatch, CONTROL_NUMBER_PAIRS } from "@cosyte/x12"; const w = controlNumberMismatch({ segmentIndex: 6 }, CONTROL_NUMBER_PAIRS.INTERCHANGE); ``` *** ### CORE\_BUSINESS\_SCENARIOS > `const` **CORE\_BUSINESS\_SCENARIOS**: `object` Identifiers for the four CORE-defined business scenarios of CORE 360 Table 4.1.1-1, a closed set of four. The value is the scenario's number in that table, so a table transcribed from a CAQH CORE publication maps its scenario column onto these values one to one. - `ADDITIONAL_INFORMATION_DOCUMENTATION` (`"scenario-1"`) - additional information required: documentation missing, invalid or incomplete. - `ADDITIONAL_INFORMATION_CLAIM_DATA` (`"scenario-2"`) - additional information required: data from the submitted claim missing, invalid or incomplete. - `SERVICE_NOT_COVERED` (`"scenario-3"`) - the billed service is not covered by the health plan. - `NOT_SEPARATELY_PAYABLE` (`"scenario-4"`) - the benefit for the billed service is not separately payable. Business scenarios a health plan defines for itself (CORE 360 permits them) are not in this set, and a check naming one answers `unevaluated`. #### Type Declaration ##### ADDITIONAL\_INFORMATION\_CLAIM\_DATA > `readonly` **ADDITIONAL\_INFORMATION\_CLAIM\_DATA**: `"scenario-2"` = `"scenario-2"` ##### ADDITIONAL\_INFORMATION\_DOCUMENTATION > `readonly` **ADDITIONAL\_INFORMATION\_DOCUMENTATION**: `"scenario-1"` = `"scenario-1"` ##### NOT\_SEPARATELY\_PAYABLE > `readonly` **NOT\_SEPARATELY\_PAYABLE**: `"scenario-4"` = `"scenario-4"` ##### SERVICE\_NOT\_COVERED > `readonly` **SERVICE\_NOT\_COVERED**: `"scenario-3"` = `"scenario-3"` #### Example ```ts import { CORE_BUSINESS_SCENARIOS } from "@cosyte/x12"; CORE_BUSINESS_SCENARIOS.SERVICE_NOT_COVERED; // "scenario-3" ``` *** ### CORE\_CODE\_COMBINATION\_ERROR\_CODES > `const` **CORE\_CODE\_COMBINATION\_ERROR\_CODES**: `object` Stable string codes for every [CoreCodeCombinationTableError](#corecodecombinationtableerror). Locked here so consumers can narrow on `err.code` exhaustively; additions-only thereafter (renaming any code is a breaking change). - `X12_CORE_COMBINATION_TABLE_INVALID` - the supplied combination table is malformed: its version label is missing, not a string or empty; its rows are not an array; a row is not an object; a row names a scenario outside the four CORE-defined business scenarios; a row's group code or reason code is missing, not a string or empty; or a row's remark code is present and not a string. #### Type Declaration ##### X12\_CORE\_COMBINATION\_TABLE\_INVALID > `readonly` **X12\_CORE\_COMBINATION\_TABLE\_INVALID**: `"X12_CORE_COMBINATION_TABLE_INVALID"` = `"X12_CORE_COMBINATION_TABLE_INVALID"` #### Example ```ts import { CORE_CODE_COMBINATION_ERROR_CODES, CoreCodeCombinationTableError, checkCoreCodeCombination, } from "@cosyte/x12"; try { checkCoreCodeCombination({ table, scenario, adjustment }); } catch (err) { if ( err instanceof CoreCodeCombinationTableError && err.code === CORE_CODE_COMBINATION_ERROR_CODES.X12_CORE_COMBINATION_TABLE_INVALID ) { // configuration bug: fix the table you load, do not post against it } } ``` *** ### CORE\_CODE\_COMBINATION\_OUTCOMES > `const` **CORE\_CODE\_COMBINATION\_OUTCOMES**: `object` The three answers [checkCoreCodeCombination](#checkcorecodecombination) gives. - `IN_TABLE` (`"in-table"`) - the table lists this group code and reason code for the scenario, and lists every supplied remark code with them. - `NOT_IN_TABLE` (`"not-in-table"`) - the table has rows for the scenario, and the combination is not among them. - `UNEVALUATED` (`"unevaluated"`) - the check could not decide. This is NOT permitted, and the result names the reason. #### Type Declaration ##### IN\_TABLE > `readonly` **IN\_TABLE**: `"in-table"` = `"in-table"` ##### NOT\_IN\_TABLE > `readonly` **NOT\_IN\_TABLE**: `"not-in-table"` = `"not-in-table"` ##### UNEVALUATED > `readonly` **UNEVALUATED**: `"unevaluated"` = `"unevaluated"` #### Example ```ts import { CORE_CODE_COMBINATION_OUTCOMES, checkCoreCodeCombination } from "@cosyte/x12"; const result = checkCoreCodeCombination({ table, scenario, adjustment, remarks }); if (result.outcome !== CORE_CODE_COMBINATION_OUTCOMES.IN_TABLE) { // route to a human: not in the table, or not decidable } ``` *** ### CORE\_CODE\_COMBINATION\_UNEVALUATED\_REASONS > `const` **CORE\_CODE\_COMBINATION\_UNEVALUATED\_REASONS**: `object` Why a check answered `unevaluated`. Each names what could not be judged and none carries a value from the inputs. - `NO_TABLE` - no table was supplied. - `UNKNOWN_SCENARIO` - the scenario named is not one of the four CORE-defined business scenarios. - `NO_ROWS_FOR_SCENARIO` - the table has no row for the named scenario. - `GROUP_CODE_UNREADABLE` - the adjustment's group code is empty or not a string. - `REASON_CODE_UNREADABLE` - the adjustment's reason code is empty or not a string. - `REMARK_NOT_RARC` - a supplied remark carries a code system other than `HE` (for example an `RX` reject reason read from LQ-01). - `REMARK_UNREADABLE` - the remarks are not a list, or one of them is not an object or carries a code that is not a string. #### Type Declaration ##### GROUP\_CODE\_UNREADABLE > `readonly` **GROUP\_CODE\_UNREADABLE**: `"group-code-unreadable"` = `"group-code-unreadable"` ##### NO\_ROWS\_FOR\_SCENARIO > `readonly` **NO\_ROWS\_FOR\_SCENARIO**: `"no-rows-for-scenario"` = `"no-rows-for-scenario"` ##### NO\_TABLE > `readonly` **NO\_TABLE**: `"no-table"` = `"no-table"` ##### REASON\_CODE\_UNREADABLE > `readonly` **REASON\_CODE\_UNREADABLE**: `"reason-code-unreadable"` = `"reason-code-unreadable"` ##### REMARK\_NOT\_RARC > `readonly` **REMARK\_NOT\_RARC**: `"remark-not-rarc"` = `"remark-not-rarc"` ##### REMARK\_UNREADABLE > `readonly` **REMARK\_UNREADABLE**: `"remark-unreadable"` = `"remark-unreadable"` ##### UNKNOWN\_SCENARIO > `readonly` **UNKNOWN\_SCENARIO**: `"unknown-scenario"` = `"unknown-scenario"` #### Example ```ts import { CORE_CODE_COMBINATION_UNEVALUATED_REASONS } from "@cosyte/x12"; CORE_CODE_COMBINATION_UNEVALUATED_REASONS.NO_TABLE; // "no-table" ``` *** ### DELIMITER\_POSITIONS > `const` **DELIMITER\_POSITIONS**: `object` **`Internal`** Zero-indexed byte positions of the four delimiter classes inside the 106-byte ISA. Locked by ASC X12 .5; do NOT make these configurable. - `element` at byte 3 - the byte immediately after the literal `"ISA"`. - `repetition` at byte 82 - ISA-11 (the Control Standards Identifier slot, repurposed as the repetition separator in 005010+). - `component` at byte 104 - ISA-16 (the LAST element). - `segment` at byte 105 - the byte immediately after ISA-16. #### Type Declaration ##### component > `readonly` **component**: `104` = `104` ##### element > `readonly` **element**: `3` = `3` ##### repetition > `readonly` **repetition**: `82` = `82` ##### segment > `readonly` **segment**: `105` = `105` #### Example ```ts import { DELIMITER_POSITIONS } from "@cosyte/x12"; // The element separator is always the 4th byte of a well-formed ISA: raw.charAt(DELIMITER_POSITIONS.element); // e.g. "*" ``` *** ### ELIGIBILITY\_270\_BUILD\_ERROR\_CODES > `const` **ELIGIBILITY\_270\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [Eligibility270BuildError](#eligibility270builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_270_BUILD_INVALID_HIERARCHY` - the nested tree cannot form a valid 270 HL spine (no information sources, a source with no receiver, a receiver with no subscriber), or a list slot was handed something that is not a list. The message carries structural indices and counts only, never a member id, a name or a trace value. - `X12_270_BUILD_INVALID_SPEC` - a non-hierarchy precondition failed: an over-long interchange control number, an empty control number, a non-string element value, or a level the builder cannot emit spec-clean (a subscriber or dependent with no name loop, or with no eligibility inquiry to ask). #### Type Declaration ##### X12\_270\_BUILD\_INVALID\_HIERARCHY > `readonly` **X12\_270\_BUILD\_INVALID\_HIERARCHY**: `"X12_270_BUILD_INVALID_HIERARCHY"` = `"X12_270_BUILD_INVALID_HIERARCHY"` ##### X12\_270\_BUILD\_INVALID\_SPEC > `readonly` **X12\_270\_BUILD\_INVALID\_SPEC**: `"X12_270_BUILD_INVALID_SPEC"` = `"X12_270_BUILD_INVALID_SPEC"` #### Example ```ts import { ELIGIBILITY_270_BUILD_ERROR_CODES, Eligibility270BuildError, build270 } from "@cosyte/x12"; try { build270(spec); } catch (err) { if ( err instanceof Eligibility270BuildError && err.code === ELIGIBILITY_270_BUILD_ERROR_CODES.X12_270_BUILD_INVALID_HIERARCHY ) { // the hierarchy is impossible - fix the tree, do not emit } } ``` *** ### ELIGIBILITY\_271\_BUILD\_ERROR\_CODES > `const` **ELIGIBILITY\_271\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [Eligibility271BuildError](#eligibility271builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_271_BUILD_INVALID_HIERARCHY` - the nested tree cannot form a valid 271 HL spine (no information sources, a source with no receiver, a receiver with no subscriber). The message carries structural indices + counts only - never a member id / name (PHI discipline). - `X12_271_BUILD_INVALID_SPEC` - a non-hierarchy precondition failed (an over-long ISA-13 interchange control number). #### Type Declaration ##### X12\_271\_BUILD\_INVALID\_HIERARCHY > `readonly` **X12\_271\_BUILD\_INVALID\_HIERARCHY**: `"X12_271_BUILD_INVALID_HIERARCHY"` = `"X12_271_BUILD_INVALID_HIERARCHY"` ##### X12\_271\_BUILD\_INVALID\_SPEC > `readonly` **X12\_271\_BUILD\_INVALID\_SPEC**: `"X12_271_BUILD_INVALID_SPEC"` = `"X12_271_BUILD_INVALID_SPEC"` #### Example ```ts import { ELIGIBILITY_271_BUILD_ERROR_CODES, Eligibility271BuildError, build271 } from "@cosyte/x12"; try { build271(spec); } catch (err) { if ( err instanceof Eligibility271BuildError && err.code === ELIGIBILITY_271_BUILD_ERROR_CODES.X12_271_BUILD_INVALID_HIERARCHY ) { // the hierarchy is impossible - fix the tree, do not emit } } ``` *** ### ELIGIBILITY\_271\_LOOP\_2000A > `const` **ELIGIBILITY\_271\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 271 Loop 2000A - Information Source HL (payer). Triggered by `HL` (`HL-03 = "20"`); the level-code check happens in the walker. Nests the Loop 2100A payer name. #### Example ```ts import { ELIGIBILITY_271_LOOP_2000A } from "@cosyte/x12"; ELIGIBILITY_271_LOOP_2000A.trigger; // "HL" ``` *** ### ELIGIBILITY\_271\_LOOP\_2000B > `const` **ELIGIBILITY\_271\_LOOP\_2000B**: [`LoopSpec`](#loopspec) 271 Loop 2000B - Information Receiver HL (provider). Triggered by `HL` (`HL-03 = "21"`). Carries the receiver NM1 + optional REF/N3/N4/PER and request-validation AAA segments. #### Example ```ts import { ELIGIBILITY_271_LOOP_2000B } from "@cosyte/x12"; ELIGIBILITY_271_LOOP_2000B.id; // "2000B" ``` *** ### ELIGIBILITY\_271\_LOOP\_2000C > `const` **ELIGIBILITY\_271\_LOOP\_2000C**: [`LoopSpec`](#loopspec) 271 Loop 2000C - Subscriber HL. Triggered by `HL` (`HL-03 = "22"`). Carries the echoed subscriber TRN traces and nests [ELIGIBILITY\_271\_LOOP\_2100C](#eligibility_271_loop_2100c). #### Example ```ts import { ELIGIBILITY_271_LOOP_2000C } from "@cosyte/x12"; ELIGIBILITY_271_LOOP_2000C.children[0]?.id; // "2100C" ``` *** ### ELIGIBILITY\_271\_LOOP\_2000D > `const` **ELIGIBILITY\_271\_LOOP\_2000D**: [`LoopSpec`](#loopspec) 271 Loop 2000D - Dependent HL. Triggered by `HL` (`HL-03 = "23"`). Carries the echoed dependent TRN traces and nests [ELIGIBILITY\_271\_LOOP\_2100D](#eligibility_271_loop_2100d). #### Example ```ts import { ELIGIBILITY_271_LOOP_2000D } from "@cosyte/x12"; ELIGIBILITY_271_LOOP_2000D.id; // "2000D" ``` *** ### ELIGIBILITY\_271\_LOOP\_2100C > `const` **ELIGIBILITY\_271\_LOOP\_2100C**: [`LoopSpec`](#loopspec) 271 Loop 2100C - Subscriber Name. Triggered by `NM1` (`NM1-01 = "IL"`); the qualifier check happens in the walker, not the spec. Nests [ELIGIBILITY\_271\_LOOP\_2110](#eligibility_271_loop_2110). #### Example ```ts import { ELIGIBILITY_271_LOOP_2100C } from "@cosyte/x12"; ELIGIBILITY_271_LOOP_2100C.children[0]?.trigger; // "EB" ``` *** ### ELIGIBILITY\_271\_LOOP\_2100D > `const` **ELIGIBILITY\_271\_LOOP\_2100D**: [`LoopSpec`](#loopspec) 271 Loop 2100D - Dependent Name. Triggered by `NM1` (`NM1-01 = "03"`). Same shape as 2100C; nests [ELIGIBILITY\_271\_LOOP\_2110](#eligibility_271_loop_2110). #### Example ```ts import { ELIGIBILITY_271_LOOP_2100D } from "@cosyte/x12"; ELIGIBILITY_271_LOOP_2100D.id; // "2100D" ``` *** ### ELIGIBILITY\_271\_LOOP\_2110 > `const` **ELIGIBILITY\_271\_LOOP\_2110**: [`LoopSpec`](#loopspec) 271 Loop 2110 - Eligibility or Benefit Information. Triggered by `EB`. Reused under both the subscriber (2110C) and dependent (2110D) name loops - the segment shape is identical. #### Example ```ts import { ELIGIBILITY_271_LOOP_2110 } from "@cosyte/x12"; ELIGIBILITY_271_LOOP_2110.trigger; // "EB" ``` *** ### ENROLLMENT\_834\_BUILD\_ERROR\_CODES > `const` **ENROLLMENT\_834\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [Enrollment834BuildError](#enrollment834builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE` - an `INS-03` or `HD-01` maintenance type code falls outside the X12 Code Source 875 subset the library validates against. The message carries the structural index of the affected member / coverage and the offending code (an X12 control code, never PHI). - `X12_834_BUILD_INVALID_SPEC` - a non-maintenance precondition failed: no member loop, an empty (required) `INS-03`, or an over-long ISA-13 interchange control number. The message carries structural indices + counts only - never a member id / name (PHI discipline). #### Type Declaration ##### X12\_834\_BUILD\_INVALID\_SPEC > `readonly` **X12\_834\_BUILD\_INVALID\_SPEC**: `"X12_834_BUILD_INVALID_SPEC"` = `"X12_834_BUILD_INVALID_SPEC"` ##### X12\_834\_BUILD\_UNKNOWN\_MAINTENANCE\_TYPE > `readonly` **X12\_834\_BUILD\_UNKNOWN\_MAINTENANCE\_TYPE**: `"X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE"` = `"X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE"` #### Example ```ts import { ENROLLMENT_834_BUILD_ERROR_CODES, Enrollment834BuildError, build834 } from "@cosyte/x12"; try { build834(spec); } catch (err) { if ( err instanceof Enrollment834BuildError && err.code === ENROLLMENT_834_BUILD_ERROR_CODES.X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE ) { // the maintenance action is unrecognized - fix the code, do not emit } } ``` *** ### ENROLLMENT\_834\_LOOP\_1000A > `const` **ENROLLMENT\_834\_LOOP\_1000A**: [`LoopSpec`](#loopspec) 834 Loop 1000A - Sponsor Name. Triggered by `N1` with `N1-01 = "P5"`. #### Example ```ts import { ENROLLMENT_834_LOOP_1000A } from "@cosyte/x12"; ENROLLMENT_834_LOOP_1000A.id; // "1000A" ``` *** ### ENROLLMENT\_834\_LOOP\_1000B > `const` **ENROLLMENT\_834\_LOOP\_1000B**: [`LoopSpec`](#loopspec) 834 Loop 1000B - Payer. Triggered by `N1` with `N1-01 = "IN"`. #### Example ```ts import { ENROLLMENT_834_LOOP_1000B } from "@cosyte/x12"; ENROLLMENT_834_LOOP_1000B.id; // "1000B" ``` *** ### ENROLLMENT\_834\_LOOP\_2000 > `const` **ENROLLMENT\_834\_LOOP\_2000**: [`LoopSpec`](#loopspec) 834 Loop 2000 - Member Level Detail. Triggered by `INS`. The streaming unit of "./get-834.js".get834Enrollments - one yielded `X12Enrollment` per `INS`. Nests Loop 2100A (member name) and Loop 2300 (health coverage). #### Example ```ts import { ENROLLMENT_834_LOOP_2000 } from "@cosyte/x12"; ENROLLMENT_834_LOOP_2000.trigger; // "INS" ENROLLMENT_834_LOOP_2000.children.map((c) => c.trigger); // ["NM1", "HD"] ``` *** ### ENROLLMENT\_834\_LOOP\_2100A > `const` **ENROLLMENT\_834\_LOOP\_2100A**: [`LoopSpec`](#loopspec) 834 Loop 2100A - Member Name. Triggered by `NM1` with `NM1-01 = "IL"`. #### Example ```ts import { ENROLLMENT_834_LOOP_2100A } from "@cosyte/x12"; ENROLLMENT_834_LOOP_2100A.trigger; // "NM1" ``` *** ### ENROLLMENT\_834\_LOOP\_2300 > `const` **ENROLLMENT\_834\_LOOP\_2300**: [`LoopSpec`](#loopspec) 834 Loop 2300 - Health Coverage. Triggered by `HD`. Nests Loop 2320. #### Example ```ts import { ENROLLMENT_834_LOOP_2300 } from "@cosyte/x12"; ENROLLMENT_834_LOOP_2300.trigger; // "HD" ENROLLMENT_834_LOOP_2300.children[0]?.trigger; // "COB" ``` *** ### ENROLLMENT\_834\_LOOP\_2320 > `const` **ENROLLMENT\_834\_LOOP\_2320**: [`LoopSpec`](#loopspec) 834 Loop 2320 - Coordination of Benefits. Triggered by `COB`. #### Example ```ts import { ENROLLMENT_834_LOOP_2320 } from "@cosyte/x12"; ENROLLMENT_834_LOOP_2320.trigger; // "COB" ``` *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable string codes for every Tier-3 fatal `parseX12` may throw. The registry is locked at four codes: anything else MUST be a Tier-2 warning. Consumers narrow on `err.code` to react to specific structural failures. #### Type Declaration ##### X12\_EMPTY\_INPUT > `readonly` **X12\_EMPTY\_INPUT**: `"X12_EMPTY_INPUT"` = `"X12_EMPTY_INPUT"` ##### X12\_INVALID\_DELIMITERS > `readonly` **X12\_INVALID\_DELIMITERS**: `"X12_INVALID_DELIMITERS"` = `"X12_INVALID_DELIMITERS"` ##### X12\_ISA\_TOO\_SHORT > `readonly` **X12\_ISA\_TOO\_SHORT**: `"X12_ISA_TOO_SHORT"` = `"X12_ISA_TOO_SHORT"` ##### X12\_NO\_ISA\_HEADER > `readonly` **X12\_NO\_ISA\_HEADER**: `"X12_NO_ISA_HEADER"` = `"X12_NO_ISA_HEADER"` #### Example ```ts import { parseX12, FATAL_CODES, X12ParseError } from "@cosyte/x12"; try { parseX12(""); } catch (err) { if (err instanceof X12ParseError && err.code === FATAL_CODES.X12_EMPTY_INPUT) { // handle empty input } } ``` *** ### HI\_QUALIFIERS > `const` **HI\_QUALIFIERS**: `object` The HI qualifier → ([X12HiCodeSystem](#x12hicodesystem) + [X12HiCategory](#x12hicategory)) registry. Frozen with `as const` so the keyset is type-narrowed for downstream consumers. Each entry cites its TR3 use site in the description so a reviewer can confirm against the source. Snapshot covers the qualifiers cited across 837P/I/D TR3s plus the most common NUBC code-set qualifiers. Additions are non-breaking; renames or removals are breaking and require a public-surface bump. #### Type Declaration ##### ABF > `readonly` **ABF**: `object` ###### ABF.category > `readonly` **category**: `"diagnosis"` = `"diagnosis"` ###### ABF.description > `readonly` **description**: `"Other (secondary) diagnosis, ICD-10-CM."` = `"Other (secondary) diagnosis, ICD-10-CM."` ###### ABF.system > `readonly` **system**: `"ICD-10-CM"` = `"ICD-10-CM"` ##### ABJ > `readonly` **ABJ**: `object` ###### ABJ.category > `readonly` **category**: `"admitting-diagnosis"` = `"admitting-diagnosis"` ###### ABJ.description > `readonly` **description**: `"Admitting diagnosis, ICD-10-CM (837I inpatient)."` = `"Admitting diagnosis, ICD-10-CM (837I inpatient)."` ###### ABJ.system > `readonly` **system**: `"ICD-10-CM"` = `"ICD-10-CM"` ##### ABK > `readonly` **ABK**: `object` ###### ABK.category > `readonly` **category**: `"principal-diagnosis"` = `"principal-diagnosis"` ###### ABK.description > `readonly` **description**: `"Principal diagnosis, ICD-10-CM (837I; 837P uses ABK in HI-01 for the principal)."` = `"Principal diagnosis, ICD-10-CM (837I; 837P uses ABK in HI-01 for the principal)."` ###### ABK.system > `readonly` **system**: `"ICD-10-CM"` = `"ICD-10-CM"` ##### ABN > `readonly` **ABN**: `object` ###### ABN.category > `readonly` **category**: `"patient-reason-for-visit"` = `"patient-reason-for-visit"` ###### ABN.description > `readonly` **description**: `"Patient's reason for visit, ICD-10-CM (837I outpatient)."` = `"Patient's reason for visit, ICD-10-CM (837I outpatient)."` ###### ABN.system > `readonly` **system**: `"ICD-10-CM"` = `"ICD-10-CM"` ##### APR > `readonly` **APR**: `object` ###### APR.category > `readonly` **category**: `"external-cause"` = `"external-cause"` ###### APR.description > `readonly` **description**: `"External cause of injury, ICD-10-CM (V-Y codes)."` = `"External cause of injury, ICD-10-CM (V-Y codes)."` ###### APR.system > `readonly` **system**: `"ICD-10-CM"` = `"ICD-10-CM"` ##### BBA > `readonly` **BBA**: `object` ###### BBA.category > `readonly` **category**: `"procedure"` = `"procedure"` ###### BBA.description > `readonly` **description**: `"Other procedure, ICD-9-PCS (legacy; replaced by BBR)."` = `"Other procedure, ICD-9-PCS (legacy; replaced by BBR)."` ###### BBA.system > `readonly` **system**: `"ICD-9-PCS"` = `"ICD-9-PCS"` ##### BBQ > `readonly` **BBQ**: `object` ###### BBQ.category > `readonly` **category**: `"treatment"` = `"treatment"` ###### BBQ.description > `readonly` **description**: `"Principal procedure, ICD-10-PCS (837I)."` = `"Principal procedure, ICD-10-PCS (837I)."` ###### BBQ.system > `readonly` **system**: `"ICD-10-PCS"` = `"ICD-10-PCS"` ##### BBR > `readonly` **BBR**: `object` ###### BBR.category > `readonly` **category**: `"procedure"` = `"procedure"` ###### BBR.description > `readonly` **description**: `"Other (secondary) procedure, ICD-10-PCS (837I)."` = `"Other (secondary) procedure, ICD-10-PCS (837I)."` ###### BBR.system > `readonly` **system**: `"ICD-10-PCS"` = `"ICD-10-PCS"` ##### BE > `readonly` **BE**: `object` ###### BE.category > `readonly` **category**: `"value"` = `"value"` ###### BE.description > `readonly` **description**: `"Value code (NUBC FL 39-41) - paired amount in HI-NN-5 as decimal."` = `"Value code (NUBC FL 39-41) - paired amount in HI-NN-5 as decimal."` ###### BE.system > `readonly` **system**: `"NUBC-VALUE"` = `"NUBC-VALUE"` ##### BF > `readonly` **BF**: `object` ###### BF.category > `readonly` **category**: `"diagnosis"` = `"diagnosis"` ###### BF.description > `readonly` **description**: `"Other diagnosis, ICD-9-CM (legacy; replaced by ABF)."` = `"Other diagnosis, ICD-9-CM (legacy; replaced by ABF)."` ###### BF.system > `readonly` **system**: `"ICD-9-CM"` = `"ICD-9-CM"` ##### BG > `readonly` **BG**: `object` ###### BG.category > `readonly` **category**: `"condition"` = `"condition"` ###### BG.description > `readonly` **description**: `"Condition code (NUBC FL 18-28)."` = `"Condition code (NUBC FL 18-28)."` ###### BG.system > `readonly` **system**: `"NUBC-CONDITION"` = `"NUBC-CONDITION"` ##### BH > `readonly` **BH**: `object` ###### BH.category > `readonly` **category**: `"occurrence"` = `"occurrence"` ###### BH.description > `readonly` **description**: `"Occurrence code (NUBC FL 31-34) - date paired in HI-NN-4."` = `"Occurrence code (NUBC FL 31-34) - date paired in HI-NN-4."` ###### BH.system > `readonly` **system**: `"NUBC-OCCURRENCE"` = `"NUBC-OCCURRENCE"` ##### BI > `readonly` **BI**: `object` ###### BI.category > `readonly` **category**: `"occurrence-span"` = `"occurrence-span"` ###### BI.description > `readonly` **description**: `"Occurrence span code (NUBC FL 35-36) - date range in HI-NN-5/6."` = `"Occurrence span code (NUBC FL 35-36) - date range in HI-NN-5/6."` ###### BI.system > `readonly` **system**: `"NUBC-OCCURRENCE-SPAN"` = `"NUBC-OCCURRENCE-SPAN"` ##### BJ > `readonly` **BJ**: `object` ###### BJ.category > `readonly` **category**: `"admitting-diagnosis"` = `"admitting-diagnosis"` ###### BJ.description > `readonly` **description**: `"Admitting diagnosis, ICD-9-CM (legacy; replaced by ABJ)."` = `"Admitting diagnosis, ICD-9-CM (legacy; replaced by ABJ)."` ###### BJ.system > `readonly` **system**: `"ICD-9-CM"` = `"ICD-9-CM"` ##### BK > `readonly` **BK**: `object` ###### BK.category > `readonly` **category**: `"principal-diagnosis"` = `"principal-diagnosis"` ###### BK.description > `readonly` **description**: `"Principal diagnosis, ICD-9-CM (legacy; replaced by ABK 2015-10-01 onward)."` = `"Principal diagnosis, ICD-9-CM (legacy; replaced by ABK 2015-10-01 onward)."` ###### BK.system > `readonly` **system**: `"ICD-9-CM"` = `"ICD-9-CM"` ##### BN > `readonly` **BN**: `object` ###### BN.category > `readonly` **category**: `"patient-reason-for-visit"` = `"patient-reason-for-visit"` ###### BN.description > `readonly` **description**: `"Reason for visit, ICD-9-CM (legacy; replaced by ABN)."` = `"Reason for visit, ICD-9-CM (legacy; replaced by ABN)."` ###### BN.system > `readonly` **system**: `"ICD-9-CM"` = `"ICD-9-CM"` ##### BQ > `readonly` **BQ**: `object` ###### BQ.category > `readonly` **category**: `"treatment"` = `"treatment"` ###### BQ.description > `readonly` **description**: `"Principal procedure, ICD-9-PCS (legacy; replaced by BBQ)."` = `"Principal procedure, ICD-9-PCS (legacy; replaced by BBQ)."` ###### BQ.system > `readonly` **system**: `"ICD-9-PCS"` = `"ICD-9-PCS"` ##### BR > `readonly` **BR**: `object` ###### BR.category > `readonly` **category**: `"external-cause"` = `"external-cause"` ###### BR.description > `readonly` **description**: `"External cause of injury, ICD-9-CM (legacy; replaced by APR)."` = `"External cause of injury, ICD-9-CM (legacy; replaced by APR)."` ###### BR.system > `readonly` **system**: `"ICD-9-CM"` = `"ICD-9-CM"` ##### DR > `readonly` **DR**: `object` ###### DR.category > `readonly` **category**: `"drg"` = `"drg"` ###### DR.description > `readonly` **description**: `"Diagnosis Related Group (DRG) - payment-grouping code."` = `"Diagnosis Related Group (DRG) - payment-grouping code."` ###### DR.system > `readonly` **system**: `"DRG"` = `"DRG"` ##### PR > `readonly` **PR**: `object` ###### PR.category > `readonly` **category**: `"patient-reason-for-visit"` = `"patient-reason-for-visit"` ###### PR.description > `readonly` **description**: `"Patient reason for visit (837I outpatient secondary code; ICD-10-CM coded)."` = `"Patient reason for visit (837I outpatient secondary code; ICD-10-CM coded)."` ###### PR.system > `readonly` **system**: `"NUBC-PATIENT-REASON"` = `"NUBC-PATIENT-REASON"` #### Example ```ts import { HI_QUALIFIERS, resolveHiQualifier } from "@cosyte/x12"; HI_QUALIFIERS.ABK.system; // "ICD-10-CM" resolveHiQualifier("ABK"); // { system: "ICD-10-CM", category: "principal-diagnosis", ... } resolveHiQualifier("ZZZ"); // undefined ``` *** ### HL\_LEVEL\_CODES > `const` **HL\_LEVEL\_CODES**: `Readonly`\<\{ `DEPENDENT`: `"23"`; `INFORMATION_RECEIVER`: `"21"`; `INFORMATION_SOURCE`: `"20"`; `SUBSCRIBER`: `"22"`; \}\> HL level codes per X12 0736 + 837 TR3 conventions. `INFORMATION_SOURCE` is the billing provider (top of the tree); `SUBSCRIBER` is the insurance subscriber; `DEPENDENT` is a non-subscriber patient. #### Example ```ts import { HL_LEVEL_CODES } from "@cosyte/x12"; HL_LEVEL_CODES.INFORMATION_SOURCE; // "20" HL_LEVEL_CODES.SUBSCRIBER; // "22" HL_LEVEL_CODES.DEPENDENT; // "23" ``` *** ### IK3\_SYNTAX\_ERROR\_CODES > `const` **IK3\_SYNTAX\_ERROR\_CODES**: `object` IK3-04 implementation segment syntax error codes (ASC X12 code list 716). Cited per IK3 segment to identify the structural issue against the inbound segment. The library NEVER auto-classifies - the application supplies the code; the library mechanically builds the IK3 around it. - `1` - Unrecognized segment ID. - `2` - Unexpected segment. - `3` - Required segment missing. - `4` - Loop occurs over maximum times. - `5` - Segment exceeds maximum use. - `6` - Segment not in defined transaction set. - `7` - Segment not in proper sequence. - `8` - Segment has data element errors. - `I4` - Implementation "Not Used" segment present. - `I6` - Implementation dependent segment missing. - `I7` - Implementation loop occurs under minimum times. - `I8` - Implementation segment below minimum use. - `I9` - Implementation dependent "Not Used" segment present. #### Type Declaration ##### 1 > `readonly` **1**: `"1"` = `"1"` ##### 2 > `readonly` **2**: `"2"` = `"2"` ##### 3 > `readonly` **3**: `"3"` = `"3"` ##### 4 > `readonly` **4**: `"4"` = `"4"` ##### 5 > `readonly` **5**: `"5"` = `"5"` ##### 6 > `readonly` **6**: `"6"` = `"6"` ##### 7 > `readonly` **7**: `"7"` = `"7"` ##### 8 > `readonly` **8**: `"8"` = `"8"` ##### I4 > `readonly` **I4**: `"I4"` = `"I4"` ##### I6 > `readonly` **I6**: `"I6"` = `"I6"` ##### I7 > `readonly` **I7**: `"I7"` = `"I7"` ##### I8 > `readonly` **I8**: `"I8"` = `"I8"` ##### I9 > `readonly` **I9**: `"I9"` = `"I9"` #### Example ```ts import type { Ik304Code } from "@cosyte/x12"; const required: Ik304Code = "3"; ``` *** ### IK4\_SYNTAX\_ERROR\_CODES > `const` **IK4\_SYNTAX\_ERROR\_CODES**: `object` IK4-03 implementation data element syntax error codes (ASC X12 code list 723). Cited per IK4 segment to identify the issue against the inbound element/component/repetition. - `1` - Required data element missing. - `2` - Conditional required data element missing. - `3` - Too many data elements. - `4` - Data element too short. - `5` - Data element too long. - `6` - Invalid character in data element. - `7` - Invalid code value. - `8` - Invalid date. - `9` - Invalid time. - `10` - Exclusion condition violated. - `12` - Too many repetitions. - `13` - Too many components. - `I6` - Code value not used in implementation. - `I9` - Implementation dependent data element missing. - `I10` - Implementation "Not Used" data element present. - `I11` - Implementation too few repetitions. - `I12` - Implementation pattern match failure. - `I13` - Implementation dependent "Not Used" data element present. #### Type Declaration ##### 1 > `readonly` **1**: `"1"` = `"1"` ##### 10 > `readonly` **10**: `"10"` = `"10"` ##### 12 > `readonly` **12**: `"12"` = `"12"` ##### 13 > `readonly` **13**: `"13"` = `"13"` ##### 2 > `readonly` **2**: `"2"` = `"2"` ##### 3 > `readonly` **3**: `"3"` = `"3"` ##### 4 > `readonly` **4**: `"4"` = `"4"` ##### 5 > `readonly` **5**: `"5"` = `"5"` ##### 6 > `readonly` **6**: `"6"` = `"6"` ##### 7 > `readonly` **7**: `"7"` = `"7"` ##### 8 > `readonly` **8**: `"8"` = `"8"` ##### 9 > `readonly` **9**: `"9"` = `"9"` ##### I10 > `readonly` **I10**: `"I10"` = `"I10"` ##### I11 > `readonly` **I11**: `"I11"` = `"I11"` ##### I12 > `readonly` **I12**: `"I12"` = `"I12"` ##### I13 > `readonly` **I13**: `"I13"` = `"I13"` ##### I6 > `readonly` **I6**: `"I6"` = `"I6"` ##### I9 > `readonly` **I9**: `"I9"` = `"I9"` #### Example ```ts import type { Ik403Code } from "@cosyte/x12"; const tooShort: Ik403Code = "4"; ``` *** ### INQUIRY\_270\_LOOP\_2000A > `const` **INQUIRY\_270\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 270 Loop 2000A - Information Source HL (the payer being asked). Triggered by `HL` (`HL-03 = "20"`); the level-code check happens in the walker. #### Example ```ts import { INQUIRY_270_LOOP_2000A } from "@cosyte/x12"; INQUIRY_270_LOOP_2000A.trigger; // "HL" ``` *** ### INQUIRY\_270\_LOOP\_2000B > `const` **INQUIRY\_270\_LOOP\_2000B**: [`LoopSpec`](#loopspec) 270 Loop 2000B - Information Receiver HL (the provider asking). Triggered by `HL` (`HL-03 = "21"`). #### Example ```ts import { INQUIRY_270_LOOP_2000B } from "@cosyte/x12"; INQUIRY_270_LOOP_2000B.id; // "2000B" ``` *** ### INQUIRY\_270\_LOOP\_2000C > `const` **INQUIRY\_270\_LOOP\_2000C**: [`LoopSpec`](#loopspec) 270 Loop 2000C - Subscriber HL. Triggered by `HL` (`HL-03 = "22"`). Carries the TRN traces a 271 must echo back and nests [INQUIRY\_270\_LOOP\_2100C](#inquiry_270_loop_2100c). #### Example ```ts import { INQUIRY_270_LOOP_2000C } from "@cosyte/x12"; INQUIRY_270_LOOP_2000C.children[0]?.id; // "2100C" ``` *** ### INQUIRY\_270\_LOOP\_2000D > `const` **INQUIRY\_270\_LOOP\_2000D**: [`LoopSpec`](#loopspec) 270 Loop 2000D - Dependent HL. Triggered by `HL` (`HL-03 = "23"`). Carries the dependent's own TRN traces and nests [INQUIRY\_270\_LOOP\_2100D](#inquiry_270_loop_2100d). #### Example ```ts import { INQUIRY_270_LOOP_2000D } from "@cosyte/x12"; INQUIRY_270_LOOP_2000D.id; // "2000D" ``` *** ### INQUIRY\_270\_LOOP\_2100C > `const` **INQUIRY\_270\_LOOP\_2100C**: [`LoopSpec`](#loopspec) 270 Loop 2100C - Subscriber Name. Triggered by `NM1` (`NM1-01 = "IL"`); the qualifier check happens in the walker, not the spec. Nests [INQUIRY\_270\_LOOP\_2110](#inquiry_270_loop_2110). #### Example ```ts import { INQUIRY_270_LOOP_2100C } from "@cosyte/x12"; INQUIRY_270_LOOP_2100C.children[0]?.trigger; // "EQ" ``` *** ### INQUIRY\_270\_LOOP\_2100D > `const` **INQUIRY\_270\_LOOP\_2100D**: [`LoopSpec`](#loopspec) 270 Loop 2100D - Dependent Name. Triggered by `NM1` (`NM1-01 = "03"`). Same shape as 2100C; nests [INQUIRY\_270\_LOOP\_2110](#inquiry_270_loop_2110). #### Example ```ts import { INQUIRY_270_LOOP_2100D } from "@cosyte/x12"; INQUIRY_270_LOOP_2100D.id; // "2100D" ``` *** ### INQUIRY\_270\_LOOP\_2110 > `const` **INQUIRY\_270\_LOOP\_2110**: [`LoopSpec`](#loopspec) 270 Loop 2110 - Eligibility or Benefit Inquiry. Triggered by `EQ`. Reused under both the subscriber (2110C) and dependent (2110D) name loops: the segment shape is identical. #### Example ```ts import { INQUIRY_270_LOOP_2110 } from "@cosyte/x12"; INQUIRY_270_LOOP_2110.trigger; // "EQ" ``` *** ### ISA\_MIN\_LENGTH > `const` **ISA\_MIN\_LENGTH**: `106` = `106` Minimum number of bytes a valid ISA segment occupies. ASC X12 .5 fixes the ISA at 106 bytes total, **including** the trailing segment terminator: 3 ("ISA") + 16 element separators + 86 element-value bytes (sum of fixed widths 2+10+2+10+2+15+2+15+6+4+1+5+9+1+1+1) + 1 terminator = 106. Anything shorter cannot carry the 16 ISA elements and is Tier-3 `X12_ISA_TOO_SHORT`. #### Example ```ts import { ISA_MIN_LENGTH } from "@cosyte/x12"; ISA_MIN_LENGTH; // 106 - a raw interchange shorter than this is X12_ISA_TOO_SHORT ``` *** ### lookupAaaFollowUpAction > `const` **lookupAaaFollowUpAction**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up an AAA-04 Follow-up Action Code description. Returns `undefined` for every code while the snapshot is empty. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupAaaFollowUpAction } from "@cosyte/x12"; lookupAaaFollowUpAction("C"); // undefined (snapshot ships empty) ``` *** ### lookupAaaRejectReason > `const` **lookupAaaRejectReason**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up an AAA-03 Reject Reason Code description. Returns `undefined` for every code while the snapshot is empty, which is the fail-safe: the verbatim inbound code is preserved on the typed result either way. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupAaaRejectReason } from "@cosyte/x12"; lookupAaaRejectReason("42"); // undefined (snapshot ships empty) ``` *** ### lookupCarc > `const` **lookupCarc**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up a CARC code's bundled description. Returns `undefined` when the code is not in the initial subset - the verbatim code is still preserved on the parsed model and the 835 walker emits `X12_UNKNOWN_CARC` so consumers know the description gap exists. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupCarc } from "@cosyte/x12"; lookupCarc("45")?.description; // "Charge exceeds fee schedule..." lookupCarc("9999"); // undefined (outside the bundled subset) ``` *** ### lookupClaimStatus > `const` **lookupClaimStatus**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up a Claim Status Code (CSC) description from the bundled snapshot. Returns `undefined` for codes outside the subset; the verbatim code is always preserved on the parsed status model. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupClaimStatus } from "@cosyte/x12"; lookupClaimStatus("20")?.description; // "Accepted for processing." lookupClaimStatus("99999"); // undefined (outside subset) ``` *** ### lookupClaimStatusCategory > `const` **lookupClaimStatusCategory**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up a Claim Status Category Code (CSCC) description from the bundled snapshot. Returns `undefined` for codes outside the subset; the verbatim code is always preserved on the parsed status model. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupClaimStatusCategory } from "@cosyte/x12"; lookupClaimStatusCategory("A2")?.description; // "Acknowledgement/Acceptance..." lookupClaimStatusCategory("ZZ"); // undefined (outside subset) ``` *** ### lookupClpStatus > `const` **lookupClpStatus**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up a CLP-02 claim status code's bundled description. Returns `undefined` for codes outside the initial subset; the verbatim code is still preserved on the parsed claim model. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupClpStatus } from "@cosyte/x12"; lookupClpStatus("1")?.description; // "Processed as Primary" lookupClpStatus("99"); // undefined (outside subset) ``` *** ### lookupMaintenanceType > `const` **lookupMaintenanceType**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up an INS-03 maintenance type code's bundled description. Returns `undefined` for codes outside the subset; the verbatim code is still preserved on the parsed enrollment, and the 834 helper raises an `X12_834_UNKNOWN_MAINTENANCE_TYPE` warning so a consumer never silently mis-applies an unknown action. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupMaintenanceType } from "@cosyte/x12"; lookupMaintenanceType("024")?.description; // "Cancellation or Termination" lookupMaintenanceType("999"); // undefined (outside subset) ``` *** ### lookupRarc > `const` **lookupRarc**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up a RARC code's bundled description. Same fail-safe semantics as "./carc.js".lookupCarc: unknown codes return `undefined`, the verbatim code is preserved on the parsed model, and the 835 walker emits `X12_UNKNOWN_RARC`. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupRarc } from "@cosyte/x12"; lookupRarc("N4")?.description; // "Missing/incomplete/invalid prior insurance carrier(s) EOB." ``` *** ### lookupServiceType > `const` **lookupServiceType**: (`code`) => [`CodeListEntry`](#codelistentry) \| `undefined` Look up a Service Type Code (EB-03) description from the bundled snapshot. Returns `undefined` for codes outside the initial subset; the verbatim code is still preserved on the parsed benefit model. #### Parameters ##### code `string` #### Returns [`CodeListEntry`](#codelistentry) \| `undefined` #### Example ```ts import { lookupServiceType } from "@cosyte/x12"; lookupServiceType("30")?.description; // "Health Benefit Plan Coverage" lookupServiceType("ZZ"); // undefined (outside subset) ``` *** ### MAINTENANCE\_TYPE\_CODES > `const` **MAINTENANCE\_TYPE\_CODES**: [`CodeListSnapshot`](#codelistsnapshot) Bundled INS-03 (Maintenance Type) snapshot. Used by the 834 helper to surface a human-readable description alongside the verbatim maintenance code, and to drive the `X12_834_UNKNOWN_MAINTENANCE_TYPE` warning when a code falls outside this set. #### Example ```ts import { MAINTENANCE_TYPE_CODES } from "@cosyte/x12"; MAINTENANCE_TYPE_CODES.codes["021"]; // "Addition" MAINTENANCE_TYPE_CODES.codes["024"]; // "Cancellation or Termination" MAINTENANCE_TYPE_CODES.meta.redistribution?.status; // "not-established" ``` *** ### NM1\_QUALIFIERS > `const` **NM1\_QUALIFIERS**: `Readonly`\<\{ `BILLING_PROVIDER`: `"85"`; `PATIENT`: `"QC"`; `PAY_TO_ADDRESS`: `"87"`; `PAY_TO_PLAN`: `"PE"`; `PAYER`: `"PR"`; `RECEIVER`: `"40"`; `SUBMITTER`: `"41"`; `SUBSCRIBER`: `"IL"`; \}\> NM1-01 entity-identifier codes used by the 837 walker to route an NM1 onto the right entity slot. Curated to the qualifiers the v1 walker recognizes; an NM1 with a qualifier outside this set falls into the Loop 2310x / 2420x line-provider verbatim bucket. Frozen for type safety + IntelliSense - consumers should rarely need these (they're a walker-internal vocabulary), but they're exported for tests + the builder API. #### Example ```ts NM1_QUALIFIERS.BILLING_PROVIDER → "85". ``` *** ### NON\_SPEC\_SEGMENT\_ID > `const` **NON\_SPEC\_SEGMENT\_ID**: `"(non-spec)"` = `"(non-spec)"` The value [X12Segment.id](#id-9) takes when the first element does not match the X12 segment-id grammar. Not a valid segment id, so it can never collide with a real one in a `seg.id === "NM1"` comparison. #### Example ```ts import { parseX12, NON_SPEC_SEGMENT_ID } from "@cosyte/x12"; const seg = parseX12(raw).groups[0]?.transactions[0]?.segments[1]; if (seg?.id === NON_SPEC_SEGMENT_ID) { // the sender's "segment" name is not a spec segment id; its bytes are on seg.raw } ``` *** ### PREMIUM\_820\_BUILD\_ERROR\_CODES > `const` **PREMIUM\_820\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [Premium820BuildError](#premium820builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_820_BUILD_INVALID_SPEC` - a structural precondition failed: no TRN trace, no remittance loop, a remittance with neither an `ENT` entity nor an `NM1` individual to open it, a remittance with no `RMR` open item, an open item with no identity (empty qualifier + reference id), or an over-long ISA-13 interchange control number. The message carries structural indices + counts only - never a member id / name (PHI discipline). #### Type Declaration ##### X12\_820\_BUILD\_INVALID\_SPEC > `readonly` **X12\_820\_BUILD\_INVALID\_SPEC**: `"X12_820_BUILD_INVALID_SPEC"` = `"X12_820_BUILD_INVALID_SPEC"` #### Example ```ts import { PREMIUM_820_BUILD_ERROR_CODES, Premium820BuildError, build820 } from "@cosyte/x12"; try { build820(spec); } catch (err) { if ( err instanceof Premium820BuildError && err.code === PREMIUM_820_BUILD_ERROR_CODES.X12_820_BUILD_INVALID_SPEC ) { // the remittance structure is impossible - fix the spec, do not emit } } ``` *** ### PREMIUM\_820\_LOOP\_1000A > `const` **PREMIUM\_820\_LOOP\_1000A**: [`LoopSpec`](#loopspec) 820 Loop 1000A - Premium Receiver's Name. Triggered by `N1` with `N1-01 = "PE"`. The qualifier check happens in the walker; `defineLoopSpec` keys off the segment id, not the element value. #### Example ```ts import { PREMIUM_820_LOOP_1000A } from "@cosyte/x12"; PREMIUM_820_LOOP_1000A.id; // "1000A" ``` *** ### PREMIUM\_820\_LOOP\_1000B > `const` **PREMIUM\_820\_LOOP\_1000B**: [`LoopSpec`](#loopspec) 820 Loop 1000B - Premium Payer's Name (remitter). Triggered by `N1` with `N1-01 = "PR"` or `"RM"`. Same shape as Loop 1000A minus `RDM`. #### Example ```ts import { PREMIUM_820_LOOP_1000B } from "@cosyte/x12"; PREMIUM_820_LOOP_1000B.id; // "1000B" ``` *** ### PREMIUM\_820\_LOOP\_2000A > `const` **PREMIUM\_820\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 820 Loop 2000A - Organization Summary Remittance. Triggered by `ENT`. Nests Loop 2100A (party name) and Loop 2300A (remittance detail). Cited as Loop 2000A in TR3 X218. #### Example ```ts import { PREMIUM_820_LOOP_2000A } from "@cosyte/x12"; PREMIUM_820_LOOP_2000A.trigger; // "ENT" PREMIUM_820_LOOP_2000A.children.length; // 2 (2100A, 2300A) ``` *** ### PREMIUM\_820\_LOOP\_2100A > `const` **PREMIUM\_820\_LOOP\_2100A**: [`LoopSpec`](#loopspec) 820 Loop 2100A - Party Name (member / entity inside an organization summary). Triggered by `NM1`. Cited as Loop 2100A in TR3 X218. #### Example ```ts import { PREMIUM_820_LOOP_2100A } from "@cosyte/x12"; PREMIUM_820_LOOP_2100A.trigger; // "NM1" ``` *** ### PREMIUM\_820\_LOOP\_2300A > `const` **PREMIUM\_820\_LOOP\_2300A**: [`LoopSpec`](#loopspec) 820 Loop 2300A - Organization Summary Remittance Detail. Triggered by `RMR` (the premium-line open-item reference). Nests Loop 2310A. Cited as Loop 2300A in TR3 X218. #### Example ```ts import { PREMIUM_820_LOOP_2300A } from "@cosyte/x12"; PREMIUM_820_LOOP_2300A.trigger; // "RMR" PREMIUM_820_LOOP_2300A.children[0]?.trigger; // "ADX" ``` *** ### PREMIUM\_820\_LOOP\_2310A > `const` **PREMIUM\_820\_LOOP\_2310A**: [`LoopSpec`](#loopspec) 820 Loop 2310A - Adjustment. Triggered by `ADX`. Innermost detail loop: a signed monetary adjustment tied to the enclosing `RMR` open item. Cited as Loop 2310A in TR3 X218. #### Example ```ts import { PREMIUM_820_LOOP_2310A } from "@cosyte/x12"; PREMIUM_820_LOOP_2310A.trigger; // "ADX" ``` *** ### profiles > `const` **profiles**: `object` Namespace object exposing the shipped built-in profiles. Each is authored via the public `defineProfile()` API and grounded in a real Tier-2 fixture. #### Type Declaration ##### availity > `readonly` **availity**: [`X12Profile`](#x12profile) ##### bcbsCommon > `readonly` **bcbsCommon**: [`X12Profile`](#x12profile) #### Example ```ts import { parseX12, profiles } from "@cosyte/x12"; const ix = parseX12(raw, { profile: profiles.availity }); ix.profile?.name; // "availity" ``` *** ### RARC > `const` **RARC**: [`DatedCodeListSnapshot`](#datedcodelistsnapshot) Bundled RARC snapshot. Companion to "./carc.js".CARC; same freshness + safety posture, and the same separation between when the descriptions were captured (`meta.snapshotDate`) and when the per-code validity dates were (`meta.datesCapturedAt`). Use [lookupRarc](#lookuprarc) for the ergonomic lookup. **The redistribution answer differs from CARC's, and that is the point of recording it per list.** This list is maintained by CMS rather than by X12 and needs no licence, so its descriptions are free to redistribute where CARC's are not. The snapshot is still only the cited part of the published list, which `meta.completeness` says. #### Example ```ts import { RARC } from "@cosyte/x12"; RARC.codes["N4"]; // "Missing/incomplete/invalid prior insurance carrier(s) EOB." RARC.codes["MA01"]; // (or undefined if outside this subset) RARC.dates["N4"]?.start; // "2000-01-01" RARC.meta.maintainingOrganization; // "CMS" RARC.meta.redistribution?.status; // "permitted" ``` *** ### RELEASE\_CHAR > `const` **RELEASE\_CHAR**: `"?"` **`Internal`** The release character is conventionally `?` per ASC X12; HIPAA 005010 does not transmit it as a fifth ISA delimiter. The library accepts `?` as the universal release character - a non-`?` release sequence has never been observed in real-world US healthcare X12 traffic. If a future consumer needs a different release character, the constant is `as const` so a parameterized variant can be added without breaking the public API. #### Example ```ts import { RELEASE_CHAR } from "@cosyte/x12"; // `?~` is an escaped segment terminator inside a value, not a real one: "PER*IC*ACME?~BILLING".includes(RELEASE_CHAR); // true ``` *** ### REMIT\_835\_BUILD\_ERROR\_CODES > `const` **REMIT\_835\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [Remit835BuildError](#remit835builderror). Locked here so consumers can narrow exhaustively on `err.code`; additions-only thereafter (renaming any code is a breaking change). - `X12_835_BUILD_BALANCE_MISMATCH` - a spec violated one of the three §1.10.2 balance invariants (service-line `SVC-02 == SVC-03 + Σ(line CAS)`, claim `CLP-03 == CLP-04 + Σ(claim+line CAS)`, or top-of-remit `BPR-02 == Σ(CLP-04) − Σ(PLB)`). The message names the invariant, the spec'd vs computed totals, and the delta - numeric values only, never PHI. - `X12_835_BUILD_INVALID_SPEC` - a structural precondition failed (no trace supplied, a claim with no patient-control number, etc.). #### Type Declaration ##### X12\_835\_BUILD\_BALANCE\_MISMATCH > `readonly` **X12\_835\_BUILD\_BALANCE\_MISMATCH**: `"X12_835_BUILD_BALANCE_MISMATCH"` = `"X12_835_BUILD_BALANCE_MISMATCH"` ##### X12\_835\_BUILD\_INVALID\_SPEC > `readonly` **X12\_835\_BUILD\_INVALID\_SPEC**: `"X12_835_BUILD_INVALID_SPEC"` = `"X12_835_BUILD_INVALID_SPEC"` #### Example ```ts import { REMIT_835_BUILD_ERROR_CODES, Remit835BuildError, build835 } from "@cosyte/x12"; try { build835(spec); } catch (err) { if ( err instanceof Remit835BuildError && err.code === REMIT_835_BUILD_ERROR_CODES.X12_835_BUILD_BALANCE_MISMATCH ) { // the remit does not balance - fix the amounts, do not emit } } ``` *** ### REMIT\_835\_LOOP\_1000A > `const` **REMIT\_835\_LOOP\_1000A**: [`LoopSpec`](#loopspec) 835 Loop 1000A - Payer Identification. Triggered by `N1` with `N1-01 = "PR"`. The trigger validation against the qualifier value (PR vs PE) happens in the walker, not the loop spec - `defineLoopSpec` keys off the segment id, not the element-level qualifier. #### Example ```ts import { REMIT_835_LOOP_1000A } from "@cosyte/x12"; REMIT_835_LOOP_1000A.id; // "1000A" REMIT_835_LOOP_1000A.trigger; // "N1" ``` *** ### REMIT\_835\_LOOP\_1000B > `const` **REMIT\_835\_LOOP\_1000B**: [`LoopSpec`](#loopspec) 835 Loop 1000B - Payee Identification. Triggered by `N1` with `N1-01 = "PE"`. Shape is the same as Loop 1000A (plus optional `RDM`) but the role is the payment recipient. #### Example ```ts import { REMIT_835_LOOP_1000B } from "@cosyte/x12"; REMIT_835_LOOP_1000B.id; // "1000B" ``` *** ### REMIT\_835\_LOOP\_2000 > `const` **REMIT\_835\_LOOP\_2000**: [`LoopSpec`](#loopspec) 835 Loop 2000 - Header Number. Triggered by `LX`. Nests Loop 2100. Cited as Loop 2000 in TR3 X221A1; situational because some payers (notably Medicare FFS) omit the LX header entirely and ship CLP loops directly under the transaction set header. The walker handles both shapes - claims appear inside Loop 2000 if `LX` is present, at top level otherwise. #### Example ```ts import { REMIT_835_LOOP_2000 } from "@cosyte/x12"; REMIT_835_LOOP_2000.trigger; // "LX" REMIT_835_LOOP_2000.children[0]?.trigger; // "CLP" (Loop 2100) ``` *** ### REMIT\_835\_LOOP\_2100 > `const` **REMIT\_835\_LOOP\_2100**: [`LoopSpec`](#loopspec) 835 Loop 2100 - Claim Payment Information. Triggered by `CLP`. Nests Loop 2110. Cited as Loop 2100 in TR3 X221A1. #### Example ```ts import { REMIT_835_LOOP_2100 } from "@cosyte/x12"; REMIT_835_LOOP_2100.trigger; // "CLP" REMIT_835_LOOP_2100.children.length; // 1 (Loop 2110) ``` *** ### REMIT\_835\_LOOP\_2110 > `const` **REMIT\_835\_LOOP\_2110**: [`LoopSpec`](#loopspec) 835 Loop 2110 - Service Payment Information. Triggered by `SVC`. Cited as Loop 2110 in TR3 X221A1. #### Example ```ts import { REMIT_835_LOOP_2110 } from "@cosyte/x12"; REMIT_835_LOOP_2110.id; // "2110" REMIT_835_LOOP_2110.trigger; // "SVC" ``` *** ### REQUIRED\_LOOPS > `const` **REQUIRED\_LOOPS**: `object` The TR3-required loops the parser reports as structurally absent. A closed set owned by the loop specs under `src/transactions/`, so both the loop id and its rationale are library constants rather than anything read out of the document. #### Type Declaration ##### BILLING\_PROVIDER\_2000A > `readonly` **BILLING\_PROVIDER\_2000A**: `"2000A"` = `"2000A"` Loop 2000A, Billing Provider HL. ##### INQUIRY\_ELIGIBILITY\_2110 > `readonly` **INQUIRY\_ELIGIBILITY\_2110**: `"270-2110"` = `"270-2110"` 270 Loop 2110, the eligibility or benefit inquiry (`EQ`) under a subscriber or dependent level, as the warning's `position` names. ##### INQUIRY\_HIERARCHY\_2000A > `readonly` **INQUIRY\_HIERARCHY\_2000A**: `"270-2000A"` = `"270-2000A"` 270 Loop 2000A, Information Source HL. Reported when a 270 transaction set carries no hierarchical level segment at all, so there is no inquiry hierarchy to build. ##### INQUIRY\_NAME\_2100 > `readonly` **INQUIRY\_NAME\_2100**: `"270-2100"` = `"270-2100"` 270 Loop 2100, the name loop of whichever hierarchical level the warning's `position` names (2100A / 2100B / 2100C / 2100D). One discriminant covers all four, because the position already says which level is short of its NM1 and the library does not need a fifth string to repeat it. ##### PAYER\_NAME\_2010BB > `readonly` **PAYER\_NAME\_2010BB**: `"2010BB"` = `"2010BB"` Loop 2010BB, Payer Name. ##### SUBSCRIBER\_2000B > `readonly` **SUBSCRIBER\_2000B**: `"2000B"` = `"2000B"` Loop 2000B, Subscriber HL. ##### SUBSCRIBER\_NAME\_2010BA > `readonly` **SUBSCRIBER\_NAME\_2010BA**: `"2010BA"` = `"2010BA"` Loop 2010BA, Subscriber Name. #### Example ```ts import { missingRequiredLoop, REQUIRED_LOOPS } from "@cosyte/x12"; const w = missingRequiredLoop({ segmentIndex: 12 }, REQUIRED_LOOPS.PAYER_NAME_2010BB); ``` *** ### RFAI\_277\_BUILD\_ERROR\_CODES > `const` **RFAI\_277\_BUILD\_ERROR\_CODES**: `object` Stable codes for every [Rfai277BuildError](#rfai277builderror). Additions only. - `X12_277_RFAI_BUILD_NO_LEVEL`: `levels` is empty. - `X12_277_RFAI_BUILD_NO_REQUEST`: no level anywhere carries a request. - `X12_277_RFAI_BUILD_NO_TRACE`: a request has no `trace`. - `X12_277_RFAI_BUILD_STATUS_CODE_EMPTY`: an STC has no composite, or a composite whose C043-01 or C043-02 is empty. - `X12_277_RFAI_BUILD_NO_SERVICE`: a service line has no `service`. - `X12_277_RFAI_BUILD_INVALID_SPEC`: the spec is not shaped like one: a value that is not a string or not an `X12Decimal`, a list that is not an array, an unusable delimiter set, an empty or over-long control number, or more than three composites or four modifiers. #### Type Declaration ##### X12\_277\_RFAI\_BUILD\_INVALID\_SPEC > `readonly` **X12\_277\_RFAI\_BUILD\_INVALID\_SPEC**: `"X12_277_RFAI_BUILD_INVALID_SPEC"` = `"X12_277_RFAI_BUILD_INVALID_SPEC"` ##### X12\_277\_RFAI\_BUILD\_NO\_LEVEL > `readonly` **X12\_277\_RFAI\_BUILD\_NO\_LEVEL**: `"X12_277_RFAI_BUILD_NO_LEVEL"` = `"X12_277_RFAI_BUILD_NO_LEVEL"` ##### X12\_277\_RFAI\_BUILD\_NO\_REQUEST > `readonly` **X12\_277\_RFAI\_BUILD\_NO\_REQUEST**: `"X12_277_RFAI_BUILD_NO_REQUEST"` = `"X12_277_RFAI_BUILD_NO_REQUEST"` ##### X12\_277\_RFAI\_BUILD\_NO\_SERVICE > `readonly` **X12\_277\_RFAI\_BUILD\_NO\_SERVICE**: `"X12_277_RFAI_BUILD_NO_SERVICE"` = `"X12_277_RFAI_BUILD_NO_SERVICE"` ##### X12\_277\_RFAI\_BUILD\_NO\_TRACE > `readonly` **X12\_277\_RFAI\_BUILD\_NO\_TRACE**: `"X12_277_RFAI_BUILD_NO_TRACE"` = `"X12_277_RFAI_BUILD_NO_TRACE"` ##### X12\_277\_RFAI\_BUILD\_STATUS\_CODE\_EMPTY > `readonly` **X12\_277\_RFAI\_BUILD\_STATUS\_CODE\_EMPTY**: `"X12_277_RFAI_BUILD_STATUS_CODE_EMPTY"` = `"X12_277_RFAI_BUILD_STATUS_CODE_EMPTY"` #### Example ```ts import { RFAI_277_BUILD_ERROR_CODES, Rfai277BuildError } from "@cosyte/x12"; try { // build277RequestForAdditionalInformation(spec); } catch (err) { if (err instanceof Rfai277BuildError && err.code === RFAI_277_BUILD_ERROR_CODES.X12_277_RFAI_BUILD_NO_TRACE) { // a claim-level request needs its TRN } } ``` *** ### RFAI\_277\_LOOP\_1000 > `const` **RFAI\_277\_LOOP\_1000**: [`LoopSpec`](#loopspec) 277 request for additional information, Loop 1000: a name in the heading. Triggered by `NM1`. The reader does not type this loop; its segments stay verbatim on the transaction set. #### Example ```ts import { RFAI_277_LOOP_1000 } from "@cosyte/x12"; RFAI_277_LOOP_1000.trigger; // "NM1" ``` *** ### RFAI\_277\_LOOP\_2000 > `const` **RFAI\_277\_LOOP\_2000**: [`LoopSpec`](#loopspec) 277 request for additional information, Loop 2000: hierarchical level. Triggered by `HL`; the level code is the sender's and is not checked here. #### Example ```ts import { RFAI_277_LOOP_2000 } from "@cosyte/x12"; RFAI_277_LOOP_2000.children.map((loop) => loop.id); // ["2100", "2200"] ``` *** ### RFAI\_277\_LOOP\_2100 > `const` **RFAI\_277\_LOOP\_2100**: [`LoopSpec`](#loopspec) 277 request for additional information, Loop 2100: a name under a hierarchical level. Triggered by `NM1`. #### Example ```ts import { RFAI_277_LOOP_2100 } from "@cosyte/x12"; RFAI_277_LOOP_2100.trigger; // "NM1" ``` *** ### RFAI\_277\_LOOP\_2200 > `const` **RFAI\_277\_LOOP\_2200**: [`LoopSpec`](#loopspec) 277 request for additional information, Loop 2200: claim-level request. Triggered by `TRN`, which the base standard makes mandatory in the loop. #### Example ```ts import { RFAI_277_LOOP_2200 } from "@cosyte/x12"; RFAI_277_LOOP_2200.trigger; // "TRN" ``` *** ### RFAI\_277\_LOOP\_2220 > `const` **RFAI\_277\_LOOP\_2220**: [`LoopSpec`](#loopspec) 277 request for additional information, Loop 2220: service line. Triggered by `SVC`. #### Example ```ts import { RFAI_277_LOOP_2220 } from "@cosyte/x12"; RFAI_277_LOOP_2220.trigger; // "SVC" ``` *** ### SERVICE\_TYPE\_CODES > `const` **SERVICE\_TYPE\_CODES**: [`CodeListSnapshot`](#codelistsnapshot) Bundled Service Type Code (EB-03) snapshot. Used by the 271 helper to surface a human-readable description alongside each verbatim service type code under an eligibility/benefit statement. #### Example ```ts import { SERVICE_TYPE_CODES } from "@cosyte/x12"; SERVICE_TYPE_CODES.codes["30"]; // "Health Benefit Plan Coverage" SERVICE_TYPE_CODES.codes["88"]; // "Pharmacy" SERVICE_TYPE_CODES.meta.redistribution?.status; // "licence-required" ``` *** ### STATUS\_276\_LOOP\_2000A > `const` **STATUS\_276\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 276 Loop 2000A - Information Source HL (the payer being asked). Triggered by `HL` (`HL-03 = "20"`); the level-code check happens in the walker. #### Example ```ts import { STATUS_276_LOOP_2000A } from "@cosyte/x12"; STATUS_276_LOOP_2000A.trigger; // "HL" ``` *** ### STATUS\_276\_LOOP\_2000B > `const` **STATUS\_276\_LOOP\_2000B**: [`LoopSpec`](#loopspec) 276 Loop 2000B - Information Receiver HL. Triggered by `HL` (`HL-03 = "21"`). #### Example ```ts import { STATUS_276_LOOP_2000B } from "@cosyte/x12"; STATUS_276_LOOP_2000B.id; // "2000B" ``` *** ### STATUS\_276\_LOOP\_2000C > `const` **STATUS\_276\_LOOP\_2000C**: [`LoopSpec`](#loopspec) 276 Loop 2000C - Service Provider HL, the provider whose claim is being asked about. Triggered by `HL` (`HL-03 = "19"`). #### Example ```ts import { STATUS_276_LOOP_2000C } from "@cosyte/x12"; STATUS_276_LOOP_2000C.id; // "2000C" ``` *** ### STATUS\_276\_LOOP\_2000D > `const` **STATUS\_276\_LOOP\_2000D**: [`LoopSpec`](#loopspec) 276 Loop 2000D - Subscriber HL. Triggered by `HL` (`HL-03 = "22"`). Carries the subscriber name loop, its demographics, and the claims asked about under it via [STATUS\_276\_LOOP\_2200](#status_276_loop_2200). #### Example ```ts import { STATUS_276_LOOP_2000D } from "@cosyte/x12"; STATUS_276_LOOP_2000D.children[0]?.id; // "2200" ``` *** ### STATUS\_276\_LOOP\_2000E > `const` **STATUS\_276\_LOOP\_2000E**: [`LoopSpec`](#loopspec) 276 Loop 2000E - Dependent HL. Triggered by `HL` (`HL-03 = "23"`). Carries the dependent's OWN name loop, demographics and claims; nothing it carries is merged onto the subscriber it hangs under. #### Example ```ts import { STATUS_276_LOOP_2000E } from "@cosyte/x12"; STATUS_276_LOOP_2000E.id; // "2000E" ``` *** ### STATUS\_276\_LOOP\_2200 > `const` **STATUS\_276\_LOOP\_2200**: [`LoopSpec`](#loopspec) 276 Loop 2200 - Claim Submitter Trace Number, the loop that carries ONE claim the submitter is asking about. Triggered by `TRN`, whose TRN-02 is the value the answering 277 echoes back. Nests [STATUS\_276\_LOOP\_2210](#status_276_loop_2210). #### Example ```ts import { STATUS_276_LOOP_2200 } from "@cosyte/x12"; STATUS_276_LOOP_2200.children[0]?.trigger; // "SVC" ``` *** ### STATUS\_276\_LOOP\_2210 > `const` **STATUS\_276\_LOOP\_2210**: [`LoopSpec`](#loopspec) 276 Loop 2210 - Service Line Information. Triggered by `SVC`. Reused under both the subscriber (2210D) and dependent (2210E) claim loops. #### Example ```ts import { STATUS_276_LOOP_2210 } from "@cosyte/x12"; STATUS_276_LOOP_2210.trigger; // "SVC" ``` *** ### STATUS\_277\_LOOP\_2000A > `const` **STATUS\_277\_LOOP\_2000A**: [`LoopSpec`](#loopspec) 277 Loop 2000A - Information Source HL (payer). Triggered by `HL` (`HL-03 = "20"`); the level-code check happens in the walker. #### Example ```ts import { STATUS_277_LOOP_2000A } from "@cosyte/x12"; STATUS_277_LOOP_2000A.trigger; // "HL" ``` *** ### STATUS\_277\_LOOP\_2000B > `const` **STATUS\_277\_LOOP\_2000B**: [`LoopSpec`](#loopspec) 277 Loop 2000B - Information Receiver HL. Triggered by `HL` (`HL-03 = "21"`). #### Example ```ts import { STATUS_277_LOOP_2000B } from "@cosyte/x12"; STATUS_277_LOOP_2000B.id; // "2000B" ``` *** ### STATUS\_277\_LOOP\_2000C > `const` **STATUS\_277\_LOOP\_2000C**: [`LoopSpec`](#loopspec) 277 Loop 2000C - Service Provider HL. Triggered by `HL` (`HL-03 = "19"`). In a 277CA this level can also carry a Loop 2200 batch acknowledgment (provider-level STC) - nested here as [STATUS\_277\_LOOP\_2200](#status_277_loop_2200). #### Example ```ts import { STATUS_277_LOOP_2000C } from "@cosyte/x12"; STATUS_277_LOOP_2000C.children[0]?.id; // "2200" ``` *** ### STATUS\_277\_LOOP\_2000D > `const` **STATUS\_277\_LOOP\_2000D**: [`LoopSpec`](#loopspec) 277 Loop 2000D - Subscriber HL. Triggered by `HL` (`HL-03 = "22"`). Carries the subscriber name (2100D) and its claim status tracking (Loop 2200). #### Example ```ts import { STATUS_277_LOOP_2000D } from "@cosyte/x12"; STATUS_277_LOOP_2000D.children[0]?.id; // "2200" ``` *** ### STATUS\_277\_LOOP\_2000E > `const` **STATUS\_277\_LOOP\_2000E**: [`LoopSpec`](#loopspec) 277 Loop 2000E - Dependent HL. Triggered by `HL` (`HL-03 = "23"`). Carries the dependent name (2100E) and its claim status tracking (Loop 2200). #### Example ```ts import { STATUS_277_LOOP_2000E } from "@cosyte/x12"; STATUS_277_LOOP_2000E.id; // "2000E" ``` *** ### STATUS\_277\_LOOP\_2200 > `const` **STATUS\_277\_LOOP\_2200**: [`LoopSpec`](#loopspec) 277 Loop 2200 - Claim Status Tracking. Triggered by `TRN` (a 277 claim status) - in a 277CA provider-level batch acknowledgment the same loop may open on a standalone `STC`; the walker handles both. Nests [STATUS\_277\_LOOP\_2220](#status_277_loop_2220). #### Example ```ts import { STATUS_277_LOOP_2200 } from "@cosyte/x12"; STATUS_277_LOOP_2200.children[0]?.trigger; // "SVC" ``` *** ### STATUS\_277\_LOOP\_2220 > `const` **STATUS\_277\_LOOP\_2220**: [`LoopSpec`](#loopspec) 277 Loop 2220 - Service Line Status Information. Triggered by `SVC`. Reused under both the subscriber (2220D) and dependent (2220E) claim status loops. #### Example ```ts import { STATUS_277_LOOP_2220 } from "@cosyte/x12"; STATUS_277_LOOP_2220.trigger; // "SVC" ``` *** ### TA1\_ACK\_CODES > `const` **TA1\_ACK\_CODES**: `object` TA1-04 interchange acknowledgment code (ASC X12 code list I13). Three values: `A` accepted, `E` accepted with errors, `R` rejected. Distinct from the 999 disposition (no `M`/`W`/`X` here - TA1 covers only the interchange envelope, not crypto failures of contained groups). #### Type Declaration ##### A > `readonly` **A**: `"A"` = `"A"` ##### E > `readonly` **E**: `"E"` = `"E"` ##### R > `readonly` **R**: `"R"` = `"R"` #### Example ```ts import type { Ta1AckCode } from "@cosyte/x12"; const accept: Ta1AckCode = "A"; ``` *** ### TA1\_NOTE\_CODES > `const` **TA1\_NOTE\_CODES**: `object` TA1-05 interchange note code (ASC X12 code list I18). Codes `000` to `028` are defined by the standard; values past `028` exist in some standard revisions and are accepted on parse (Postel's Law) but not enumerated here - the parsed model carries the un-narrowed string on `noteCodeRaw`, post-`?`-unescape as of a later release. **A clause here said "verbatim" and is deleted rather than reworded**; `raw.elements[5]` is the byte surface. `000` is the canonical "no error" note paired with a `TA1-04 == 'A'` acceptance. The build-time safety guard refuses a fabricated `A` paired with any non-`000` note (see "./errors.js".AckBuildError). Standard-issued values: - `000` - No error. - `001` - Interchange Control Number in the Header and Trailer do not match. - `002` - Standard as noted in the Control Standards Identifier is not supported. - `003` - Version of the controls is not supported. - `004` - Segment Terminator is invalid. - `005` - Invalid Interchange ID Qualifier for Sender. - `006` - Invalid Interchange Sender ID. - `007` - Invalid Interchange ID Qualifier for Receiver. - `008` - Invalid Interchange Receiver ID. - `009` - Unknown Interchange Receiver ID. - `010` - Invalid Authorization Information Qualifier value. - `011` - Invalid Authorization Information value. - `012` - Invalid Security Information Qualifier value. - `013` - Invalid Security Information value. - `014` - Invalid Interchange Date value. - `015` - Invalid Interchange Time value. - `016` - Invalid Interchange Standards Identifier value. - `017` - Invalid Interchange Version ID value. - `018` - Invalid Interchange Control Number value. - `019` - Invalid Acknowledgment Requested value. - `020` - Invalid Test Indicator value. - `021` - Invalid Number of Included Groups value. - `022` - Invalid Control Structure. - `023` - Improper (premature) end-of-file (transmission). - `024` - Invalid Interchange Content (e.g., invalid GS segment). - `025` - Duplicate Interchange Control Number. - `026` - Invalid Data Element Separator. - `027` - Invalid Component Element Separator. - `028` - Invalid Delivery Date in Deferred Delivery Request. #### Type Declaration ##### 000 > `readonly` **000**: `"000"` = `"000"` ##### 001 > `readonly` **001**: `"001"` = `"001"` ##### 002 > `readonly` **002**: `"002"` = `"002"` ##### 003 > `readonly` **003**: `"003"` = `"003"` ##### 004 > `readonly` **004**: `"004"` = `"004"` ##### 005 > `readonly` **005**: `"005"` = `"005"` ##### 006 > `readonly` **006**: `"006"` = `"006"` ##### 007 > `readonly` **007**: `"007"` = `"007"` ##### 008 > `readonly` **008**: `"008"` = `"008"` ##### 009 > `readonly` **009**: `"009"` = `"009"` ##### 010 > `readonly` **010**: `"010"` = `"010"` ##### 011 > `readonly` **011**: `"011"` = `"011"` ##### 012 > `readonly` **012**: `"012"` = `"012"` ##### 013 > `readonly` **013**: `"013"` = `"013"` ##### 014 > `readonly` **014**: `"014"` = `"014"` ##### 015 > `readonly` **015**: `"015"` = `"015"` ##### 016 > `readonly` **016**: `"016"` = `"016"` ##### 017 > `readonly` **017**: `"017"` = `"017"` ##### 018 > `readonly` **018**: `"018"` = `"018"` ##### 019 > `readonly` **019**: `"019"` = `"019"` ##### 020 > `readonly` **020**: `"020"` = `"020"` ##### 021 > `readonly` **021**: `"021"` = `"021"` ##### 022 > `readonly` **022**: `"022"` = `"022"` ##### 023 > `readonly` **023**: `"023"` = `"023"` ##### 024 > `readonly` **024**: `"024"` = `"024"` ##### 025 > `readonly` **025**: `"025"` = `"025"` ##### 026 > `readonly` **026**: `"026"` = `"026"` ##### 027 > `readonly` **027**: `"027"` = `"027"` ##### 028 > `readonly` **028**: `"028"` = `"028"` #### Example ```ts import { TA1_NOTE_CODES, type Ta1NoteCode } from "@cosyte/x12"; const noError: Ta1NoteCode = TA1_NOTE_CODES["000"]; ``` *** ### UNEXPECTED\_SEGMENT\_CONTEXTS > `const` **UNEXPECTED\_SEGMENT\_CONTEXTS**: `object` Which structural rule an out-of-place segment broke. A closed, library-owned set. The segment's own id is deliberately NOT part of it: a segment that is not where it belongs is exactly the case where its first element is arbitrary sender bytes rather than a spec name. #### Type Declaration ##### BODY\_OUTSIDE\_TRANSACTION > `readonly` **BODY\_OUTSIDE\_TRANSACTION**: `"body-outside-transaction"` = `"body-outside-transaction"` A body segment appeared outside any open transaction set. ##### GE\_WITHOUT\_GS > `readonly` **GE\_WITHOUT\_GS**: `"ge-without-gs"` = `"ge-without-gs"` A `GE` appeared with no open `GS`. ##### SE\_WITHOUT\_ST > `readonly` **SE\_WITHOUT\_ST**: `"se-without-st"` = `"se-without-st"` An `SE` appeared with no open transaction set. ##### ST\_WITHOUT\_GS > `readonly` **ST\_WITHOUT\_GS**: `"st-without-gs"` = `"st-without-gs"` An `ST` appeared with no open functional group. ##### TA1\_INSIDE\_GROUP > `readonly` **TA1\_INSIDE\_GROUP**: `"ta1-inside-group"` = `"ta1-inside-group"` A `TA1` (envelope-level by spec) appeared inside an open functional group. #### Example ```ts import { unexpectedSegment, UNEXPECTED_SEGMENT_CONTEXTS } from "@cosyte/x12"; const w = unexpectedSegment({ segmentIndex: 12 }, UNEXPECTED_SEGMENT_CONTEXTS.GE_WITHOUT_GS); ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.2"` Library version string, synced with `package.json#version` by the release's version step. Exported now so consumers (and the type-check pipeline) have at least one symbol to resolve through the `exports` map. #### Example ```ts import { VERSION } from "@cosyte/x12"; console.log(VERSION); ``` *** ### WARNING\_CODES > `const` **WARNING\_CODES**: `object` Stable string codes for every Tier-2 warning the parser may emit. The registry is frozen via `as const` so TypeScript infers the exact string literal union for [X12WarningCode](#x12warningcode) - zero runtime cost, no magic- string comparisons for consumers. #### Type Declaration ##### X12\_270\_DATE\_ROW\_DROPPED > `readonly` **X12\_270\_DATE\_ROW\_DROPPED**: `"X12_270_DATE_ROW_DROPPED"` = `"X12_270_DATE_ROW_DROPPED"` ##### X12\_270\_DUPLICATE\_HIERARCHY\_ID > `readonly` **X12\_270\_DUPLICATE\_HIERARCHY\_ID**: `"X12_270_DUPLICATE_HIERARCHY_ID"` = `"X12_270_DUPLICATE_HIERARCHY_ID"` ##### X12\_270\_HIERARCHY\_CYCLE > `readonly` **X12\_270\_HIERARCHY\_CYCLE**: `"X12_270_HIERARCHY_CYCLE"` = `"X12_270_HIERARCHY_CYCLE"` ##### X12\_270\_INTER\_SEGMENT\_LINE\_BREAK > `readonly` **X12\_270\_INTER\_SEGMENT\_LINE\_BREAK**: `"X12_270_INTER_SEGMENT_LINE_BREAK"` = `"X12_270_INTER_SEGMENT_LINE_BREAK"` ##### X12\_270\_LEVEL\_DETACHED > `readonly` **X12\_270\_LEVEL\_DETACHED**: `"X12_270_LEVEL_DETACHED"` = `"X12_270_LEVEL_DETACHED"` ##### X12\_270\_NON\_CONVENTIONAL\_DELIMITER > `readonly` **X12\_270\_NON\_CONVENTIONAL\_DELIMITER**: `"X12_270_NON_CONVENTIONAL_DELIMITER"` = `"X12_270_NON_CONVENTIONAL_DELIMITER"` ##### X12\_271\_AAA\_LOOP\_UNIDENTIFIED > `readonly` **X12\_271\_AAA\_LOOP\_UNIDENTIFIED**: `"X12_271_AAA_LOOP_UNIDENTIFIED"` = `"X12_271_AAA_LOOP_UNIDENTIFIED"` ##### X12\_271\_AAA\_REJECT\_REASON\_ABSENT > `readonly` **X12\_271\_AAA\_REJECT\_REASON\_ABSENT**: `"X12_271_AAA_REJECT_REASON_ABSENT"` = `"X12_271_AAA_REJECT_REASON_ABSENT"` ##### X12\_271\_AAA\_SEGMENT\_MALFORMED > `readonly` **X12\_271\_AAA\_SEGMENT\_MALFORMED**: `"X12_271_AAA_SEGMENT_MALFORMED"` = `"X12_271_AAA_SEGMENT_MALFORMED"` ##### X12\_271\_AAA\_UNKNOWN\_CODE > `readonly` **X12\_271\_AAA\_UNKNOWN\_CODE**: `"X12_271_AAA_UNKNOWN_CODE"` = `"X12_271_AAA_UNKNOWN_CODE"` ##### X12\_275\_ATTACHMENT\_ABSENT > `readonly` **X12\_275\_ATTACHMENT\_ABSENT**: `"X12_275_ATTACHMENT_ABSENT"` = `"X12_275_ATTACHMENT_ABSENT"` ##### X12\_276\_DATE\_ROW\_DROPPED > `readonly` **X12\_276\_DATE\_ROW\_DROPPED**: `"X12_276_DATE_ROW_DROPPED"` = `"X12_276_DATE_ROW_DROPPED"` ##### X12\_276\_DUPLICATE\_HIERARCHY\_ID > `readonly` **X12\_276\_DUPLICATE\_HIERARCHY\_ID**: `"X12_276_DUPLICATE_HIERARCHY_ID"` = `"X12_276_DUPLICATE_HIERARCHY_ID"` ##### X12\_276\_HIERARCHY\_CYCLE > `readonly` **X12\_276\_HIERARCHY\_CYCLE**: `"X12_276_HIERARCHY_CYCLE"` = `"X12_276_HIERARCHY_CYCLE"` ##### X12\_276\_LEVEL\_DETACHED > `readonly` **X12\_276\_LEVEL\_DETACHED**: `"X12_276_LEVEL_DETACHED"` = `"X12_276_LEVEL_DETACHED"` ##### X12\_276\_REFERENCE\_ROW\_DROPPED > `readonly` **X12\_276\_REFERENCE\_ROW\_DROPPED**: `"X12_276_REFERENCE_ROW_DROPPED"` = `"X12_276_REFERENCE_ROW_DROPPED"` ##### X12\_277\_RFAI\_HEADER\_ABSENT > `readonly` **X12\_277\_RFAI\_HEADER\_ABSENT**: `"X12_277_RFAI_HEADER_ABSENT"` = `"X12_277_RFAI_HEADER_ABSENT"` ##### X12\_277\_RFAI\_LEVEL\_ABSENT > `readonly` **X12\_277\_RFAI\_LEVEL\_ABSENT**: `"X12_277_RFAI_LEVEL_ABSENT"` = `"X12_277_RFAI_LEVEL_ABSENT"` ##### X12\_277\_RFAI\_REQUEST\_ABSENT > `readonly` **X12\_277\_RFAI\_REQUEST\_ABSENT**: `"X12_277_RFAI_REQUEST_ABSENT"` = `"X12_277_RFAI_REQUEST_ABSENT"` ##### X12\_834\_UNKNOWN\_MAINTENANCE\_TYPE > `readonly` **X12\_834\_UNKNOWN\_MAINTENANCE\_TYPE**: `"X12_834_UNKNOWN_MAINTENANCE_TYPE"` = `"X12_834_UNKNOWN_MAINTENANCE_TYPE"` ##### X12\_835\_BALANCE\_NOT\_EVALUABLE > `readonly` **X12\_835\_BALANCE\_NOT\_EVALUABLE**: `"X12_835_BALANCE_NOT_EVALUABLE"` = `"X12_835_BALANCE_NOT_EVALUABLE"` ##### X12\_835\_REMIT\_BALANCE\_MISMATCH > `readonly` **X12\_835\_REMIT\_BALANCE\_MISMATCH**: `"X12_835_REMIT_BALANCE_MISMATCH"` = `"X12_835_REMIT_BALANCE_MISMATCH"` ##### X12\_837\_AMBIGUOUS\_VARIANT > `readonly` **X12\_837\_AMBIGUOUS\_VARIANT**: `"X12_837_AMBIGUOUS_VARIANT"` = `"X12_837_AMBIGUOUS_VARIANT"` ##### X12\_837\_ENTITY\_SEGMENT\_DISCARDED\_AFTER\_LX > `readonly` **X12\_837\_ENTITY\_SEGMENT\_DISCARDED\_AFTER\_LX**: `"X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX"` = `"X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX"` ##### X12\_837\_PAY\_TO\_ADDRESS\_REPEATED > `readonly` **X12\_837\_PAY\_TO\_ADDRESS\_REPEATED**: `"X12_837_PAY_TO_ADDRESS_REPEATED"` = `"X12_837_PAY_TO_ADDRESS_REPEATED"` ##### X12\_837\_SERVICE\_LINE\_DROPPED > `readonly` **X12\_837\_SERVICE\_LINE\_DROPPED**: `"X12_837_SERVICE_LINE_DROPPED"` = `"X12_837_SERVICE_LINE_DROPPED"` ##### X12\_837\_SERVICE\_LINE\_NOT\_DECODED > `readonly` **X12\_837\_SERVICE\_LINE\_NOT\_DECODED**: `"X12_837_SERVICE_LINE_NOT_DECODED"` = `"X12_837_SERVICE_LINE_NOT_DECODED"` ##### X12\_837\_SERVICE\_SEGMENT\_REPEATED > `readonly` **X12\_837\_SERVICE\_SEGMENT\_REPEATED**: `"X12_837_SERVICE_SEGMENT_REPEATED"` = `"X12_837_SERVICE_SEGMENT_REPEATED"` ##### X12\_837\_SERVICE\_SEGMENT\_WITHOUT\_LX > `readonly` **X12\_837\_SERVICE\_SEGMENT\_WITHOUT\_LX**: `"X12_837_SERVICE_SEGMENT_WITHOUT_LX"` = `"X12_837_SERVICE_SEGMENT_WITHOUT_LX"` ##### X12\_837\_UNKNOWN\_VARIANT > `readonly` **X12\_837\_UNKNOWN\_VARIANT**: `"X12_837_UNKNOWN_VARIANT"` = `"X12_837_UNKNOWN_VARIANT"` ##### X12\_AMOUNT\_ROW\_DROPPED > `readonly` **X12\_AMOUNT\_ROW\_DROPPED**: `"X12_AMOUNT_ROW_DROPPED"` = `"X12_AMOUNT_ROW_DROPPED"` ##### X12\_BINARY\_DATA\_TRUNCATED > `readonly` **X12\_BINARY\_DATA\_TRUNCATED**: `"X12_BINARY_DATA_TRUNCATED"` = `"X12_BINARY_DATA_TRUNCATED"` ##### X12\_BINARY\_LENGTH\_INVALID > `readonly` **X12\_BINARY\_LENGTH\_INVALID**: `"X12_BINARY_LENGTH_INVALID"` = `"X12_BINARY_LENGTH_INVALID"` ##### X12\_BINARY\_LENGTH\_MISMATCH > `readonly` **X12\_BINARY\_LENGTH\_MISMATCH**: `"X12_BINARY_LENGTH_MISMATCH"` = `"X12_BINARY_LENGTH_MISMATCH"` ##### X12\_BINARY\_LENGTH\_UNVERIFIABLE > `readonly` **X12\_BINARY\_LENGTH\_UNVERIFIABLE**: `"X12_BINARY_LENGTH_UNVERIFIABLE"` = `"X12_BINARY_LENGTH_UNVERIFIABLE"` ##### X12\_CONTROL\_NUMBER\_MISMATCH > `readonly` **X12\_CONTROL\_NUMBER\_MISMATCH**: `"X12_CONTROL_NUMBER_MISMATCH"` = `"X12_CONTROL_NUMBER_MISMATCH"` ##### X12\_DANGLING\_RELEASE\_CHAR > `readonly` **X12\_DANGLING\_RELEASE\_CHAR**: `"X12_DANGLING_RELEASE_CHAR"` = `"X12_DANGLING_RELEASE_CHAR"` ##### X12\_GROUP\_COUNT\_MISMATCH > `readonly` **X12\_GROUP\_COUNT\_MISMATCH**: `"X12_GROUP_COUNT_MISMATCH"` = `"X12_GROUP_COUNT_MISMATCH"` ##### X12\_GUIDE\_NOT\_DECLARED > `readonly` **X12\_GUIDE\_NOT\_DECLARED**: `"X12_GUIDE_NOT_DECLARED"` = `"X12_GUIDE_NOT_DECLARED"` ##### X12\_GUIDE\_NOT\_IMPLEMENTED > `readonly` **X12\_GUIDE\_NOT\_IMPLEMENTED**: `"X12_GUIDE_NOT_IMPLEMENTED"` = `"X12_GUIDE_NOT_IMPLEMENTED"` ##### X12\_HL\_PARENT\_LEVEL\_INVALID > `readonly` **X12\_HL\_PARENT\_LEVEL\_INVALID**: `"X12_HL_PARENT_LEVEL_INVALID"` = `"X12_HL_PARENT_LEVEL_INVALID"` ##### X12\_HL\_PARENT\_MISMATCH > `readonly` **X12\_HL\_PARENT\_MISMATCH**: `"X12_HL_PARENT_MISMATCH"` = `"X12_HL_PARENT_MISMATCH"` ##### X12\_ISA\_EXTRA\_ELEMENT\_SEPARATOR > `readonly` **X12\_ISA\_EXTRA\_ELEMENT\_SEPARATOR**: `"X12_ISA_EXTRA_ELEMENT_SEPARATOR"` = `"X12_ISA_EXTRA_ELEMENT_SEPARATOR"` ##### X12\_MISSING\_GE > `readonly` **X12\_MISSING\_GE**: `"X12_MISSING_GE"` = `"X12_MISSING_GE"` ##### X12\_MISSING\_IEA > `readonly` **X12\_MISSING\_IEA**: `"X12_MISSING_IEA"` = `"X12_MISSING_IEA"` ##### X12\_MISSING\_REQUIRED\_LOOP > `readonly` **X12\_MISSING\_REQUIRED\_LOOP**: `"X12_MISSING_REQUIRED_LOOP"` = `"X12_MISSING_REQUIRED_LOOP"` ##### X12\_MISSING\_SE > `readonly` **X12\_MISSING\_SE**: `"X12_MISSING_SE"` = `"X12_MISSING_SE"` ##### X12\_PRE\_005010 > `readonly` **X12\_PRE\_005010**: `"X12_PRE_005010"` = `"X12_PRE_005010"` ##### X12\_SEGMENT\_COUNT\_MISMATCH > `readonly` **X12\_SEGMENT\_COUNT\_MISMATCH**: `"X12_SEGMENT_COUNT_MISMATCH"` = `"X12_SEGMENT_COUNT_MISMATCH"` ##### X12\_STATED\_AMOUNT\_DISCARDED > `readonly` **X12\_STATED\_AMOUNT\_DISCARDED**: `"X12_STATED_AMOUNT_DISCARDED"` = `"X12_STATED_AMOUNT_DISCARDED"` ##### X12\_TRAILING\_GARBAGE > `readonly` **X12\_TRAILING\_GARBAGE**: `"X12_TRAILING_GARBAGE"` = `"X12_TRAILING_GARBAGE"` ##### X12\_TRANSACTION\_COUNT\_MISMATCH > `readonly` **X12\_TRANSACTION\_COUNT\_MISMATCH**: `"X12_TRANSACTION_COUNT_MISMATCH"` = `"X12_TRANSACTION_COUNT_MISMATCH"` ##### X12\_UNEXPECTED\_SEGMENT > `readonly` **X12\_UNEXPECTED\_SEGMENT**: `"X12_UNEXPECTED_SEGMENT"` = `"X12_UNEXPECTED_SEGMENT"` ##### X12\_UNKNOWN\_CARC > `readonly` **X12\_UNKNOWN\_CARC**: `"X12_UNKNOWN_CARC"` = `"X12_UNKNOWN_CARC"` ##### X12\_UNKNOWN\_CLAIM\_STATUS > `readonly` **X12\_UNKNOWN\_CLAIM\_STATUS**: `"X12_UNKNOWN_CLAIM_STATUS"` = `"X12_UNKNOWN_CLAIM_STATUS"` ##### X12\_UNKNOWN\_CLAIM\_STATUS\_CATEGORY > `readonly` **X12\_UNKNOWN\_CLAIM\_STATUS\_CATEGORY**: `"X12_UNKNOWN_CLAIM_STATUS_CATEGORY"` = `"X12_UNKNOWN_CLAIM_STATUS_CATEGORY"` ##### X12\_UNKNOWN\_HI\_QUALIFIER > `readonly` **X12\_UNKNOWN\_HI\_QUALIFIER**: `"X12_UNKNOWN_HI_QUALIFIER"` = `"X12_UNKNOWN_HI_QUALIFIER"` ##### X12\_UNKNOWN\_RARC > `readonly` **X12\_UNKNOWN\_RARC**: `"X12_UNKNOWN_RARC"` = `"X12_UNKNOWN_RARC"` ##### X12\_UNPARSEABLE\_DECIMAL > `readonly` **X12\_UNPARSEABLE\_DECIMAL**: `"X12_UNPARSEABLE_DECIMAL"` = `"X12_UNPARSEABLE_DECIMAL"` #### Example ```ts import { parseX12, WARNING_CODES } from "@cosyte/x12"; const ix = parseX12(raw); if (ix.warnings.some((w) => w.code === WARNING_CODES.X12_PRE_005010)) { // sender is on a pre-005010 version family } ``` *** ### X12\_ACK\_DISPOSITION\_CODES > `const` **X12\_ACK\_DISPOSITION\_CODES**: `object` AK9-01 (functional group) + IK5-01 (transaction set) disposition codes. Sourced from ASC X12 code list 715. The implementation acknowledges the inbound functional group / transaction set with one of these dispositions; the LIBRARY does not decide accept-vs-reject - the application does. `build999` / `buildTA1` MECHANICALLY build the cited disposition; a fabricated `A` against a non-empty error list is a bug and refused at build time (see "./errors.js".AckBuildError). - `A` - Accepted. - `E` - Accepted, but errors were noted. - `P` - Partially accepted: at least one transaction set was rejected while the functional group as a whole was accepted. - `R` - Rejected. - `M` - Rejected: message authentication code (MAC) failed. - `W` - Rejected: assurance failed validity tests. - `X` - Rejected: content after decryption could not be analyzed. #### Type Declaration ##### A > `readonly` **A**: `"A"` = `"A"` ##### E > `readonly` **E**: `"E"` = `"E"` ##### M > `readonly` **M**: `"M"` = `"M"` ##### P > `readonly` **P**: `"P"` = `"P"` ##### R > `readonly` **R**: `"R"` = `"R"` ##### W > `readonly` **W**: `"W"` = `"W"` ##### X > `readonly` **X**: `"X"` = `"X"` #### Example ```ts import type { X12AckDispositionCode } from "@cosyte/x12"; const accept: X12AckDispositionCode = "A"; ``` *** ### X12\_BUILD\_ERROR\_CODES > `const` **X12\_BUILD\_ERROR\_CODES**: `object` Stable string codes for every [X12BuildError](#x12builderror). Locked here so consumers can narrow on `err.code` exhaustively; additions-only thereafter (renaming any code is a breaking change). - `X12_BUILD_INVALID_SPEC` - a spec field violated a structural constraint the builder cannot recover from (an ISA-13 / IEA-02 control number longer than the 9-char fixed width, a segment spec with no segment id, etc.). #### Type Declaration ##### X12\_BUILD\_INVALID\_SPEC > `readonly` **X12\_BUILD\_INVALID\_SPEC**: `"X12_BUILD_INVALID_SPEC"` = `"X12_BUILD_INVALID_SPEC"` #### Example ```ts import { X12_BUILD_ERROR_CODES, X12BuildError } from "@cosyte/x12"; try { buildInterchange(spec); } catch (err) { if (err instanceof X12BuildError && err.code === X12_BUILD_ERROR_CODES.X12_BUILD_INVALID_SPEC) { // application bug - the envelope spec is structurally impossible } } ``` *** ### X12\_CODE\_LIST\_ERROR\_CODES > `const` **X12\_CODE\_LIST\_ERROR\_CODES**: `object` Stable string codes for every [X12CodeListError](#x12codelisterror). Locked here so consumers can narrow on `err.code` exhaustively; additions-only thereafter (renaming any code is a breaking change). - `X12_CODE_LIST_INVALID_DOCUMENT_DATE` - the document date supplied to a date-aware code-list query was not a calendar day in `YYYY-MM-DD` or `CCYYMMDD` form. #### Type Declaration ##### X12\_CODE\_LIST\_INVALID\_DOCUMENT\_DATE > `readonly` **X12\_CODE\_LIST\_INVALID\_DOCUMENT\_DATE**: `"X12_CODE_LIST_INVALID_DOCUMENT_DATE"` = `"X12_CODE_LIST_INVALID_DOCUMENT_DATE"` #### Example ```ts import { X12_CODE_LIST_ERROR_CODES, X12CodeListError, checkCarcValidity } from "@cosyte/x12"; try { checkCarcValidity("1", "2026-6-27"); } catch (err) { if ( err instanceof X12CodeListError && err.code === X12_CODE_LIST_ERROR_CODES.X12_CODE_LIST_INVALID_DOCUMENT_DATE ) { // application bug - the date was never a calendar day } } ``` *** ### X12\_TR3\_CONFORMANCE > `const` **X12\_TR3\_CONFORMANCE**: readonly [`X12Tr3Conformance`](#x12tr3conformance)[] Which implementation guide this package implements for each transaction it reads or builds, and what 45 CFR 162.920 names for that transaction. Frozen at every level: the list, each row, and each row's arrays. Assign to any of them and the value a later reader sees is unchanged. The 005010 `cfrAdopted` values are read from the official Government Publishing Office XML of 45 CFR 162.920, title 45 volume 2, 2024 annual edition, retrieved 2026-08-25. Thirteen 005010 identifiers appear in that section and no others: 005010X212, 005010X212E1, 005010X217, 005010X217E1, 005010X218, 005010X220, 005010X221, 005010X222, 005010X223, 005010X223A1, 005010X224, 005010X224A1 and 005010X279. The 006020 `cfrAdopted` values are read from the current eCFR text of the same section, as amended by 91 FR 14404, which adds two paragraphs and two identifiers: (a)(19) names 006020X314 for the 275 and (a)(20) names 006020X313 for the 277 request for additional information. 45 CFR 162.2002 adopts both for the period on and after May 26, 2028. No other 006020 identifier appears in the section. #### Example ```ts import { X12_TR3_CONFORMANCE } from "@cosyte/x12"; const remit = X12_TR3_CONFORMANCE.find((row) => row.transaction === "835"); remit?.tr3; // "005010X221A1" remit?.cfrAdopted; // ["005010X221"] remit?.directions; // ["read", "build"] ``` ## Functions ### aaaLoopUnidentified() > **aaaLoopUnidentified**(`position`, `level`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_271_AAA_LOOP_UNIDENTIFIED` warning. Raised where an AAA cannot be attributed to an identified hierarchical loop: the enclosing level states no HL-01, or no level of a kind this surface names encloses the segment. The AAA is never dropped for it, and no identifier and no level is synthesised. Every part of the key that could be determined is still carried, and the occurrence index is always one of them. #### Parameters ##### position [`X12Position`](#x12position) ##### level [`X12AaaLevelContext`](#x12aaalevelcontext) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { aaaLoopUnidentified, AAA_LEVEL_CONTEXTS } from "@cosyte/x12"; const w = aaaLoopUnidentified({ segmentIndex: 4 }, AAA_LEVEL_CONTEXTS.UNATTACHED); ``` *** ### aaaProvenanceIsComplete() > **aaaProvenanceIsComplete**(`meta`): `boolean` True only when all FOUR provenance parts are established AND the terms as recorded permit bundling. Exported so a consumer can apply the same bar to a snapshot rather than re-deriving it. #### Parameters ##### meta [`AaaCodeListMeta`](#aaacodelistmeta) #### Returns `boolean` #### Example ```ts import { AAA_REJECT_REASON_CODES, aaaProvenanceIsComplete } from "@cosyte/x12"; aaaProvenanceIsComplete(AAA_REJECT_REASON_CODES.meta); // false ``` *** ### aaaRejectReasonAbsent() > **aaaRejectReasonAbsent**(`position`, `level`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_271_AAA_REJECT_REASON_ABSENT` warning. Raised where a 271 AAA request-validation segment states no reject reason code, either because the element is absent or because it is present and empty. The AAA is still surfaced on the typed result with that code ABSENT: this reports a code the payer did not state, never a code this reader replaced. Its counterpart is [aaaUnknownCode](#aaaunknowncode), and the two are deliberately different codes so a consumer can tell "no reason given" from "a reason given that this package cannot describe". #### Parameters ##### position [`X12Position`](#x12position) ##### level [`X12AaaLevelContext`](#x12aaalevelcontext) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { aaaRejectReasonAbsent, AAA_LEVEL_CONTEXTS } from "@cosyte/x12"; const w = aaaRejectReasonAbsent( { segmentIndex: 7, transactionIndex: 0, elementIndex: 3 }, AAA_LEVEL_CONTEXTS.SUBSCRIBER, ); ``` *** ### aaaSegmentMalformed() > **aaaSegmentMalformed**(`position`, `level`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_271_AAA_SEGMENT_MALFORMED` warning. Raised where an AAA element sits PAST the highest element position this reader has a source for, or where the follow-up action code element is present and empty. It reports that a position is occupied and asserts NOTHING about what occupies it. Reading a meaning into an element whose layout is not established is the failure this code exists to avoid, so the reader warns about the position and moves on. #### Parameters ##### position [`X12Position`](#x12position) ##### level [`X12AaaLevelContext`](#x12aaalevelcontext) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { aaaSegmentMalformed, AAA_LEVEL_CONTEXTS } from "@cosyte/x12"; const w = aaaSegmentMalformed( { segmentIndex: 7, transactionIndex: 0, elementIndex: 5 }, AAA_LEVEL_CONTEXTS.INFORMATION_SOURCE, ); ``` *** ### aaaUnknownCode() > **aaaUnknownCode**(`position`, `level`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_271_AAA_UNKNOWN_CODE` warning. Raised once per stated AAA code occurrence that resolves to no description in the bundled snapshot, whether it is the reject reason code or the follow-up action code. The bundled AAA snapshots ship empty, so today every stated AAA code raises this. That is the intended consequence of shipping no descriptions rather than descriptions whose redistribution terms nobody recorded. #### Parameters ##### position [`X12Position`](#x12position) ##### level [`X12AaaLevelContext`](#x12aaalevelcontext) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { aaaUnknownCode, AAA_LEVEL_CONTEXTS } from "@cosyte/x12"; const w = aaaUnknownCode( { segmentIndex: 7, transactionIndex: 0, elementIndex: 3 }, AAA_LEVEL_CONTEXTS.DEPENDENT, ); ``` *** ### ambiguous837Variant() > **ambiguous837Variant**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_AMBIGUOUS_VARIANT` warning. Emitted by the 837 helper when the SVx fall-back is what resolved the variant AND the transaction body carries service segments naming more than one variant, so the resolution is a guess between contradictory evidence. The submission still ships with the resolved variant on `submission.variant`; nothing about how any claim or line decodes is changed by this warning. It reports the RESOLUTION, not the document. A caller-supplied `opts.type` wins ahead of the fall-back, and so does an ST-03 naming one of the three known implementation-convention references, and in either case no guess was made and this code is not raised however mixed the body is. Which service segment is the stray one is deliberately not decided: this reader cannot tell a stray service segment from a conformant one, and the fall-back takes the first in the body whether or not a Loop 2400 was open at it. Distinct from [unknown837Variant](#unknown837variant), which is raised where NOTHING resolved a variant; the two can never travel together, because a body with conflicting service segments has at least one to fall back on. `get837Claims` anchors this at the **ST**, which is `tx.segments[0]` and carries the ST-03 that would have settled the question. No `elementIndex` is set: the conflict is a property of the body rather than of an element, and one route into it is an ST-03 that is absent altogether. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { ambiguous837Variant } from "@cosyte/x12"; const w = ambiguous837Variant({ segmentIndex: 0, transactionIndex: 0 }); ``` *** ### amountRowDropped() > **amountRowDropped**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_AMOUNT_ROW_DROPPED` warning. Emitted where an `AMT` or `ADX` decoded no value from its amount element (AMT-02, ADX-01) and the reader therefore built no row at all, so the qualifier or adjustment reason code the sender did state is off the model with it. `position` names the `AMT` / `ADX` segment itself and carries NO `elementIndex`: one of the two routes here is an absent element, and an absent element has no index to name. The element the reader was reading is fixed by the segment anyway - AMT-02 or ADX-01 - so the segment locates the loss exactly. It is raised for BOTH routes to an undecoded amount, and [unparseableDecimal](#unparseabledecimal) is unchanged by it: a present-but-undecodable element still raises `X12_UNPARSEABLE_DECIMAL` at its own `elementIndex`, now ALONGSIDE this code rather than instead of it, so a consumer predicate written against that code alone still fires exactly where it did. The absent route raises this code and nothing else, which is what tells the two apart. Read the bound literally. This reports a row whose AMOUNT was read and decoded no value. A segment a reader discards before reading its amount is not on this channel, and neither is one whose amount decoded and then found nothing open to attach the row to. An 820 `RMR` is not on it either, for its own reason: `decodeRmr` drops on open-item IDENTITY, RMR-01 and RMR-02 both empty, before the amount is read - so an `RMR` that states an open item and no amount keeps its row with `amountPaid` `undefined`, while one that states an amount and no open item is dropped whole. That second case is a separate loss, as is an 837 `AMT` that decoded while a Loop 2430 adjudication was open; [statedAmountDiscarded](#statedamountdiscarded) reports both and this code reports neither. Nothing is fabricated to stand in and the segments stay verbatim on the transaction set. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { amountRowDropped } from "@cosyte/x12"; const w = amountRowDropped({ segmentIndex: 7, transactionIndex: 0 }); ``` *** ### attachmentAbsent() > **attachmentAbsent**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_275_ATTACHMENT_ABSENT` warning. Raised by `get275Attachments` when the transaction set carries no BDS segment, so the reading's `attachments` list is empty. The reader anchors it at the ST (`segmentIndex: 0`). It takes a position and nothing else, so no byte of the document reaches the message. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { attachmentAbsent } from "@cosyte/x12"; const w = attachmentAbsent({ segmentIndex: 0, transactionIndex: 0 }); w.code; // "X12_275_ATTACHMENT_ABSENT" ``` *** ### balanceNotEvaluable() > **balanceNotEvaluable**(`position`, `invariant`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_835_BALANCE_NOT_EVALUABLE` warning. Emitted where a term of a TR3 X221A1 §1.10.2 invariant is `undefined` on the model, so the equation has nothing to compare on one side. `invariant` names which equation could not be run. This is deliberately a DIFFERENT code from [remitBalanceMismatch](#remitbalancemismatch): that one asserts a computed inequality between amounts the sender supplied, and this one asserts only that the comparison could not be made. Reading an absent term as `0` is what made the two indistinguishable before `X12Decimal | undefined`, and it is the thing this code exists to stop. #### Parameters ##### position [`X12Position`](#x12position) ##### invariant [`X12BalanceInvariant`](#x12balanceinvariant) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { balanceNotEvaluable, BALANCE_INVARIANTS } from "@cosyte/x12"; const w = balanceNotEvaluable( { segmentIndex: 12, groupIndex: 0, transactionIndex: 0 }, BALANCE_INVARIANTS.CLAIM, ); ``` *** ### binaryDataTruncated() > **binaryDataTruncated**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_BINARY_DATA_TRUNCATED` warning. Raised where the input ends before a BDS or BIN segment's binary data reaches the octet count its length element declares. The data element holds only the octets that are present, so nothing past the end of the input is read, and the declared count is left as the sender wrote it. The parser anchors it at the BDS or BIN segment, with `elementIndex` on the data element (BDS-03 or BIN-02). It takes a position and nothing else, so no byte of the length or the data reaches the message. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { binaryDataTruncated } from "@cosyte/x12"; const w = binaryDataTruncated({ segmentIndex: 3, transactionIndex: 0, elementIndex: 2 }); w.code; // "X12_BINARY_DATA_TRUNCATED" ``` *** ### binaryLengthInvalid() > **binaryLengthInvalid**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_BINARY_LENGTH_INVALID` warning. Raised where a BDS or BIN segment's length element (BDS-02 or BIN-01) is absent, empty, or not a non-negative integer written as 1 to 15 ASCII digits, so a sign, a decimal point or surrounding whitespace is refused and leading zeros are not. No count is inferred: the segment is framed by its delimiters, exactly as a segment of any other id is. The parser anchors it at the BDS or BIN segment, with `elementIndex` on the length element. It takes a position and nothing else. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { binaryLengthInvalid } from "@cosyte/x12"; const w = binaryLengthInvalid({ segmentIndex: 3, transactionIndex: 0, elementIndex: 1 }); w.code; // "X12_BINARY_LENGTH_INVALID" ``` *** ### binaryLengthMismatch() > **binaryLengthMismatch**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_BINARY_LENGTH_MISMATCH` warning. Raised where the byte after a BDS or BIN segment's declared span is not the segment terminator, which covers a declared length shorter than the data and one longer than it, and where the segment ends before a data element begins although a non-zero length was declared. The data element holds exactly the declared span; the bytes after it stay on the segment's raw text, so nothing the sender transmitted is lost, and the length element is never rewritten. The parser anchors it at the BDS or BIN segment, with `elementIndex` on the data element. It takes a position and nothing else. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { binaryLengthMismatch } from "@cosyte/x12"; const w = binaryLengthMismatch({ segmentIndex: 3, transactionIndex: 0, elementIndex: 3 }); w.code; // "X12_BINARY_LENGTH_MISMATCH" ``` *** ### binaryLengthUnverifiable() > **binaryLengthUnverifiable**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_BINARY_LENGTH_UNVERIFIABLE` warning. Raised where `parseX12` was handed a string and a BDS or BIN segment's declared span holds a UTF-16 code unit above U+00FF. Every code unit at or below U+00FF is the latin1 image of one octet, so the count is applied one code unit per octet; above that the octet count the sender meant cannot be known from a string, and this reports it rather than guessing. The span's characters are still carried verbatim. A `Buffer` input never raises it. The parser anchors it at the BDS or BIN segment, with `elementIndex` on the data element. It takes a position and nothing else. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { binaryLengthUnverifiable } from "@cosyte/x12"; const w = binaryLengthUnverifiable({ segmentIndex: 3, transactionIndex: 0, elementIndex: 2 }); w.code; // "X12_BINARY_LENGTH_UNVERIFIABLE" ``` *** ### build270() > **build270**(`spec`): [`X12Interchange`](#x12interchange) `build270` - assemble a 005010X279A1 270 around the supplied spec. Refused via "./build-270-errors.js".Eligibility270BuildError: - No information sources, a source with no receivers, a receiver with no subscribers, or a SPINE list slot (`informationSources`, `receivers`, `subscribers`, `dependents`, `inquiries`, `serviceTypeCodes`) handed something that is not a list or left with an empty slot in it, gives `X12_270_BUILD_INVALID_HIERARCHY`. - A level with no name loop, at ANY of the four levels, a subscriber with neither an inquiry nor a dependent, a dependent with no inquiry, an inquiry that asks nothing, an empty or over-long control number, a non-string element value, or a LEAF list slot (`traces`, `references`, `dates`, `address.lines`, `diagnosisCodePointers`, `procedure.modifiers`) handed something that is not a list or left with an empty slot in it, gives `X12_270_BUILD_INVALID_SPEC`. EVERY list slot on the spec is read through the package's array chokepoint and checked for holes at the same point, so a forged array-like, and equally a real list with a gap in it, draws one of those two typed refusals wherever it stands. That is deliberately stricter than the sibling builders, whose leaf lists throw an untyped `TypeError` a consumer cannot branch on. Every refusal message names structural indices and counts. None of them names a member identifier, a member name, a patient name, a trace value or a diagnosis code, and the one caller value any of them renders (a control number) goes through the package's bounded renderer. #### Parameters ##### spec [`Build270Spec`](#build270spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build270 } from "@cosyte/x12"; const ix = build270({ envelope: { senderId: "ANYTOWNCLINIC", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ name: { entityIdentifierCode: "PR", entityTypeQualifier: "2", lastNameOrOrganizationName: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "PAYER01" }, receivers: [{ name: { entityIdentifierCode: "1P", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ traces: [{ traceTypeCode: "1", referenceId: "ELIG0001" }], name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, inquiries: [{ serviceTypeCodes: [{ code: "30" }] }], }], }], }], }); ``` *** ### build271() > **build271**(`spec`): [`X12Interchange`](#x12interchange) `build271` - assemble a 005010X279A1 271 around the supplied spec. Refused via "./build-errors.js".Eligibility271BuildError: - No information sources, a source with no receivers, or a receiver with no subscribers → `X12_271_BUILD_INVALID_HIERARCHY`. - An over-long (>9 char) interchange control number → `X12_271_BUILD_INVALID_SPEC`. #### Parameters ##### spec [`Build271Spec`](#build271spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build271, X12Decimal } from "@cosyte/x12"; const ix = build271({ envelope: { senderId: "MEDPAY", receiverId: "PROVIDER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ entity: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123" }, receivers: [{ entity: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ traces: [{ traceTypeCode: "2", referenceId: "ELIG001" }], name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, benefits: [{ eligibilityCode: "1", coverageLevelCode: "IND", serviceTypeCodes: [{ code: "30" }], monetaryAmount: X12Decimal.fromString("1000.00")! }], }], }], }], }); ``` *** ### build275() > **build275**(`spec`): [`X12Interchange`](#x12interchange) Build a 275 declaring `006020X314`. #### Parameters ##### spec [`Build275Spec`](#build275spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build275, serializeX12 } from "@cosyte/x12"; const ix = build275({ envelope: { senderId: "CLINIC", receiverId: "PAYER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, lines: [{ attachments: [{ filterCode: "B64", data: "U1lOVEhFVElD" }] }], }); const octets = Buffer.from(serializeX12(ix), "latin1"); // BDS*B64*12*U1lOVEhFVElD ``` *** ### build276() > **build276**(`spec`): [`X12Interchange`](#x12interchange) `build276` - assemble a 005010X212 276 Health Care Claim Status Request around the supplied spec. Refused via "./build-276-errors.js".ClaimStatus276BuildError: - No information sources, a source with no receivers, a receiver with no service providers, a provider with no subscribers, or a SPINE list slot (`informationSources`, `receivers`, `providers`, `subscribers`, `dependents`, `claims`, `serviceLines`) handed something that is not a list or left with an empty slot in it, gives `X12_276_BUILD_INVALID_HIERARCHY`. - A level with no name loop, at ANY of the five levels, a subscriber that asks about no claim and carries no dependent that does, a dependent with no claim, a claim carrying nothing a payer could look it up by, a service line identifying no service, an empty or over-long control number, a non-string element value, a non-`X12Decimal` amount, or a LEAF list slot (`references`, `amounts`, `dates`, `procedure.modifiers`) handed something that is not a list or left with an empty slot in it, gives `X12_276_BUILD_INVALID_SPEC`. EVERY list slot on the spec is read through the package's array chokepoint and checked for holes at the same point, so a forged array-like, and equally a real list with a gap in it, draws one of those two typed refusals wherever it stands. Every refusal message names structural indices and counts. None of them names a member identifier, a member name, a patient name, a trace value, a claim number or a diagnosis code, and the one caller value any of them renders (a control number) goes through the package's bounded renderer. #### Parameters ##### spec [`Build276Spec`](#build276spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build276 } from "@cosyte/x12"; const ix = build276({ envelope: { senderId: "ANYTOWNCLINIC", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ name: { entityIdentifierCode: "PR", entityTypeQualifier: "2", lastNameOrOrganizationName: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "PAYER01" }, receivers: [{ name: { entityIdentifierCode: "41", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC", idQualifier: "46", idCode: "RECVR01" }, providers: [{ name: { entityIdentifierCode: "1P", entityTypeQualifier: "2", lastNameOrOrganizationName: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ name: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastNameOrOrganizationName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, claims: [{ trace: { traceTypeCode: "1", referenceId: "STATUS0001" }, references: [{ qualifier: "1K", value: "PCN0001" }], }], }], }], }], }], }); ``` *** ### build277() > **build277**(`spec`): [`X12Interchange`](#x12interchange) `build277` - assemble a 005010X212 277 Claim Status Response around the supplied spec. #### Parameters ##### spec [`Build277Spec`](#build277spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build277, X12Decimal } from "@cosyte/x12"; const ix = build277({ envelope: { senderId: "MEDPAY", receiverId: "PROVIDER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, informationSources: [{ entity: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "MEDPAY INSURANCE", idQualifier: "PI", idCode: "00123" }, receivers: [{ entity: { entityIdentifierCode: "41", entityTypeQualifier: "2", name: "CLEARINGHOUSE", idQualifier: "46", idCode: "CH001" }, providers: [{ entity: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "ANYTOWN CLINIC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ member: { entityIdentifierCode: "QC", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE" }, claims: [{ trace: { traceTypeCode: "2", referenceId: "CLAIM001" }, statuses: [{ statuses: [{ categoryCode: "A2", statusCode: "20" }] }] }], }], }], }], }], }); ``` *** ### build277CA() > **build277CA**(`spec`): [`X12Interchange`](#x12interchange) `build277CA` - assemble a 005010X214 277CA Claim Acknowledgment around the supplied spec. Identical body to [build277](#build277); only the ST-03 / GS-08 version differs (so the parsed result is admitted by "./get-277.js".get277CADisposition and carries `transactionType: "claim-acknowledgment"`). #### Parameters ##### spec [`Build277Spec`](#build277spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build277CA } from "@cosyte/x12"; declare const spec: import("@cosyte/x12").Build277Spec; const ix = build277CA(spec); // ST-03 = 005010X214 ``` *** ### build277RequestForAdditionalInformation() > **build277RequestForAdditionalInformation**(`spec`): [`X12Interchange`](#x12interchange) Build a 277 request for additional information declaring `006020X313`. #### Parameters ##### spec [`Build277RfaiSpec`](#build277rfaispec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build277RequestForAdditionalInformation, serializeX12 } from "@cosyte/x12"; const ix = build277RequestForAdditionalInformation({ envelope: { senderId: "PAYER", receiverId: "CLINIC", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, header: { hierarchicalStructureCode: "0010", transactionSetPurposeCode: "08" }, levels: [{ levelCode: "20", requests: [{ trace: { traceTypeCode: "1", referenceId: "TRACE-0001" }, statuses: [{ codes: [{ categoryCode: "R4", statusCode: "18842-5", codeListQualifier: "LOI" }] }], }], }], }); serializeX12(ix); // an interchange whose GS-08 and ST-03 read 006020X313 ``` *** ### build278Request() > **build278Request**(`spec`): [`X12Interchange`](#x12interchange) `build278Request` - assemble a 005010X217 278 Request for Review around the supplied spec. Refuses any review carrying an HCR `decision` (HCR is response-only), and any review whose HL-03 `levelCode` is outside `EV` / `SS`. #### Parameters ##### spec [`Build278Spec`](#build278spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build278Request } from "@cosyte/x12"; const ix = build278Request({ envelope: { senderId: "SUBMITTER", receiverId: "UMOPAYER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, header: { structurePurposeCode: "0078", purposeCode: "13", referenceId: "AUTHREQ-202606" }, utilizationManagementOrganization: { entityIdentifierCode: "X3", entityTypeQualifier: "2", name: "UTILIZATION REVIEW CO", idQualifier: "PI", idCode: "UMO001" }, requester: { entityIdentifierCode: "1P", entityTypeQualifier: "2", name: "RENDERING CLINIC", idQualifier: "XX", idCode: "1234567893" }, subscriber: { member: { entityIdentifierCode: "IL", entityTypeQualifier: "1", lastName: "DOE", firstName: "JANE", idQualifier: "MI", idCode: "MBR0001" }, reviews: [{ requestCategoryCode: "HS", certificationTypeCode: "I", serviceTypeCode: "1" }], }, }); ``` *** ### build278Response() > **build278Response**(`spec`): [`X12Interchange`](#x12interchange) `build278Response` - assemble a 005010X217 278 Response around the supplied spec, declaring that guide in GS-08 and ST-03 exactly as [build278Request](#build278request) does. The HCR `actionCode` on each review's `decision` is emitted VERBATIM (never inferred), so the response round-trips the exact certification outcome through "./get-278.js".get278Response. A review whose HL-03 `levelCode` is outside `EV` / `SS` is REFUSED, because that round trip is what an out-of-enum level breaks: the emitted review loop is one no reader opens, so the decision fails to decode. #### Parameters ##### spec [`Build278Spec`](#build278spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build278Response } from "@cosyte/x12"; declare const base: import("@cosyte/x12").Build278Spec; const ix = build278Response({ ...base, subscriber: { ...base.subscriber, reviews: [{ requestCategoryCode: "HS", certificationTypeCode: "I", decision: { actionCode: "A1", reviewIdentificationNumber: "AUTH123456" } }], }, }); // GS-08 = ST-03 = 005010X217, HCR*A1*AUTH123456 ``` *** ### build820() > **build820**(`spec`): [`X12Interchange`](#x12interchange) `build820` - assemble a 005010X218 820 around the supplied spec. Refused via "./build-errors.js".Premium820BuildError with code `X12_820_BUILD_INVALID_SPEC`: - no TRN trace, or no remittance loop; - a remittance with neither an `entity` nor an `individual` (nothing opens its loop), or with no `openItems`; - an open item with empty `qualifier` AND empty `referenceId` (the read side drops it); - an over-long (> 9 char) interchange control number. #### Parameters ##### spec [`Build820Spec`](#build820spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build820, X12Decimal } from "@cosyte/x12"; const ix = build820({ envelope: { senderId: "EMPLOYERCO", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, payment: { transactionHandlingCode: "I", totalPremiumAmount: X12Decimal.fromString("250.00")!, creditDebitFlag: "C", method: "ACH", paymentDate: "20260601", }, traces: [{ traceTypeCode: "1", referenceId: "PREM-202606" }], remittances: [{ individual: { entityIdentifierCode: "IL", lastName: "DOE", idQualifier: "34", idCode: "MBR0001" }, openItems: [{ qualifier: "AZ", referenceId: "POL-0001", amountPaid: X12Decimal.fromString("250.00")! }], }], }); ``` *** ### build834() > **build834**(`spec`): [`X12Interchange`](#x12interchange) `build834` - assemble a 005010X220A1 834 around the supplied spec. Refused via "./build-errors.js".Enrollment834BuildError: - an `INS-03` or `HD-01` maintenance type code outside the validated X12 875 subset → `X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE`; - no member loop, an empty (required) `INS-03`, or an over-long (> 9 char) interchange control number → `X12_834_BUILD_INVALID_SPEC`. #### Parameters ##### spec [`Build834Spec`](#build834spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build834 } from "@cosyte/x12"; const ix = build834({ envelope: { senderId: "EMPLOYERCO", receiverId: "MEDPAY", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, header: { transactionSetPurposeCode: "00", sponsor: { entityIdentifierCode: "P5", name: "EMPLOYER CO" }, payer: { entityIdentifierCode: "IN", name: "MEDPAY INSURANCE" }, }, members: [{ subscriberIndicator: "Y", relationshipCode: "18", maintenanceTypeCode: "021", member: { lastName: "DOE", firstName: "JANE", idQualifier: "34", idCode: "MBR0001" }, healthCoverages: [{ maintenanceTypeCode: "021", insuranceLineCode: "HLT" }], }], }); ``` *** ### build835() > **build835**(`spec`): [`X12Interchange`](#x12interchange) `build835` - assemble a 005010X221A1 835 around the supplied spec. Refused via "./build-errors.js".Remit835BuildError: - No trace supplied, or a claim with no patient-control number, etc. → `X12_835_BUILD_INVALID_SPEC`. - Any §1.10.2 balance invariant violated (service-line `SVC-02 == SVC-03 + Σ(line CAS)`, claim `CLP-03 == CLP-04 + Σ(claim+line CAS)`, or top-of-remit `BPR-02 == Σ(CLP-04) − Σ(PLB)`) → `X12_835_BUILD_BALANCE_MISMATCH`. #### Parameters ##### spec [`Build835Spec`](#build835spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build835, X12Decimal } from "@cosyte/x12"; const ix = build835({ envelope: { senderId: "MEDICARE", receiverId: "SUBMITTER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, payment: { transactionHandlingCode: "I", totalActualPayment: X12Decimal.fromString("450.00")!, creditDebitFlag: "C", method: "ACH", paymentDate: "20260601", }, traces: [{ traceTypeCode: "1", referenceId: "0012345", originatingCompanyId: "1512345678" }], claims: [ { patientControlNumber: "PT-ACCT-001", claimStatusCode: "1", totalChargeAmount: X12Decimal.fromString("500.00")!, totalPaymentAmount: X12Decimal.fromString("450.00")!, patientResponsibilityAmount: X12Decimal.fromString("50.00")!, adjustments: [{ groupCode: "PR", reasonCode: "1", amount: X12Decimal.fromString("50.00")! }], }, ], }); ``` *** ### build837D() > **build837D**(`spec`): [`X12Interchange`](#x12interchange) `build837D` - assemble a Dental 837 around the spec, declaring `005010X224A2` unless `envelope.implementationConventionReference` states the guide your trading partner requires. Service lines must be `variant: "D"` (SV3); per-line tooth detail rides on TOO. #### Parameters ##### spec [`Build837Spec`](#build837spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build837D, X12Decimal } from "@cosyte/x12"; declare const spec: import("@cosyte/x12").Build837Spec; const ix = build837D(spec); // each serviceLine: { variant: "D", procedureQualifier: "AD", ... } ``` *** ### build837I() > **build837I**(`spec`): [`X12Interchange`](#x12interchange) `build837I` - assemble an Institutional 837 around the spec, declaring `005010X223A3` unless `envelope.implementationConventionReference` states the guide your trading partner requires. Service lines must be `variant: "I"` (SV2). #### Parameters ##### spec [`Build837Spec`](#build837spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build837I, X12Decimal } from "@cosyte/x12"; declare const spec: import("@cosyte/x12").Build837Spec; const ix = build837I(spec); // each serviceLine: { variant: "I", revenueCode: "0120", ... } ``` *** ### build837P() > **build837P**(`spec`): [`X12Interchange`](#x12interchange) `build837P` - assemble a Professional 837 around the spec, declaring `005010X222A2` unless `envelope.implementationConventionReference` states the guide your trading partner requires. #### Parameters ##### spec [`Build837Spec`](#build837spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build837P, X12Decimal } from "@cosyte/x12"; const ix = build837P({ envelope: { senderId: "SUBMITTER", receiverId: "RECEIVER", interchangeDate: "260601", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, submitter: { entityIdentifierCode: "41", entityTypeQualifier: "2", name: "SUBMITTER ONE", idQualifier: "46", idCode: "SUB001" }, receiver: { entityIdentifierCode: "40", entityTypeQualifier: "2", name: "RECEIVER ONE", idQualifier: "46", idCode: "REC001" }, billingProviders: [{ provider: { entityIdentifierCode: "85", entityTypeQualifier: "2", name: "BILLING CLINIC INC", idQualifier: "XX", idCode: "1234567890" }, subscribers: [{ info: { payerResponsibilityCode: "P", individualRelationshipCode: "18", claimFilingIndicator: "MB" }, subscriber: { entityIdentifierCode: "IL", entityTypeQualifier: "1", name: "PATIENT", firstName: "TEST", idQualifier: "MI", idCode: "MEMBER001" }, payer: { entityIdentifierCode: "PR", entityTypeQualifier: "2", name: "PAYER ONE", idQualifier: "PI", idCode: "PAYER01" }, claims: [{ claimId: "PT-ACCT-001", totalCharge: X12Decimal.fromString("150.00")!, diagnoses: [{ qualifier: "ABK", code: "J20.9" }], serviceLines: [{ variant: "P", procedureQualifier: "HC", procedureCode: "99213", charge: X12Decimal.fromString("150.00")!, unitOfMeasure: "UN", units: X12Decimal.fromString("1")!, diagnosisPointers: ["1"] }], }], }], }], }); ``` *** ### build999() > **build999**(`spec`): [`X12Interchange`](#x12interchange) `build999` - assemble a 005010X231A1 Implementation Acknowledgment around the supplied envelope + functional-group spec. Safety guards (refused via [AckBuildError](#ackbuilderror)): - Functional-level `A` disposition paired with any per-transaction non-`A` response, or any per-transaction segment-error / syntax-error payload anywhere in the spec → "./errors.js".ACK\_BUILD\_ERROR\_CODES.X12\_ACK\_ACCEPT\_WITH\_ERRORS. - Per-transaction `A` disposition paired with non-empty `segmentErrors` / `syntaxErrorCodes` → same. - AK9-04 (accepted) > AK9-03 (received), AK9-03 > AK9-02 (declared), or response-list length not equal to AK9-03 → "./errors.js".ACK\_BUILD\_ERROR\_CODES.X12\_ACK\_COUNT\_MISMATCH. #### Parameters ##### spec [`Build999Spec`](#build999spec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { build999 } from "@cosyte/x12"; const ix = build999({ envelope: { senderId: "SENDER", receiverId: "RECEIVER", interchangeDate: "250101", interchangeTime: "1200", interchangeControlNumber: "000000001", groupControlNumber: "1", transactionSetControlNumber: "0001", }, functionalGroup: { functionalIdCode: "HC", groupControlNumber: "1", versionRelease: "005010X222A2", disposition: "A", numberOfTransactionSets: 1, numberOfReceivedTransactionSets: 1, numberOfAcceptedTransactionSets: 1, transactionResponses: [ { transactionSetIdCode: "837", transactionSetControlNumber: "0001", disposition: "A" }, ], }, }); ``` *** ### buildInterchange() > **buildInterchange**(`spec`): [`X12Interchange`](#x12interchange) Assemble a complete [X12Interchange](#x12interchange) from a segment-level [InterchangeSpec](#interchangespec). See the module header for the envelope mechanics the builder owns. #### Parameters ##### spec [`InterchangeSpec`](#interchangespec) #### Returns [`X12Interchange`](#x12interchange) #### Example ```ts import { buildInterchange } from "@cosyte/x12"; const ix = buildInterchange({ senderId: "SENDER", receiverId: "RECEIVER", interchangeDate: "250101", interchangeTime: "1200", interchangeControlNumber: "000000001", groups: [ { functionalIdCode: "HC", groupControlNumber: "1", versionRelease: "005010X222A2", transactions: [ { transactionSetIdCode: "837", transactionSetControlNumber: "0001", implementationConventionReference: "005010X222A2", segments: [["BHT", "0019", "00", "REF", "20250101", "1200", "CH"]], }, ], }, ], }); ``` *** ### buildTA1() > **buildTA1**(`spec`, `options?`): [`Ta1Segment`](#ta1segment) `buildTA1` - assemble a TA1 Interchange Acknowledgment segment from the supplied spec. The returned [Ta1Segment](#ta1segment) carries the 1-indexed 5-element value array (`elements[0]` = `"TA1"`, `elements[1]` = TA1-01, …, `elements[5]` = TA1-05) plus the verbatim wire text on `raw` (no segment terminator appended - that's the envelope's job). Safety guards (refused via [AckBuildError](#ackbuilderror)): - `ackCode === "A"` paired with `noteCode !== "000"` → "./errors.js".ACK\_BUILD\_ERROR\_CODES.X12\_TA1\_ACCEPT\_WITH\_NOTE. Accept must mean accept. Use `E` (accept with errors) when the inbound had structural defects you elected to ignore. This one runs FIRST and its precedence is unchanged. - Any of the five element values that is not a `string` → "./errors.js".ACK\_BUILD\_ERROR\_CODES.X12\_ACK\_INVALID\_SPEC. This is not a bonus guard: releasing a value means routing it through "../../builder/caller-string.js".makeCallerEscaper, and the bare `escapeRelease` underneath it returns its empty accumulator for a `number`, so escaping without the type check would have replaced the shifted-element defect with a silently VANISHED TA1-01. **A number or an `undefined` control number used to emit as `TA1*12345*…` (the number surviving onto `elements`, in a `readonly string[]`) or `TA1**250101*…`; both now refuse.** #### Parameters ##### spec [`BuildTA1Spec`](#buildta1spec) The TA1 fields. `interchangeControlNumber` echoes the inbound ISA-13; `interchangeDate` / `interchangeTime` echo the inbound ISA-09 / ISA-10; `ackCode` is the disposition; `noteCode` is the note (`000` for no-error pairings). ##### options? [`BuildTA1Options`](#buildta1options) = `{}` Optional delimiter overrides for callers building TA1 segments embedded in non-default envelopes. The defaults match the cosyte parser archetype (`*` element, `~` segment) - override when wrapping a TA1 in an ISA envelope whose declared delimiters differ. #### Returns [`Ta1Segment`](#ta1segment) #### Example ```ts import { buildTA1 } from "@cosyte/x12"; // Accept (canonical "no error" pairing). const ok = buildTA1({ interchangeControlNumber: "000000001", interchangeDate: "250101", interchangeTime: "1200", ackCode: "A", noteCode: "000", }); ok.raw; // 'TA1*000000001*250101*1200*A*000' // Reject (control number mismatch). const reject = buildTA1({ interchangeControlNumber: "000000007", interchangeDate: "250101", interchangeTime: "1200", ackCode: "R", noteCode: "001", }); ``` *** ### bundleAaaCodes() > **bundleAaaCodes**(`meta`, `codes`): [`AaaCodeListSnapshot`](#aaacodelistsnapshot) **`Internal`** Bundle `codes` under `meta`, DROPPING every description unless the four-part record is complete and permits it. The refusal is the whole point: a description cannot reach a consumer on the strength of terms nobody recorded, however it got into the source table. - exported for the two snapshots below and their tests. #### Parameters ##### meta [`AaaCodeListMeta`](#aaacodelistmeta) ##### codes `Readonly`\<`Record`\<`string`, `string`\>\> #### Returns [`AaaCodeListSnapshot`](#aaacodelistsnapshot) *** ### checkClaimBalance() > **checkClaimBalance**(`claim`, `position`): [`X12ParseWarning`](#x12parsewarning) \| `undefined` Check the claim-level invariant `CLP-03 === CLP-04 + Σ(all CAS in claim, both claim and line level)`. Returns the warning when out of balance, or `undefined` otherwise. Pure - no side effects. #### Parameters ##### claim [`X12RemitClaim`](#x12remitclaim) ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) \| `undefined` #### Example ```ts import { checkClaimBalance } from "@cosyte/x12"; declare const claim: X12RemitClaim; const w = checkClaimBalance(claim, { segmentIndex: 12 }); if (w !== undefined) { // not balanced - w.message names the TR3 equation; the amounts are on `claim`, // and w.position.segmentIndex is the CLP's own index in the transaction body } ``` *** ### checkCoreCodeCombination() > **checkCoreCodeCombination**(`query`): [`CoreCodeCombinationResult`](#corecodecombinationresult) Answer whether one 835 adjustment's group code, reason code and accompanying remark codes are a combination the caller's CORE Code Combinations table lists for a named CORE-defined business scenario. - **`in-table`** when the table has a row for that scenario with that group code and that reason code, and every supplied remark code appears in some row for that same scenario, group code and reason code. With no remarks, the pair's own row suffices. A permitted pair does not license an arbitrary remark. - **`not-in-table`** when the table has rows for the scenario and the combination is not among them, including a combination the table lists only under a different scenario. - **`unevaluated`**, with a `reason`, when there is no table, the scenario is not one of the four, the table has no row for the scenario, the group code or reason code is empty, or a remark is not an `HE` remark code. **Never read `unevaluated` as permitted.** Codes are matched exactly as supplied: no case folding and no trimming, so `co` and `CO ` are not `CO`. The table's `version` comes back beside every answer given against it, exactly as supplied. Nothing supplied is mutated. What it does not do: decide which scenario an adjustment belongs to, walk a whole remittance, bind a remark to an adjustment, say which remark code failed, check a code's effective dates, or evaluate a scenario a health plan defines for itself. No CORE table ships with this package. #### Parameters ##### query [`CoreCodeCombinationQuery`](#corecodecombinationquery) The table, the scenario, the adjustment and its remarks. #### Returns [`CoreCodeCombinationResult`](#corecodecombinationresult) The answer, with the table's version label. #### Throws When a supplied table is malformed (`X12_CORE_COMBINATION_TABLE_INVALID`). The message names the field and row index only, never a value from the table. #### Example ```ts import { checkCoreCodeCombination, CORE_BUSINESS_SCENARIOS } from "@cosyte/x12"; const table = { version: "my-transcription-2026-02", rows: [{ scenario: "scenario-3", groupCode: "CO", reasonCode: "ZZ901" }] as const, }; checkCoreCodeCombination({ table, scenario: CORE_BUSINESS_SCENARIOS.SERVICE_NOT_COVERED, adjustment: { groupCode: "CO", reasonCode: "ZZ901" }, }); // { outcome: "in-table", tableVersion: "my-transcription-2026-02" } checkCoreCodeCombination({ scenario: CORE_BUSINESS_SCENARIOS.SERVICE_NOT_COVERED, adjustment: { groupCode: "CO", reasonCode: "ZZ901" }, }); // { outcome: "unevaluated", reason: "no-table", tableVersion: undefined } ``` *** ### checkRemitTotalBalance() > **checkRemitTotalBalance**(`bpr02`, `claims`, `providerAdjustments`, `position`): [`X12ParseWarning`](#x12parsewarning) \| `undefined` Check the top-of-remit invariant `BPR-02 === Σ(CLP-04) - Σ(PLB amounts)`. PLB amounts are preserved with their **raw EDI sign** (positive = take-back from provider; negative = credit to provider - see "./types.js".X12RemitProviderAdjustment), so the *subtraction* is what makes the equation balance. Returns the warning when out of balance, or `undefined` when balanced. #### Parameters ##### bpr02 [`X12Decimal`](#x12decimal) \| `undefined` ##### claims readonly [`X12RemitClaim`](#x12remitclaim)[] ##### providerAdjustments readonly [`X12RemitProviderAdjustment`](#x12remitprovideradjustment)[] ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) \| `undefined` #### Example ```ts import { checkRemitTotalBalance } from "@cosyte/x12"; declare const remit: import("./types.js").X12Remittance; const w = checkRemitTotalBalance( remit.payment.totalActualPayment, remit.claims, remit.providerAdjustments, { segmentIndex: 2, transactionIndex: 0 }, ); ``` *** ### checkServiceLineBalance() > **checkServiceLineBalance**(`claim`, `position`): readonly [`X12ParseWarning`](#x12parsewarning)[] Check the per-service-line invariant `SVC-02 === SVC-03 + Σ(line CAS)`. Returns one warning per out-of-balance service line. Header-only adjudications (claims with zero service lines) produce no per-line warning. Pure - no side effects. #### Parameters ##### claim [`X12RemitClaim`](#x12remitclaim) ##### position [`X12Position`](#x12position) #### Returns readonly [`X12ParseWarning`](#x12parsewarning)[] #### Example ```ts import { checkServiceLineBalance } from "@cosyte/x12"; declare const claim: X12RemitClaim; const warnings = checkServiceLineBalance(claim, { segmentIndex: 14 }); ``` *** ### codeListRedistributionIsPermitted() > **codeListRedistributionIsPermitted**(`meta`): `boolean` Whether this list's descriptions may be regenerated and redistributed, read off its own record and nothing else. Only a recorded `"permitted"` answers `true`: a licence requirement, an unsettled status and a missing record all answer `false`, so a permission is never inferred from an absence. Exported so a consumer applies the same bar this package applies to itself, rather than re-deriving one from the status string. #### Parameters ##### meta [`CodeListMeta`](#codelistmeta) #### Returns `boolean` #### Example ```ts import { CARC, RARC, codeListRedistributionIsPermitted } from "@cosyte/x12"; codeListRedistributionIsPermitted(RARC.meta); // true (CMS, no licence) codeListRedistributionIsPermitted(CARC.meta); // false (X12, licence required) ``` *** ### collectElementValues() > **collectElementValues**(`seg`, `start`, `end`, `delimiters`): `string`[] Collect non-empty element values across the 1-indexed inclusive range `[start, end]`. Returns a new array preserving order, omitting empty / missing slots. Common for sweeping N3 address-line composites, MIA / MOA remark code slots, etc. #### Parameters ##### seg [`X12Segment`](#x12segment) ##### start `number` ##### end `number` ##### delimiters [`Delimiters`](#delimiters) #### Returns `string`[] #### Example ```ts import { collectElementValues } from "@cosyte/x12"; collectElementValues(n3Segment, 1, 2, delim); // ["123 Main St", "Apt 4"] ``` *** ### componentOptional() > **componentOptional**(`seg`, `n`, `p`, `delimiters`): `string` \| `undefined` Read component P (1-indexed) of element N (1-indexed) as a string, or `undefined` when absent / empty. The common shape for X12 composite elements (`HI-01-2`, `CLM-05-1`, `SVC-01-2`, etc.). #### Parameters ##### seg [`X12Segment`](#x12segment) ##### n `number` ##### p `number` ##### delimiters [`Delimiters`](#delimiters) #### Returns `string` \| `undefined` #### Example ```ts import { componentOptional } from "@cosyte/x12"; componentOptional(seg, 1, 2, delim); // HI-01-2 (the actual code) ``` *** ### controlNumberMismatch() > **controlNumberMismatch**(`position`, `pair`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_CONTROL_NUMBER_MISMATCH` warning. Emitted when an envelope-trailer control number does not match its matching header: ISA-13 ↔ IEA-02, GS-06 ↔ GE-02, or ST-02 ↔ SE-02. Neither side is echoed, not truncated and not hashed. A control number is free-form trading-partner text on five of the six slots (only ISA-13 is fixed-width), routinely carries a batch or patient-account identifier in the field, and is entirely sender-controlled. #### Parameters ##### position [`X12Position`](#x12position) ##### pair [`X12ControlNumberPair`](#x12controlnumberpair) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { controlNumberMismatch, CONTROL_NUMBER_PAIRS } from "@cosyte/x12"; const w = controlNumberMismatch( { segmentIndex: 6, interchangeIndex: 0, elementIndex: 2 }, CONTROL_NUMBER_PAIRS.INTERCHANGE, ); ``` *** ### danglingReleaseChar() > **danglingReleaseChar**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_DANGLING_RELEASE_CHAR` warning. Emitted when a release character (`?` per ASC X12 convention; see "./release.js".RELEASE\_CHAR) appears at the end of a segment or element with no following byte to escape - the bytes are preserved verbatim so round-trip stays byte-exact, but the structural truncation is flagged so consumers can decide how to react. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { danglingReleaseChar } from "@cosyte/x12"; const w = danglingReleaseChar({ segmentIndex: 7, interchangeIndex: 0 }); ``` *** ### dateRowDropped() > **dateRowDropped**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_270_DATE_ROW_DROPPED` warning. Raised where a 270's DTP reached the reader short of its qualifier (DTP-01) or its value (DTP-03), so no date row was built and the whole segment, the format qualifier included, is absent from the typed model. A DTP is a RECORD and not a slot, which is why the loss is the row rather than one element, and why it is reported at all: without this code an empty `dates` list reads the same whether the sender stated no date or stated one this reader could not build a row from. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { dateRowDropped } from "@cosyte/x12"; const w = dateRowDropped({ segmentIndex: 9, transactionIndex: 0 }); ``` *** ### decodeSegment() > **decodeSegment**(`raw`, `delimiters`, `emit`, `position`): [`X12Segment`](#x12segment) Decode a raw segment string into an [X12Segment](#x12segment). Splits on the detected element separator (honouring the `?`-release-character escape), preserves the verbatim raw text, and surfaces dangling-release warnings via the supplied emitter with positional context tied to the segment. Never throws - malformed input produces a best-effort segment and any issues surface as Tier-2 warnings via `emit`. An empty input string decodes to an empty segment (`id: ""`, `elements: [""]`); callers that care can guard. **A degenerate delimiter set whose ELEMENT SEPARATOR is itself `?` splits literally**, because one byte cannot both separate and escape. Only that role is guarded here: a `?` repetition or component separator is a different call and is deliberately unchanged (see "./release.js".splitWithRelease). **A BDS or BIN segment's binary data element is framed by the octet count its length element declares** (BDS-02 / BIN-01), not by scanning for delimiters: the data element (BDS-03 / BIN-02) is exactly that many characters of `raw`, whatever delimiter or `?` bytes it holds, and is the last element. Where the count cannot be honoured this raises `X12_BINARY_DATA_TRUNCATED`, `X12_BINARY_LENGTH_INVALID`, `X12_BINARY_LENGTH_MISMATCH` or `X12_BINARY_LENGTH_UNVERIFIABLE`, anchored at `position` with the element it concerns. An absent, empty or malformed length element frames the segment by its delimiters, as any other segment is framed. `raw` is always the sender's bytes, so bytes after a declared span that the terminator did not follow stay on it. #### Parameters ##### raw `string` ##### delimiters [`Delimiters`](#delimiters) ##### emit (`w`) => `void` ##### position [`X12Position`](#x12position) #### Returns [`X12Segment`](#x12segment) #### Example ```ts import { decodeSegment } from "@cosyte/x12"; const d = { element: "*", repetition: "^", component: ":", segment: "~" }; const seg = decodeSegment("NM1*IL*1*DOE*JANE", d, () => {}, { segmentIndex: 5 }); seg.id; // "NM1" seg.elements[3]; // "DOE" const bin = decodeSegment("BIN*5*A*B:C", d, () => {}, { segmentIndex: 6 }); bin.elements[2]; // "A*B:C" (five octets, one element) ``` *** ### defineLoopSpec() > **defineLoopSpec**(`input`): [`LoopSpec`](#loopspec) Define a TR3 loop specification. Validates structurally, freezes the resulting [LoopSpec](#loopspec) (along with its `segments` and `children` arrays), and returns it. Pure - no I/O, no global state. #### Parameters ##### input [`DefineLoopSpecInput`](#defineloopspecinput) #### Returns [`LoopSpec`](#loopspec) #### Example ```ts import { defineLoopSpec } from "@cosyte/x12"; const Loop2110 = defineLoopSpec({ id: "2110", description: "835 Service Payment Information", trigger: "SVC", segments: [ { id: "SVC", usage: "required", max: 1 }, { id: "DTM", usage: "situational", max: ">1" }, { id: "CAS", usage: "situational", max: ">1" }, { id: "REF", usage: "situational", max: ">1" }, { id: "AMT", usage: "situational", max: ">1" }, { id: "LQ", usage: "situational", max: ">1" }, ], }); Loop2110.trigger; // "SVC" ``` *** ### defineProfile() > **defineProfile**(`opts`): [`X12Profile`](#x12profile) Build a readonly [X12Profile](#x12profile) from a validated spec. Invalid input throws "./errors.js".X12ProfileError with an actionable message: missing/empty name, unknown option key (with a typo hint), or a quirk that violates the locked hard rule (missing `fixture`, bad `effect`, unknown `expectedWarnings` code). `extends` composes parent profiles: lineage flattens + dedupes, quirks merge by id (child wins on collision, non-colliding parent quirks survive), and `description` is last-wins. #### Parameters ##### opts [`X12ProfileSpec`](#x12profilespec) #### Returns [`X12Profile`](#x12profile) #### Example ```ts import { defineProfile } from "@cosyte/x12"; const availity = defineProfile({ name: "availity", description: "Availity clearinghouse 835 ERA conventions", quirks: [ { id: "payer-loop-ref-2u", effect: "adds", summary: "Payer Loop 1000A carries a REF*2U additional payer identifier.", fixture: "remit/835-availity-quirk.edi", sourceCategory: "Availity 835 ERA companion guide - payer-loop REF", }, ], }); availity.name; // "availity" availity.lineage; // ["availity"] availity.describe().adds; // [{ id: "payer-loop-ref-2u", ... }] ``` *** ### detectDelimiters() > **detectDelimiters**(`raw`): [`Delimiters`](#delimiters) Detect the four X12 delimiters from a raw input string by reading fixed byte positions inside the ISA envelope. Validates that: - the input is at least [ISA\_MIN\_LENGTH](#isa_min_length) bytes (else `X12_ISA_TOO_SHORT`), - the input begins with the literal `"ISA"` (else `X12_NO_ISA_HEADER`), - each delimiter is a single visible (non-whitespace, non-control) character and the four are mutually distinct (else `X12_INVALID_DELIMITERS`), - the detected element separator actually appears at every fixed ISA element-separator position (else `X12_INVALID_DELIMITERS`) - guards against an input that begins with `"ISA"` followed by structurally wrong bytes (e.g. a tab as element separator with `:` further in). The parser is liberal in what it accepts AFTER the ISA - downstream envelope walking emits Tier-2 warnings rather than throwing. But the ISA itself MUST be structurally readable; otherwise no later stage has a delimiter set to work with. #### Parameters ##### raw `string` #### Returns [`Delimiters`](#delimiters) #### Example ```ts import { detectDelimiters } from "@cosyte/x12"; const d = detectDelimiters("ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER *250101*1200*^*00501*000000001*0*P*:~GS*HC*S*R*20250101*1200*1*X*005010X222A2~..."); d.element; // "*" d.repetition; // "^" d.component; // ":" d.segment; // "~" ``` *** ### duplicateHierarchyId() > **duplicateHierarchyId**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_270_DUPLICATE_HIERARCHY_ID` warning. Raised at the second and each subsequent HL in a 270 transaction set to carry an HL-01 an earlier one already carried. `position` names the repeat, never the first occurrence. Attachment is what the duplication decides, and the rule is fixed so the decode is deterministic: a child naming that identifier attaches to the FIRST level carrying it in transmitted order. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { duplicateHierarchyId } from "@cosyte/x12"; const w = duplicateHierarchyId({ segmentIndex: 7, transactionIndex: 0 }); ``` *** ### elementDecimal() > **elementDecimal**(`seg`, `n`, `delimiters`, `sink?`): [`X12Decimal`](#x12decimal) \| `undefined` Read element N (1-indexed) as an [X12Decimal](#x12decimal), or `undefined` when absent / empty / malformed. The walker discipline for every monetary or quantity field - NEVER `parseFloat`. **`undefined` here means "not decoded", not "absent."** When `sink` is supplied, a PRESENT element that does not decode emits `X12_UNPARSEABLE_DECIMAL` carrying `elementIndex: n`, so the two cases are told apart on the warning channel. Without a sink the ambiguity is silent, which is why every reader in this library passes one; use [readElementDecimal](#readelementdecimal) when you want the distinction in-band instead. An ABSENT element returns `undefined` and does NOT warn. #### Parameters ##### seg [`X12Segment`](#x12segment) ##### n `number` ##### delimiters [`Delimiters`](#delimiters) ##### sink? [`X12DecimalWarningSink`](#x12decimalwarningsink) #### Returns [`X12Decimal`](#x12decimal) \| `undefined` #### Example ```ts import { elementDecimal } from "@cosyte/x12"; elementDecimal(seg, 2, delim)?.toString(); // BPR-02 verbatim decimal ``` *** ### elementDecimalOrZero() > **elementDecimalOrZero**(`seg`, `n`, `delimiters`, `sink?`): [`X12Decimal`](#x12decimal) Read element N (1-indexed) as an [X12Decimal](#x12decimal), defaulting to `X12Decimal.ZERO` when absent. Convenient for fields the walker treats as "missing means zero" (charge totals, etc.). **The `0` this returns for a PRESENT-but-unparseable element is a stand-in, not a reading.** A slot typed `X12Decimal` cannot express "did not decode", so a consumer looking only at the model cannot tell that `0` from a `0` the sender sent: on a paid amount that is a fabricated amount presented as read. When `sink` is supplied the case emits `X12_UNPARSEABLE_DECIMAL`, which is the only signal that distinguishes them. Pass one, or use [readElementDecimal](#readelementdecimal) and decide for yourself. An ABSENT element still returns `X12Decimal.ZERO` and does NOT warn: that is the documented "missing means zero" convention of the slots that use this helper, and it is unchanged. #### Parameters ##### seg [`X12Segment`](#x12segment) ##### n `number` ##### delimiters [`Delimiters`](#delimiters) ##### sink? [`X12DecimalWarningSink`](#x12decimalwarningsink) #### Returns [`X12Decimal`](#x12decimal) #### Example ```ts import { elementDecimalOrZero } from "@cosyte/x12"; elementDecimalOrZero(seg, 2, delim).toString(); ``` *** ### elementOptional() > **elementOptional**(`seg`, `n`, `delimiters`): `string` \| `undefined` Read element N (1-indexed) as `string | undefined`. Empty strings collapse to `undefined` - the conventional "absent" sentinel for optional element slots. #### Parameters ##### seg [`X12Segment`](#x12segment) ##### n `number` ##### delimiters [`Delimiters`](#delimiters) #### Returns `string` \| `undefined` #### Example ```ts import { elementOptional } from "@cosyte/x12"; // returns undefined when NM1-04 is "" or missing ``` *** ### elementValue() > **elementValue**(`seg`, `n`, `delimiters`): `string` Read element N (1-indexed) from a decoded segment as a string. Returns `""` when the element is absent or empty - convenient when downstream code wants to fold "absent" and "empty" into one branch. For "absent / empty → `undefined`" use [elementOptional](#elementoptional). #### Parameters ##### seg [`X12Segment`](#x12segment) ##### n `number` ##### delimiters [`Delimiters`](#delimiters) #### Returns `string` #### Example ```ts import { decodeSegment, elementValue } from "@cosyte/x12"; declare const seg: ReturnType; declare const delim: import("./types.js").Delimiters; elementValue(seg, 3, delim); // verbatim text of element 3, or "". ``` *** ### entitySegmentDiscardedAfterLx() > **entitySegmentDiscardedAfterLx**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX` warning. Emitted by the 837 helper for an `N3` / `N4` / `PER` / `REF` that reached no party because an earlier `LX` with no `CLM` open closed the entity loop that was current at it. `position` names the discarded segment itself, not the `LX`: the loss is per segment (two `N3`s are two losses), and the segment is the one thing a consumer needs to resolve back through `tx.segments`. It is the narrow companion to [serviceLineDropped](#servicelinedropped), which is raised at that same `LX` and reports the SERVICE LINE. Neither reports what the other does, and both can be on one transaction's channel. **Read its bound literally: this is not a general "unattached entity segment" code.** It fires only after such an `LX` and only while nothing has opened a new loop since, so an `N3` / `N4` / `PER` / `REF` that reaches no party by any other route stays silent, exactly as it did before this code existed. Widening it is a guard change and would be its own decision. It reports that the segment reached no party, NOT that it would have reached one: this reader surfaces neither a `PER` on a patient nor one on a pay-to address, on any release. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { entitySegmentDiscardedAfterLx } from "@cosyte/x12"; const w = entitySegmentDiscardedAfterLx({ segmentIndex: 8, transactionIndex: 0 }); ``` *** ### escapeRelease() > **escapeRelease**(`value`, `delimiters`): `string` Apply X12 release-character escapes to a value so it can be emitted inside a segment without ambiguity. Every occurrence of the four delimiters (or the release character itself) is preceded by `?`; other bytes pass through verbatim. The companion to [unescapeRelease](#unescaperelease). **Throws `TypeError` on a non-string, and the previous behaviour was the defect.** This function is declared over `string`, but a JavaScript or JSON-driven caller is not held to that. Reading `value.length` gave three different wrong answers depending on what arrived: a `number`, a `boolean` or a plain object has no `.length`, so `undefined === 0` was false, `i < undefined` was false, and the function **returned the empty accumulator** - the value vanished with no error and no warning; `null` and `undefined` threw on the property read; an array or an array-like threw on `charAt`. The silent one was by far the worst: `build835` emitted `CLP**1*500.00*…` with `warnings.length === 0`, dropping the CLP-01 reassociation key TR3 005010X221A1 requires. All three now terminate the same way. `TypeError` and not a code-tagged library error is deliberate, and it is not the untyped-refusal defect this package has fixed elsewhere. This is a pure text utility with no spec, no element and no caller context to name, so `TypeError` is the accurate answer for a wrong-typed argument. **Nothing inside this library can reach it**: every builder routes its elements through `src/builder/caller-string.ts`, which refuses first with the calling builder's own typed, code-tagged error. (No count: it is asserted by `test/builder-string-type.test.ts` and was stale here.) This throw is the backstop for a consumer calling the export directly, where the alternative was silent data loss. #### Parameters ##### value `string` ##### delimiters [`Delimiters`](#delimiters) #### Returns `string` #### Throws TypeError if `value` is not a string #### Example ```ts import { escapeRelease } from "@cosyte/x12"; const d = { element: "*", repetition: "^", component: ":", segment: "~" }; escapeRelease("ab~cd*ef:gh", d); // "ab?~cd?*ef?:gh" escapeRelease("a?b", d); // "a??b" escapeRelease(1 as unknown as string, d); // TypeError (was "") ``` *** ### get270Inquiry() > **get270Inquiry**(`delimiters`, `tx`): [`X12Inquiry`](#x12inquiry) \| `undefined` Extract the typed [X12Inquiry](#x12inquiry) from one 270 transaction set. Pure function: no I/O, no global state, never throws. Returns `undefined` only when the transaction set's ST-01 is not `"270"` (a mis-routed call) - the same refusal shape `get271Eligibility`, `get835`, `get820Payments` and every other per-transaction reader in this package uses. Every other deviation is recoverable and surfaces on `result.warnings`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12Inquiry`](#x12inquiry) \| `undefined` #### Example ```ts import { parseX12, get270Inquiry } from "@cosyte/x12"; const ix = parseX12(raw); for (const group of ix.groups) { for (const tx of group.transactions) { if (tx.st.elements[1] !== "270") continue; const inquiry = get270Inquiry(ix.delimiters, tx); const sub = inquiry?.informationSources[0]?.receivers[0]?.subscribers[0]; sub?.traces[0]?.referenceId; // trace to reassociate on sub?.inquiries[0]?.serviceTypeCodes[0]?.code; // "30" } } ``` *** ### get271Eligibility() > **get271Eligibility**(`delimiters`, `tx`): [`X12Eligibility`](#x12eligibility) \| `undefined` Extract a typed [X12Eligibility](#x12eligibility) from a 271 transaction set. Pure function - no I/O, no global state. Returns `undefined` only when the input transaction's ST-01 is not `"271"` (mis-routed call); every other deviation is recoverable and surfaces on `result.warnings`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12Eligibility`](#x12eligibility) \| `undefined` #### Example ```ts import { parseX12, get271Eligibility } from "@cosyte/x12"; const ix = parseX12(raw); for (const group of ix.groups) { for (const tx of group.transactions) { if (tx.st.elements[1] !== "271") continue; const elig = get271Eligibility(ix.delimiters, tx); for (const sub of elig?.subscribers ?? []) { sub.traces[0]?.referenceId; // echoed 270 trace number sub.benefits[0]?.eligibilityCode; // "1" (Active Coverage) } } } ``` *** ### get275Attachments() > **get275Attachments**(`delimiters`, `tx`): [`X12AttachmentSubmission`](#x12attachmentsubmission) \| `undefined` Read a 275. Returns `undefined` only when ST-01 is not `275`; every other deviation is read leniently and surfaces on `warnings`, never as a throw. Pure: no I/O, no global state, and the transaction set is not modified. Pass the interchange to `parseX12` as a `Buffer` where the attachments hold octets above 0x7F: a `Buffer` is read one character per octet, so every count is exact. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12AttachmentSubmission`](#x12attachmentsubmission) \| `undefined` #### Example ```ts import { parseX12, get275Attachments } from "@cosyte/x12"; const ix = parseX12(buffer); for (const tx of ix.groups[0]?.transactions ?? []) { const reading = get275Attachments(ix.delimiters, tx); for (const attachment of reading?.attachments ?? []) { if (!attachment.lengthVerified) continue; // a framing warning names why const octets = attachment.data.readOctets(); // verbatim, still filtered } } ``` *** ### get276StatusInquiry() > **get276StatusInquiry**(`delimiters`, `tx`): [`X12StatusInquiry`](#x12statusinquiry) \| `undefined` Extract the typed [X12StatusInquiry](#x12statusinquiry) from one 276 transaction set. Pure function: no I/O, no global state, never throws. Returns `undefined` only when the transaction set's ST-01 is not `"276"` (a mis-routed call) - the same refusal shape `get277Status`, `get270Inquiry` and every other per-transaction reader in this package uses. Every other deviation is recoverable and surfaces on `result.warnings`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12StatusInquiry`](#x12statusinquiry) \| `undefined` #### Example ```ts import { parseX12, get276StatusInquiry } from "@cosyte/x12"; const ix = parseX12(raw); for (const group of ix.groups) { for (const tx of group.transactions) { if (tx.st.elements[1] !== "276") continue; const inquiry = get276StatusInquiry(ix.delimiters, tx); const sub = inquiry?.informationSources[0]?.receivers[0]?.providers[0]?.subscribers[0]; sub?.claims[0]?.trace?.referenceId; // the trace a 277 echoes back sub?.claims[0]?.references[0]?.value; // "PCN0001" } } ``` *** ### get277CADisposition() > **get277CADisposition**(`delimiters`, `tx`): [`X12ClaimStatusResponse`](#x12claimstatusresponse) \| `undefined` Extract a 277CA Claim Acknowledgment (`005010X214`). Returns `undefined` unless the transaction is a 277 whose `ST-03` is `005010X214` - use [get277Status](#get277status) for the general 277 Claim Status Response. The returned model is the same [X12ClaimStatusResponse](#x12claimstatusresponse); the `transactionType` is always `"claim-acknowledgment"`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12ClaimStatusResponse`](#x12claimstatusresponse) \| `undefined` #### Example ```ts import { parseX12, get277CADisposition } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "277"); const ack = tx === undefined ? undefined : get277CADisposition(ix.delimiters, tx); ack?.claims[0]?.statuses[0]?.statuses[0]?.categoryCode; // "A1" / "A2" / "A3" ``` *** ### get277RequestForAdditionalInformation() > **get277RequestForAdditionalInformation**(`delimiters`, `tx`): [`X12AdditionalInformationRequest`](#x12additionalinformationrequest) \| `undefined` Read a 277 request for additional information. Returns `undefined` unless ST-01 is `277` and the declared guide is `006020X313`; every other deviation is read leniently and surfaces on `warnings`, never as a throw. Pure: no I/O, no global state, and the transaction set is not modified. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12AdditionalInformationRequest`](#x12additionalinformationrequest) \| `undefined` #### Example ```ts import { parseX12, get277RequestForAdditionalInformation } from "@cosyte/x12"; const ix = parseX12(raw); for (const tx of ix.groups[0]?.transactions ?? []) { const rfai = get277RequestForAdditionalInformation(ix.delimiters, tx); if (rfai === undefined) continue; // not a 006020X313 277 for (const level of rfai.levels) { for (const request of level.requests) { request.traces[0]?.referenceId; request.statuses[0]?.codes[0]?.statusCode; // e.g. a LOINC code } } } ``` *** ### get277Status() > **get277Status**(`delimiters`, `tx`): [`X12ClaimStatusResponse`](#x12claimstatusresponse) \| `undefined` Extract a typed [X12ClaimStatusResponse](#x12claimstatusresponse) from a 277 / 277CA transaction set. Pure function - no I/O, no global state. Returns `undefined` only when `ST-01` is not `"277"` (mis-routed call); every other deviation is recoverable and surfaces on `result.warnings`. The declared guide (ST-03, or GS-08 where ST-03 is absent or empty) is checked against the guides this reader implements, which are those of the claim status and claim acknowledgment rows of `X12_TR3_CONFORMANCE`, and not the request for additional information row. Where it is outside them, or nothing is declared, the reading is still walked and returned, carries `X12_GUIDE_NOT_IMPLEMENTED` or `X12_GUIDE_NOT_DECLARED`, and its `transactionType` is `"unrecognized-guide"`. Otherwise `transactionType` is derived from ST-03 as framed: `"claim-acknowledgment"` where it reads `005010X214`, and `"claim-status"` for every other implemented declaration. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12ClaimStatusResponse`](#x12claimstatusresponse) \| `undefined` #### Example ```ts import { parseX12, get277Status } from "@cosyte/x12"; const ix = parseX12(raw); for (const group of ix.groups) { for (const tx of group.transactions) { if (tx.st.elements[1] !== "277") continue; const status = get277Status(ix.delimiters, tx); for (const claim of status?.claims ?? []) { claim.traces[0]?.referenceId; // echoed 276 trace claim.statuses[0]?.statuses[0]?.statusCode; // "20" } } } ``` *** ### get278Request() > **get278Request**(`delimiters`, `tx`): [`X12ServicesReview`](#x12servicesreview) \| `undefined` Extract a typed [X12ServicesReview](#x12servicesreview) from a 278 **request** (`005010X217`). Pure function - no I/O. Returns `undefined` only when the transaction's ST-01 is not `"278"` (mis-routed call); every other deviation is recoverable and surfaces on `result.warnings`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12ServicesReview`](#x12servicesreview) \| `undefined` #### Example ```ts import { parseX12, get278Request } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "278"); if (tx !== undefined) { const req = get278Request(ix.delimiters, tx); req?.reviews[0]?.requestCategoryCode; // "HS" req?.reviews[0]?.diagnoses[0]?.code; // "E1165" } ``` *** ### get278Response() > **get278Response**(`delimiters`, `tx`): [`X12ServicesReview`](#x12servicesreview) \| `undefined` Extract a typed [X12ServicesReview](#x12servicesreview) from a 278 **response** (`005010X217`, the same guide as the request). Same lenient walk as [get278Request](#get278request); the `HCR` decision under each event / service review is the response's safety-critical addition. Returns `undefined` only on a mis-routed ST-01. A response declaring `005010X216`, the 278 notification guide, is read like any other and carries `X12_GUIDE_NOT_IMPLEMENTED`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12ServicesReview`](#x12servicesreview) \| `undefined` #### Example ```ts import { parseX12, get278Response } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "278"); if (tx !== undefined) { const resp = get278Response(ix.delimiters, tx); resp?.reviews[0]?.decision?.actionCode; // "A1" resp?.reviews[0]?.decision?.reviewIdentificationNumber; // "AUTH123456" } ``` *** ### get820Payments() > **get820Payments**(`delimiters`, `tx`): [`X12PremiumPayments`](#x12premiumpayments) \| `undefined` Extract a typed [X12PremiumPayments](#x12premiumpayments) from an 820 transaction set. Pure function - no I/O, no global state. Returns `undefined` only if the input transaction's ST-01 is not `"820"` (mis-routed call); every other deviation is recoverable and the verbatim segments remain on `tx.segments`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12PremiumPayments`](#x12premiumpayments) \| `undefined` #### Example ```ts import { parseX12, get820Payments } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "820"); if (tx !== undefined) { const prem = get820Payments(ix.delimiters, tx); prem?.payment.totalPremiumAmount?.toString(); for (const r of prem?.remittances ?? []) { r.openItems[0]?.amountPaid?.toString(); } } ``` *** ### get834Enrollments() > **get834Enrollments**(`delimiters`, `tx`): `AsyncIterable`\<[`X12Enrollment`](#x12enrollment)\> Stream the 834 member-level detail loops - one [X12Enrollment](#x12enrollment) per `INS` segment. For a non-834 transaction the iterable yields nothing. Async-generator typed so the contract leaves room for a future file→iterator streaming source; the v1 implementation iterates the already-parsed `tx.segments` synchronously under the hood. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns `AsyncIterable`\<[`X12Enrollment`](#x12enrollment)\> #### Example ```ts import { parseX12, get834Enrollments } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "834"); if (tx !== undefined) { for await (const member of get834Enrollments(ix.delimiters, tx)) { member.maintenanceTypeCode; // "021" member.member?.idCode; // verbatim member id } } ``` *** ### get834Header() > **get834Header**(`delimiters`, `tx`): [`X12EnrollmentHeader`](#x12enrollmentheader) \| `undefined` Extract the 834 header - BGN + sponsor (`N1*P5`) + payer (`N1*IN`). Pure function. Returns `undefined` only if the input transaction's ST-01 is not `"834"` (mis-routed call). Stops collecting header parties at the first `INS` (member-level detail) - those belong to the member stream. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12EnrollmentHeader`](#x12enrollmentheader) \| `undefined` #### Example ```ts import { parseX12, get834Header } from "@cosyte/x12"; const ix = parseX12(raw); const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "834"); const header = tx === undefined ? undefined : get834Header(ix.delimiters, tx); header?.sponsor?.name; ``` *** ### get835() > **get835**(`delimiters`, `tx`): [`X12Remittance`](#x12remittance) \| `undefined` Extract a typed [X12Remittance](#x12remittance) from an 835 transaction set. Pure function - no I/O, no global state. Returns `undefined` only if the input transaction's ST-01 is not `"835"` (mis-routed call); every other deviation is recoverable and surfaces on `result.warnings`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) #### Returns [`X12Remittance`](#x12remittance) \| `undefined` #### Example ```ts import { parseX12, get835 } from "@cosyte/x12"; const ix = parseX12(raw); for (const group of ix.groups) { for (const tx of group.transactions) { if (tx.st.elements[1] !== "835") continue; const remit = get835(ix.delimiters, tx); remit?.payment.totalActualPayment?.toString(); for (const claim of remit?.claims ?? []) { claim.totalPaymentAmount?.toString(); } } } ``` *** ### get837Claims() > **get837Claims**(`delimiters`, `tx`, `opts?`): [`X12_837Submission`](#x12_837submission) \| `undefined` Extract a typed [X12\_837Submission](#x12_837submission) from an 837 transaction set. Pure function - no I/O, no global state. Returns `undefined` when the input transaction's ST-01 is not `"837"` (mis-routed call); every other deviation is recoverable and surfaces on `submission.warnings`. #### Parameters ##### delimiters [`Delimiters`](#delimiters) ##### tx [`X12TransactionSet`](#x12transactionset) ##### opts? ###### type? `"P"` \| `"I"` \| `"D"` #### Returns [`X12_837Submission`](#x12_837submission) \| `undefined` #### Example ```ts import { parseX12, get837Claims } from "@cosyte/x12"; const ix = parseX12(raw); for (const group of ix.groups) { for (const tx of group.transactions) { if (tx.st.elements[1] !== "837") continue; const sub = get837Claims(ix.delimiters, tx); sub?.variant; // "P" / "I" / "D" / "unknown" for (const claim of sub?.claims ?? []) { claim.totalCharge?.toString(); claim.diagnoses[0]?.codeSystem; // "ICD-10-CM" } } } ``` *** ### getAllSegmentValues() > **getAllSegmentValues**(`segment`, `path`, `delimiters`, `emit?`): readonly `string`[] Return every repetition / component matching the dot-path. For a path with no `[N]` and no `-N` (e.g. `"03"`), returns every repetition's decoded element text. For a path with `-N` and no `[N]`, returns each repetition's Nth component. With both `[N]` and `-N` specified, returns a single-element array (or empty if the path doesn't resolve). Every returned string is post-`?`-unescape, except a BDS or BIN binary data element, which comes back whole and verbatim exactly as [getSegmentValue](#getsegmentvalue) returns it. #### Parameters ##### segment [`X12Segment`](#x12segment) ##### path `string` ##### delimiters [`Delimiters`](#delimiters) ##### emit? (`w`) => `void` #### Returns readonly `string`[] #### Example ```ts import { decodeSegment, getAllSegmentValues } from "@cosyte/x12"; const d = { element: "*", repetition: "^", component: ":", segment: "~" }; const seg = decodeSegment( "HI*ABK:J45.50^ABF:I10", d, () => {}, { segmentIndex: 0 }, ); getAllSegmentValues(seg, "01", d); // ["ABK:J45.50", "ABF:I10"] getAllSegmentValues(seg, "01-1", d); // ["ABK", "ABF"] ``` *** ### getDefaultProfile() > **getDefaultProfile**(): [`X12Profile`](#x12profile) \| `undefined` Return the current default profile, or `undefined` if none is registered. #### Returns [`X12Profile`](#x12profile) \| `undefined` #### Example ```ts import { getDefaultProfile } from "@cosyte/x12"; const p = getDefaultProfile(); if (p !== undefined) console.log("default profile:", p.name); ``` *** ### getSegmentValue() > **getSegmentValue**(`segment`, `path`, `delimiters`, `emit?`): `string` \| `undefined` Resolve a dot-path against a decoded [X12Segment](#x12segment) and return the decoded leaf value (post-`?`-unescape) or `undefined` if the path does not resolve (missing element, out-of-range repetition, out-of-range component). Throws `TypeError` only when `path` itself is malformed. Optional `emit` collects any dangling-release warnings discovered on the read path; pass a no-op to silently decode. **A BDS or BIN binary data element (`"03"` on a BDS, `"02"` on a BIN) is returned verbatim**: no release-character unescape and no split on the repetition or component separator, because every byte in it is data. Its only repetition is `[0]` and its only component is `-1`; any other index returns `undefined`. BDS-01's filter is not applied. #### Parameters ##### segment [`X12Segment`](#x12segment) ##### path `string` ##### delimiters [`Delimiters`](#delimiters) ##### emit? (`w`) => `void` #### Returns `string` \| `undefined` #### Example ```ts import { decodeSegment, getSegmentValue } from "@cosyte/x12"; const d = { element: "*", repetition: "^", component: ":", segment: "~" }; const seg = decodeSegment( "HI*ABK:J45.50*ABF:I10", d, () => {}, { segmentIndex: 0 }, ); getSegmentValue(seg, "01-1", d); // "ABK" getSegmentValue(seg, "01-2", d); // "J45.50" getSegmentValue(seg, "02-1", d); // "ABF" ``` *** ### groupCountMismatch() > **groupCountMismatch**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_GROUP_COUNT_MISMATCH` warning. Emitted when IEA-01 does not equal the actual number of GS..GE groups present in the interchange. Trading partners use this to detect transmission truncation. Both numbers stay on the model and neither is silently corrected. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { groupCountMismatch } from "@cosyte/x12"; const w = groupCountMismatch({ segmentIndex: 5, interchangeIndex: 0, elementIndex: 1 }); ``` *** ### guideNotDeclared() > **guideNotDeclared**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_GUIDE_NOT_DECLARED` warning. Raised by a typed reader when a transaction set declares no implementation guide at all: ST-03 is absent or empty and GS-08 of the enclosing functional group is absent or empty too, or no functional group header reached the transaction set (one assembled by hand, say). A whitespace-only declaration is NOT empty and is reported as `X12_GUIDE_NOT_IMPLEMENTED` instead, because nothing is trimmed. The reading is still decoded and returned against the guide the reader implements. Readers anchor it at the ST (`segmentIndex: 0`). #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { guideNotDeclared } from "@cosyte/x12"; const w = guideNotDeclared({ segmentIndex: 0, transactionIndex: 0 }); w.code; // "X12_GUIDE_NOT_DECLARED" ``` *** ### guideNotImplemented() > **guideNotImplemented**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_GUIDE_NOT_IMPLEMENTED` warning. Raised by a typed reader when the implementation guide a transaction set declares is not one that reader implements. The declaration is ST-03, decoded of any release escape, or GS-08 of the enclosing functional group where ST-03 is absent or empty; the comparison is exact, and the guides a reader implements are the ones its rows in `X12_TR3_CONFORMANCE` name. The reading is still decoded and returned: this code refuses, drops and re-decodes nothing. It takes a position and no value, so the declared identifier never reaches the message; readers anchor it at the ST (`segmentIndex: 0`) with no `elementIndex`, because the declaration may have come from GS-08 rather than from an element of the ST. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { guideNotImplemented } from "@cosyte/x12"; const w = guideNotImplemented({ segmentIndex: 0, transactionIndex: 0 }); w.code; // "X12_GUIDE_NOT_IMPLEMENTED" ``` *** ### hierarchyCycle() > **hierarchyCycle**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_270_HIERARCHY_CYCLE` warning. Raised where the chain of HL-02 parent pointers from the level at `position` returns to a level already on that chain. Disjoint from [hlParentMismatch](#hlparentmismatch) by construction: this one requires every pointer on the chain to name a level that IS present, and that one reports a pointer that names none. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { hierarchyCycle } from "@cosyte/x12"; const w = hierarchyCycle({ segmentIndex: 5, transactionIndex: 0 }); ``` *** ### hlParentLevelInvalid() > **hlParentLevelInvalid**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_HL_PARENT_LEVEL_INVALID` warning. Emitted when an HL's level code (HL-03) is inconsistent with its declared parent's level code per the TR3 (e.g. a `22` Subscriber claiming a `22` Subscriber as its parent, where the parent must be `20` Information Source). Both level codes stay verbatim on the model. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { hlParentLevelInvalid } from "@cosyte/x12"; const w = hlParentLevelInvalid({ segmentIndex: 14, groupIndex: 0, transactionIndex: 0 }); ``` *** ### hlParentMismatch() > **hlParentMismatch**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_HL_PARENT_MISMATCH` warning. Emitted when an HL segment's HL-02 (parent id) does not match any earlier-emitted HL-01 in the same transaction. The walker NEVER silently re-numbers the hierarchy (HL parent-pointer integrity is the safety primitive of the 837), so the declared pointer stays verbatim on `hierarchy.parentHlId` and the warning reports only that the pointer is dangling, and where. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { hlParentMismatch } from "@cosyte/x12"; const w = hlParentMismatch({ segmentIndex: 14, groupIndex: 0, transactionIndex: 0 }); ``` *** ### interSegmentLineBreak() > **interSegmentLineBreak**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_270_INTER_SEGMENT_LINE_BREAK` warning. Raised by the 270 reader, once per 270 transaction set, where the document carries a run of CR or LF bytes between segments. Same scoping as [nonConventionalDelimiter](#nonconventionaldelimiter) and for the same reason: the shared parse consumes such a run and is not changed here. **CR and LF and nothing else, which is why the name says line break rather than whitespace.** A space or a tab between segments is not consumed by the shared parse: it is taken as the head of the next segment's identifier, so the functional group never frames and there is no 270 to report on. Widening that is a change to the shared parse, which this reader deliberately does not make; the gap is recorded in `KNOWN-LIMITATIONS.md`. The run is observable in the BYTES and not on the model, so only the entry point that receives the bytes can raise it. `position` is the ISA, because the framing is a property of the document rather than of one segment. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { interSegmentLineBreak } from "@cosyte/x12"; const w = interSegmentLineBreak({ segmentIndex: 0, interchangeIndex: 0 }); ``` *** ### isAcceptDisposition() > **isAcceptDisposition**(`code`): `boolean` True when the supplied disposition is some form of "accept" (`A`, `E`, `P`) rather than reject (`R`, `M`, `W`, `X`). Used by the build-time safety guard and by consumer code that wants a yes/no on the inbound. #### Parameters ##### code [`X12AckDispositionCode`](#x12ackdispositioncode) #### Returns `boolean` #### Example ```ts import { isAcceptDisposition } from "@cosyte/x12"; isAcceptDisposition("A"); // true isAcceptDisposition("R"); // false ``` *** ### isaExtraElementSeparator() > **isaExtraElementSeparator**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_ISA_EXTRA_ELEMENT_SEPARATOR` warning. Emitted when the ISA element area (bytes 0..104) splits on the detected element separator into anything other than `["ISA", e1, …, e16]`. `detectDelimiters` has already verified that the separator sits at all 16 fixed 005010 positions, so the split can only come out LONGER than 17: an ISA element value is itself carrying the byte that was declared in-band as the element separator. The interchange is not 005010-conformant either way, and the parser does not choose between the two readings of that byte - it reports that the header did not frame and leaves `isa.elements` exactly as the split produced it, with `isa.raw` carrying all 106 bytes verbatim. `decodeEnvelope` raises this ahead of every warning it raises after it, because when it is present the ISA-derived diagnostics that follow (`X12_PRE_005010` off `elements[12]`, `X12_CONTROL_NUMBER_MISMATCH` off `elements[13]`) may be reading a displaced element rather than the one they name. **That is a statement about `parseX12`'s `warnings` (and `onWarning`) and about nothing else.** `serializeX12(ix, { specClean: true })` runs its own reconciliation off `interchange.isa.elements[13]` with no arity awareness and never raises this code, so on that channel a lone `X12_CONTROL_NUMBER_MISMATCH` can be a displaced read - and its absence is not evidence the header framed. Filed, not fixed here. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { isaExtraElementSeparator } from "@cosyte/x12"; const w = isaExtraElementSeparator({ segmentIndex: 0, interchangeIndex: 0 }); ``` *** ### isClaimAdjustmentGroupCode() > **isClaimAdjustmentGroupCode**(`value`): `value is ClaimAdjustmentGroupCode` Narrow an inbound CAS-01 string to a [ClaimAdjustmentGroupCode](#claimadjustmentgroupcode). `false` means the inbound code is not one of the 4 spec-defined values (e.g. a typo `"CR"` from a quirky payer); the verbatim value is still preserved on the parsed model, and the consumer can branch on the narrow result. #### Parameters ##### value `string` #### Returns `value is ClaimAdjustmentGroupCode` #### Example ```ts import { isClaimAdjustmentGroupCode } from "@cosyte/x12"; isClaimAdjustmentGroupCode("PR"); // true isClaimAdjustmentGroupCode("CR"); // false (unknown) ``` *** ### isDiagnosisQualifier() > **isDiagnosisQualifier**(`qualifier`): `boolean` Decide whether an HI qualifier represents a diagnosis (principal, secondary, admitting, reason-for-visit, external-cause). Used by variant-specific extractors that split the HI segment family into diagnoses vs procedures. #### Parameters ##### qualifier `string` #### Returns `boolean` #### Example ```ts import { isDiagnosisQualifier } from "@cosyte/x12"; isDiagnosisQualifier("ABK"); // true (principal diagnosis) isDiagnosisQualifier("BBR"); // false (procedure) isDiagnosisQualifier("XYZ"); // false ``` *** ### isProcedureQualifier() > **isProcedureQualifier**(`qualifier`): `boolean` Decide whether an HI qualifier represents a procedure (principal or other) - current ICD-10-PCS or legacy ICD-9-PCS. #### Parameters ##### qualifier `string` #### Returns `boolean` #### Example ```ts import { isProcedureQualifier } from "@cosyte/x12"; isProcedureQualifier("BBR"); // true (other procedure, ICD-10-PCS) isProcedureQualifier("ABK"); // false (diagnosis) ``` *** ### levelDetached() > **levelDetached**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_270_LEVEL_DETACHED` warning. Raised where a 270 hierarchical level's declared parent pointer did not resolve to a level of the parent kind the TR3 gives it, so the level and everything transmitted beneath it is absent from the returned tree. It reports the LOSS. The defect in the pointer itself is reported by its own code at the same position, so a level named here always carries one of [hlParentMismatch](#hlparentmismatch), [hlParentLevelInvalid](#hlparentlevelinvalid) or [hierarchyCycle](#hierarchycycle) beside it. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { levelDetached } from "@cosyte/x12"; const w = levelDetached({ segmentIndex: 5, transactionIndex: 0 }); ``` *** ### missingGe() > **missingGe**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_MISSING_GE` warning. Emitted when a GS opened a functional group but no matching GE appeared before the next GS or IEA. The parser returns the group with `ge: undefined` and the transactions it managed to collect. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { missingGe } from "@cosyte/x12"; const w = missingGe({ segmentIndex: 3, interchangeIndex: 0, groupIndex: 0 }); ``` *** ### missingIea() > **missingIea**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_MISSING_IEA` warning. Emitted when the input opened a valid ISA but EOF arrived before any IEA segment. The parser returns the groups it managed to decode with `iea: undefined`; the warning surfaces the structural break so consumers know the interchange is truncated. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { missingIea } from "@cosyte/x12"; const w = missingIea({ segmentIndex: 4, interchangeIndex: 0 }); ``` *** ### missingRequiredLoop() > **missingRequiredLoop**(`position`, `loop`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_MISSING_REQUIRED_LOOP` warning. Emitted when a TR3-required loop is structurally absent (e.g. no Loop 2010BB Payer Name inside a Subscriber HL). The parser does not enforce situational rules: only loops marked `usage: "required"` in the loop spec fire this warning, so the loop id and its rationale are both library constants. #### Parameters ##### position [`X12Position`](#x12position) ##### loop [`X12RequiredLoop`](#x12requiredloop) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { missingRequiredLoop, REQUIRED_LOOPS } from "@cosyte/x12"; const w = missingRequiredLoop( { segmentIndex: 12, groupIndex: 0, transactionIndex: 0 }, REQUIRED_LOOPS.PAYER_NAME_2010BB, ); ``` *** ### missingSe() > **missingSe**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_MISSING_SE` warning. Emitted when an ST opened a transaction set but no matching SE appeared before the next ST, GE, or IEA. The parser returns the transaction with `se: undefined` and the segments it managed to collect. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { missingSe } from "@cosyte/x12"; const w = missingSe({ segmentIndex: 2, groupIndex: 0, transactionIndex: 0 }); ``` *** ### nonConventionalDelimiter() > **nonConventionalDelimiter**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_270_NON_CONVENTIONAL_DELIMITER` warning. Raised by the 270 reader, once per 270 transaction set, where the delimiter set the shared interchange parse detected out of the ISA is not the conventional one. **The 270 path raises it and shared code does not**, which is deliberate rather than incidental. Both halves of the tolerance this code reports live in the ENVELOPE, so the obvious place to detect them is the shared parse, and raising it there would give every pre-existing non-270 fixture that declares a non-conventional separator a warning it did not have. This reader TAKES the detected set from that parse and reports its own tolerance of it instead; nothing about delimiter handling in shared code moves. `position` is the ISA, which is where the set is declared. No byte is echoed and no role is named: `isa.raw` carries all 106 bytes. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { nonConventionalDelimiter } from "@cosyte/x12"; const w = nonConventionalDelimiter({ segmentIndex: 0, interchangeIndex: 0 }); ``` *** ### parse270Inquiries() > **parse270Inquiries**(`raw`, `options?`): readonly [`X12Inquiry`](#x12inquiry)[] Decode every 270 in a raw interchange, in transmitted order. Returns one [X12Inquiry](#x12inquiry) per 270 transaction set, each with its own model and its own warnings, and an EMPTY list when the interchange carries no 270 at all. Two 270s are never merged into one model and the first is never returned in place of the rest. This is also the entry point that reports an inter-segment line break (`X12_270_INTER_SEGMENT_LINE_BREAK`): the shared parse consumes a run of CR or LF before the next segment opens, so it is visible in the bytes and nowhere on the model, and [get270Inquiry](#get270inquiry) is handed the model. A space or a tab between segments is a different case: the shared parse does not consume it, the group never frames, and this function answers the empty list with the loss reported on the interchange's own warning channel. A structural fatal from the shared parse (an input truncated before its ISA is readable, say) is raised by that parse and passes through here unchanged: it is deliberately NOT caught, downgraded or re-raised, because the frame not parsing is a different fact from a 270 body being incomplete, and the lenient guarantee of this reader begins where the frame ended. #### Parameters ##### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ##### options? [`X12ParseOptions`](#x12parseoptions) = `{}` #### Returns readonly [`X12Inquiry`](#x12inquiry)[] #### Example ```ts import { parse270Inquiries } from "@cosyte/x12"; const inquiries = parse270Inquiries(rawBytes); inquiries.length; // 0 when the interchange holds no 270 inquiries[0]?.informationSources.length; ``` *** ### parse276StatusInquiries() > **parse276StatusInquiries**(`raw`, `options?`): readonly [`X12StatusInquiry`](#x12statusinquiry)[] Decode every 276 in a raw interchange, in transmitted order. Returns one [X12StatusInquiry](#x12statusinquiry) per 276 transaction set, each with its own model and its own warnings, and an EMPTY list when the interchange carries no 276 at all. Two 276s are never merged into one model and the first is never returned in place of the rest. **No warning is raised about an interchange that carries no 276.** An interchange full of 277s is not a defective 276 and this reader says nothing about it: the empty list IS the answer. A structural fatal from the shared parse (an input truncated before its ISA is readable, say) is raised by that parse and passes through here unchanged: it is deliberately NOT caught, downgraded or re-raised, because the frame not parsing is a different fact from a 276 body being incomplete, and the lenient guarantee of this reader begins where the frame ended. #### Parameters ##### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ##### options? [`X12ParseOptions`](#x12parseoptions) = `{}` #### Returns readonly [`X12StatusInquiry`](#x12statusinquiry)[] #### Example ```ts import { parse276StatusInquiries } from "@cosyte/x12"; const inquiries = parse276StatusInquiries(rawBytes); inquiries.length; // 0 when the interchange holds no 276 inquiries[0]?.informationSources.length; ``` *** ### parse999() > **parse999**(`raw`, `options?`): [`X12Ack999`](#x12ack999) \| `undefined` Decode a 005010X231A1 Implementation Acknowledgment from a raw `string` or `Buffer`. Returns the typed [X12Ack999](#x12ack999) or `undefined` when the input does not contain any 999 transaction set. Lenient on parse: every recoverable deviation surfaces as an envelope-level warning (see [X12Ack999.warnings](#warnings-1)). #### Parameters ##### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ##### options? [`X12ParseOptions`](#x12parseoptions) = `{}` #### Returns [`X12Ack999`](#x12ack999) \| `undefined` #### Example ```ts import { parse999 } from "@cosyte/x12"; const ack = parse999(rawBytes); if (ack !== undefined) { ack.ak9.disposition; // "A" | "E" | "P" | "R" | "M" | "W" | "X" ack.transactionResponses[0]?.ik5.disposition; } ``` *** ### parseTA1() > **parseTA1**(`interchange`): [`X12AckTA1`](#x12ackta1) \| `undefined` Decode the first TA1 Interchange Acknowledgment on the supplied `interchange`. Returns the typed [X12AckTA1](#x12ackta1) or `undefined` when the interchange has no TA1 (the common case for non-ack inbounds). #### Parameters ##### interchange [`X12Interchange`](#x12interchange) #### Returns [`X12AckTA1`](#x12ackta1) \| `undefined` #### Example ```ts import { parseTA1, parseX12 } from "@cosyte/x12"; const ix = parseX12(rawAckBytes); const ta1 = parseTA1(ix); if (ta1?.ackCode === "R") { // inbound interchange was rejected } ``` *** ### parseX12() **`Internal`** - implementation signature; overload signatures above carry the public JSDoc + @example. #### Call Signature > **parseX12**(`raw`): [`X12Interchange`](#x12interchange) Parse a raw X12 healthcare interchange (string or `Buffer`) into an [X12Interchange](#x12interchange). The parser is lenient by default: recoverable deviations from 005010 are reported via `ix.warnings` and (optionally) `options.onWarning`, never thrown. Four unrecoverable structural errors throw [X12ParseError](#x12parseerror): `X12_EMPTY_INPUT`, `X12_NO_ISA_HEADER`, `X12_ISA_TOO_SHORT`, `X12_INVALID_DELIMITERS`. Opt into strict mode with `{ strict: true }` to escalate every Tier-2 warning into an `X12ParseError` carrying the warning's code. A strict-mode escalation carries an empty `snippet`: the warning it wraps is registry-built and fully located by `position`, so there is nothing to redact. The envelope decode reads ISA / GS / ST / SE / GE / IEA and detects the four delimiters from fixed ISA byte positions. Transaction-set bodies inside each ST..SE are kept **opaque** there - `tx.segments` carries the raw segment strings (terminator stripped). Segment decode adds element/composite/repetition decode on top. ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ##### Returns [`X12Interchange`](#x12interchange) ##### Example ```ts import { parseX12, WARNING_CODES } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER *250101*1200*^*00501*000000001*0*P*:~GS*HC*S*R*20250101*1200*1*X*005010X222A2~ST*837*0001~SE*2*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); ix.delimiters.element; // "*" ix.delimiters.component; // ":" ix.delimiters.segment; // "~" ix.groups[0]?.gs.elements[1]; // raw text of GS-01 (pre-?-unescape) for (const w of ix.warnings) { if (w.code === WARNING_CODES.X12_PRE_005010) { // sender on pre-005010 version family } } ``` #### Call Signature > **parseX12**(`raw`, `options`): [`X12Interchange`](#x12interchange) Parse a raw X12 healthcare interchange (string or `Buffer`) into an [X12Interchange](#x12interchange). The parser is lenient by default: recoverable deviations from 005010 are reported via `ix.warnings` and (optionally) `options.onWarning`, never thrown. Four unrecoverable structural errors throw [X12ParseError](#x12parseerror): `X12_EMPTY_INPUT`, `X12_NO_ISA_HEADER`, `X12_ISA_TOO_SHORT`, `X12_INVALID_DELIMITERS`. Opt into strict mode with `{ strict: true }` to escalate every Tier-2 warning into an `X12ParseError` carrying the warning's code. A strict-mode escalation carries an empty `snippet`: the warning it wraps is registry-built and fully located by `position`, so there is nothing to redact. The envelope decode reads ISA / GS / ST / SE / GE / IEA and detects the four delimiters from fixed ISA byte positions. Transaction-set bodies inside each ST..SE are kept **opaque** there - `tx.segments` carries the raw segment strings (terminator stripped). Segment decode adds element/composite/repetition decode on top. ##### Parameters ###### raw `string` \| `Buffer`\<`ArrayBufferLike`\> ###### options [`X12ParseOptions`](#x12parseoptions) ##### Returns [`X12Interchange`](#x12interchange) ##### Example ```ts import { parseX12, WARNING_CODES } from "@cosyte/x12"; const raw = "ISA*00* *00* *ZZ*SENDER *ZZ*RECEIVER *250101*1200*^*00501*000000001*0*P*:~GS*HC*S*R*20250101*1200*1*X*005010X222A2~ST*837*0001~SE*2*0001~GE*1*1~IEA*1*000000001~"; const ix = parseX12(raw); ix.delimiters.element; // "*" ix.delimiters.component; // ":" ix.delimiters.segment; // "~" ix.groups[0]?.gs.elements[1]; // raw text of GS-01 (pre-?-unescape) for (const w of ix.warnings) { if (w.code === WARNING_CODES.X12_PRE_005010) { // sender on pre-005010 version family } } ``` *** ### partitionWarnings() > **partitionWarnings**(`warnings`, `profile`): [`X12WarningPartition`](#x12warningpartition) Split a parse's warnings against a profile's expected-warning union. A warning whose `code` is in the profile's `expectedWarnings` lands in `expected`; everything else lands in `unexpected`. Order within each bucket preserves the input order. #### Parameters ##### warnings readonly [`X12ParseWarning`](#x12parsewarning)[] ##### profile [`X12Profile`](#x12profile) #### Returns [`X12WarningPartition`](#x12warningpartition) #### Example ```ts import { parseX12, partitionWarnings, profiles } from "@cosyte/x12"; const ix = parseX12(raw, { profile: profiles.availity }); const { expected, unexpected } = partitionWarnings(ix.warnings, profiles.availity); if (unexpected.length > 0) flagForReview(unexpected); ``` *** ### payToAddressRepeated() > **payToAddressRepeated**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_PAY_TO_ADDRESS_REPEATED` warning. Emitted by the 837 helper at the second and each subsequent `NM1*87` within one Loop 2000A, where the TR3s allow Loop 2010AB at most once. `position` names the repeated `NM1*87` itself, which is the segment a consumer resolves back through `tx.segments`; there is no `elementIndex`, because what is being reported is a second occurrence of the segment rather than a defect in any element of it. Once per repeat, so two repeats in one Loop 2000A are two warnings. The counter resets at the Loop 2000A `HL`, beside the pay-to slot it guards - a first `NM1*87` under a later billing provider is a first, not a repeat. It reports that the DOCUMENT named the pay-to address more than once. It does NOT report that anything was mis-read, and it is not a service-line or entity-segment code: nothing else on the channel says this, and this says nothing about the other 837 codes' subjects. **The rule the reader applies, because a consumer cannot infer it from a one-slot model:** occurrences are never merged, the last occurrence that states an address of its own wins, and an occurrence that states none does not blank one that did. "States an address" means exactly what the emit side would write a segment for - see `./address-segments.ts`, which both sides share so they cannot drift. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { payToAddressRepeated } from "@cosyte/x12"; const w = payToAddressRepeated({ segmentIndex: 12, transactionIndex: 0 }); ``` *** ### pre005010() > **pre005010**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_PRE_005010` warning. Emitted when the twelfth element of the ISA split does not read `00501`, the HIPAA-mandated baseline interchange control version number. The code name reads as a "pre-005010" test and the guard is an inequality, so a LATER family (`00602`, `00700`) raises it too. The parser still accepts the input (Postel's Law: lenient on parse) but flags the mismatch so consumers know the input may diverge from 005010 semantics. The guard reads that element, so raising this code does NOT establish that the header split into `ISA` plus 16 elements and does not assert what ISA-12 itself declares. Where `X12_ISA_EXTRA_ELEMENT_SEPARATOR` is also present the element read may be a displaced one; `isa.raw` carries all 106 bytes and with the ISA fixed widths is the route back. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { pre005010 } from "@cosyte/x12"; const w = pre005010({ segmentIndex: 0, interchangeIndex: 0, elementIndex: 12 }); ``` *** ### readElementDecimal() > **readElementDecimal**(`seg`, `n`, `delimiters`): [`X12DecimalRead`](#x12decimalread) Read element N (1-indexed) as an [X12Decimal](#x12decimal) and say WHY when there is no value. This is the chokepoint every decimal read in the library goes through, and the only surface that distinguishes an element the sender omitted from one this library could not decode. Pure: it never warns and never throws. [elementDecimal](#elementdecimal) and [elementDecimalOrZero](#elementdecimalorzero) wrap it and add the diagnostic. #### Parameters ##### seg [`X12Segment`](#x12segment) ##### n `number` ##### delimiters [`Delimiters`](#delimiters) #### Returns [`X12DecimalRead`](#x12decimalread) #### Example ```ts import { readElementDecimal } from "@cosyte/x12"; readElementDecimal(seg, 2, delim); // { value: undefined, status: "unparseable" } ``` *** ### remitBalanceMismatch() > **remitBalanceMismatch**(`position`, `invariant`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_835_REMIT_BALANCE_MISMATCH` warning. Emitted by the 835 helper when a TR3 X221A1 §1.10.2 balance invariant fails. `invariant` names which equation broke; the spec'd, computed and delta amounts stay on the model as "../decimal.js".X12Decimal values and are never rendered into the message. The parser ALWAYS surfaces this and NEVER silently rebalances. To report the numbers, recompute them from the model: `claim.totalChargeAmount`, `claim.totalPaymentAmount` and the CAS adjustments are all present. #### Parameters ##### position [`X12Position`](#x12position) ##### invariant [`X12BalanceInvariant`](#x12balanceinvariant) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { remitBalanceMismatch, BALANCE_INVARIANTS } from "@cosyte/x12"; const w = remitBalanceMismatch( { segmentIndex: 12, groupIndex: 0, transactionIndex: 0 }, BALANCE_INVARIANTS.CLAIM, ); ``` *** ### renderCallerValue() > **renderCallerValue**(`value`): `string` Render a caller-supplied value as a bounded, quoted fragment for a `build*` refusal message. **This is the only sanctioned route a caller value takes into a thrown message**, and `test/builder-refusal-bounds.test.ts` scans `src/` for any other route - a twenty-fifth refusal site that interpolates a value directly reds that test. That scan keys on `throw new *BuildError(` and on template-literal holes, so it is a strong tripwire for the shape this library uses rather than a proof over every shape one could write. Returns the value quoted when it fits, and otherwise the first [BUILD\_REFUSAL\_VALUE\_MAX\_LENGTH](#build_refusal_value_max_length) characters, an ellipsis, and the true length - the length is the diagnostically useful part when the value is over-long, since that is usually *why* the builder refused. #### Parameters ##### value `string` \| `number` #### Returns `string` #### Example ```ts import { renderCallerValue } from "@cosyte/x12"; renderCallerValue("000000001"); // '"000000001"' renderCallerValue("9".repeat(120000)); // '"999…" (120000 characters)' ``` *** ### resolveHiQualifier() > **resolveHiQualifier**(`qualifier`): `HiQualifierEntry` \| `undefined` Resolve an HI qualifier string into a HiQualifierEntry, or `undefined` if the qualifier is outside the bundled snapshot. Unknown qualifiers still parse - the parser preserves the verbatim qualifier on the diagnosis/procedure and emits `X12_UNKNOWN_HI_QUALIFIER`. The lookup is case-sensitive (TR3 qualifiers are uppercase). #### Parameters ##### qualifier `string` #### Returns `HiQualifierEntry` \| `undefined` #### Example ```ts import { resolveHiQualifier } from "@cosyte/x12"; resolveHiQualifier("ABK")?.system; // "ICD-10-CM" resolveHiQualifier("ABK")?.category; // "principal-diagnosis" resolveHiQualifier("XYZ"); // undefined ``` *** ### rfaiHeaderAbsent() > **rfaiHeaderAbsent**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_277_RFAI_HEADER_ABSENT` warning. Raised by `get277RequestForAdditionalInformation` when the transaction set carries no BHT, so the reading's `header` is left undefined rather than filled in. The reader anchors it at the ST (`segmentIndex: 0`), because an absent segment has no position of its own. It takes a position and nothing else. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { rfaiHeaderAbsent } from "@cosyte/x12"; const w = rfaiHeaderAbsent({ segmentIndex: 0, transactionIndex: 0 }); w.code; // "X12_277_RFAI_HEADER_ABSENT" ``` *** ### rfaiLevelAbsent() > **rfaiLevelAbsent**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_277_RFAI_LEVEL_ABSENT` warning. Raised by `get277RequestForAdditionalInformation` when the transaction set carries no HL segment, so the reading's `levels` list is empty and no level is synthesized. The reader anchors it at the ST (`segmentIndex: 0`). It takes a position and nothing else. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { rfaiLevelAbsent } from "@cosyte/x12"; const w = rfaiLevelAbsent({ segmentIndex: 0, transactionIndex: 0 }); w.code; // "X12_277_RFAI_LEVEL_ABSENT" ``` *** ### rfaiRequestAbsent() > **rfaiRequestAbsent**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_277_RFAI_REQUEST_ABSENT` warning. Raised by `get277RequestForAdditionalInformation` when no level on the reading carries a claim-level request, so the reading names no claim and no requested item. The reader anchors it at the ST (`segmentIndex: 0`). It takes a position and nothing else. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { rfaiRequestAbsent } from "@cosyte/x12"; const w = rfaiRequestAbsent({ segmentIndex: 0, transactionIndex: 0 }); w.code; // "X12_277_RFAI_REQUEST_ABSENT" ``` *** ### segmentCountMismatch() > **segmentCountMismatch**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_SEGMENT_COUNT_MISMATCH` warning. Emitted by the spec-clean serializer when an SE-01 value does not equal the actual number of segments in the transaction set it closes (ST through SE inclusive). Corrected counts are emitted only on `serializeX12(ix, { specClean: true, recomputeCounts: true })`. The parser does not emit this code (it leaves SE-01 reconciliation to the emit half); it is a serializer diagnostic. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { segmentCountMismatch } from "@cosyte/x12"; const w = segmentCountMismatch({ segmentIndex: 2, transactionIndex: 0, elementIndex: 1 }); ``` *** ### serializeX12() > **serializeX12**(`interchange`, `opts?`): `string` Serialize an [X12Interchange](#x12interchange) back to an X12 byte stream. Pure function - never throws, never mutates the input, never performs I/O. See the module header for the two emit modes. #### Parameters ##### interchange [`X12Interchange`](#x12interchange) ##### opts? [`SerializeOptions`](#serializeoptions) = `{}` #### Returns `string` #### Example ```ts import { parseX12, serializeX12 } from "@cosyte/x12"; const ix = parseX12(raw); // Segments on the model come back verbatim, orphans included. Anything the // parser did not record (line breaks, a doubled terminator, ...) does not, // so this is not guaranteed to equal `raw`. See the module header. const bytes = serializeX12(ix); ``` *** ### serviceLineDropped() > **serviceLineDropped**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_SERVICE_LINE_DROPPED` warning. Emitted by the 837 helper when an LX opens no Loop 2400 at all, so the service line never reaches any claim's `serviceLines`: either no CLM is open at that point in the walk, or the submission's variant is not one of `P` / `I` / `D` and there is no variant-specific line shape to build. That second cause is reachable WITHOUT `X12_837_UNKNOWN_VARIANT`, because a caller-supplied `opts.type` outside the union (from JavaScript or a JSON payload) is a variant this reader never resolved and never warned about. Distinct from [serviceLineNotDecoded](#servicelinenotdecoded), where the line IS retained and only its service segment went unread. `position` names the LX itself - the same anchor, for the same reason: it is the one segment present in every case. Nothing is fabricated to stand in, and the segments stay verbatim on the transaction set. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { serviceLineDropped } from "@cosyte/x12"; const w = serviceLineDropped({ segmentIndex: 7, transactionIndex: 0 }); ``` *** ### serviceLineNotDecoded() > **serviceLineNotDecoded**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_SERVICE_LINE_NOT_DECODED` warning. Emitted by the 837 helper when a Loop 2400 service line is closed without ever having decoded an SV1 / SV2 / SV3 for the resolved variant: either the line carries no SVx at all, or it carries one belonging to a different 837 variant than ST-03 (or the caller's `type` option) named. The line is still retained, and every segment stays verbatim on the transaction set, but the line's `charge` and `units` are `undefined` rather than anything read off the wire. Through `0.0.12` they were the accumulator's seeded `X12Decimal.ZERO`, which a consumer could not tell from a charge of zero the sender did state. `position` names the LX segment that opened the line. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { serviceLineNotDecoded } from "@cosyte/x12"; const w = serviceLineNotDecoded({ segmentIndex: 9, transactionIndex: 0 }); ``` *** ### serviceSegmentRepeated() > **serviceSegmentRepeated**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_SERVICE_SEGMENT_REPEATED` warning. Emitted by the 837 helper at the second and each subsequent `SV1` / `SV2` / `SV3` to arrive inside one open Loop 2400. `position` names the repeated service segment itself, which is the segment a consumer resolves back through `tx.segments`; there is no `elementIndex`, because what is reported is a second occurrence of the segment rather than a defect in any element of it. Once per repeat, so three service segments in one Loop 2400 are two warnings. The count lives on the line and is cleared when the line flushes, so a first service segment under a later `LX` is a first and never a repeat - a scope, not a latch. **The rule the reader applies, because a consumer cannot infer it from a one-slot model:** occurrences are never merged and the LAST one matching the submission's resolved variant wins, writing every slot its kind writes. So an earlier matching occurrence leaves nothing on the line, not even in a slot the later one's own element is absent from - there the later one's `undefined` replaces an amount the earlier one stated. Through `0.0.13` a charge of `8500` and a CPT of `99213` were replaced by a repeat's `12` and `99999` with `warnings: []`. An occurrence whose kind does NOT match the resolved variant is read into nothing and overwrites nothing. It reports that the DOCUMENT sent more than one, and asserts nothing about what usage the TR3s give the segment. It does not decide which occurrence the sender meant: this reader cannot tell a stray service segment from a conformant one, exactly as [ambiguous837Variant](#ambiguous837variant) records for the variant fallback. Disjoint from [serviceSegmentWithoutLx](#servicesegmentwithoutlx) by construction - that one fires only where no Loop 2400 is open and this one only where one is - so the two can never name the same segment. Read that as disjointness only: a service segment following an `LX` that opened no line is named by NEITHER, because [serviceLineDropped](#servicelinedropped) at that `LX` already reports the loss and suppresses the orphan code. [serviceLineNotDecoded](#servicelinenotdecoded) is raised at the `LX` and reports that no matching service segment decoded onto the line at all; a document can carry both codes on different segments. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { serviceSegmentRepeated } from "@cosyte/x12"; const w = serviceSegmentRepeated({ segmentIndex: 9, transactionIndex: 0 }); ``` *** ### serviceSegmentWithoutLx() > **serviceSegmentWithoutLx**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_SERVICE_SEGMENT_WITHOUT_LX` warning. Emitted by the 837 helper when an SV1 / SV2 / SV3 arrives with no Loop 2400 open, so there is no service line to decode it into and nothing the segment carries is read. `position` names the service segment itself, because it is the only segment the case has: the other two 837 service-line codes both anchor at an LX, and there is no LX in scope here to anchor to. An LX may still appear elsewhere in the transaction - in an earlier claim, say - so read the condition as "no line was open", not as "the file has no LX". The three do not overlap on one segment. [serviceLineDropped](#servicelinedropped) is raised at an LX that opened no line, [serviceLineNotDecoded](#servicelinenotdecoded) at an LX whose line was retained undecoded, and this one only where no line is open. Nothing is fabricated to stand in and no line or claim is synthesized; the segments stay verbatim on the transaction set. It says nothing about the submission's variant. A caller-supplied `type` option wins first; absent one, and where ST-03 names no known implementation convention, the reader falls back to the first SV1 / SV2 / SV3 in the transaction, and a segment reported here is eligible for that fallback like any other - pre-existing behaviour, documented in `KNOWN-LIMITATIONS.md` and unchanged. Where that fallback decided the variant and the body names more than one, [ambiguous837Variant](#ambiguous837variant) reports the resolution as contested; it is additive and does not change which documents reach this code. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { serviceSegmentWithoutLx } from "@cosyte/x12"; const w = serviceSegmentWithoutLx({ segmentIndex: 8, transactionIndex: 0 }); ``` *** ### setDefaultProfile() > **setDefaultProfile**(`profile`): `void` Register a process-scoped default profile. `parseX12(raw)` (no explicit profile arg) consults `getDefaultProfile()` and attaches the returned profile to the result. Pass `null` (or `undefined`) to clear. Explicit args ALWAYS win - `parseX12(raw, { profile: myProfile })` uses `myProfile` regardless of the default; `parseX12(raw, { profile: null })` opts out of the default for a single call without changing the registered default. **Test hygiene:** the only mutable module-scoped state in the library. Tests that call this MUST clean up in `afterEach` (`setDefaultProfile(null)`). #### Parameters ##### profile [`X12Profile`](#x12profile) \| `null` #### Returns `void` #### Example ```ts import { setDefaultProfile, getDefaultProfile, profiles, parseX12 } from "@cosyte/x12"; setDefaultProfile(profiles.availity); const ix = parseX12(raw); ix.profile?.name; // "availity" setDefaultProfile(null); // clear (or in test teardown) ``` *** ### statedAmountDiscarded() > **statedAmountDiscarded**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_STATED_AMOUNT_DISCARDED` warning. Emitted where a segment POPULATED its amount element, the loop that would carry its row was open, and the reader built no row anyway - for a reason that is not a failure to decode that amount. The money the sender wrote is on no part of the typed model. **This code asserts nothing about whether that amount is decodable**: route 2 below decoded it, route 1 never attempted the decode. Two routes, enumerated because a count without its list cannot correct itself: 1. An 820 `RMR` under an open remittance loop whose RMR-01 and RMR-02 are BOTH empty while RMR-04 or RMR-05 is populated. `decodeRmr` refuses the open item on identity before either amount element is read, so a stated payment, a stated amount due and the payment action code beside them are lost together. 2. An 837 `AMT` arriving while a Loop 2430 line adjudication (`SVD`) is open, whose AMT-02 decoded. The v1 adjudication model carries no amount row, so the row is skipped rather than attached to the service line, which is this reader's own line and not the other payer's. `position` names the `RMR` / `AMT` segment itself and carries NO `elementIndex`: on the first route the loss spans RMR-04 and RMR-05 and no single element names it, and on the second the element is fixed by the segment. Read the bound as a property of the READ. This does NOT report an `AMT` or `ADX` that reaches a reader with no loop open to carry its row at all: the 834's `AMT` with no `HD` open, the 820's `ADX` with no remittance open, and the 835's and the 837's `AMT` that decodes before any claim or service line is open are all still silent, and recorded in `KNOWN-LIMITATIONS.md`. It is additive: [unparseableDecimal](#unparseabledecimal) and [amountRowDropped](#amountrowdropped) fire on exactly the documents they fired on before. On route 1 no `X12_UNPARSEABLE_DECIMAL` accompanies this code even where the amount bytes are unreadable, because that route never attempts the decode - so never read an unaccompanied instance as evidence the bytes are postable. Read them off the segment and decode them yourself. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { statedAmountDiscarded } from "@cosyte/x12"; const w = statedAmountDiscarded({ segmentIndex: 9, transactionIndex: 0 }); ``` *** ### statusInquiryDateRowDropped() > **statusInquiryDateRowDropped**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_276_DATE_ROW_DROPPED` warning. The 276's sibling of [dateRowDropped](#daterowdropped), raised where a 276's DTP reached the reader short of its qualifier (DTP-01) or its value (DTP-03), so no date row was built and the whole segment, the format qualifier included, is absent from the typed model. A DTP is a RECORD and not a slot, which is why the loss is the row rather than one element, and why it is reported at all. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { statusInquiryDateRowDropped } from "@cosyte/x12"; const w = statusInquiryDateRowDropped({ segmentIndex: 9, transactionIndex: 0 }); ``` *** ### statusInquiryDuplicateHierarchyId() > **statusInquiryDuplicateHierarchyId**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_276_DUPLICATE_HIERARCHY_ID` warning. The 276's sibling of [duplicateHierarchyId](#duplicatehierarchyid), raised at the second and each subsequent HL in a 276 transaction set to carry an HL-01 an earlier one already carried. A sibling rather than a widening of the 270's code, and for the reason the builder error classes are siblings too: a consumer narrowing on `X12_270_DUPLICATE_HIERARCHY_ID` would start seeing claim-status requests on a predicate they wrote for eligibility inquiries, which is a silent change to a published surface. One code per direction, additions only. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { statusInquiryDuplicateHierarchyId } from "@cosyte/x12"; const w = statusInquiryDuplicateHierarchyId({ segmentIndex: 7, transactionIndex: 0 }); ``` *** ### statusInquiryHierarchyCycle() > **statusInquiryHierarchyCycle**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_276_HIERARCHY_CYCLE` warning. The 276's sibling of [hierarchyCycle](#hierarchycycle), raised where the chain of HL-02 parent pointers from the level at `position` returns to a level already on that chain. Disjoint from [hlParentMismatch](#hlparentmismatch) by construction: this one requires every pointer on the chain to name a level that IS present, and that one reports a pointer that names none. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { statusInquiryHierarchyCycle } from "@cosyte/x12"; const w = statusInquiryHierarchyCycle({ segmentIndex: 5, transactionIndex: 0 }); ``` *** ### statusInquiryLevelDetached() > **statusInquiryLevelDetached**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_276_LEVEL_DETACHED` warning. The 276's sibling of [levelDetached](#leveldetached), raised where a 276 hierarchical level's declared parent pointer did not resolve to a level of the parent kind the TR3 gives it, so the level and everything transmitted beneath it is absent from the returned tree. It reports the LOSS. The defect in the pointer itself is reported by its own code at the same position, so a level named here always carries one of [hlParentMismatch](#hlparentmismatch), [hlParentLevelInvalid](#hlparentlevelinvalid) or [statusInquiryHierarchyCycle](#statusinquiryhierarchycycle) beside it. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { statusInquiryLevelDetached } from "@cosyte/x12"; const w = statusInquiryLevelDetached({ segmentIndex: 5, transactionIndex: 0 }); ``` *** ### statusInquiryReferenceRowDropped() > **statusInquiryReferenceRowDropped**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_276_REFERENCE_ROW_DROPPED` warning. Raised where a 276's REF reached the reader short of its qualifier (REF-01) or its identifier (REF-02), so no reference row was built and the whole segment, the description included, is absent from the typed model. A REF is a RECORD and not a slot for the same reason a DTP is: a qualifier with no identifier states nothing that can be looked up, and an identifier with no qualifier states nothing about WHAT was identified. Building half a row from either would put a value on the model the sender did not state. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { statusInquiryReferenceRowDropped } from "@cosyte/x12"; const w = statusInquiryReferenceRowDropped({ segmentIndex: 9, transactionIndex: 0 }); ``` *** ### toDate() > **toDate**(`value`, `options?`): `Date` \| `undefined` An X12 date carrier as an absolute-instant `Date`, ONLY when the caller states the zone. No X12 date element this package decodes carries a UTC offset, so without `options.assumeOffsetMinutes` there is no instant and the answer is `undefined`. The host machine's timezone is never read and UTC is never assumed. With an offset supplied, midnight on that calendar day in that zone is returned: `0` yields UTC midnight and `-300` yields 05:00Z the same day. The time is filled to midnight for INSTANT CONSTRUCTION ONLY. The carrier's own precision is unchanged, and `toObject` and `toISO` on the same value return exactly what they returned before. A four-digit year below 100 stays that year: `0050` is year 50, never 1950. It never throws. #### Parameters ##### value [`X12DateValue`](#x12datevalue) \| `null` \| `undefined` ##### options? [`ToDateOptions`](#todateoptions) #### Returns `Date` \| `undefined` #### Example ```ts import { toDate } from "@cosyte/x12"; const day = { formatQualifier: "D8", value: "20260601" }; toDate(day); // undefined: no zone was stated toDate(day, { assumeOffsetMinutes: 0 }); // 2026-06-01T00:00:00.000Z toDate(day, { assumeOffsetMinutes: -300 }); // 2026-06-01T05:00:00.000Z ``` *** ### toISO() > **toISO**(`value`): `string` \| `undefined` An X12 date carrier as an ISO-8601 string truncated to the precision it stated, or `undefined`. A `D8` value renders `YYYY-MM-DD` and NOTHING is appended: the element stated no UTC offset, so the string is deliberately zone-less and no `Z` is fabricated. The digits are the sender's, verbatim, so a year below 100 keeps its leading zeroes. It never throws. #### Parameters ##### value [`X12DateValue`](#x12datevalue) \| `null` \| `undefined` #### Returns `string` \| `undefined` #### Example ```ts import { toISO } from "@cosyte/x12"; toISO({ formatQualifier: "D8", value: "20260601" }); // "2026-06-01" toISO({ formatQualifier: "D8", value: "00500101" }); // "0050-01-01" toISO({ formatQualifier: "RD8", value: "20260601-20260605" }); // undefined ``` *** ### toObject() > **toObject**(`value`): [`DateParts`](#dateparts) \| `undefined` The calendar components an X12 date carrier stated, or `undefined`. Returns a frozen plain object holding exactly `year`, `month` and `day` for a `D8` value; `month` is 1 to 12. There is no `qualifier`, `formatQualifier`, `precision`, `raw` or `valid` key, and no key holding `undefined`. A range, an unhandled or absent format qualifier, a value that does not match its declared shape, a calendar-invalid day, `null` and `undefined` all answer `undefined`. It never throws. #### Parameters ##### value [`X12DateValue`](#x12datevalue) \| `null` \| `undefined` #### Returns [`DateParts`](#dateparts) \| `undefined` #### Example ```ts import { toObject } from "@cosyte/x12"; toObject({ formatQualifier: "D8", value: "20260601" }); // { year: 2026, month: 6, day: 1 } toObject({ formatQualifier: "D8", value: "20260230" }); // undefined (February 30 is not a day) toObject({ formatQualifier: "RD8", value: "20260601-20260605" }); // undefined (a range) ``` *** ### trailingGarbage() > **trailingGarbage**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_TRAILING_GARBAGE` warning. Emitted when non-empty bytes appear after the IEA segment terminator and any optional CRLF. The bytes are preserved verbatim on "./types.js".X12Interchange.`trailingBytes` so consumers can inspect, measure, or re-emit them. Common cause: a second interchange concatenated into the same file (multi-ISA, out of v1 scope; only the first interchange is decoded). #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { trailingGarbage } from "@cosyte/x12"; const w = trailingGarbage({ segmentIndex: 6, interchangeIndex: 0 }); ``` *** ### transactionCountMismatch() > **transactionCountMismatch**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_TRANSACTION_COUNT_MISMATCH` warning. Emitted when GE-01 does not equal the actual number of ST..SE transaction sets present in the group. Both numbers stay on the model. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { transactionCountMismatch } from "@cosyte/x12"; const w = transactionCountMismatch({ segmentIndex: 4, groupIndex: 0, elementIndex: 1 }); ``` *** ### unescapeRelease() > **unescapeRelease**(`input`, `delimiters`, `emit`, `position`): `string` Reverse a single X12 release-character escape sequence. Consumes `?` plus its target byte when the target is one of the four detected delimiters or another `?`; preserves the `?` verbatim when the target is anything else (Postel's-Law tolerance - the spec leaves the `?A` case ambiguous and preserving the bytes is the most defensible behavior). Emits a single Tier-2 `X12_DANGLING_RELEASE_CHAR` warning when the input ends with a bare `?` (no target byte to escape) - the bytes are preserved verbatim so round-trip is still byte-exact. #### Parameters ##### input `string` ##### delimiters [`Delimiters`](#delimiters) ##### emit (`w`) => `void` ##### position [`X12Position`](#x12position) #### Returns `string` #### Example ```ts import { unescapeRelease } from "@cosyte/x12"; const d = { element: "*", repetition: "^", component: ":", segment: "~" }; unescapeRelease("a?~b", d, () => {}, { segmentIndex: 0 }); // "a~b" unescapeRelease("a??b", d, () => {}, { segmentIndex: 0 }); // "a?b" unescapeRelease("a?Xb", d, () => {}, { segmentIndex: 0 }); // "a?Xb" (preserved) ``` *** ### unexpectedSegment() > **unexpectedSegment**(`position`, `context`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_UNEXPECTED_SEGMENT` warning. Emitted when a structurally meaningful segment (`GE`, `SE`, `ST`, `TA1`, or a body segment) appears outside its expected parent. The parser preserves lenient-never-throw and continues; `context` names which structural rule broke and `position` locates the segment. The segment's own id is deliberately not a parameter. Note that a segment in one of these positions is NOT retained on the model at all (it has no open container to belong to), so `position.segmentIndex` against the input is the only way back to its bytes. For a segment that IS retained, the `X12Segment.id` a consumer reads is bounded to the X12 segment-id grammar (see "./segment.js".decodeSegment) while `seg.raw` and `seg.elements[0]` stay verbatim. #### Parameters ##### position [`X12Position`](#x12position) ##### context [`X12UnexpectedSegmentContext`](#x12unexpectedsegmentcontext) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unexpectedSegment, UNEXPECTED_SEGMENT_CONTEXTS } from "@cosyte/x12"; const w = unexpectedSegment( { segmentIndex: 12, interchangeIndex: 0 }, UNEXPECTED_SEGMENT_CONTEXTS.GE_WITHOUT_GS, ); ``` *** ### unknown837Variant() > **unknown837Variant**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_837_UNKNOWN_VARIANT` warning. Emitted when the 837 helper cannot resolve the variant from ST-03's implementation-convention reference AND no SVx service-line segment is present to fall back on. The parsed submission still ships with `variant: "unknown"`, and the walker does its best on shared structure (envelope, HL, claim header) and skips variant-specific service-line decoding. **This message deliberately points at no model field.** Recognition reads `ST-03` as FRAMED while `submission.implementationConventionReference` is decoded of any `?` release escape, so on a document that escapes a delimiter inside `ST-03` the two differ and the model field can hold an identifier this code just said was not recognised. A pointer stood here and in the message text and is DELETED, not reworded. `get837Claims` anchors this at the **ST**, which is `tx.segments[0]` and carries the ST-03 the resolution reads. Through `0.0.10` it passed `segmentIndex: 1`, which is the BHT and has no part in resolving a variant. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unknown837Variant } from "@cosyte/x12"; const w = unknown837Variant({ segmentIndex: 0, groupIndex: 0, transactionIndex: 0 }); ``` *** ### unknownCarc() > **unknownCarc**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_UNKNOWN_CARC` warning. Emitted when a CAS adjustment carries a CARC code outside the bundled snapshot (see "../code-lists/carc.js".CARC). The verbatim code is preserved on the parsed adjustment (`adjustment.reasonCode`); only the description is missing. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unknownCarc } from "@cosyte/x12"; const w = unknownCarc({ segmentIndex: 14, groupIndex: 0, transactionIndex: 0 }); ``` *** ### unknownClaimStatus() > **unknownClaimStatus**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_UNKNOWN_CLAIM_STATUS` warning. Companion to [unknownClaimStatusCategory](#unknownclaimstatuscategory) for the Claim Status Code (CSC, the second component of an STC composite; X12 code source 508). Same verbatim-preserve posture. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unknownClaimStatus } from "@cosyte/x12"; const w = unknownClaimStatus({ segmentIndex: 18, transactionIndex: 0 }); ``` *** ### unknownClaimStatusCategory() > **unknownClaimStatusCategory**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_UNKNOWN_CLAIM_STATUS_CATEGORY` warning. Emitted by the 277 / 277CA helpers when an STC composite's Claim Status Category Code (CSCC, first component; X12 code source 507) is outside the bundled snapshot. The verbatim CSCC is preserved on the parsed status; only the description is missing. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unknownClaimStatusCategory } from "@cosyte/x12"; const w = unknownClaimStatusCategory({ segmentIndex: 18, transactionIndex: 0 }); ``` *** ### unknownHiQualifier() > **unknownHiQualifier**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_UNKNOWN_HI_QUALIFIER` warning. Emitted by the 837 / 278 helpers when an HI composite's qualifier (first component) is outside the bundled snapshot at "../code-lists/hi-qualifiers.js". HI\_QUALIFIERS. The verbatim qualifier and code are preserved on the parsed diagnosis / procedure with `codeSystem: "unknown"` so consumers can still react. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unknownHiQualifier } from "@cosyte/x12"; const w = unknownHiQualifier({ segmentIndex: 25, groupIndex: 0, transactionIndex: 0 }); ``` *** ### unknownMaintenanceType() > **unknownMaintenanceType**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_834_UNKNOWN_MAINTENANCE_TYPE` warning. Emitted by the 834 helper when a member-level `INS-03` (or a health-coverage `HD-01`) maintenance type code falls outside the bundled snapshot (see "../code-lists/maintenance-type.js".MAINTENANCE\_TYPE\_CODES). Maintenance type is the 834's safety-critical field: an unknown action code must NEVER be silently coerced to add / change / terminate, so the verbatim code is preserved on the parsed enrollment and this warning flags the gap. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unknownMaintenanceType } from "@cosyte/x12"; const w = unknownMaintenanceType({ segmentIndex: 9, transactionIndex: 0 }); ``` *** ### unknownRarc() > **unknownRarc**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_UNKNOWN_RARC` warning. Companion to [unknownCarc](#unknowncarc) for RARC codes on `MIA` / `MOA` / `LQ` / `NTE`. Same verbatim-preserve posture; the code lives on the parsed remark. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unknownRarc } from "@cosyte/x12"; const w = unknownRarc({ segmentIndex: 16, groupIndex: 0, transactionIndex: 0 }); ``` *** ### unparseableDecimal() > **unparseableDecimal**(`position`): [`X12ParseWarning`](#x12parsewarning) Build an `X12_UNPARSEABLE_DECIMAL` warning. Emitted whenever an element read as a decimal held bytes that are NOT empty and do not match the shape "../decimal.js".X12Decimal decodes, `[+-]?digits(.digits?)?` - a thousands separator, a currency symbol, `N/A`, two decimal points, a trailing sign, and so on. That shape is what this library reads, stated as such: no clause of X12.6 is cited for it here, so do not read the code as an assertion about what type R does and does not permit. The warning is a property of the READ, not of what the caller does with the result: it fires whether the decoded slot ends up on the model, is discarded, or is replaced by a stand-in. That is what makes it countable against the input rather than against the walker's control flow. `position.elementIndex` is the 1-indexed element that failed, so a consumer can go read the verbatim bytes off the segment. The message takes NO discriminant and deliberately does not name what landed in the slot instead: this library's own readers now either leave the slot `undefined` or drop the row entirely, and a reader built on "./segment.js".elementDecimalOrZero still substitutes `X12Decimal.ZERO`. The one thing true of all of them is that whatever occupies that slot is not the sender's. Read the model field itself to see which happened. #### Parameters ##### position [`X12Position`](#x12position) #### Returns [`X12ParseWarning`](#x12parsewarning) #### Example ```ts import { unparseableDecimal } from "@cosyte/x12"; const w = unparseableDecimal({ segmentIndex: 3, transactionIndex: 0, elementIndex: 2 }); ``` --- URL: https://docs.cosyte.com/x12/troubleshooting Markdown: https://docs.cosyte.com/x12/troubleshooting.md # Troubleshooting & known limitations `@cosyte/x12` is built to be **correct and honest about its edges** rather than to claim more than it delivers. Mis-reading a payer's remittance, a claim's diagnosis, or a member's coverage can cause real financial or clinical harm, so this page is the deliberate "do not over-trust" list: the error model, the common symptoms, and the intentional boundaries. Everything here is a documented boundary, not a bug. The lenient parser never silently drops or garbles a **decoded value**; where a limitation applies, the raw value is preserved (often with a warning), it is simply not further decoded. Two things the **emit** does not reproduce are worth knowing before you rely on a round trip, and both are silent: **line breaks between segments** and **a doubled segment terminator** outside a transaction. Both are in the symptoms table below. **Segments outside a transaction** used to be a third: they are now warned, kept on the model at `ix.orphanSegments`, and re-emitted at the structural anchor recorded with them, so both the bytes and the warning survive a round trip. ## When does it throw vs warn? Only **four** unrecoverable structural conditions throw; everything else is a warning on the model. ```ts runnable throws import { parseX12 } from "@cosyte/x12"; parseX12(""); // throws X12ParseError (X12_EMPTY_INPUT) ``` | Fatal code (throws) | Meaning | | ------------------------ | --------------------------------------------------------------- | | `X12_EMPTY_INPUT` | Nothing to parse. | | `X12_NO_ISA_HEADER` | Input does not begin with an ISA. It is not an X12 interchange. | | `X12_ISA_TOO_SHORT` | ISA truncated below its fixed 106 bytes; delimiters unreadable. | | `X12_INVALID_DELIMITERS` | Delimiters can't be recovered from the ISA. | Catch them by narrowing on `X12ParseError`: ```ts import { parseX12, X12ParseError, FATAL_CODES } from "@cosyte/x12"; try { parseX12(maybeGarbage); } catch (err) { if (err instanceof X12ParseError && err.code === FATAL_CODES.X12_NO_ISA_HEADER) { // The bytes aren't X12. Reject the file, don't retry. } } ``` Everything a real payer or clearinghouse does short of that (miscounts, dangling release characters, unknown CARC/RARC/HI codes, HL parent mismatches, balance mismatches, pre-005010 versions) is a Tier-2 warning you triage, not an exception you catch. See [Tolerance tiers](https://docs.cosyte.com/x12/spec-notes-tolerance.md). ## Common symptoms | Symptom | Likely cause | What to do | | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `get835` / `get837Claims` returns `undefined` | The transaction set isn't the one that reader decodes (wrong `ST-01`, or `get277CADisposition` on a non-`X214` message) | Route on `tx.st.elements[1]` (and GS-01) first; hand each transaction to the matching reader. | | An adjustment's `reasonDescription` is `undefined` | The CARC/RARC code is outside the bundled snapshot | The verbatim code is still on the model; an `X12_UNKNOWN_CARC` / `X12_UNKNOWN_RARC` warning is raised. A stale snapshot yields a missing description, never a wrong code. | | A `X12_835_REMIT_BALANCE_MISMATCH` warning | The payer's numbers don't add up under the §1.10.2 invariants | Do **not** auto-post; the library preserves the inbound values and will not rebalance. Route to a human. | | A `X12_835_BALANCE_NOT_EVALUABLE` warning | A term of that §1.10.2 equation is `undefined` on the model, so the equation was not run. Nothing was measured out of balance and nothing was measured to balance either. Through `0.0.12` the term collapsed to `X12Decimal.ZERO` and this document raised `X12_835_REMIT_BALANCE_MISMATCH` instead | Do **not** auto-post, on the same footing as the row above, and **gate on both codes**: a posting gate written against the mismatch alone stops firing on these documents when you upgrade. Read the model to find which term is `undefined`; the message names the equation and no amount. Do not substitute `0` for it. | | A `X12_HL_PARENT_MISMATCH` warning on an 837/271/277 | A broken HL parent pointer in the hierarchy | The pointer is preserved verbatim, never re-numbered; decide whether to trust the loop nesting. | | An amount or quantity reads `undefined` where you expected a number | Either the sender omitted the element, or an `X12_UNPARSEABLE_DECIMAL` warning at that element says the sender put bytes there that are not an X12 R-type decimal (a thousands separator, a currency symbol, `N/A`). No reader substitutes `X12Decimal.ZERO` for a value it did not decode any more: every slot that used to get one is `X12Decimal | undefined`, and `undefined`means this library decoded no value. Read that as a rule about the substitution and not as a census of the model - a row whose amount does not decode is sometimes dropped whole instead, which`X12_AMOUNT_ROW_DROPPED`reports (two rows down). Through`0.0.12`those slots fell back to`X12Decimal.ZERO`, which read as a confident zero | Do not substitute `0`. The warning's `position.elementIndex` names the element and separates the two causes; the verbatim bytes are on `tx.segments[…].raw`. An **absent** element does not raise `X12_UNPARSEABLE_DECIMAL`, so an unwarned value **at an element a reader decoded** is what the sender sent. Do not widen that into "an absent element is silent": where it takes a whole `AMT` / `ADX` row off the model, `X12_AMOUNT_ROW_DROPPED` reports the drop. That is not a guarantee about every slot on the model: a slot a reader never read cannot warn. The known instance of that in this library, an 837 service line whose `SVx` never decoded, has a warning of its own (the next row); no census of never-read slots is published. See [Decimal-exact money](https://docs.cosyte.com/x12/spec-notes-money.md). | | An optional amount or quantity is `undefined` | Either the sender omitted the element, or it was present and did not decode | Look for an `X12_UNPARSEABLE_DECIMAL` at that `position.elementIndex`. One present means the second; none means the first. That reading is about a slot that is still on the model - an `AMT` / `ADX` whose amount decoded nothing has no slot at all, and is reported by `X12_AMOUNT_ROW_DROPPED` at its own segment instead (the next row). `readElementDecimal` gives the same distinction in-band if you walk segments yourself. | | An `AMT` or `ADX` you can see in the file is on no model row at all | An `X12_AMOUNT_ROW_DROPPED` warning at that segment: its amount element (`AMT-02`, `ADX-01`) decoded no value, and these segments carry an amount plus the thing the amount is about, so there was no row to build and the qualifier or adjustment reason code went with it. Through `0.0.12` this was silent on every channel: `AMT*B6~` gave `claim.amounts: []` and `warnings: []`. Raised by the 835's `AMT`, the 837's `AMT`, the 834's coverage `AMT` (on that **member's** own `warnings`) and the 820's `ADX`. On the 835 and the 837 an `AMT` attaches to the open **service line** first and to the claim only when there is none, so **the row lost may be a line-level one** and an unchanged `claim.amounts` is not evidence the warning is stale | Read the segment off `tx.segments[…].raw`; nothing was fabricated to stand in. The warning carries **no** `position.elementIndex`, because one of its two routes is an absent element and an absent element has no index to name. To tell the routes apart, look for an `X12_UNPARSEABLE_DECIMAL` at the same `position.segmentIndex`: present means the sender put unreadable bytes there, absent means the sender stated no amount. **This code is additive** - the unparseable route still raises `X12_UNPARSEABLE_DECIMAL` exactly where it always did, now alongside this one, so a gate you wrote against that code is not narrowed by it. What such a gate never caught, on any release, is the absent-amount row, so gate on both. Read the bound as a property of the READ, not of the walker's control flow: what is reported is a row whose amount was read and decoded no value. A segment discarded **before** its amount is read is not on this channel (an `AMT` with no `HD` open in the 834, an `ADX` with no remittance open in the 820), and neither is one whose amount decoded and then found nothing open to attach the row to. **Do not shorten that to "nothing open means silent"** - the 835 and the 837 decode first, so an `AMT` with an absent amount and no claim open does raise this code. An 820 `RMR` is not on this channel either, and **not** because its row survives: `decodeRmr` drops on open-item identity (RMR-01 and RMR-02 both empty) before RMR-04 is read, so an `RMR` stating an open item and no amount keeps its row with `amountPaid` `undefined`, while one stating an amount and **no** open item is dropped whole. That second case is a separate loss, reported by `X12_STATED_AMOUNT_DISCARDED` (the next row) alongside the 837's Loop 2430 `AMT`, which an open `SVD` discards outright. The two codes are disjoint and never name the same segment | | An `RMR` or `AMT` you can see in the file is on no model row, and nothing says its amount failed to decode | An `X12_STATED_AMOUNT_DISCARDED` warning at that segment: the sender populated the amount element and this reader built no row anyway, for a reason that is not a failure to decode it. Two routes. An 820 `RMR` under an open remittance loop with RMR-01 and RMR-02 **both empty**: `decodeRmr` refuses the open item on identity before RMR-04 or RMR-05 is read, so a stated payment, a stated amount due and RMR-03's payment action code leave together. Or an 837 `AMT` arriving while a Loop 2430 line adjudication (`SVD`) is open, whose `AMT-02` decoded: the v1 adjudication model carries no amount row, and attaching one to this submission's own service line would put another payer's figure on it. Through `0.0.12` both were silent on every channel, and under an open `SVD` the report read backwards, present on the row that decoded **nothing** and absent on the row that decoded a real amount | Read the segment off `tx.segments[…].raw` and decode the amount yourself. **This code does not promise it will decode**: on the `RMR` route the row is refused before the decode is attempted, so it is raised on `RMR****1,234.56~` exactly as on `RMR****150.00*150.00~`, and an unaccompanied instance is not evidence the bytes are postable. Only the `AMT` route guarantees a decodable amount, because there `AMT-02` decoded before the row was skipped. The warning carries **no** `position.elementIndex` (on the `RMR` route the loss spans RMR-04 and RMR-05). **This code is additive** and disjoint from `X12_AMOUNT_ROW_DROPPED`: the two can never name the same segment, because this one needs an amount element the sender populated and that one needs one that decoded no value, so the code you get is the discriminant. Gate on both. Read the bound literally: this reports a segment that arrived while the loop that would carry its row **was open**. One reaching a reader with no such loop open is a different loss and is still silent (the 834's `AMT` with no `HD`, the 820's `ADX` with no remittance, the 835's and 837's `AMT` before any claim). And it says nothing about whether the amount would have decoded: the `RMR` route refuses the row before attempting the decode, so **no `X12_UNPARSEABLE_DECIMAL` accompanies it even on unreadable bytes** | | An 837 service line's `charge` and `units` both read `undefined` | An `X12_837_SERVICE_LINE_NOT_DECODED` warning: the Loop 2400 line carries no `SV1` / `SV2` / `SV3` matching the variant the submission resolved to, either because it carries none at all or because it carries one for a different 837 variant than `ST-03` (or your `type` option) named. Nothing on the service segment was read. | Treat `charge`, `units`, the procedure code, the modifiers, the unit of measure and the place of service on that line as **unread**, not as zero or empty. `undefined` on its own does not distinguish this row from a service segment that WAS read and carried no charge element; this warning is what does. The warning's `position.segmentIndex` names the `LX` that opened the line; the segments are verbatim on `tx.segments[…].raw`. If `ST-03` and the `SVx` disagree, decide which one the sender meant before re-reading with the matching `type`. | | A claim's `serviceLines` is empty but the file has `LX` segments | An `X12_837_SERVICE_LINE_DROPPED` warning at each such `LX`: it opened no Loop 2400, so the line reached no claim at all. Either no `CLM` was open at that point in the transaction, or the submission's variant is not one of `P` / `I` / `D`. | Do **not** read the empty `serviceLines` as "the claim had no service lines". Nothing was fabricated to stand in and no claim was synthesized; the segments are verbatim on `tx.segments[…].raw`. Read `submission.variant` to tell the two causes apart: this code does **not** travel with `X12_837_UNKNOWN_VARIANT`, because a caller-supplied `type` outside `"P" \| "I" \| "D"` reaches the same route without it. If the cause is the variant, re-read with a valid `type`. Two bounds: an `SVx` with **no `LX` at all** is reported by `X12_837_SERVICE_SEGMENT_WITHOUT_LX` instead (the next row), because this code is anchored at the `LX`, and a `DTP` / `AMT` / `NTE` / `REF` / `N3` / `N4` / `PER` after a dropped `LX` is **not** simply absent, and which of the two routes above fired decides it: with a `CLM` open the date, amount, note and reference land among the **claim-level** ones, and with **no** `CLM` open all seven are **discarded**, including where the `LX` was stray inside an entity loop and they were that entity's own. Where that `LX` landed inside an entity loop, each `N3` / `N4` / `PER` / `REF` it discards raises `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX` at itself; read that code's own row below for its bound, which is **narrower than this row's route**, because a discarded entity segment is not always on that channel. This code reports the service line, not an entity's address, id or contact. Read the segments off `tx.segments[…].raw` rather than inferring either. This is a different loss from the row above, where the line **is** on the model and only its service segment went unread. | | An `SV1`/`SV2`/`SV3` in the file has its charge and units on no service line at all | An `X12_837_SERVICE_SEGMENT_WITHOUT_LX` warning at that service segment: no Loop 2400 was open when it arrived, so there was no line to decode it into and nothing it carries was read. | Treat the charge, units, procedure code, modifiers, unit of measure and place of service on that segment as **unread**. Nothing was fabricated to stand in and no line or claim was synthesized; the warning's `position.segmentIndex` names the service segment itself, and its bytes are verbatim on `tx.segments[…].raw`. It reports once per service segment, not once per loop. **Read the condition literally: "no line open", not "the file contains no `LX`"** - an `LX` in an earlier claim is still an `LX`, so other claims in the same file can have perfectly good service lines. It never reports the same segment as the two rows above, which are both raised at an `LX`, though a file with several claims can carry all three codes. It also says nothing about how `submission.variant` resolved: a `type` option you pass wins first, and absent one, where `ST-03` names no known implementation convention, the reader falls back to the first `SVx` in the transaction - a segment reported here is eligible for that fallback like any other. | | An 837 service line's `charge` or `procedureCode` does not match the `SV1` / `SV2` / `SV3` you can see in the file | An `X12_837_SERVICE_SEGMENT_REPEATED` warning at a service segment: a SECOND `SV1` / `SV2` / `SV3` arrived inside a Loop 2400 that was already open. The line carries one service segment's worth of slots, so it holds what the **last** one matching the submission's resolved variant wrote, and every decoder writes all of the slots its kind writes. `SV1*HC:99213*8500*…` then `SV1*HC:99999*12*…` leaves one line reading `charge` `12` and `procedureCode` `99999`; through `0.0.13` that happened with `warnings: []`. | Treat the line as carrying **one** of the service segments the loop contained, not a reconciliation of them, and read the rest off `tx.segments[…].raw` before you post an amount. Nothing of the losing occurrence is on the model in any form, and the corner that costs most is a repeat whose own charge element is **absent**: it writes `undefined` over the amount the first stated, and `X12_837_SERVICE_LINE_NOT_DECODED` does **not** fire there, because a service segment did decode. A repeat of a kind that does **not** match the resolved variant is read into nothing and overwrites nothing, and is reported the same way. **Which occurrence the sender meant is not decided** - this reader cannot tell a stray service segment from a conformant one, and picking a winner would be inventing - and the decode is byte-for-byte what it was at `0.0.13`, so re-read the raw loop rather than expecting a later release to choose differently. It fires once per repeat and the count resets with the line, so a first service segment under a later `LX` is a first. It never names the same segment as `X12_837_SERVICE_SEGMENT_WITHOUT_LX` (the row above), which requires that **no** Loop 2400 be open where this one requires that one is. Read that as disjointness and not as coverage: a service segment following an `LX` that opened no line is named by neither, because `X12_837_SERVICE_LINE_DROPPED` at that `LX` already reports the loss. | | `submission.variant` names a flavor you did not expect, on a file whose `ST-03` you do not control | An `X12_837_AMBIGUOUS_VARIANT` warning at the `ST`: no `type` option was passed and `ST-03` named no implementation convention this reader recognises, so the variant came from the **first** `SV1` / `SV2` / `SV3` in the transaction body, and another one in that same body names a different variant. | Treat `submission.variant` as a **guess between contradictory evidence** and do not route on it. Re-read with `get837Claims(delimiters, tx, { type })` to decode the document against a variant you trust: a `type` you pass wins ahead of the fallback. **Which service segment is the stray one is not decided here**, because this reader cannot tell a stray service segment from a conformant one, and the fallback takes the first in the body whether or not a Loop 2400 was open at it. Read the bound as a property of the **resolution**, not of the document: where a `type` was passed, or `ST-03` named a known convention, no guess was made and this code is not raised however mixed the body is. It is **additive** and moves nothing **of its own**: every warning this reader raised on such a document through `0.0.13` it still raises, at the same position, and this one is added beside them. Read that as a claim about this code and **not** about the release, because the release it shipped in also changed which documents reach the fallback at all (next paragraph). Read that as the invariance claim it is and **not** as a list of what else you will see on a contested document: it does not promise that any particular loss on one is reported at all, and one that is not was not reported before this code existed either. It never travels with `X12_837_UNKNOWN_VARIANT`, which reports the opposite outcome of the same resolution: nothing to fall back on at all. **Which references it recognises changed in this release**: the set through `0.0.13` held none of the identifiers HIPAA adopts and was missing the ones production 837P and 837I traffic actually carries, so this code fired on conformant files as the normal path. On a file whose `ST-03` is now recognised it does not fire at all, `submission.variant` can differ from what `0.0.13` read, and **a service line whose `SVx` kind disagrees with the declaration is no longer decoded**: its `charge` and `units` read `undefined`, the rest of the service segment is undecoded, and `X12_837_SERVICE_LINE_NOT_DECODED` is raised at that line's `LX`, on a document that may have carried `warnings: []` before. Gate on the warning and never on a slot: an undecoded line SEEDS its identity fields, so `procedureCode` is `""` on a P or D line and `revenueCode` is `""` on an I one. The bytes stay verbatim on `tx.segments[…].raw`. | | An entity's `address`, `contacts` or `references` is empty and the file has those segments | An `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX` warning at each `N3` / `N4` / `PER` / `REF` that reached no party: an `LX` earlier in the transaction opened no Loop 2400 (no `CLM` was open at it) and closed the entity loop those segments belonged to, so nothing they carry is on the model. | Read the segments off `tx.segments[…].raw`: the bytes are verbatim there, and nothing was fabricated to stand in. The `LX` itself is reported separately by `X12_837_SERVICE_LINE_DROPPED`, which names the **service line's** loss and never an entity segment, so the two codes report different things about the same stretch of the document. **Read this code's bound literally: it is not a general "this segment reached no party" report.** It fires only after such an `LX`, and only until the next `NM1` / `HL` / `CLM` opens a loop - a party named after that `LX` is outside this code's scope again, and its trailing segments are silent whether or not this reader surfaces them on it. An `N3` / `N4` / `PER` / `REF` that reaches no party by any other route is silent, as are a `DTP` / `AMT` / `NTE` on this one, which never attach to a party on any route. Which party a segment following a stray `LX` belongs to is not derivable from the TR3s in either direction, so it is discarded rather than attributed; releases through `0.0.10` attached it to whichever party the last `NM1` left active, wherever this reader surfaces that segment kind on that party at all - a `PER` on a patient or a pay-to address reached the model on no release, so this code reports that a segment reached **no** party and not that it would otherwise have reached one. `KNOWN-LIMITATIONS.md` records the trade. | | A claim's `payToAddress` names a street the sender never paired with that city | An `X12_837_PAY_TO_ADDRESS_REPEATED` warning at each `NM1*87` after the first in one Loop 2000A: the document names Loop 2010AB more than once, and the TR3s allow it at most once there. | The model has **one** pay-to address slot, so it carries one of them, and this code is the only thing that says there were more. The rule: occurrences are never merged, the **last occurrence that states an address of its own** wins, and an occurrence that states none (no `N3` or `N4` at all, or only a valueless one) does **not** blank one that did. Read the losing occurrence's bytes off `tx.segments[...].raw` - it is not on the model in any form, and which occurrence the sender meant is not derivable from the TR3s. **Through `0.0.12` the two were FUSED**, so a `payToAddress` read on that release or earlier can carry a street line from one address and a city or country code from another, with `warnings: []`; treat it as unreliable rather than as either sender's address. One consequence worth expecting rather than reporting: a repeat that states only **part** of an address (an `N4` and no `N3`) puts only that part on the model and re-emits a Loop 2010AB with no `N3` - keeping the earlier street lines there is the fusion itself. Two bounds: this is scoped to the Loop 2000A pay-to route, so an `NM1*87` arriving while a `CLM` is open never reaches it and is neither resolved nor warned by this code, landing instead on that service line's `serviceLine.providers` where a Loop 2400 is open and on `claim.providers` where a claim but no line is (both pre-existing, and this row deliberately names no single destination); and a document with at most one `NM1*87` per Loop 2000A is unaffected in every respect. | | `build277` throws `X12_277_BUILD_INVALID_SPEC` naming `SVC-07` | A Loop 2220 service line in the spec has no `unitsOfService`. SVC-07, the units of service count, is a **required** element in `005010X212`, so emitting the line without it would put a non-conformant 277 on the wire. | Supply the count the submitter sent. The builder will not default it: a quantity nobody sent is invented data, and a units figure is one a payer reprices against. The same spec is accepted by `build277CA`, where TR3 `005010X214` makes SVC-07 situational - that asymmetry is deliberate, not a gap. | | Fields parse but `parseFloat` gives odd totals | You called `parseFloat` on an EDI amount | Read the `X12Decimal` and do exact arithmetic on it; never `parseFloat`. See [Decimal-exact money](https://docs.cosyte.com/x12/spec-notes-money.md). | | An `X12_ISA_EXTRA_ELEMENT_SEPARATOR` warning, or an ISA whose control number, usage indicator or version reads as some other element's value | An ISA element value carries the element separator, so the header split into more than `ISA` + 16 elements: that element came back a prefix and everything after it is displaced. How far is NOT derivable from `isa.elements`: more than one element can carry an extra separator, and one sitting between two of them is displaced less than one sitting after both, so `isa.raw` plus the fixed widths is the only route back. The parser reports it and re-frames nothing, because the byte is both content under the ISA's fixed widths and the separator the segment declares in-band, and nothing anyone here has read settles which reading to take. All 106 bytes are still on `isa.raw`, which is the route back. Treat every other ISA-derived warning in `ix.warnings` as provisional: `parseX12` raises this one ahead of them for that reason. `serializeX12` runs its own reconciliation off `isa.elements[13]` and never raises this code, so its absence there is not evidence the header framed. | | A `X12_PRE_005010` warning | The twelfth element of the ISA split does not read `00501`; where `X12_ISA_EXTRA_ELEMENT_SEPARATOR` is also present it need not be ISA-12 | Tolerated and flagged, not decoded against older field maps. Pass `{ strict: true }` to make it a hard failure for a trusted partner. | | `serializeX12(parseX12(file)) !== file` | Usually pretty-printing: the parser absorbs the line break after each terminator and the model does not record it, so the emit is compact. But it is **not** the only cause, and the others fire on files with no line breaks at all. | If the file is merely pretty-printed the difference is line breaks only, and diffing your emit against `serializeX12(parseX12(source))` ignores that noise. Otherwise see [Line endings between segments](https://docs.cosyte.com/x12/spec-notes-envelope.md) for everything the emit does not reproduce. | | An `X12_UNEXPECTED_SEGMENT` warning | A segment arrived where the envelope grammar has no place for it: outside any open `ST..SE` transaction set, such as a stray segment between `GE` and `IEA` or a `TA1` inside an open group. | The segment is **kept**, verbatim, on `ix.orphanSegments`, and its `segmentIndex` matches the warning's `position.segmentIndex`. No `get*` reader will see it, so read it there. `serializeX12` **does** re-emit it, at the structural `anchor` recorded with it, so the segment and this warning both survive a round trip. See [Segments outside a transaction](https://docs.cosyte.com/x12/spec-notes-envelope.md). | | A segment is missing from `ix.groups` | It fell outside every transaction set, so the typed tree has nowhere for it | Check `ix.orphanSegments` before concluding the sender omitted it. That array is empty for a well-formed interchange, so a non-empty one tells you the framing did not match the envelope grammar. | ## Keeping PHI out of logs A warning `message` is a **lookup into a frozen registry**, never anything built from your document. No warning factory in the library takes a value parameter at all, so a `message` cannot interpolate an element no matter what an interchange contains: it names the deviation, `position` says where to look, and the bytes stay on the model. `ALL_WARNING_MESSAGES` is exported so you can assert that yourself: `ix.warnings.every((w) => ALL_WARNING_MESSAGES.has(w.message))` is true for every input. That means logging `w.code`, `w.position` and `w.message` is safe. Keep the same discipline in your own code: the values are one dereference away on the model (`isa.elements`, `seg.raw`, `adjustment.reasonCode`), and putting them in a log line is your decision to make, not one the library makes for you. **`ix.orphanSegments` is on the model side of that line, not the warning side.** Because a segment outside a transaction is reported as a warning, it is tempting to log the orphan next to it, but an orphan carries the sender's bytes verbatim like any other model field and nothing guarantees a segment outside a transaction is free of patient data. Log `o.context` and `o.segmentIndex` (a library-owned discriminant and an integer), not `o.raw` or `o.segment.elements`. **The builders are a different surface, and the guarantee there is weaker on purpose.** A `build*` function that refuses a spec throws a typed error. Most of those messages carry structural locators and numeric totals only; twenty-four sites across ten builder modules also name a value you passed in, so you can tell which control number, count, maintenance code, review level code or note code was refused. **The values a refusal names are the ones its own template names by field, and nothing else.** A builder's message will show you a control number, an X12 control code or a count. It will not show you a `claimId`, a member id, a member name, a trace or a diagnosis code, whatever type you sent them as. That second half used to be false and was fixed in the release after `0.0.10`: the shared guards that check the TYPE of an element - the ones that fire when a JSON-driven caller sends a number where the types say string - used to describe the value as `a number ("900412345678")`, bounded to 90 characters and not redacted. They stand on **every element of every builder**, so the value in front of them was as often `CLP-01` or `NM1-09` as a control number. They report the type now (`build835: every element value must be a string, but received a number.`) and never the value. If you were parsing a value back out of one of those messages, that is the change to know about. **One arm is deliberately NOT redacted:** the array guard still reports the `length` and the class tag of a forged array-like, bounded, because those describe the shape you forged rather than the contents of a document element, and they are the whole diagnostic for `{ length: "9".repeat(120000) }`. Since `0.0.4` every one of those twenty-four goes through `renderCallerValue`, and the fragment it produces is capped at `BUILD_REFUSAL_VALUE_MAX_RENDERED` (90 characters: up to `BUILD_REFUSAL_VALUE_MAX_LENGTH` = 63 of your value, then an ellipsis and the true length). All three are exported, so you can assert the ceiling instead of trusting it. **That ceiling is on the fragment, not on the whole message.** A message is the fragment plus the site's own fixed text, so it is bounded by a constant but a larger one. Measured: a 120,000-character control number produced a 120,066-character `X12BuildError.message` from `buildInterchange` before the change and produces a 150-character one now. Be exact about what that buys, because it is not what the parse-side registry buys. **These are values you passed in.** You handed them to the builder, you still have them, and bounding them redacts nothing: put patient data in a control number and the refusal shows up to 63 characters of it. The bound is there so `Error.message` has a fixed size rather than growing with your input, which is what makes it safe to put in a log line or a JSON error envelope. It is also **not escaped** - the surviving characters are whatever you supplied, newline included. One qualification, stated precisely because the categorical version would be false: **on the ack path the value is not always strictly your own.** TR3 005010X231A1 requires AK2-02 to be a verbatim copy of the acknowledged transaction set's ST-02, and `buildTA1` echoes an inbound ISA-13, so a document's control numbers reach those refusals by design. They are envelope control numbers, not clinical content, and they are bounded like the rest. **`defineProfile()` follows the same rule, since `0.0.6`.** An `X12ProfileError` naming a bad profile name, quirk id, effect, fixture path or expected-warning code used to interpolate it verbatim: one call measured a **360,181-character** message. Twelve refusal sites now route all twenty-three of their caller values through the same bound, and that same refusal now measures 431 characters. Read 431 as a measurement at a 120,000-character value rather than a maximum: the reported length widens with its own decimal width, so that site's ceiling is 443, and every site is asserted under 500. Where the value's **type** is the mistake, the rendering keeps `null` distinguishable from `"null"`. One asymmetry worth knowing: **`X12ProfileError.profileName` is not bounded**, on purpose, so it still matches the name you passed. Log `err.message`, not the whole error object. See [Trading-partner profiles](https://docs.cosyte.com/x12/profiles.md) for what a profile does and does not do to a parse. **Hand a builder something that is not an array and it refuses, in most places.** The types say `readonly T[]`, but a JSON-driven caller can pass anything. As of `0.0.6` every indexed loop in every builder takes its bound from a checked array, so `{ length: "9".repeat(120000) }` draws that builder's own typed refusal - before this the length coerced to `Infinity` and the builder **looped forever instead of refusing** (measured over the entry-point probes the suite shipped at that base: most hung, and each of them refuses cleanly now). No probe count is quoted here; the suite re-derives it. A list you send as `null` is still treated as absent, exactly as before. The places a builder reads a caller array with `for…of` are not covered: `buildInterchange`'s `spec.groups`, `build999`'s `functionalGroup.transactionResponses` and the optional leaf arrays such as `claim.dates` throw `TypeError: … is not iterable`, which terminates but carries **no `code`**. Validate the shape at your own boundary if the spec comes from JSON. `build270` is the exception: every list slot on its spec, spine and leaf alike, goes through the checked-array chokepoint, so a forged array-like anywhere in a 270 spec draws the typed `Eligibility270BuildError`. The same slots are checked for HOLES there, because an array with `undefined` or `null` in a slot IS an array and clears the chokepoint: that is what a JSON payload with a dropped record looks like, and on the other builders it still reaches whatever dereferences it. On a 270 it refuses, naming the slot. **A separate, long-standing hazard on the same JSON-caller path, FIXED in `0.0.9`: passing a builder a number where the types say string used to emit an EMPTY element, with no warning and no refusal.** On an 835 that emptied CLP-01, the patient control number that reassociates the remittance back to the 837's CLM-01; the same one line reached every escaped slot in all nine builders, including the 837's own CLM-01. It now draws that builder's typed, code-tagged refusal before anything is emitted. **It refuses rather than coercing, on purpose. Do not just wrap your value in `String()` without thinking.** A JSON payload that carried `"0012345"` as a number lost the leading zeros before the library saw it, so coercion would emit `12345`: a well-formed identifier that is not the one you sent, and a remittance that reassociates to the **wrong** claim. Convert at your own boundary, where you can still tell whether the zeros mattered. **The type check now covers every element of every segment a builder emits through its segment joiner, not only the ones routed through the escape helper.** Two earlier drafts of this page published a counted list of the slots that escaped the check and both were measured incomplete, so the check moved to the place every element of those segments has to pass, the joiner. A number, `null`, `undefined`, a boolean or an object in a slot that goes through one draws that builder's typed refusal, naming the slot the way the spec does: `build999: "AK9"-01 must be a string, …`. It names the slot and the TYPE and never the value, per the redaction above; the slot itself is admitted only when it matches the X12 segment-id grammar, so free text you park in element 0 of a `buildInterchange` segment spec degrades to `element N` rather than being echoed. `buildTA1` does not use a segment joiner and is not covered; see below. **Monetary and quantity slots have their own guard on top of that**, because a raw `number` answers `.toString()` with a perfectly good string and used to sail straight through: a `patientResponsibilityAmount` of `0.1 + 0.2` emitted `…*0.30000000000000004*…`, `1e21` emitted `…*1e+21*…` and `NaN` emitted `…*NaN*…`, each with zero warnings, and the library cannot parse the last two back. A slot typed `X12Decimal` now refuses anything that is not one. It will **not** round for you: choosing between `0.30` and `0.3` is a decision about your money. **Four things are still worth validating at your own boundary.** First, a `string` carrying an active delimiter: the type check passes it, and only slots routed through the escape helper release it (`"1*BOGUS"` emits as `1?*BOGUS`). Second, **the fixed-width ISA slots**, which go through padding and not through the segment joiner at all. A number there throws an untyped `TypeError`, or for `interchangeControlNumber` a typed refusal whose text misleadingly says "exceeds the 9-char spec limit". Both terminate, which is why they are the smaller hazard. Third, **the delimiter set `buildTA1` releases against.** It uses no segment joiner, so its refusal names the builder rather than `TA1-01`, but its five elements do go through the escape helper: a numeric or `undefined` `interchangeControlNumber` now refuses instead of emitting `TA1**250101*1200*A*000`, and an active delimiter is released instead of shifting the disposition element. An **empty** element refuses too, at all five slots; a whitespace-only one does not, and that residual is in [KNOWN-LIMITATIONS.md](https://github.com/cosyte/x12/blob/main/KNOWN-LIMITATIONS.md). What it cannot verify is the envelope you will embed the segment in. The separators default to the Cosyte archetype, so state them on `BuildTA1Options` if yours differ, or a value carrying a byte that is a delimiter here and not there comes back with a stray `?`. On the read side, **`parseTA1`'s five decoded fields are post-`?`-unescape**, so a released reassociation key comes back as the value rather than the bytes; `ta1.raw.elements` is still the verbatim byte surface. If you were applying `unescapeRelease` to those fields yourself, drop it. The same correction reaches `ST-03`. **`implementationConventionReference` is post-`?`-unescape in every typed reader that publishes it** - `get837Claims`, `get277Status`, `get277CADisposition`, `get278Request` and `get278Response` - so an `ST-03` framed as `A?*B` now publishes `A*B` where it used to publish the escape. `tx.st.elements` is unchanged and is still the verbatim framed surface. **What DECIDES an outcome did not move:** the 837 variant lookup, the `transactionType` choice between `claim-status` and `claim-acknowledgment`, and `get277CADisposition`'s admission gate all still key on the raw element text, so no document changes variant or admission because of this. The two can differ only where the sender escaped a byte the ISA declared as a delimiter, and the difference is one-way - nothing that resolved or was admitted before stops doing so. Nothing is trimmed or case-folded, and a dangling `?` in `ST-03` still raises no warning on these readers. **So the published reference can name a guide the reader did not resolve to, and nothing warns about the divergence.** With `componentSeparator: "X"` and an `ST-03` framed `005010?X222A1`, `submission.implementationConventionReference` reads `005010X222A1` while `submission.variant` came from the `SVx` fallback. **"Nothing warns" is not the boundary of it:** on the same delimiters a body with no `SVx` publishes that same reference with `variant: "unknown"` and raises `X12_837_UNKNOWN_VARIANT`, and a body naming more than one variant raises `X12_837_AMBIGUOUS_VARIANT` - each time the code that fired says `ST-03` named no identifier this reader recognises while the model field holds one it does. **`X12_837_UNKNOWN_VARIANT` therefore no longer tells you to read the reference off the model.** On a 277 the model can publish `005010X214` while `transactionType` is `claim-status` and `get277CADisposition` returns `undefined`. Through `0.0.15` the published value WAS the keyed value, so the model could not disagree with itself. **Gate on `variant` / `transactionType`, never on the published reference.** Fourth, **`build835`'s balance-equation amounts refuse UNTYPED.** The balance guard runs before the escape helper is built and calls `X12Decimal` methods on your value, so a raw `number` there throws a plain `TypeError` with **no `code`** rather than the typed refusal. The rule is the equation, not a list: an amount refuses untyped exactly when the balance guard reads it as a term of one of the three TR3 X221A1 §1.10.2 invariants. Named by spec field rather than element number, the untyped set is `payment.totalActualPayment`, `claim.totalChargeAmount`, `claim.totalPaymentAmount`, every `adjustments[].amount` at claim and line level, `serviceLine.chargeAmount`, `serviceLine.paymentAmount` and `providerAdjustments[].amount`. Every other `X12Decimal` field refuses typed, including `claim.patientResponsibilityAmount`, `serviceLine.paidUnitsOfService` and every `amounts[].amount`. **An EMPTY control number is refused now too, and that one used to be INVENTED rather than dropped.** Every builder that assembles an ISA zero-pads the control number to nine characters, so `interchangeControlNumber: ""` came back as `000000000`: a frozen, well-formed interchange with zero warnings, ISA-13 reconciling against IEA-02, carrying a control number you never sent. The group and transaction-set control numbers went out short a required element instead: empty in `buildInterchange` and `build999`, and dropped altogether by the seven domain builders, whose segment helper trims a trailing empty, so their `GE` and `SE` carried no control number at all. The acknowledgment builders did the same at the slots that echo what they are acknowledging (`AK1*HC**…`, `AK2*837*~`, `TA1**260601*1200*A*000`). Every one of those emitted `warnings: []`. All of them refuse as of this release, naming the slot and the property: `build837: groupControlNumber is empty. GS-06 / GE-02 is a required control number …`. No new error code and no warning code, and a build that used to hand you a document now throws. **One ordering did move, so read this before you branch on a builder error code.** The guards sit at the envelope-assembly site, so everything that already ran before them still wins: `build835`'s balance equation, `build999`'s AK9 counts, `buildTA1`'s accept-must-mean-accept check, the hierarchy checks. A defect detected **later**, during body assembly, now reports the control-number refusal instead. `build999` with an empty `interchangeControlNumber` and six AK9 syntax error codes threw `X12_ACK_COUNT_MISMATCH` at `0.0.15` and throws `X12_ACK_INVALID_SPEC` now. **The check is byte-strict, which leaves blanks open and is the one thing to screen for.** A whitespace-only control number is still accepted and still padded, so `" "` emits ISA-13 as `00000000 `; `buildTA1` does no padding at all and emits whatever whitespace you hand it, verbatim. Trimming would be a normalisation rule and nothing this library can cite says to. A **short** control number is not affected and never was: `"1"` still pads to `000000001`, which is what the padding is for. One other behaviour change: the exported `escapeRelease` now throws `TypeError` on a non-string instead of returning `""`, and a boxed `new String("…")` is refused where it built at `0.0.8`. See `KNOWN-LIMITATIONS.md`. **`err.code` is still the thing to branch on and the safest thing to log.** Tracked in `KNOWN-LIMITATIONS.md`; the parse side above is unaffected and stronger. > **This page previously said the opposite of what the code did.** Until `0.0.4` it read "warning > messages are bounded and PHI-free by construction … you can log the full `.warnings` array > without leaking", and named `.snippet` as the one exception. `X12_CONTROL_NUMBER_MISMATCH` echoed > **both** control numbers verbatim and unbounded, on all six ISA-13 / IEA-02 / GS-06 / GE-02 / ST-02 > / SE-02 slots, and the three declared counts and ISA-12 did the same. `.snippet` is not even a field > on a warning. If you are on `0.0.3` or earlier, treat `w.message` as untrusted. **`X12ParseError.snippet` on a Tier-3 fatal is the one exception, and it is deliberate.** The four structural fatals (`X12_NO_ISA_HEADER`, `X12_ISA_TOO_SHORT`, `X12_INVALID_DELIMITERS`, `X12_EMPTY_INPUT`) are raised before the envelope is readable and are undebuggable without a few bytes of context, so each carries a bounded (≤ 64 character) copy of the start of the input. On real traffic those bytes can be patient data. The library does **not** redact it. Redact at your call site, or log `err.code` and `err.position` and drop `err.snippet`. A **strict-mode escalation carries no snippet** (`err.snippet` is `""`). `{ strict: true }` turns the first Tier-2 warning into a thrown error, and that error's `message` is the same registry entry the warning carried, so there is nothing to redact. Until `0.0.4` it attached 64 bytes of the interchange, which put document bytes into `err.stack` and from there into whatever an error reporter ships to a third party. ## Known limitations & non-goals ### Data / decode boundaries - **Bundled code-list snapshots are pre-launch initial subsets, not the full WPC-published lists.** CARC, RARC, Claim-Status-Category (CSCC), Claim-Status (CSC), service-type, CLP-status, and maintenance-type ship as versioned data artifacts. An inbound code outside a snapshot still parses: the verbatim code is preserved and an `X12_UNKNOWN_*` warning is raised. Only the human-readable **description** is absent. A stale or partial snapshot yields a missing description, **never a wrong code**. - **`serialize(parse(s)) === s` is not guaranteed.** Every segment on the model comes back verbatim, in the order the model holds it. Six constructs are known not to survive: line breaks between segments, a doubled terminator outside a transaction, a missing final terminator, post-IEA `trailingBytes`, a TA1 that followed a functional group (emitted right after the ISA, so reordered, though nothing is lost), and a segment whose first element is empty outside a transaction (skipped entirely, with no warning at all). The last five fire on inputs with no line breaks, so a compact file is not guaranteed to round-trip either, and five of the six are silent, so a clean warnings list is not evidence of byte-exactness. Measured across every committed fixture: each emit is a fixed point and re-parses to an identical model with an identical warning stream, the fixtures with no line breaks return byte-identical (all but one of those are `golden/*.edi`, serializer output by construction), and the rest differ by line breaks and nothing else. No corpus size is quoted: it moves with every fixture added, and the suite re-derives the sweep from the tree. See [Line endings between segments](https://docs.cosyte.com/x12/spec-notes-envelope.md). - **A segment outside a transaction is retained and re-emitted, but never decoded.** The envelope walker binds body segments to an open ST..SE transaction. Anything else raises `X12_UNEXPECTED_SEGMENT` and is kept verbatim on `ix.orphanSegments`, joinable to the warning by `segmentIndex` and carrying the structural `anchor` `serializeX12` puts it back at, so it survives a round trip along with its warning. One boundary remains: no `get*` reader sees an orphan, so read them from `ix.orphanSegments` yourself. - **837 claim-/line-level provider addresses (Loop 2310 / 2420 `N3`/`N4`) are not surfaced.** The provider **identities** (`NM1`) round-trip; the street-address lines do not decode onto the model. Read them from the raw segments if you need them. - **`get834Enrollments` streams members but still parses the whole file up front.** It yields one decoded member per `INS` loop (so a consumer holds one member at a time), but the underlying interchange is fully parsed into `tx.segments` before iteration begins. It is not a byte-streaming reader for arbitrarily large files. - **Balance and integrity checks warn; they never rebalance or renumber.** The 835 §1.10.2 balance invariants, 837 HL parent-pointer integrity, and envelope-count reconciliation surface a warning on a mismatch and preserve the inbound values verbatim. ### Conformance testing not yet wired - **No external-oracle differential corpus yet.** A best-effort differential harness against CMS Medicare 835 public examples (and/or another external X12 reader) is planned for the first real release but is **not yet wired**, pending a redistribution-terms review. Conformance today rests on the three-tier synthetic corpus (spec-clean → vendor-quirk → round-trip goldens), property/round-trip tests, and a byte-flip fuzz job, not on parity with a third-party implementation. Do not assume byte-for-byte agreement with any specific vendor parser. ### Scope (non-goals for v1) - **Healthcare HIPAA 005010 only.** Non-healthcare sets (850/856/810/204, …), the EDIFACT syntax family, and pre-005010 versions are out of v1 scope. Pre-005010 input is tolerated and flagged (`X12_PRE_005010`), not decoded to older field maps. - **No transport.** AS2, SFTP, and MLLP-style delivery are out of scope. This is a parser/serializer, not a communications stack. - **Below 1.0.** The package is published on npm as `@cosyte/x12` from a public repo, at `0.1`. Its exported readers, builders, types and warning codes are the surface we keep stable, and below 1.0 a breaking change bumps the minor version and is called out in the changelog with its migration. `npm view @cosyte/x12 version` is the only source of truth for the current version. - **A 276 hierarchical level whose declared parent does not resolve is left off the returned tree.** The 276 reader attaches a level by its own HL-02 and by nothing else, so a dangling pointer, a pointer naming a level of the wrong kind, and a parent chain that returns to itself each leave that level, and everything beneath it, absent from the model. The loss is reported (`X12_276_LEVEL_DETACHED`) beside the code for the pointer defect, and the declared pointer and the segments both stay verbatim. - **A 276 REF, DTP or AMT short of what its row is built from loses the whole row, and says so.** Each is a record and not a slot: `X12_276_REFERENCE_ROW_DROPPED` for a REF without both REF-01 and REF-02, `X12_276_DATE_ROW_DROPPED` for a DTP without both DTP-01 and DTP-03, and the existing `X12_AMOUNT_ROW_DROPPED` for an AMT whose amount did not decode. Nothing stands in for the row. - **The 276 reader surfaces four SVC elements and no postal address.** SVC-01, SVC-02, SVC-04 and SVC-07 reach the typed service line; SVC-03, SVC-05 and SVC-06 do not, because this is the request direction and a submitter states what it billed rather than what was paid. An N3 or N4 under a 276 name loop reaches no typed field either. All of them stay verbatim on `tx.segments`. - **A 270 hierarchical level whose declared parent does not resolve is left off the returned tree.** The 270 reader attaches a level by its own HL-02 and by nothing else, so a dangling pointer, a pointer naming a level of the wrong kind, and a parent chain that returns to itself each leave that level, and everything beneath it, absent from the model. The loss is reported (`X12_270_LEVEL_DETACHED`) beside the code for the pointer defect, and the declared pointer and the segments both stay verbatim. - **A 270 DTP short of its qualifier (DTP-01) or its value (DTP-03) loses the whole date row, and says so.** A DTP is a record and not a slot, so there is no half a row to keep and the format qualifier that says single date or range goes with the rest. The loss is reported (`X12_270_DATE_ROW_DROPPED`) at that segment, which is what lets you tell an empty `dates` list apart from a date the sender stated. The report is bounded to a row the reader tried to build: a segment arriving before the first HL opens a level reaches no part of the model and stays silent. For the per-transaction read and emit surface, and the exact fields each helper decodes, see the [Cookbook](https://docs.cosyte.com/x12/cookbook.md). --- # @cosyte/ccda > C-CDA (HL7 CDA R2) clinical document parser. These are the 11 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/ccda/intro Markdown: https://docs.cosyte.com/ccda/intro.md # @cosyte/ccda Parse real-world, vendor-quirky C-CDA and pull fields out in one line, without reading the spec. `@cosyte/ccda` is a near-zero-dependency TypeScript toolkit following the Cosyte parser archetype: a lenient parser (quirks become **warnings**, not failures: Postel's Law), an immutable model, and a spec-clean, round-trip serializer. It mirrors the API shape of the reference parser, `@cosyte/hl7`. Its single runtime dependency is the W3C-DOM substrate `@xmldom/xmldom` (exact-pinned), configured XXE-safe. > **Status:** `0.1`, **published on npm** and **public**. The exported names, options, return > shapes and warning codes are the surface we keep stable, and below 1.0 a breaking change bumps > the minor version and is called out in the changelog with its migration. Run > `npm view @cosyte/ccda version` for the current version. The parser ships > document recognition (all 12 US Realm types), the US Realm header + patient demographics, section > framing, the reconciliation triad (Problems / Medications / Allergies), the discrete-data families > (Results / Vital Signs / Immunizations) with a computable UCUM unit check, Procedures / Encounters / > Social-History smoking status, the care-planning and history families (Plan of Treatment / > Functional Status / Mental Status / Family History / Past Medical History), per-document-type > required-section (SHALL) > validation, and a **round-trip serializer** (`serializeCcda` / `toString()`). A document **builder** > (`buildCcda`) emits a spec-clean **CCD**, **Referral Note** or inpatient **Discharge Summary**, a > document **editor** (`editCcda`) > re-emits a parsed document with a section added or replaced (every untouched section byte-preserved), > and a **bring-your-own terminology adapter** semantically validates coded values against your own > licensed service. The builder emits three of the twelve document types; the other nine are not > implemented. See [Troubleshooting](https://docs.cosyte.com/ccda/troubleshooting.md) for exactly what the parser, builder, and > editor do and do not do today. ## Install ```bash npm install @cosyte/ccda ``` ## Parse a document ```ts runnable import { parseCcda } from "@cosyte/ccda"; // Synthetic CCD: invented "Jane Q. Doe", fake OIDs/IDs. Never a real document. const xml = ` Synthetic Continuity of Care Document JaneQDoe
Problems Essential hypertension
`; const doc = parseCcda(xml); doc.documentType; // => "ccd" doc.getPatient()?.name?.family; // => "Doe" doc.getMrn(); // => "MRN-00042" doc.findSection("problems")?.title; // => "Problems" doc.getProblems()[0]?.status; // => "active" doc.getProblems()[0]?.problems[0]?.value?.code; // => "59621000" // This CCD carries only a Problems section, so the required-section (SHALL) // check flags the sections it expects but does not find, as warnings, never // fatals, so the data that IS present still parses. doc.warnings.every((w) => w.code === "REQUIRED_SECTION_MISSING"); // => true ``` The parser is **lenient by default**: recoverable vendor quirks become stable-coded warnings on `doc.warnings` (also delivered live to `options.onWarning`), not failures. `{ strict: true }` escalates the first tolerated deviation to a thrown `CcdaParseError`; unrecoverable or hostile input (DTD/XXE, billion-laughs entity expansion, oversized/over-deep/over-wide documents, XML that does not parse, a non-`ClinicalDocument` root) always throws. ## What it extracts today - **Document type**: all 12 US Realm document types resolved from the root `templateId`. - **US Realm header**: document identity and the `recordTarget` patient (demographics + identifiers), via `getPatient()` / `getMrn()`. - **Sections**: framed by `templateId` with a LOINC-code fallback, with nested subsections, narrative text, and a narrative `ID`→text index, via `findSection()` / `allSections()`. - **Clinical entries**: Problems, Medications, Allergies, Results, Vital Signs, Immunizations, Procedures, Encounters, Smoking Status, Plan of Treatment, Functional/Mental Status, Family History, and Past Medical History, with the safety-critical distinctions (performed-vs-planned, severity-vs-criticality, negated-vs-unknown) kept apart, never conflated. - **HL7 v3 datatypes**: `II`, `ST`, `BL`, `CD`, `PQ`, `IVL_PQ`, `TS`, `IVL_TS`, `ED`, with variable-precision v3 datetime parsing and null-flavor handling. - **Serialize**: `serializeCcda(doc)` / `doc.toString()` re-emits a **parsed** document as spec-clean XML (a fixed point), with no silent loss. - **Build**: `buildCcda(init)` constructs a spec-clean **CCD** or **Referral Note** from structured input, round-tripping through the same parse model; `editCcda(doc, …)` re-emits a parsed document with a section added or replaced, byte-preserving every untouched section. ## Next - **[Installation](https://docs.cosyte.com/ccda/installation.md)**: prerequisites + a smoke test. - **[Quickstart](https://docs.cosyte.com/ccda/quickstart.md)**: parse a CCD and pull demographics + the problem/med/allergy triad. - **[Core Concepts](https://docs.cosyte.com/ccda/spec-notes-model.md)**: the document model, the tolerance tiers, the clinical entry layer, and the datatype/code-system machinery. - **[Cookbook](https://docs.cosyte.com/ccda/cookbook.md)**: task-oriented recipes. - **[Troubleshooting & known limitations](https://docs.cosyte.com/ccda/troubleshooting.md)**: the error model and exactly what the parser, builder, and editor do and do not do today. - The **API reference** for every export is generated from source by the docs site (TypeDoc), not hand-authored here. --- URL: https://docs.cosyte.com/ccda/installation Markdown: https://docs.cosyte.com/ccda/installation.md # Installation `@cosyte/ccda` is a TypeScript C-CDA toolkit for Node.js. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration, and it has a **single** exact-pinned runtime dependency: the W3C-DOM substrate `@xmldom/xmldom` (C-CDA is XML, so a DOM is unavoidable). > **Status:** `0.1`, **published on npm** and **public**. Below 1.0 a breaking change bumps the > minor version and is called out in the changelog with its migration. Run > `npm view @cosyte/ccda version` for the current version. The install command below is live. ## Prerequisites - **Node.js >= 22.** The whole `@cosyte/*` suite targets ES2023 / Node 22+. - A package manager: `pnpm`, `npm`, or `yarn`. - **One runtime dependency.** `@xmldom/xmldom` (exact-pinned) is the only runtime dep; the parser caps itself at **≤ 3** justified deps. There is no native build and no post-install script. ## Install ```bash npm install @cosyte/ccda ``` ## Smoke test Confirm the package resolves and a real entry point is callable. Parse the smallest valid US Realm `ClinicalDocument` and read its recognized document type back: ```ts runnable import { parseCcda } from "@cosyte/ccda"; // Synthetic header-only CCD: invented patient, fake OIDs. No real PHI. const xml = ` Synthetic CCD JaneDoe `; const doc = parseCcda(xml); doc.documentType; // => "ccd" doc.header.title; // => "Synthetic CCD" doc.getPatient()?.name?.family; // => "Doe" Array.isArray(doc.warnings); // => true ``` If that resolves and returns, the install is good. Head to the [Quickstart](https://docs.cosyte.com/ccda/quickstart.md). ## Module systems `@cosyte/ccda` is `"type": "module"` and exposes both conditions, so both of these resolve to the right build without extra configuration: ```ts // ESM / TypeScript import { parseCcda, serializeCcda } from "@cosyte/ccda"; ``` ```js // CommonJS const { parseCcda, serializeCcda } = require("@cosyte/ccda"); ``` The single top-level entry point (`@cosyte/ccda`) publishes per-condition types (`.d.ts` for `import`, `.d.cts` for `require`), gated by `attw` on every release. Editor IntelliSense matches the build you actually load. ## PHI discipline Every example in this documentation uses **synthetic** fixtures: an invented patient, obviously-fake OIDs and MRNs. Do the same in your own tests: a C-CDA document is a clinical record, and a real one committed to a repository is a PHI leak the moment it publishes. The parser helps: every warning and error message comes **whole from a frozen registry** and interpolates nothing at all, so no attribute value, coded token or element name from your document can reach one. See [Troubleshooting](https://docs.cosyte.com/ccda/troubleshooting.md) for the full posture, including what changed and when. --- URL: https://docs.cosyte.com/ccda/quickstart Markdown: https://docs.cosyte.com/ccda/quickstart.md # Quickstart This page gives you a first useful result: read a **Continuity of Care Document (CCD)**, the most common C-CDA, and pull out who the patient is and their reconciliation triad (Problems, Medications, Allergies), plus a lab result and an immunization, in a few lines. The CCD is what a hospital hands you on a transition of care, so it is the fastest way to see the library earn its keep. The parser is **lenient** (vendor quirks become stable-coded `warnings`, never silent failures) and every coded value is preserved verbatim: a code that disagrees with its narrative surfaces **both** and picks no winner (`CODE_NARRATIVE_MISMATCH`); a missing dose or route is flagged, never defaulted. > Every document below is **synthetic**: an invented patient ("Jane Q. Doe"), obviously-fake OIDs and > MRNs. A C-CDA is PHI; a fixture must never hold a real one. ## Parse a CCD and read the clinical data `parseCcda(xml)` returns an immutable `CcdaDocument`. The convenience accessors (`getPatient`, `getMrn`, `findSection`, `getProblems`, `getMedications`, …) answer "whose document, what kind, what's in it" without re-walking the DOM. ```ts runnable import { parseCcda } from "@cosyte/ccda"; const xml = ` Synthetic Continuity of Care Document JaneQDoe
Problems Essential hypertension
Medications Lisinopril 10 MG Oral Tablet
Allergies No known allergies.
Results Hemoglobin 13.5 g/dL
Vital Signs Heart rate: 72 /min
Social History Never smoker
Immunizations Influenza, split virus, trivalent, injectable, preservative free
`; const doc = parseCcda(xml); // Identity: whose document, what kind. doc.documentType; // => "ccd" doc.getPatient()?.name?.family; // => "Doe" doc.getPatient()?.genderCode?.code; // => "F" doc.getPatient()?.birthTime?.raw; // => "19800101" doc.getMrn(); // => "MRN-00042" // Problems: the coded condition (SNOMED CT), and the concern's active/resolved status. const concern = doc.getProblems()[0]; concern?.status; // => "active" concern?.problems[0]?.value?.code; // => "59621000" // Medications: the RxNorm drug and its dose. const med = doc.getMedications()[0]; med?.drug?.code; // => "314076" med?.dose?.value; // => 10 med?.dose?.unit; // => "mg" // Allergies: "No Known Allergies" is a distinct flag, never confused with "unknown". doc.getAllergies()[0]?.allergies[0]?.noKnownAllergy; // => true // Results: the polymorphic value is UCUM-checked; here a physical quantity. const result = doc.getResults()[0]?.results[0]; result?.code?.code; // => "718-7" result?.value?.kind; // => "physicalQuantity" result?.interpretation?.code; // => "N" // Immunizations: the CVX vaccine code. doc.getImmunizations()[0]?.vaccine?.code; // => "140" // Vital Signs and Social History complete the CCD's six SHALL sections. doc.getVitals()[0]?.vitals[0]?.code?.code; // => "8867-4" doc.getSmokingStatus()[0]?.value?.code; // => "266919005" // Clean, spec-conformant input: nothing tolerated, nothing flagged. This sample // carries all six sections a CCD SHALL contain -- Problems, Medications, // Allergies, Results, Social History and Vital Signs -- so no // REQUIRED_SECTION_MISSING warning fires. doc.warnings.length; // => 0 ``` The `noKnownAllergy` flag is the safety primitive on the allergy side: a negated "no known allergies" assertion is never read as an absent or unknown one. On the medication side, `dose` and `route` are preserved-as-absent and flagged (`MISSING_DOSE_QUANTITY` / `MISSING_ROUTE_CODE`) when missing, never silently defaulted. ## Lenient by default; strict when you want it A recoverable vendor quirk becomes a stable-coded warning, not a failure. Here an unrecognized section LOINC code is tolerated. The section is retained as narrative-only and an `UNKNOWN_SECTION_CODE` warning is raised: ```ts runnable import { parseCcda, WARNING_CODES } from "@cosyte/ccda"; const xml = ` Synthetic Progress Note JaneDoe
Vendor-specific Some local content.
`; // Lenient (default): the quirk is a warning, the document still parses. const doc = parseCcda(xml); doc.documentType; // => "progressNote" doc.warnings.some((w) => w.code === WARNING_CODES.UNKNOWN_SECTION_CODE); // => true // Strict: the first tolerated deviation is escalated to a throw. The thrown // CcdaParseError.code carries the escalated warning code. let escalated: string | undefined; try { parseCcda(xml, { strict: true }); } catch (err) { escalated = (err as { code?: string }).code; } typeof escalated; // => "string" ``` ## Unrecoverable input throws: everything else is a warning Only unrecoverable structural / hostile input throws a typed `CcdaParseError`: XML that does not parse, a non-`ClinicalDocument` root, or a security tripwire (DTD/XXE, entity-expansion, or size/depth/node limits). A well-formed document with vendor quirks never throws; the quirks collect on `.warnings`. ```ts runnable throws import { parseCcda } from "@cosyte/ccda"; // Not a ClinicalDocument at all: a structural fatal, not a tolerated quirk. parseCcda("not a clinical document"); // throws CcdaParseError (NOT_A_CLINICAL_DOCUMENT) ``` ## Next - [Core Concepts](https://docs.cosyte.com/ccda/spec-notes-model.md): the document model, the tolerance tiers, the clinical entry layer, and the datatype/code-system machinery. - [Cookbook](https://docs.cosyte.com/ccda/cookbook.md): recipes for active-problem filtering, the code/narrative fail-safe, the performed-vs-planned split, warning triage, and the round-trip serializer. - [Troubleshooting & known limitations](https://docs.cosyte.com/ccda/troubleshooting.md): the fatal codes, the fail-safe rules, and exactly what the parser, builder, and editor do and do not do today. > **About runnable examples.** The blocks tagged ` ```ts runnable ` above are extracted by the test > suite, executed against the built package, and their `// =>` results asserted, so a documented > example can never silently drift from the code (`docSnippetSuite()`, the documentation analog of the > parser conformance runners). Blocks shown as plain ` ```ts ` are illustrative. --- URL: https://docs.cosyte.com/ccda/spec-notes-model Markdown: https://docs.cosyte.com/ccda/spec-notes-model.md # The document model: recognition, header, sections A C-CDA is a CDA R2 `ClinicalDocument`: a **US Realm header** (who, what kind, when) wrapping a **body** that is either a `structuredBody` (a tree of `
`s) or a `nonXMLBody` (a wrapped PDF / scanned document). `parseCcda` frames all three into one immutable `CcdaDocument`. ## Document recognition The document **type** is resolved from the root `templateId` OIDs against the 12 recognized US Realm types (CCD, Discharge Summary, Referral Note, Consultation Note, History & Physical, Progress Note, Procedure Note, Operative Note, Care Plan, Diagnostic Imaging Report, Unstructured Document, Transfer Summary). Recognition is fail-safe: - No `templateId` at all → `MISSING_TEMPLATE_ID`, `documentType` is `undefined`. - `templateId`s present but none map to a known type → `UNKNOWN_DOCUMENT_TEMPLATE`, still parsed as a generic `ClinicalDocument`. - A matched type whose `templateId` carries **no** `@extension` version stamp at all → `TEMPLATE_EXTENSION_ABSENT`, matched by root alone (it may pre-date R2.1). - A matched type whose `templateId` carries an `@extension` that is **not** the R2.1 stamp (`2015-08-01`) → `TEMPLATE_EXTENSION_UNMODELED_RELEASE`. A different code, because it is a different document: one written for a release later than the one these tables target. The generic US Realm Header / CDA-base templates are deliberately **not** in the type table, so they are passed over: only a specific document-type `templateId` resolves a `documentType`. ## Which release a document was written for The stamp on the resolving `templateId` reads into exactly three states, and the third is why a boolean was not enough: `r21-stamped`, `unstamped` (no `@extension`, the R1.1-origin shape) and `unmodeled-release` (a stamp these tables do not model). C-CDA 3.0.0 restamped every document template `2024-05-01` and 4.0.0 and 5.0.0 kept it, so a post-R2.1 document is detectable. **Recognizing a release is not targeting it.** `CCDA_CONFORMANCE_RELEASE` names the release this package's conformance tables are written against, and it does not move because a later stamp is recognized: ```ts runnable import { CCDA_CONFORMANCE_RELEASE, CCDA_RELEASE_STAMPS, R21_EXTENSION, R30_EXTENSION, readTemplateStamp, releaseForTemplateExtension, } from "@cosyte/ccda"; // The targeted release, as a value rather than a sentence in a README. CCDA_CONFORMANCE_RELEASE; // => "R2.1" // The closed table of stamps this package can NAME. Both are recognized; only // one is targeted, and a diagnostic may never report anything outside it. CCDA_RELEASE_STAMPS.map((entry) => entry.stamp).join(","); // => "2015-08-01,2024-05-01" releaseForTemplateExtension(R21_EXTENSION); // => "R2.1" releaseForTemplateExtension(R30_EXTENSION); // => "R3.0 or later" releaseForTemplateExtension("1999-12-31"); // => undefined // The three-state reading a required-section lookup is carried out under. readTemplateStamp(undefined); // => "unstamped" readTemplateStamp(R21_EXTENSION); // => "r21-stamped" readTemplateStamp(R30_EXTENSION); // => "unmodeled-release" ``` A document in the third state is still parsed leniently, and its clinical reading is identical to the same document stamped `2015-08-01`. What changes is the conformance claim: its required-section obligation is reported **unevaluated** (`REQUIRED_SECTIONS_NOT_EVALUATED`, and `evaluation: "not-evaluated"` on `requiredSectionStatus`) rather than computed under a reading that does not reach it. ## The US Realm header `getPatient()` returns the first `recordTarget` patient (a document with more than one emits `MULTIPLE_RECORD_TARGETS` and this resolves the first); `getMrn()` returns the patient's medical record number, the **first** `patientRole/id` extension, via `pickMrn`, and `undefined` when that id carries a `nullFlavor`: reading it out would be a selection rather than a report, and a bare `string` has nowhere to carry the marking that qualified it. It withholds rather than falling through to the next id, since nothing in a C-CDA ranks `patientRole/id` entries and the next one is as likely to be an account or member number. The verbatim value is still on `getPatient()?.identifiers`, with its `nullFlavor` beside it. The header also carries the document `code`, `title`, `effectiveTime`, `confidentialityCode`, and `languageCode`. ## Provenance: who authored it, who holds it, which encounter it covers Three header participations answer the questions a clinician asks of a list assembled by three systems. All three are **absent when the document carries none**, and nothing else in the document is substituted for an absent one. - `header.authorship` is the document-level author reading, a `CcdaAuthorship`. Its `authors` are every `` in document order as `CcdaAuthor`s, each with its `identifiers`, its `person` (a `HumanName`) or its `device` (a `CcdaAuthoringDevice`), its `representedOrganization` (a `CcdaOrganization`), and its `time` at exactly the precision the document stated. A partial author time stays partial: nothing here completes a date. - `header.custodian` is a `CcdaCustodian`, the organization responsible for the document. Its `organization` is omitted when the `` carried no `assignedCustodian/representedCustodianOrganization` to read, so a malformed custodian is reported as present-with-nothing-readable rather than invented or dropped. - `header.encompassingEncounter` is a `CcdaEncompassingEncounter`, the `componentOf/encompassingEncounter` an inpatient Discharge Summary carries: an `effectiveTime` interval whose bounds keep the document's own precision and `nullFlavor`s, and a `dischargeDispositionCode`. **Its bounds are never derived** from the document `effectiveTime`, from a `documentationOf` service event, or from any other date in the document. No `componentOf` means no encounter frame at all. ### An inherited author reading is marked, never asserted CDA conducts an author down from the document to a section and on to that section's entries. This parser reports that conduction rather than performing it silently. `CcdaSection.authorship` and each entry's reading in `CcdaSection.entryAuthorship` (a `CcdaEntryAuthorship`, which also carries the act's `ids` so you can join it to an extracted entry) carry an `inherited` flag: - `inherited: false`: this level carried these `` participations itself. - `inherited: true`: this level carried none, and these are the nearest enclosing level's. The distinction is the whole point. "The nearest enclosing author is Dr Lirio" and "this entry was authored by Dr Lirio" are different claims and only the first is one the document made. When no level carries an author, the reading is **absent** at every level: the record target, the custodian, a legal authenticator and an informant are never read as the author. An `` whose `assignedAuthor` carries neither an `assignedPerson` nor an `assignedAuthoringDevice` is kept, marked `unidentified: true`, and reported with `UNIDENTIFIED_AUTHOR`. It still conducts to nested levels. Dropping it would turn "the document names an author whose identity it never states" into "the document names no author", which is a more reassuring claim than the document supports. Entries an overriding `` declaration governs are absent from `entryAuthorship` entirely, not even their `ids`, exactly as they are absent from every extracted entry family. `entryAuthorship` is optional on the type and populated on every section the parser frames, empty where the section has no entry act to read. It is optional because `CcdaSection` is an input surface too (`CcdaDocumentInit.sections`), so a section literal you already build keeps compiling. Framing reads each act's ``s without reporting on them: the entry-extraction walk parses those same elements, so a deviation on one is reported once, by that walk, exactly as before. ```ts runnable import { parseCcda } from "@cosyte/ccda"; const xml = ` Synthetic Progress Note JaneDoe Synthetic Health Organization
Allergies No known allergies.
Problems None recorded.
`; const doc = parseCcda(xml); // The document names its author, and the author time keeps month precision. doc.header.authorship?.inherited; // => false doc.header.authorship?.authors[0]?.person?.family; // => "Lirio" doc.header.authorship?.authors[0]?.time?.raw; // => "202403" doc.header.authorship?.authors[0]?.representedOrganization?.name; // => "Synthetic Cardiology Practice" doc.header.custodian?.organization?.name; // => "Synthetic Health Organization" // The Allergies section states its own author, so nothing is inherited there. doc.findSection("allergies")?.authorship?.inherited; // => false doc.findSection("allergies")?.authorship?.authors[0]?.person?.family; // => "Okonkwo" // The Problems section states none, so it reports the document's, marked inherited. doc.findSection("problems")?.authorship?.inherited; // => true doc.findSection("problems")?.authorship?.authors[0]?.person?.family; // => "Lirio" // No componentOf in this document, so there is no encounter frame at all. doc.header.encompassingEncounter; // => undefined ``` ## Section framing Every `
` is framed by `templateId` root (primary) with a LOINC `code` fallback: - Recognized by `templateId` → `recognizedBy: "templateId"`. - Recognized only by LOINC code → `SECTION_MATCHED_BY_LOINC_FALLBACK`, `recognizedBy: "loinc"`. - Neither recognizes it, but it carries the Hospital Course root (`1.3.6.1.4.1.19376.1.5.3.1.3.5`) → `key: "hospitalCourse"`, `recognizedBy: "templateId"`, no warning. Asked last and by that root only, so it never changes a key the first two found and its LOINC code alone recognizes nothing. - Neither recognizes it → `UNKNOWN_SECTION_CODE`, retained as **narrative-only** (nothing is dropped). `findSection(key)` walks top-level sections then their subsections (depth-first); `allSections()` returns every section flattened in document order. Each section carries its `title`, `code`, `narrativeText`, and a narrative `ID`→text index (`narrativeById`) so the clinical-entry layer can resolve `` back to the human-readable text. ```ts runnable import { parseCcda } from "@cosyte/ccda"; const xml = ` Synthetic CCD JaneDoe
Allergies No known allergies.
`; const doc = parseCcda(xml); doc.documentType; // => "ccd" doc.header.title; // => "Synthetic CCD" doc.allSections().map((s) => s.key); // => ["allergies"] doc.findSection("allergies")?.recognizedBy; // => "templateId" doc.findSection("allergies")?.narrativeText; // => "No known allergies." ``` ## Unstructured documents An Unstructured Document carries a `nonXMLBody` instead of a `structuredBody`. The parser exposes its wrapped content on `doc.nonXmlBody` as an `ED` datatype and **leaves any base64 payload inert**: it is never decoded (decoding an arbitrary embedded blob is a needless attack surface and a PHI-handling decision the caller owns). ## Immutability A `CcdaDocument` is frozen at the model boundary: accessors return the parsed data by reference and callers cannot mutate parser output. The one sanctioned copy-with is `doc.withWarnings(extra)`, which returns a **new** document with extra warnings appended, sharing every parsed field by reference and leaving the original untouched. `buildCcda` and `editCcda` follow the same discipline: neither mutates a document in place, each returns a new one. --- URL: https://docs.cosyte.com/ccda/spec-notes-tolerance Markdown: https://docs.cosyte.com/ccda/spec-notes-tolerance.md # 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 | Tier | Behavior | Example | | --------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | **0 / 1** | Accepted silently: conformant or trivially recoverable. | A section recognized by its `templateId`. | | **2** | **Warning** 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. | | **3** | **Fatal**: a thrown `CcdaParseError`, always (even in lenient mode). | XML that does not parse, 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 `` root) and with the two document-level stamp codes, `TEMPLATE_EXTENSION_ABSENT` and `TEMPLATE_EXTENSION_UNMODELED_RELEASE` (the matched document-type root, which is the one template either warning is about). 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, `UNKNOWN_DOCUMENT_TEMPLATE`, whose subject is the whole `templateId` set rather than any one root, and `REQUIRED_SECTIONS_NOT_EVALUATED`, whose subject is the document's whole obligation. ## The version stamp, and what a profile may quiet about it The two stamp codes are deliberately separate, and which one you may tolerate is a decision about your own pipeline rather than a formality: - `TEMPLATE_EXTENSION_ABSENT` means the resolving `templateId` carried **no** `@extension` at all. That is the R1.1-origin shape, which a certified receiver must accept, and the built-in `legacyR11` profile tolerates it on exactly that grounding. - `TEMPLATE_EXTENSION_UNMODELED_RELEASE` means it carried a stamp that is **not** the R2.1 one, so the document was written for a release these tables do not model. **`legacyR11` does not tolerate it**, and that omission is deliberate: a receive-tolerance profile for documents from the past has no business silencing a stamp from the future. Neither code is safety-critical, so a consumer who has actually read the later guide can still write their own profile for it, with their own provenance, which is what `defineCcdaProfile` is for. Neither code is a refusal. A stamp this package does not model is a warning, the document is parsed leniently, and its clinical reading is unchanged; what stops is the R2.1-scoped conformance claim (`REQUIRED_SECTIONS_NOT_EVALUATED`). Under `{ strict: true }` both escalate to a throw exactly as every other Tier-2 warning does, which is the caller's own opt-in and not a new behaviour here. A stamp a message **names** always comes from this package's own closed table (`CCDA_RELEASE_STAMPS`), never from the document: a sender-controlled `@extension` selects the generic wording, which names no stamp at all. Most of the specifics a message used to name are still on the model: a section's `` 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: ```ts runnable import { parseCcda, WARNING_CODES } from "@cosyte/ccda"; // A medication with no doseQuantity and no routeCode: both safety-critical, both flagged. const xml = ` Synthetic Progress Note JaneDoe
Medications Aspirin
`; 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 XML substrate before/while building the DOM, the load-bearing defense against hostile XML: | Fatal code | Meaning | | ------------------------------ | ------------------------------------------------------------ | | `XXE_OR_DTD_PRESENT` | The document declared a DTD or an external entity. | | `ENTITY_EXPANSION_LIMIT` | Too many `&…;` entity references (billion-laughs). | | `INPUT_SIZE_LIMIT_EXCEEDED` | Decoded input exceeds the byte cap. | | `ELEMENT_DEPTH_LIMIT_EXCEEDED` | Element nesting too deep. | | `NODE_COUNT_LIMIT_EXCEEDED` | Too many element nodes. | | `NOT_WELL_FORMED_XML` | The bytes did not parse as XML. | | `NOT_A_CLINICAL_DOCUMENT` | Well-formed, but the root element is not `ClinicalDocument`. | Narrow on `err.code`: ```ts runnable import { parseCcda, CcdaParseError, FATAL_CODES } from "@cosyte/ccda"; let code: string | undefined; try { // Well-formed XML, but the root is not a ClinicalDocument. parseCcda("hello"); } 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`. --- URL: https://docs.cosyte.com/ccda/spec-notes-clinical Markdown: https://docs.cosyte.com/ccda/spec-notes-clinical.md # The clinical entry layer Beyond identity + narrative, `parseCcda` extracts the structured clinical entries into typed accessors. Each family is reached in one call; every safety-critical distinction is **kept apart, never conflated**. | Accessor | What it returns | | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `getProblems()` | Problem Concern Acts → coded conditions (SNOMED CT / ICD-10-CM) + active/resolved status. | | `getMedications()` | Medication Activities → RxNorm drug, dose, route, therapy window vs periodic frequency. | | `getAllergies()` | Allergy Concern Acts → allergen, reactions (manifestation + severity), criticality, the "No Known Allergies" flag. | | `getResults()` | Result Organizers → LOINC analyte, polymorphic UCUM-checked value, reference range, interpretation. | | `getVitals()` | Vital Signs Organizers → the same UCUM-checked value machinery. | | `getImmunizations()` | Immunization Activities → CVX vaccine, dose, route, date, the `refused` flag. | | `getProcedures()` | Performed **or** planned procedures, split by `moodCode`. | | `getEncounters()` | Encounter Activities → visit type, status, period. | | `getSmokingStatus()` | Smoking Status observations → SNOMED value + an explicit `unknown` flag. | | `getPlannedItems()` | Plan of Treatment entries, seven templates (act, encounter, procedure, medication, supply, observation, immunization): all future/ordered, never performed. Four of the section's eleven admissible entry templates are not planned items and are not returned; three of those four (Instruction, Handoff Communication Participants, Nutrition Recommendation) are **reported** as `PLAN_ENTRY_NOT_MODELED` rather than excluded in silence, and Goal Observation deliberately is not. An entry is read as a section entry's own act or nested inside a Planned Intervention Act (`…22.4.146`), the one container that holds all seven inline; a pointer (Entry Reference) is stepped over rather than followed. Nesting is not solved in general: a planned act inside a Nutrition Recommendation (`…22.4.130`) or an Intervention Act (`…22.4.131`) is still not reached. | | `getFunctionalStatus()` / `getMentalStatus()` | Functional / Mental Status findings + direct-entry Assessment Scale Observations (`assessmentScale`, with an `INT` score + `supporting` items), domain-tagged so the two never merge. | | `getFamilyHistory()` | One entry per relative: structured identity + their conditions. | | `getPastMedicalHistory()` | Bare historical Problem Observations (never double-counted as active). | ## The safety-critical distinctions These are the reconciliations where a silent guess could harm someone, so the parser refuses to guess: - **Performed vs planned** (Procedures, Plan of Treatment): the `moodCode` drives a `disposition` of `"performed"` (`EVN`) vs `"planned"` (`INT`/`RQO`/…). A missing mood is `PLANNED_VS_PERFORMED_AMBIGUOUS` and an unrecognized one is `PROCEDURE_MOOD_UNEXPECTED`; both leave `disposition` undefined rather than guess. A planned colonoscopy is **never** read as a performed one. - **Severity vs criticality** (Allergies): a reaction's `severity` (how bad this event was) and the propensity's `criticality` (how dangerous future exposure is) are different axes, kept on different fields, never merged. - **Negated vs unknown**: "No Known Allergies" (`noKnownAllergy`, from `negationInd`) and a refused immunization (`refused`) are distinct from a `nullFlavor` "unknown". A refusal is never read as an administration. - **Code vs narrative**: when a coded value disagrees with the narrative text it references, the parser surfaces **both** (`CODE_NARRATIVE_MISMATCH`) and picks no winner. - **Missing safety fields**: a missing `doseQuantity` / `routeCode` is preserved-as-absent and flagged, never defaulted. The performed-vs-planned split, exercised: ```ts runnable import { parseCcda } from "@cosyte/ccda"; const xml = ` Synthetic Procedure Note JaneDoe
Procedures Appendectomy
`; const procs = doc(xml); // The performed appendectomy and the planned colonoscopy are kept strictly apart. procs[0]?.disposition; // => "performed" procs[0]?.code?.code; // => "80146002" procs[1]?.disposition; // => "planned" procs[1]?.code?.code; // => "73761001" function doc(x: string) { return parseCcda(x).getProcedures(); } ``` ## Required-section (SHALL) validation For a recognized document type, an absent required (SHALL) catalog section surfaces a `REQUIRED_SECTION_MISSING` **warning**, never a fatal, so a missing section never blocks reading the data that _is_ present. The table is **conservative**: it asserts only unconditional, in-catalog, high-confidence SHALL constraints and omits choice constraints (`SHALL contain A OR B`), SHOULD/MAY sections, and SHALL sections outside the recognized catalog. `requiredSectionKeys(documentType)` and `missingRequiredSections(documentType, presentKeys)` expose the table directly. The **CCD** row is the one traced end to end. It asserts **six** sections, read off the normative C-CDA R2.1 Schematron's CCD (V3) errors rule: Allergies (CONF:1198-30662), Medications (-30664), Problems (-30666), Results (-30670), Social History (-30688) and Vital Signs (-30690). Procedures (-30668) and Plan of Treatment (-30686) sit in that template's _warnings_ rule as SHOULD, so neither is asserted. `buildCcda` emits exactly this set for a CCD, so the two halves cannot drift. Those CONF ids are **scoped to the R2.1 stamp**: their Schematron rule matches only a document whose CCD `templateId` carries `@extension="2015-08-01"`. Social History and Vital Signs are therefore asserted only against an R2.1-stamped document; an R1.1-origin CCD keeps the older four-key reading, because there is no R1.1 Schematron in hand and narrowing would be as unsourced as broadening. Pass `{ r21Stamped: false }` to `requiredSectionKeys` / `missingRequiredSections` for that reading. **Five more types were read off the same source, and every type now reports how far it was verified.** `requiredSectionStatus(documentType)` returns the asserted `keys` with a `verification` of `traced-complete`, `traced-partial`, `untraced` or `not-applicable`, the conformance id and the source's own section name behind each traced key, and every SHALL section left unasserted with its reason (outside the recognized catalog, or not unconditionally required). Consultation Note asserts History of Present Illness (CONF:1198-28907), Allergies (-28911) and Problems (-28929), all scoped to the R2.1 stamp their constraints carry; Progress Note, Procedure Note, Operative Note and Diagnostic Imaging Report assert nothing, because every SHALL section their templates name is either outside the recognized catalog or a choice. Unstructured Document is `not-applicable`: its component SHALL be a `nonXMLBody` (-31086), so it has no section to require. The six types that already asserted keys stay `untraced`, sets and citations unchanged, because the source was not re-read for them. --- URL: https://docs.cosyte.com/ccda/spec-notes-datatypes Markdown: https://docs.cosyte.com/ccda/spec-notes-datatypes.md # HL7 v3 datatypes, code systems & the serializer ## HL7 v3 datatypes C-CDA is built on the HL7 v3 abstract datatypes. The parser reads the ones that carry clinical meaning into typed shapes: `II` (instance identifier), `ST` (string), `BL` (boolean), `CD` (coded: the constrained `CE` parses into the same `CD` shape), `PQ` (physical quantity), `IVL_PQ` (quantity interval), `TS` (point in time), `IVL_TS` (time interval), and `ED` (encapsulated data). A `TS` supports **variable precision** (year → year-month → … → second) and exposes both the verbatim `raw` string and a parsed `date`; a malformed value keeps its `raw` and leaves `date` undefined (`MALFORMED_DATETIME`). Following the canonical CDA R2 / HL7 v3 `TS` literal `YYYYMMDDHHMMSS.UUUU[±ZZzz]` (and the ISO 8601 it derives from), a fractional-second or `±ZZZZ` timezone offset is only accepted once the **time-of-day** (at least the hour) is present. An offset or fraction hung on a bare date, such as the dropped-dash `"2026-0721"`, is a malformed value, not a year `2026` with a `-07:21` offset. `@nullFlavor` is preserved verbatim throughout, and a value outside the HL7 v3 NullFlavor code system is flagged (`INVALID_NULL_FLAVOR`) rather than dropped. `NULL_FLAVORS` is the **whole** code system (`2.16.840.1.113883.5.1008`), all seventeen concepts, so a conforming `nullFlavor="PINF"` on a `PQ` or the `nullFlavor="NP"` a real Plan of Treatment carries on a `` reads as conforming. It was eight of the seventeen through `0.0.4`, which flagged those as invalid. `NP` is retired in the published code system and is admitted all the same, because `INVALID_NULL_FLAVOR` asserts that a token is not a concept of the system, and it is one. ### A `nullFlavor` asserted beside a value In v3, `nullFlavor` is a property of `ANY` marking the instance as an **exceptional value**, one with no proper value. So `` is a document contradicting itself: this quantity is unknown, and this quantity is 10 mg. The CDA R2 schema declares the attributes independently, so the shape is schema-valid and no normative SHALL is cited here (none is invented either); the parser's response rests on the datatype semantics above and on the harm ordering. Every v3 datatype flags it as `CONTRADICTORY_NULL_FLAVOR`, which is **safety-critical**, so no vendor profile can tolerate it back into silence. That includes the `INT` and `ST` arms of an observation ``, the slot carrying lab values and assessment-scale scores, which are parsed inline rather than through the datatype layer. The contradiction is then resolved **against the derived reading**: `PQ.value`, `TS.date` and an `integer` observation value's `value` are withheld, while `raw`, `unit` and the `nullFlavor` are all preserved. A caller reading `med.dose.value` gets `undefined` rather than `10`, and one reading a contradicted PHQ-9 score gets `undefined` rather than `12`. This is exactly what `MALFORMED_DATETIME` already does to `TS.date`, a second reason not to trust an interpretation rather than a new rule, and it costs nothing because `value`/`date` are readings the parser manufactured from a `raw` that survives. Withholding stops there **at the datatype layer**, deliberately. `PQ`, `TS` and the `integer` observation value are the only shapes in this model that keep both a verbatim string and a parsed interpretation. On `CD`, `II`, `ST`, `ED` and `BL` the value-bearing field **is** the document's own text, with no second copy, so it is kept and the warning plus the co-located `nullFlavor` is the signal. The test is whether the reading was _manufactured_ beside a surviving verbatim copy, never whether the field looks dangerous. Where a datatype's field is the document's bytes but something **above** it derives a reading, the withholding moves up there. This model does that once for an identifier: `pickMrn` (behind `getMrn()`) selects one `` out of a list and flattens it to a bare `string` that no longer carries the `nullFlavor`, which is exactly the relationship `PQ.value` has to `PQ.raw`. So `getMrn()` returns `undefined` when the first `patientRole/id` is null-marked, `parseIi` still keeps `@extension`, and a caller reading `getPatient()?.identifiers` still sees both. It withholds rather than substituting the next id, because declining a manufactured reading is not the same act as manufacturing a replacement, and nothing in the document ranks the ids. The other identity slots (document `id`, `setId`, `parentDocument/id`, entry-level ``s) are only ever reported whole beside the warning, so there is nothing there to withhold. `templateId` is the stated exception rather than a member of that list: recognition does derive the document type (and its required-section SHALL set) from `templateId.@root`, and a null-marked `templateId` still resolves it. That is deliberate, it asserts a document _shape_ rather than a person or a record, so a mis-read costs a spurious `REQUIRED_SECTION_MISSING` rather than a misattributed clinical fact, and refusing would replace a working type with `UNKNOWN_DOCUMENT_TEMPLATE`. The emit side is guarded separately: `editCcda` refuses to build an `RPLC` `parentDocument` out of a null-marked source `` rather than copy `root`/`extension` forward and lose the marking. One thing the check deliberately does **not** do is short-circuit the rest of the slot's validation. A `nullFlavor`-only `CD` still names a terminology, so a wrong or deprecated `@codeSystem` on it still draws `UNEXPECTED_CODE_SYSTEM` / `DEPRECATED_CODE_SYSTEM` exactly as before. Only a value-bearing assertion contradicts. A `PQ` `@unit` with no `@value` (a dimension without a magnitude), an `II` `@root` with no `@extension` (a namespace without a local identifier), and a `CD`'s `originalText` / `` / `displayName` / bare `@codeSystem` (the documented C-CDA idiom for "not codable in the bound value set, here is the source text or an alternate coding") all describe a null value rather than contradicting it, and stay silent. On the **emit** side `buildCcda` is symmetric but strict (Postel's Law, conservative on emit): every date it writes into an ``/`low`/`high`/`value`/`birthTime` is validated against the same v3 TS grammar the parser reads, so a malformed caller input (`"2026-07-21"` with dashes, `"July 2026"`, or a calendar-invalid `"20260230"`) **throws a `TypeError` at build time** rather than serializing a schema-invalid or clinically-misread timestamp. Legitimate variable precision (`"2026"`, `"202607"`, `"20260721"`) is accepted unchanged. The builder never guesses or coerces a date it was given in the wrong shape. It fails loud. Everything it does accept round-trips back through `parseCcda` without a `MALFORMED_DATETIME` warning. ### Converting a `TS`: `toObject`, `toISO`, `toDate` `TS.date` answers a question most C-CDA documents never asked. It is eager and total: a value stated to the day is resolved to the first instant of that day, and a value carrying no `±ZZZZ` offset is resolved **as if it had said UTC**. That is convenient, and on an offset-less date of birth read in any negative-offset zone it is a day out. `toObject`, `toISO` and `toDate` are the honest reading of the same bytes, and they are the same three names, with the same return shapes and the same timezone rule, that every sibling `@cosyte/*` parser exports. They read `TS.raw`, the document's own `@value`, and never `TS.date`. ```ts runnable import { toDate, toISO, toObject } from "@cosyte/ccda"; // The `effectiveTime` of a problem observation, as this parser read it: the document // stated a day and no timezone, which is what a C-CDA onset date usually looks like. const onset = { raw: "20260628" }; toObject(onset); // => { year: 2026, month: 6, day: 28 } toISO(onset); // => "2026-06-28" toDate(onset); // => undefined // The zone is the caller's to supply, and 0 is a real answer meaning "read it as UTC". toDate(onset, { assumeOffsetMinutes: -300 }); // => new Date("2026-06-28T05:00:00.000Z") toDate(onset, { assumeOffsetMinutes: 0 }); // => new Date("2026-06-28T00:00:00.000Z") // A value that states its own offset needs no help, and ignores an assumed one. const collected = { raw: "20260628153045.5-0500" }; toISO(collected); // => "2026-06-28T15:30:45.5-05:00" toObject(collected)?.offsetMinutes; // => -300 toObject(collected)?.millisecond; // => 500 toDate(collected, { assumeOffsetMinutes: 600 }); // => new Date("2026-06-28T20:30:45.500Z") // An offset that names no usable zone is refused, rather than answered with an Invalid Date. toDate(onset, { assumeOffsetMinutes: Number.NaN }); // => undefined toDate(onset, { assumeOffsetMinutes: 1e15 }); // => undefined // The same rule reaches an offset the DOCUMENT states. The v3 offset token is four digits // wide and constrains neither field, so `+9999` is a legal literal stating 6039 minutes: // not a zone, and one `+HH:MM` can only misspell. The value is refused whole, while // 23 hours 59 minutes, the widest the slot can state, still converts. toObject({ raw: "20260628153045+2359" })?.offsetMinutes; // => 1439 toISO({ raw: "20260628153045+9999" }); // => undefined ``` Five rules are worth stating outright, because each is a place a conversion could have invented something and does not: - **`toObject` reports only what the value stated.** The result is a frozen `DateParts` whose keys are exactly the components present in the document, so the precision survives the conversion: `Object.keys()` on a year-precision value is `["year"]` and nothing else. Nothing is zero-filled, `month` is the spec-native 1 to 12, the names are singular (`hour`, `minute`, `second`), and there is no `raw`, `precision` or `valid` key. `millisecond` comes from the first three digits of the stated fraction, taken verbatim and right-padded, so `.5` is 500 and `.0500` is 50. `offsetMinutes` is present **if and only if** the document wrote an offset, and a stated zero offset is present as `0`. Delete `offsetMinutes` and what is left is accepted, unchanged and unrenamed, by `Temporal.PlainDateTime.from` and by luxon's `DateTime.fromObject`. Neither library is a dependency here and neither is imported; the shape is chosen so that handing the parts to one costs nothing. - **`toISO` truncates and appends nothing.** A month-precision value renders `"2026-06"`, not `"2026-06-01T00:00:00Z"`, and fractional digits are rendered exactly as written rather than padded to three. An offset is appended when the value stated one, `Z` for a stated zero and `+HH:MM` / `-HH:MM` otherwise. **When the value stated no offset, no `Z` is fabricated**: the string is deliberately zone-less. Because a stated zero renders `Z` rather than `+0000`, this is not a byte round-trip of the wire value; `serializeCcda` is still the round-tripping route. - **`toDate` returns a `Date` only when the zone is determinate.** The value carried an offset, or the caller passed `assumeOffsetMinutes` (a `ToDateOptions`, signed minutes east of UTC, and an explicit `0` means "treat this naive value as UTC"). With neither, the answer is `undefined`. **The host machine's timezone is never read and UTC is never assumed**, so the same document converts to the same instant on a laptop in Denver and a container in Frankfurt. A value's own offset always wins over an assumed one. - **`toDate` never answers with an `Invalid Date`.** An `assumeOffsetMinutes` of `NaN` or either infinity names no zone, and a finite offset large enough to push the result outside the range a JS `Date` holds denotes no instant; both are refused with `undefined`, exactly as an offset-less value with no assumption is. An `Invalid Date` would satisfy the declared `Date | undefined` return and defeat its point, because a caller cannot tell one from a real `Date` without testing `getTime()` for `NaN`, and `toISOString()` on it throws. Every sibling `@cosyte/*` parser refuses the same inputs the same way. - **An offset the document itself states is bounded, at 23 hours 59 minutes.** `±ZZZZ` is four digits wide in the v3 literal and constrains neither field, so a document may legally write `+2400`, `+9999` or `-9999`. Those state 1440, 6039 and -6039 minutes: finite numbers, and none of them a zone. The shared rendering has one slot for an offset, `+HH:MM` or `-HH:MM` with a two-digit hour of the day, so 23:59 is the widest offset it can state at all and `new Date` reads back nothing beyond it. Past that bound the value is refused **whole**, by all three functions together, rather than `toObject` reporting a hundred-hour zone that `toISO` renders as `+100:39`. `@cosyte/hl7` bounds at the same 23:59; `@cosyte/dicom` bounds tighter, at 14:59, because a DICOM offset cannot be written wider. This is the one place the three functions refuse a value `parseV3DateTime` accepts: `TS.date` still resolves such a literal, and no warning changes. So there are values where `ts.date` is a populated `Date` and `toDate(ts)` is `undefined`, on the same object, and that divergence is deliberate. `TS.date` is unchanged and stays unchanged: code reading it today reads exactly what it read before. Reach for `TS.date` when you want the existing eager behaviour and know it assumes UTC. Reach for `toObject` when the precision matters (a birth date stated to the month is not a birth date at midnight on the first), for `toISO` when you are writing the value out or comparing it as text, and for `toDate` when you need an instant and are willing to say which zone an offset-less value was written in. Because all six `@cosyte/*` parsers export these three names, a file consuming two of them has to alias the imports or namespace them: ```ts import { parseCcda, toISO as ccdaToISO } from "@cosyte/ccda"; import { parseDtm, toISO as hl7ToISO } from "@cosyte/hl7"; const document = parseCcda(xml); // A C-CDA encounter period: `effectiveTime` is an `IVL_TS`, so each bound is a `TS`. const admitted = ccdaToISO(document.getEncounters()[0]?.effectiveTime?.low); // The same conversion, over the value the v2 parser read out of an OBX-14. const observedAt = hl7ToISO(parseDtm("20260628153045-0500")); ``` The namespace form is the alternative, and reads better when a file uses several of the conversions from each package: ```ts import * as ccda from "@cosyte/ccda"; import * as hl7 from "@cosyte/hl7"; ccda.toObject(effectiveTime); hl7.toObject(dtm); ``` ## Code systems: recognition, not membership Coded slots are validated **structurally**: `checkCodeSlot` checks that a value's `@codeSystem` OID is one expected for its slot (e.g. RxNorm on a medication, SNOMED CT / ICD-10-CM on a problem) and flags a deprecated (`DEPRECATED_CODE_SYSTEM`, e.g. ICD-9) or unexpected (`UNEXPECTED_CODE_SYSTEM`) terminology. A value that asserts a `@code` with **no** `@codeSystem` at all is flagged `MISSING_CODE_SYSTEM`: a code without its system is not a code (`250.00` is diabetes in ICD-9-CM and an unrelated concept elsewhere), so it is preserved verbatim and no system is ever inferred for it, neither from the slot's expected list nor from a `@codeSystemName` label, which is display text rather than an identifier. A value that is **present** but asserts no usable `@code` and declares no `@nullFlavor` to say why is flagged `MISSING_CODE_VALUE`, the mirror shape: a system without a symbol names a concept no better than a symbol without a system. A `nullFlavor`-only value stays silent, it is a complete statement ("this concept is unknown"), and so does an absent element, there is nothing there to judge. It deliberately does **not** verify that a code is a real member of its system: that needs licensed terminology content (SNOMED CT / RxNorm via UMLS) this suite never bundles. The exported OIDs (`SNOMED_CT`, `RXNORM`, `ICD10_CM`, `LOINC`, `NDC`, `UNII`, `CVX`, …) are public identifiers, not redistributable code-system data. For membership checks, bring your own: pass a `TerminologyAdapter` to `parseCcda` or `buildCcda` and it is consulted at the five recognized coded slots (`problem`, `medication`, `allergen`, `route`, `vaccine`). A code your adapter rejects is flagged `SEMANTIC_CODE_INVALID` and preserved **verbatim**, never rewritten. A code with no `@codeSystem` cannot reach the adapter at all (it validates a system + code pair), which is why the structural `MISSING_CODE_SYSTEM` above is the signal for it. ## Computable UCUM units Every physical quantity (`PQ`) `@unit` is checked against a **computable, zero-dependency UCUM grammar**. A non-UCUM unit is flagged `NON_UCUM_UNIT`; a letter-case slip (e.g. `ML` for `mL`) is caught as `UCUM_CASE_SUSPECT`. The **raw unit is always preserved, never normalized away**, so a quantity is never silently re-dimensioned. The validators are exported for your own use: ```ts runnable import { isValidUcumUnit, isUcumCaseSuspect } from "@cosyte/ccda"; isValidUcumUnit("g/dL"); // => true isValidUcumUnit("mm[Hg]"); // => true isValidUcumUnit("cc"); // => false isUcumCaseSuspect("ML"); // => true isUcumCaseSuspect("mg"); // => false ``` The grammar covers a **curated atom subset** (the prefixes and atoms that appear in lab Results and Vital Signs), not the full UCUM atom registry. A valid but uncurated atom may read as `NON_UCUM_UNIT`; because the raw unit is preserved, nothing is lost. ## The serializer: spec-clean, round-trip emit `serializeCcda(doc)` (equivalently `doc.toString()`) is the conservative **emit** half of Postel's Law. It re-emits a **parsed** document as spec-clean C-CDA XML with a guaranteed UTF-8 declaration. The output is snapshotted from the source XML at parse time, not rebuilt from the read-model, so every attribute, namespace declaration, `templateId`, and unmodeled element survives: **no silent loss**. Serialization is a **fixed point**: `parseCcda(serializeCcda(doc))` re-serializes to the identical string. ```ts runnable import { parseCcda, serializeCcda } from "@cosyte/ccda"; const xml = ` Synthetic CCD JaneDoe `; const doc = parseCcda(xml); const out = serializeCcda(doc); out === doc.toString(); // => true out.startsWith(" true // Serialization is a fixed point: re-parse + re-serialize is byte-identical. serializeCcda(parseCcda(out)) === out; // => true ``` > A hand-constructed `CcdaDocument` (not produced by `parseCcda` or `buildCcda`) retains no source XML, > so `toString()` throws. To construct a document from scratch, use `buildCcda`: it emits a spec-clean > CCD or Referral Note with a US Realm header and the clinical sections you supply. See > [Troubleshooting](https://docs.cosyte.com/ccda/troubleshooting.md) for exactly what the parser, builder, and editor do and do not > do today. --- URL: https://docs.cosyte.com/ccda/cookbook Markdown: https://docs.cosyte.com/ccda/cookbook.md # Cookbook Task-oriented recipes for the C-CDA jobs you actually get handed. Each one is: here's the problem, here's the code, here's what you get back. Every symbol below is a real `@cosyte/ccda` export: no pseudo-API. All sample XML is **synthetic** (an invented patient, obviously-fake OIDs); never paste a real clinical document into a doc or a test. Read [Getting started](https://docs.cosyte.com/ccda/intro.md) first for the parse model; the recipes here assume you can already get a parsed `CcdaDocument`. --- ## 1. Read the active problem list **The problem:** you have a CCD and want the patient's **active** conditions with their coded values, not the resolved or inactive ones. The concern act's `status` is the safety-relevant field: an inactive or resolved problem must never read as active, so filter on it. The coded condition lives on each problem's `value` (SNOMED CT or ICD-10-CM). ```ts runnable import { parseCcda } from "@cosyte/ccda"; const xml = ` Synthetic CCD JaneDoe
Problems Essential hypertensionAppendicitis
`; const doc = parseCcda(xml); const active = doc.getProblems().filter((c) => c.status === "active"); active.length; // => 1 active[0]?.problems[0]?.value?.code; // => "59621000" // The resolved concern is still present. It is just not "active". doc.getProblems().length; // => 2 doc.getProblems().map((c) => c.status); // => ["active", "resolved"] ``` --- ## 2. Respect the code/narrative fail-safe **The problem:** the structured code and the human-readable narrative can disagree, a real and dangerous vendor defect. You must never silently trust one over the other. When a coded value's label disagrees with the narrative it references, the parser surfaces **both** and raises `CODE_NARRATIVE_MISMATCH`, picking no winner. Gate on it and route the record to a human. ```ts runnable import { parseCcda, WARNING_CODES } from "@cosyte/ccda"; const xml = ` Synthetic Progress Note JaneDoe
Problems Type 2 diabetes mellitus
`; const doc = parseCcda(xml); const problem = doc.getProblems()[0]?.problems[0]; // Both views are preserved. The parser picks no winner. problem?.value?.displayName; // => "Essential hypertension" problem?.narrative; // => "Type 2 diabetes mellitus" const conflicted = doc.warnings.some((w) => w.code === WARNING_CODES.CODE_NARRATIVE_MISMATCH); conflicted; // => true // if (conflicted) routeToHumanReview(doc); ``` --- ## 3. Triage warnings: the lenient, never-throw contract **The problem:** you want to log or triage every tolerated deviation without your pipeline throwing on a vendor quirk. `@cosyte/ccda` is liberal on input: only the seven Tier-3 structural/security errors ever throw; everything else is a warning carrying a stable code and a bounded structural position. Every warning collects on `doc.warnings`; you can also stream them live via `onWarning`: ```ts runnable import { parseCcda, WARNING_CODES, type CcdaWarning } from "@cosyte/ccda"; // A section with an unrecognized LOINC code: tolerated, retained as narrative-only. const xml = ` Synthetic CCD JaneDoe
Vendor-specific Local content.
`; const streamed: CcdaWarning[] = []; const doc = parseCcda(xml, { onWarning: (w) => { streamed.push(w); // w.code: a stable string from WARNING_CODES // w.message: a frozen registry string; it interpolates nothing at all // w.position: where in the document it occurred (bounded element path, LOINC, line/column) }, }); // Or read them after the fact: doc.warnings.some((w) => w.code === WARNING_CODES.UNKNOWN_SECTION_CODE); // => true streamed.length > 0; // => true // Every message is safe to log: no PHI by construction. doc.warnings.every((w) => typeof w.message === "string"); // => true ``` **Escalate when you want strictness.** Pass `{ strict: true }` to turn the first tolerated deviation into a thrown `CcdaParseError` carrying the same code: a spec-conformance gate for a trusted sender. --- ## 4. Round-trip a document through the serializer **The problem:** you parsed a document, and now need spec-clean XML back out (for storage, forwarding, or a diff) with a guarantee that nothing was silently lost. `serializeCcda(doc)` (or `doc.toString()`) re-emits the **parsed** document faithfully: every attribute, namespace, `templateId`, and unmodeled element survives, and the result is a fixed point. ```ts runnable import { parseCcda, serializeCcda } from "@cosyte/ccda"; const xml = ` Synthetic CCD JaneDoe `; const doc = parseCcda(xml); const out = serializeCcda(doc); out === doc.toString(); // => true // Re-parsing and re-serializing yields the identical bytes (a fixed point). serializeCcda(parseCcda(out)) === out; // => true ``` > `serializeCcda` re-emits a **parsed** document. To construct one from scratch, use `buildCcda` > (recipe 5 below); to change a section of a document you already parsed, use `editCcda` (recipe 6). > For exactly which document types and sections each one covers, see > [Troubleshooting](https://docs.cosyte.com/ccda/troubleshooting.md#building-a-document). --- ## 5. Build a spec-clean CCD from scratch **The problem:** you have structured clinical data (a patient, some problems, some allergies) and need a valid C-CDA document to send, without hand-writing XML or memorizing templateIds. `buildCcda(init)` is the emit _factory_ symmetric with `parseCcda`. It emits through the same DOM the parser reads, so a built document round-trips by construction and a clean build carries zero warnings. ```ts runnable import { buildCcda, serializeCcda, parseCcda } from "@cosyte/ccda"; const doc = buildCcda({ patient: { mrn: "MRN-00042", given: ["Jane"], family: "Doe", gender: "F", birthTime: "19800101" }, problems: [ { problem: { code: "59621000", displayName: "Essential hypertension" }, status: "active" }, ], allergies: [ { allergen: { code: "7980", displayName: "Penicillin G" }, reaction: { code: "247472004", displayName: "Hives" }, }, { noKnownAllergy: true }, // a negation, never an "unknown" ], }); doc.getMrn(); // => "MRN-00042" doc.getProblems().length; // => 1 doc.warnings.length; // => 0 // Round-trips by construction: re-parsing the emitted XML is a fixed point. const out = serializeCcda(doc); parseCcda(out).toString() === out; // => true ``` `buildCcda` populates the reconciliation triad (Problems, Allergies, Medications) plus the discrete-data families: Results, Vital Signs, and **Immunizations**. A vaccine is coded with CVX; its dose and route are emitted only when supplied (never guessed), and a **refused** shot is a `negationInd` record the parser reads back distinctly, never confused with an "unknown": ```ts runnable import { buildCcda } from "@cosyte/ccda"; const doc = buildCcda({ patient: { mrn: "MRN-00042" }, immunizations: [ { vaccine: { code: "140", displayName: "Influenza, split virus, trivalent, injectable, preservative free", }, // CVX dose: { value: 0.5, unit: "mL" }, // UCUM route: { code: "C28161", displayName: "Intramuscular" }, // NCI Thesaurus effectiveTime: "20240101", }, // A refused shot: emitted as negationInd="true", flagged IMMUNIZATION_REFUSED, never an "unknown". { vaccine: { code: "140", displayName: "Influenza, split virus, trivalent, injectable, preservative free", }, refused: true, }, ], }); doc.getImmunizations()[0]?.vaccine?.code; // => "140" doc.getImmunizations()[1]?.refused; // => true doc.warnings.map((w) => w.code).includes("IMMUNIZATION_REFUSED"); // => true ``` `buildCcda` also emits **Procedures** and **Encounters** when supplied. A procedure's `disposition` sets the performed-vs-planned `moodCode` (`performed` → `EVN`, `planned` → `INT`). The parser reads it back distinctly, so a _planned_ procedure is never reported as performed. An encounter carries its coded type (CPT by default) and a visit period: ```ts runnable import { buildCcda } from "@cosyte/ccda"; const doc = buildCcda({ patient: { mrn: "MRN-00042" }, procedures: [ { code: { code: "80146002", displayName: "Appendectomy" }, // SNOMED CT disposition: "performed", effectiveTime: "20230615", }, // A planned colonoscopy: moodCode="INT", read back as disposition "planned". { code: { code: "73761001", displayName: "Colonoscopy" }, disposition: "planned" }, ], encounters: [ { type: { code: "99213", displayName: "Office outpatient visit 15 minutes" }, // CPT period: { low: "20230615", high: "20230615" }, }, ], }); doc.getProcedures()[0]?.disposition; // => "performed" // A planned procedure is never read as performed: doc.getProcedures()[1]?.disposition; // => "planned" doc.getEncounters()[0]?.code?.code; // => "99213" doc.warnings.length; // => 0 ``` `buildCcda` also emits a **Social History** section carrying **Smoking Status** observations when supplied. A known status is a SNOMED CT concept from the Current Smoking Status value set. An _unknown_ status is never guessed: omit `value` and the builder emits an explicit `nullFlavor="UNK"`, which the parser reads back as `unknown`, absent status is never read as "never smoker": ```ts runnable import { buildCcda } from "@cosyte/ccda"; const doc = buildCcda({ patient: { mrn: "MRN-00042" }, smokingStatus: [ { value: { code: "8517006", displayName: "Former smoker" }, effectiveTime: "20240101" }, // SNOMED CT {}, // status not recorded → an EXPLICIT nullFlavor="UNK", never invented as a reading ], }); doc.getSmokingStatus()[0]?.value?.code; // => "8517006" doc.getSmokingStatus()[0]?.unknown; // => false // A missing status is surfaced as unknown, not defaulted to a value: doc.getSmokingStatus()[1]?.unknown; // => true ``` > Current builder scope: `buildCcda` emits a **CCD** (the default), a **Referral Note** > (`documentType: "referralNote"`) or an inpatient **Discharge Summary** > (`documentType: "dischargeSummary"`). A CCD carries the US Realm header, the six CCD SHALL sections > (Problems, Allergies, Medications, Results, Vital Signs and **Social History** (Smoking Status), > emitted empty as `nullFlavor="NI"` when no > content is supplied), and **Immunizations**, **Procedures**, **Encounters**, > **Functional Status**, **Mental Status** (each carrying standalone findings, > Functional/Mental Status Organizers, and **direct-entry Assessment Scale Observations** `…22.4.69` with > their Supporting Observations `…22.4.86` and an `INT` score), **Past Medical History**, **Plan of > Treatment** (planned entries, never conflated with performed), and **Family History** (a Family > History Organizer per relative, with conditions carrying optional age-at-onset + cause-of-death) > sections when populated. A Referral Note specializes the header and always emits Problems, > Allergies, Medications, **Reason for Referral**, **Assessment**, and Plan of Treatment, which makes > Results, Vital Signs and Social History populated-only. A Discharge Summary specializes it again > and always emits Allergies, **Hospital Course**, **Discharge Diagnosis** and Plan of Treatment, > plus the `componentOf/encompassingEncounter` frame its template requires. All three accept an > optional bring-your-own `terminology` > adapter. The other nine C-CDA R2.1 document types are not implemented. ### Build a Discharge Summary, with its encounter frame `BuildableDocumentType` is the union of the three types `buildCcda` emits (`"ccd"`, `"referralNote"`, `"dischargeSummary"`); ask for any of the other nine and you get a `TypeError` naming what you asked for, never a document that merely resembles it. A Discharge Summary carries one more thing than the other two types: the encounter it summarises. Supply the period and the discharge disposition through `encompassingEncounter`, a `BuildCcdaEncompassingEncounter`. Any slot you leave out is emitted as an explicit `nullFlavor="UNK"` and reparses as absent, never as a guessed date or a guessed disposition; an admission date, a discharge date and a discharge disposition are each facts a clinician acts on. ```ts import { buildCcda } from "@cosyte/ccda"; const summary = buildCcda({ documentType: "dischargeSummary", patient: { mrn: "MRN001", given: ["Jane"], family: "Doe", gender: "F" }, hospitalCourse: "Admitted for observation. Uneventful course, discharged home.", dischargeDiagnoses: [ { problem: { code: "59621000", displayName: "Essential hypertension" }, onset: "20240102" }, ], encompassingEncounter: { period: { low: "20240102", high: "20240108" }, // `codeSystem` defaults to the NUBC UB-04 Patient Discharge Status code set. dischargeDisposition: { code: "01", displayName: "Discharged to Home or Self Care" }, }, }); summary.header.encompassingEncounter?.effectiveTime?.low?.raw; // => "20240102" summary.header.encompassingEncounter?.dischargeDispositionCode?.code; // => "01" summary.warnings.length; // => 0 summary.findSection("hospitalCourse")?.narrativeText; // => "Admitted for observation. Uneventful course, discharged home." ``` > **A Discharge Summary reparses with zero warnings, and its Hospital Course is recognized but not > required.** The SHALL Hospital Course Section is an IHE PCC template the parser recognizes by its > root, so `findSection("hospitalCourse")` resolves. The parser does not assert it as a required > section, so a reparse will not report it MISSING from a document that lacks it: `pnpm conformance` > is the check on its presence. > > **What you supply is emitted or refused.** `problems` and `medications` are emitted when you pass > them (the medications in the Medications Section, not the Discharge Medications Section). Passing > `assessment` or `reasonForReferral`, which are Referral Note sections, throws a `TypeError`, as > does passing `hospitalCourse`, `dischargeDiagnoses` or `encompassingEncounter` to a CCD or a > Referral Note. ## 6. Edit a parsed document: add or replace a section, keep a revision trail **The problem:** you have a real C-CDA document, and you need to correct one section (add a Problem, swap out the Medications) and send the fix, **without** disturbing every other section or losing the audit trail that this is a new version of the original. `editCcda(doc, options)` is the read→edit→write loop. It takes a document from `parseCcda`, rebuilds only the section you target (through the same emitters `buildCcda` uses), and carries every other section through **byte-for-byte**. By default it stamps a CDA R2 revision: a new document `id`, the same version-series `setId`, an incremented `versionNumber`, and a `relatedDocument typeCode="RPLC"` naming the version it replaces. When the source carries no `setId`, one is **minted** (CDA R2 requires the replacement and its `parentDocument` to share one) and it is labelled as synthetic: `SYNTHETIC-SETID-…` under a synthetic assigning-authority root. Call `isSyntheticSetId(doc.header.setId)` to tell a minted version series from one a source system asserted. Nothing forces a receiver to read that label, so treat it as a way to recognise a fabricated id, never as a guarantee anyone downstream will notice it. Check `revised.warnings` after an edit, not just after a parse. An edit reports the conformance gaps in what **it** wrote: a grafted Plan of Treatment carrying a planned medication with no `effectiveTime` draws `MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME`, the same code `buildCcda` raises for the same document. It is scoped to the sections that edit grafted and that survived into the output, so an edit superseded by a later one in the same call is not reported, and an offending act the source already carried is not re-reported on every unrelated edit. ```ts runnable import { buildCcda, editCcda, parseCcda } from "@cosyte/ccda"; // Start from a document the parser produced (here, one we built and re-parsed). const original = parseCcda( buildCcda({ patient: { mrn: "MRN-00042", given: ["Jane"], family: "Doe" }, documentId: "DOC-1", problems: [{ problem: { code: "38341003", displayName: "Hypertension" }, status: "active" }], medications: [{ drug: { code: "197361", displayName: "Amlodipine 5 MG" } }], }).toString(), ); const revised = editCcda(original, { sections: [ // Replace the Medications section wholesale… { kind: "medications", mode: "replace", content: [{ drug: { code: "314076", displayName: "Lisinopril 10 MG" } }], }, // …and add a Family History section the source did not have. { kind: "familyHistory", content: [ { relative: { relationship: { code: "72705000", displayName: "Mother" } }, observations: [{ condition: { code: "73211009", displayName: "Diabetes mellitus" } }], }, ], }, ], }); revised.getMedications()[0]?.drug?.code; // => "314076" revised.getFamilyHistory().length; // => 1 // The untouched Problems section is preserved: revised.getProblems()[0]?.problems[0]?.value?.code; // => "38341003" // A CDA R2 revision that replaces DOC-1 (version 1): revised.header.versionNumber; // => 2 revised.header.relatedDocuments[0]?.typeCode; // => "RPLC" revised.header.relatedDocuments[0]?.parentDocument.ids[0]?.extension; // => "DOC-1" // Re-parsing the edited XML is a fixed point. parseCcda(revised.toString()).toString() === revised.toString(); // => true ``` `editCcda` is fail-safe by construction: an unedited section is carried by reference (never dropped or corrupted), an empty content list emits a spec-clean `nullFlavor="NI"` shell rather than fabricated entries, and an edit that would drop a document type's SHALL required section throws a typed `CcdaEditError` instead of emitting an invalid document. Use `mode: "add"` to require the section be absent (or `"replace"` to require it present); the default `"upsert"` replaces when present and adds when absent. Pass `revision: false` to edit in place without stamping a new version. Revising a source that carries no `ClinicalDocument.id` throws `CcdaEditError` (`SOURCE_MISSING_ID`): the RPLC `parentDocument.id` is a CDA R2 SHALL (1..\*) and there is no prior-version id to name, so `editCcda` refuses rather than mint a fabricated identifier. Edit such a document in place with `revision: false`. > Editing scope: whole-section **add** / **replace** across the twelve single-list section kinds > (Problems, Allergies, Medications, Results, Vital Signs, Immunizations, Procedures, Encounters, > Social History, Past Medical History, Plan of Treatment, Family History). Four things are **not > supported**: entry-level append that byte-preserves a section's other entries (supply the full entry > set via a `replace` instead, which rebuilds that section from your input), the Functional/Mental > Status and narrative-only sections as edit targets, section removal, and the addendum (`APND`) / > transform (`XFRM`) relationships. --- URL: https://docs.cosyte.com/ccda/conformance Markdown: https://docs.cosyte.com/ccda/conformance.md # Conformance, profiles & terminology Four things you tune once, per deployment, and then stop thinking about: **which deviations your senders are allowed to make** (a vendor profile), **which codes you consider real** (a terminology adapter), **how much of a document type's obligation this package can actually check** (the required-section conformance status), and **which codes the templates actually allow** (a value-set source, against the bindings C-CDA declares). All four are read-side controls. None of them changes what the parser extracts, and none of them can make a safety-critical deviation quiet. Every example below runs against the shipped package with no network and no licensed service. All sample XML is **synthetic** (an invented patient, obviously-fake OIDs); never paste a real clinical document into a doc or a test. --- ## 1. Apply a vendor profile **The problem:** your upstream sender has a known, benign quirk. It ships deprecated LOINC codes, or omits the R2.1 version stamp. You do not want that noise in your triage queue every day, but you also do not want to disable warnings wholesale and lose the ones that matter. A **profile** is a named, frozen declaration of the deviations you expect from a source, each one carrying a written rationale. `parseCcda(raw, { profile })` downgrades exactly those and nothing else. The built-ins live on the `ccdaProfiles` namespace; `getCcdaProfile` and `listCcdaProfiles` reach the registry by name, and `setDefaultCcdaProfile` / `getDefaultCcdaProfile` set a process-wide default so you do not have to thread the option through every call site. Build your own with `defineCcdaProfile`, extending a built-in where one is close. `lineage` records what it was built from, and `describe()` prints a reviewable summary. **The safety gate is not advisory.** `defineCcdaProfile` **throws** a `CcdaProfileDefinitionError` if you try to tolerate a code in `SAFETY_CRITICAL_CODES` (patient identity, allergy, dose, unit, value integrity). Check one yourself with `isSafetyCriticalCode`. A profile quiets structural noise; it can never quiet a deviation that could change a clinical reading. ```ts runnable import { defineCcdaProfile, ccdaProfiles, getCcdaProfile, listCcdaProfiles, isSafetyCriticalCode, CcdaProfileDefinitionError, } from "@cosyte/ccda"; // The built-ins, each authored through the public factory and carrying its cited grounding. // `listCcdaProfiles` returns registry NAMES; `getCcdaProfile` resolves one to the profile. listCcdaProfiles(); // => ["default", "smartScorecard", "legacyR11"] getCcdaProfile("legacyR11")?.name; // => "legacyR11" ccdaProfiles.smartScorecard.tolerate.map((t) => t.code); // => ["DEPRECATED_LOINC", "DEPRECATED_CODE_SYSTEM", "INVALID_NULL_FLAVOR"] // Extend one for a single sender. `tolerate` is additive over the parent. const siteProfile = defineCcdaProfile({ name: "example-site", description: "Intake tolerances for one regional sender.", extends: ccdaProfiles.smartScorecard, tolerate: [ { code: "UNKNOWN_SECTION_CODE", rationale: "This sender carries two site-local sections we have reviewed and accept.", }, ], }); siteProfile.lineage; // => ["smartScorecard", "example-site"] siteProfile.tolerate.map((t) => t.code); // => ["DEPRECATED_LOINC", "DEPRECATED_CODE_SYSTEM", "INVALID_NULL_FLAVOR", "UNKNOWN_SECTION_CODE"] typeof siteProfile.describe; // => "function" // The safety gate refuses at definition time, not at parse time. isSafetyCriticalCode("SUBJECT_CONTEXT_OVERRIDE"); // => true isSafetyCriticalCode("DEPRECATED_LOINC"); // => false let refused = false; try { defineCcdaProfile({ name: "over-tolerant", tolerate: [{ code: "SUBJECT_CONTEXT_OVERRIDE", rationale: "we would rather not see it" }], }); } catch (err) { refused = err instanceof CcdaProfileDefinitionError; } refused; // => true ``` Pass it to a parse with `parseCcda(raw, { profile: siteProfile })`. The parsed document records which profile shaped it on `doc.profile` (a `ProfileAttribution`), so a downstream reviewer can see what was tolerated and why rather than wondering where a warning went. `applyProfile` and `wrapEmitterWithProfile` are the same transform exposed directly, for a pipeline that collects warnings on its own channel. **Types you will import here:** `CcdaProfile` (the frozen profile), `DefineCcdaProfileOptions` (the factory's input), `QuirkTolerance` (one declared deviation, with its `rationale`), `QuirkMatch` (the optional structural narrowing, on `sectionCode` or `templateId` only), `ProfileProvenance` (the cited grounding) and `ProfileAttribution` (what a parsed document records about the profile that shaped it). --- ## 2. Supply a bring-your-own terminology adapter **The problem:** you want to know whether a coded value is a real member of its code system. This package cannot tell you. SNOMED CT is affiliate-licensed, RxNorm is UMLS-gated, CPT is AMA-licensed, and a zero-dependency parser bundles none of their content. So terminology is **bring-your-own**: you implement `TerminologyAdapter` (one required method, `validateCode`) and hand it to `parseCcda`. The parser calls it at the five clinical `CodeSlot`s (`problem`, `medication`, `allergen`, `route`, `vaccine`) on each slot's primary coding. The contract is **fail-safe**: a negative verdict raises `SEMANTIC_CODE_INVALID` and the document's own code is **preserved verbatim**, never corrected, never replaced by your adapter's label. Returning `undefined` means "no opinion" and is the right answer for a system you do not cover. The adapter below is an in-process `Set`. It opens no socket, which is what makes this example runnable here and what makes an adapter testable in your own suite. ```ts runnable import { parseCcda, WARNING_CODES, type TerminologyAdapter } from "@cosyte/ccda"; const SNOMED = "2.16.840.1.113883.6.96"; // A stub adapter: in-process, no network, no licensed service. Yours would call // your own terminology engine here. const knownSnomed = new Set(["59621000"]); const adapter: TerminologyAdapter = { validateCode: (coding) => coding.system === SNOMED ? { result: knownSnomed.has(coding.code) } : undefined, }; const ccd = (problemCode: string) => ` Synthetic CCD JaneDoe
Problems Essential hypertension
`; // A code the adapter recognizes: no semantic warning. const clean = parseCcda(ccd("59621000"), { terminology: adapter }); clean.warnings.some((w) => w.code === WARNING_CODES.SEMANTIC_CODE_INVALID); // => false // A code it rejects: warned, and the document's own value is left exactly as it was. const flagged = parseCcda(ccd("99999999"), { terminology: adapter }); flagged.warnings.some((w) => w.code === WARNING_CODES.SEMANTIC_CODE_INVALID); // => true flagged.getProblems()[0]?.problems[0]?.value?.code; // => "99999999" ``` **Read the clean run precisely.** It means those five slots passed your adapter, not that the document was terminology-verified. `` alternates are preserved but never slot-checked, and `MISSING_CODE_SYSTEM` / `MISSING_CODE_VALUE` are safety-critical: no profile can tolerate them, whatever your adapter says. `translate` is an optional second method. `buildCcda` emits each returned coding **beside** the primary code as a ``, never replacing it, and an empty `matches` array emits nothing. The library never fabricates a target. **Types you will import here:** `TerminologyAdapter` (the contract you implement), `TerminologyCoding` (one coding handed to you, whose `system` is the C-CDA `@codeSystem` OID, never a URI), `CodeValidationResult` (your verdict), `CodeTranslationResult` (the optional `translate` reply) and `CodeSlot` (the five slot names an adapter is consulted at). `checkCodeSlot` is the structural check that runs regardless of whether you supply an adapter. --- ## 3. Read a document type's required-section status **The problem:** `REQUIRED_SECTION_MISSING` tells you a section this package expects is absent. Before you route on that, you need to know how much of that document type's obligation this package has actually read off the normative source, and whether it evaluated the obligation at all. `requiredSectionKeys` gives the asserted SHALL keys for a type. `missingRequiredSections` diffs them against the sections you framed. `requiredSectionStatus` is the honest surface behind both: it carries the keys **plus** the provenance of each one, plus two independent state axes. - **`verification`** is a claim about the _type_: `traced-complete`, `traced-partial`, `untraced` or `not-applicable`. How much of that type's obligation was read off the normative artifact. - **`evaluation`** is a claim about the _lookup_: `evaluated` or `not-evaluated`. Whether the version stamp you supplied put the document inside the tables at all. That split is the point. An empty `keys` set is never ambiguous, because the two states together say **which** emptiness it is: nothing required, nothing asserted, or nothing asked. ```ts runnable import { requiredSectionKeys, missingRequiredSections, requiredSectionStatus } from "@cosyte/ccda"; // A CCD's obligation is fully traced to the normative artifact, and every key // carries the conformance statement it was read from. const ccd = requiredSectionStatus("ccd"); ccd.verification; // => "traced-complete" ccd.evaluation; // => "evaluated" ccd.keys; // => ["allergies", "medications", "problems", "results", "socialHistory", "vitalSigns"] ccd.traced[0]?.conformanceId; // => "CONF:1198-30662" ccd.source?.revision; // => "2025-09-08" // Same keys, without the provenance, from the lighter accessor. requiredSectionKeys("ccd"); // => ["allergies", "medications", "problems", "results", "socialHistory", "vitalSigns"] // What a document that framed only two of them is missing. missingRequiredSections("ccd", new Set(["allergies", "problems"])); // => ["medications", "results", "socialHistory", "vitalSigns"] // EMPTY BECAUSE NOTHING IS REQUIRED: an Unstructured Document carries no // structuredBody at all, so the obligation does not apply to it. const unstructured = requiredSectionStatus("unstructuredDocument"); unstructured.verification; // => "not-applicable" unstructured.keys; // => [] // EMPTY BECAUSE NOTHING WAS ASKED: the document names a C-CDA release this // package has not read. Reducing the obligation instead would be a confident // wrong statement about conformance. const future = requiredSectionStatus("ccd", { stamp: "unmodeled-release" }); future.evaluation; // => "not-evaluated" future.keys; // => [] ``` Two readings that are easy to get backwards, and both are load-bearing: - **An unmodelled version stamp reports the obligation _unevaluated_, never _reduced_.** It is not the same as the R1.1-origin reduction, which applies to a document carrying **no** stamp at all and does drop the R2.1-scoped keys (`requiredSectionKeys("ccd", { r21Stamped: false })` returns four, not six). Treating a future release like an old one is how a document silently loses Social History and Vital Signs. - **`unasserted` is not a gap list.** It names SHALL sections the source states that this package deliberately does not assert, each with one of three reasons: the section is outside the recognized catalog (`outside-section-catalog`), the source requires it only conditionally (`not-unconditionally-required`), or the parser recognizes it and asserting it would make the parse of documents that omit it stricter (`assertion-would-tighten-parse`, today only the Discharge Summary's Hospital Course). Asserting a SHOULD, or one half of a choice, mis-flags a conformant document, which is the same defect as missing a SHALL with the sign flipped. `requiredSectionStatuses()` returns the status for every recognized document type at once, which is the call to make if you are building a conformance dashboard rather than checking one document. **Types you will import here:** `RequiredSectionStatus` (the whole record), `RequiredSectionVerification` and `RequiredSectionEvaluation` (the two state axes above), `RequiredSectionOptions` (the `r21Stamped` and `stamp` inputs), `RequiredSectionSource` (the normative artifact and its own revision date), `TracedRequiredSection` (one asserted key with its `CONF:` statement) and `UnassertedRequiredSection` with `UnassertedSectionReason` (a named SHALL section this package does not assert, and which of the two permitted reasons applies). --- ## 4. Check a coded value against the value set its slot is bound to **The problem:** the code system is right, the code is real, and the value is still wrong. C-CDA does not only bind a slot to a code system, it binds it to a **value set**, and where that binding is **Required** the standard states membership as a SHALL. A code that passes section 2's adapter can still sit outside the bound value set, and a certifier and a receiving clinician both care about exactly that case. Member codes are licensed data (SNOMED CT, RxNorm and CVX expansions), so this is bring-your-own too: this package declares the **value set identifiers** and their provenance, you hold the expansions, and `isMember` is the one question it asks you. `valueSetBinding` is the declaration, per checked slot. Every row says how strongly C-CDA R2.1 binds the value set, and names the conformance statement, the template and the artifact revision the reading was taken from, so no binding is asserted that the normative source was not read for: ```ts runnable import { valueSetBinding, valueSetBindings } from "@cosyte/ccda"; const medication = valueSetBinding("medication"); medication.strength; // => "required" medication.valueSet; // => "2.16.840.1.113762.1.4.1010.4" medication.conformanceId; // => "CONF:1098-7412" medication.source.revision; // => "2025-09-08" // Not every checked slot carries a Required binding, and the difference is the // difference between a conformance failure and a suggestion. The Problem // Observation's value SHALL be present, and its code only SHOULD come from the // Problem value set, which C-CDA represents as a Preferred binding. valueSetBinding("problem").strength; // => "preferred" valueSetBindings().filter((b) => b.strength === "required").length; // => 4 ``` Supply a `ValueSetSource` and the parser reports what you say. It never refuses, rewrites, reorders or drops a value on a negative verdict, and it never asks about a Preferred row: ```ts runnable import { parseCcda, WARNING_CODES, type ValueSetSource } from "@cosyte/ccda"; // Your package, in process. One expansion held, deliberately: the second slot // below shows what happens to a value set you do NOT hold. const routes = new Set(["C38288"]); const valueSets: ValueSetSource = { release: "example-value-set-package-2026.1", isMember: ({ valueSet, coding }) => { if (valueSet !== "2.16.840.1.113883.3.88.12.3221.8.7") return { membership: "no-expansion" }; return { membership: routes.has(coding.code) ? "member" : "not-a-member" }; }, }; const ccd = (routeCode: string) => ` Synthetic progress note JaneDoe
Medications Lisinopril
`; // No source supplied: nothing is asked, and membership is never inferred from // the code system alone. const quiet = parseCcda(ccd("C38288")); quiet.warnings.some((w) => w.code === WARNING_CODES.VALUE_SET_BINDING_VIOLATED); // => false // A route your expansion does not contain, at a Required binding: a SHALL // violation, reported with the value set and the release that answered. const flagged = parseCcda(ccd("C38276"), { valueSets }); const violation = flagged.warnings.find((w) => w.code === WARNING_CODES.VALUE_SET_BINDING_VIOLATED); violation?.valueSet; // => "2.16.840.1.113883.3.88.12.3221.8.7" violation?.valueSetRelease; // => "example-value-set-package-2026.1" // The document's own value is untouched, exactly as with the terminology adapter. flagged.getMedications()[0]?.route?.code; // => "C38276" // The medication product's value set is one this source does not hold, so that // binding is reported NOT EVALUATED rather than passed over in silence. flagged.warnings.some((w) => w.code === WARNING_CODES.VALUE_SET_BINDING_NOT_EVALUATED); // => true ``` **Three answers, and the third is why this is not a boolean.** `member` is silent, `not-a-member` at a Required binding raises `VALUE_SET_BINDING_VIOLATED`, and `no-expansion` raises `VALUE_SET_BINDING_NOT_EVALUATED`: a package that quietly skipped a value set it does not hold would read exactly like one that checked and found the code fine. Returning `undefined` is the fourth answer and the only silent one, and it means "not my authority", the same "no opinion" an adapter gives. **`release` is required, and it is what keeps the finding honest.** C-CDA states that for a Required binding any valid expansion of a value set is conformant, so "not a member" is only ever a statement about the expansion that answered. The release you declare rides on every finding beside the value set OID. Neither ever appears in a message: messages come whole from the frozen registry, and the coded value that caused the finding appears nowhere in it at all. An exception your `isMember` raises is **not** swallowed. A failing value-set service surfaces to you rather than being masked into a document that merely looks checked. **Types you will import here:** `ValueSetSource` (the contract you implement, carrying `release` and `isMember`), `ValueSetMembershipQuery` (the question: a bound value set OID and the document's coding), `ValueSetMembershipAnswer` (your three-state reply), `ValueSetBinding` (one declared row), `ValueSetBindingStrength` (`required` or `preferred`), `ValueSetBindingSource` (the normative artifact and its own revision) and `BoundValueSet` (the closed set of value set OIDs a finding can name). `valueSetBinding` and `valueSetBindings` read the declaration; `valueSetBindingViolated` and `valueSetBindingNotEvaluated` are the warning factories behind the two codes, exported for the same reason `semanticCodeInvalid` is. --- URL: https://docs.cosyte.com/ccda/api Markdown: https://docs.cosyte.com/ccda/api.md # @cosyte/ccda ## Classes ### CcdaDocument The immutable parsed C-CDA document. Carries the recognized identity, the header, the top-level sections (empty for an unstructured document, see `nonXmlBody`), and the frozen lenient-parse warnings. #### Example ```ts import { parseCcda } from "@cosyte/ccda"; const doc = parseCcda(xml); console.log(doc.documentType, doc.getPatient()?.name?.text, doc.getMrn()); ``` #### Constructors ##### Constructor > **new CcdaDocument**(`init`): [`CcdaDocument`](#ccdadocument) **`Internal`** Construct a `CcdaDocument`. Freezes the `warnings` array (after a `slice`) so callers cannot mutate parser output after handoff. ###### Parameters ###### init [`CcdaDocumentInit`](#ccdadocumentinit) ###### Returns [`CcdaDocument`](#ccdadocument) #### Properties ##### allergies > `readonly` **allergies**: readonly [`AllergyConcern`](#allergyconcern)[] Extracted Allergy Concern Acts (across all sections). Empty when none. ##### documentType > `readonly` **documentType**: [`DocumentType`](#documenttype-4) \| `undefined` The recognized document type, or `undefined` when the root `templateId` is unrecognized/absent. ##### encounters > `readonly` **encounters**: readonly [`Encounter`](#encounter)[] Extracted Encounter Activities, visits/admissions (across all sections). Empty when none. ##### familyHistory > `readonly` **familyHistory**: readonly [`FamilyHistory`](#familyhistory-4)[] Extracted Family History Organizers, one per relative (across all sections). Empty when none. ##### functionalStatus > `readonly` **functionalStatus**: readonly [`StatusObservation`](#statusobservation)[] Extracted Functional Status findings (across all sections). Empty when none. ##### header > `readonly` **header**: [`CcdaHeader`](#ccdaheader) The parsed US Realm Header. ##### immunizations > `readonly` **immunizations**: readonly [`Immunization`](#immunization)[] Extracted Immunization Activities (across all sections). Empty when none. ##### medications > `readonly` **medications**: readonly [`Medication`](#medication)[] Extracted Medication Activities (across all sections). Empty when none. ##### mentalStatus > `readonly` **mentalStatus**: readonly [`StatusObservation`](#statusobservation)[] Extracted Mental Status findings (across all sections). Empty when none. ##### nonXmlBody > `readonly` **nonXmlBody**: [`ED`](#ed) \| `undefined` The quarantined `nonXMLBody` content for an unstructured document (base64 never decoded). ##### pastMedicalHistory > `readonly` **pastMedicalHistory**: readonly [`Problem`](#problem-1)[] Extracted Past Medical History problems, bare historical Problem Observations. Empty when none. ##### plannedItems > `readonly` **plannedItems**: readonly [`PlannedItem`](#planneditem)[] Extracted planned items from the Plan of Treatment, all future/ordered (across all sections). Empty when none. ##### problems > `readonly` **problems**: readonly [`ProblemConcern`](#problemconcern)[] Extracted Problem Concern Acts (across all sections). Empty when none. ##### procedures > `readonly` **procedures**: readonly [`Procedure`](#procedure)[] Extracted procedures, performed or planned (across all sections). Empty when none. ##### profile > `readonly` **profile**: [`ProfileAttribution`](#profileattribution) \| `undefined` The profile applied at parse time (name + lineage), or `undefined` when none was active. ##### results > `readonly` **results**: readonly [`ResultOrganizer`](#resultorganizer)[] Extracted Result Organizers, lab/diagnostic panels (across all sections). Empty when none. ##### sections > `readonly` **sections**: readonly [`CcdaSection`](#ccdasection)[] Top-level framed sections from the `structuredBody`. Empty for an unstructured document. ##### smokingStatus > `readonly` **smokingStatus**: readonly [`SmokingStatus`](#smokingstatus-4)[] Extracted Smoking Status observations from Social History. Empty when none. ##### templateIds > `readonly` **templateIds**: readonly [`II`](#ii)[] The root `templateId`s, in document order (raw recognition signal). ##### vitals > `readonly` **vitals**: readonly [`VitalSignsOrganizer`](#vitalsignsorganizer)[] Extracted Vital Signs Organizers, vital-reading clusters (across all sections). Empty when none. ##### warnings > `readonly` **warnings**: readonly [`CcdaWarning`](#ccdawarning)[] Lenient-parse warnings, frozen at the model boundary. #### Methods ##### allSections() > **allSections**(): readonly [`CcdaSection`](#ccdasection)[] Every section in the document, flattened depth-first (top-level sections followed by their nested subsections). Order is document order. ###### Returns readonly [`CcdaSection`](#ccdasection)[] ###### Example ```ts for (const s of doc.allSections()) console.log(s.key ?? "(unrecognized)"); ``` ##### findSection() > **findSection**(`key`): [`CcdaSection`](#ccdasection) \| `undefined` Find the first recognized section with the given catalog `key`, searching top-level sections then their subsections (depth-first). Returns `undefined` when no recognized section matches. ###### Parameters ###### key `string` ###### Returns [`CcdaSection`](#ccdasection) \| `undefined` ###### Example ```ts const allergies = doc.findSection("allergies"); console.log(allergies?.narrativeText); ``` ##### getAllergies() > **getAllergies**(): readonly [`AllergyConcern`](#allergyconcern)[] The patient's Allergy Concern Acts, each wrapping one or more allergy/intolerance observations (including the "No Known Allergies" negated form). Empty when the document carries no Allergies entries. ###### Returns readonly [`AllergyConcern`](#allergyconcern)[] ###### Example ```ts const nka = doc.getAllergies().some((c) => c.allergies.some((a) => a.noKnownAllergy)); ``` ##### getEncounters() > **getEncounters**(): readonly [`Encounter`](#encounter)[] The patient's Encounter Activities, each carrying the encounter type `code`, status, and visit period. Empty when the document carries no Encounters entries. ###### Returns readonly [`Encounter`](#encounter)[] ###### Example ```ts for (const e of doc.getEncounters()) console.log(e.code?.code, e.effectiveTime); ``` ##### getFamilyHistory() > **getFamilyHistory**(): readonly [`FamilyHistory`](#familyhistory-4)[] The patient's Family History, one [FamilyHistory](#familyhistory-4) per relative, each carrying the relative's structured identity and their recorded conditions. Empty when the document carries no Family History entries. ###### Returns readonly [`FamilyHistory`](#familyhistory-4)[] ###### Example ```ts for (const h of doc.getFamilyHistory()) console.log(h.relative.relationship?.code, h.observations.length); ``` ##### getFunctionalStatus() > **getFunctionalStatus**(): readonly [`StatusObservation`](#statusobservation)[] The patient's Functional Status findings, ADLs, mobility, and self-care observations (plus any scored assessment scales). Empty when the document carries no Functional Status entries. ###### Returns readonly [`StatusObservation`](#statusobservation)[] ###### Example ```ts const scales = doc.getFunctionalStatus().filter((o) => o.assessmentScale); console.log(scales[0]?.code?.code); ``` ##### getImmunizations() > **getImmunizations**(): readonly [`Immunization`](#immunization)[] The patient's Immunization Activities, each carrying the CVX vaccine, dose, route, and date (including the `refused` not-administered form). Empty when the document carries no Immunizations entries. ###### Returns readonly [`Immunization`](#immunization)[] ###### Example ```ts const given = doc.getImmunizations().filter((i) => i.refused !== true); console.log(given[0]?.vaccine?.code); ``` ##### getMedications() > **getMedications**(): readonly [`Medication`](#medication)[] The patient's Medication Activities, each carrying the RxNorm drug, dose, route, and timing. Empty when the document carries no Medications entries. ###### Returns readonly [`Medication`](#medication)[] ###### Example ```ts for (const m of doc.getMedications()) console.log(m.drug?.code, m.dose?.value); ``` ##### getMentalStatus() > **getMentalStatus**(): readonly [`StatusObservation`](#statusobservation)[] The patient's Mental Status findings, cognition and mood observations (plus any scored assessment scales such as a PHQ-9). Empty when the document carries no Mental Status entries. ###### Returns readonly [`StatusObservation`](#statusobservation)[] ###### Example ```ts for (const o of doc.getMentalStatus()) console.log(o.code?.code, o.value?.kind); ``` ##### getMrn() > **getMrn**(): `string` \| `undefined` The patient's MRN string, the **first** `patientRole/id` extension (see [pickMrn](#pickmrn)). `undefined` when there is no patient, when that id has no `extension`, or when it carries a `nullFlavor`: the document disowned that identifier, and a bare `string` return has nowhere to carry the marking that qualified it. It withholds rather than falling through to the next ``, since nothing in a C-CDA ranks `patientRole/id` entries and the next one is as likely to be an account or member number. The verbatim value is still reachable at `getPatient()?.identifiers`, where the `nullFlavor` travels with it. ###### Returns `string` \| `undefined` ###### Example ```ts console.log(doc.getMrn() ?? "no MRN"); ``` ##### getPastMedicalHistory() > **getPastMedicalHistory**(): readonly [`Problem`](#problem-1)[] The patient's Past Medical History, historical problems carried as bare Problem Observations (distinct from the active-concern Problems section). Empty when the document carries no Past Medical History entries. ###### Returns readonly [`Problem`](#problem-1)[] ###### Example ```ts console.log(doc.getPastMedicalHistory()[0]?.value?.code); ``` ##### getPatient() > **getPatient**(): [`CcdaPatient`](#ccdapatient) \| `undefined` The first `recordTarget` patient, or `undefined` when the document carries none. A document with multiple record targets emits `MULTIPLE_RECORD_TARGETS` at parse time; this returns the first. ###### Returns [`CcdaPatient`](#ccdapatient) \| `undefined` ###### Example ```ts const p = doc.getPatient(); console.log(p?.name?.text ?? "unknown patient"); ``` ##### getPlannedItems() > **getPlannedItems**(): readonly [`PlannedItem`](#planneditem)[] The patient's planned items from the Plan of Treatment, each future/ordered (never performed), carrying its planned `code`, `kind`, and a `disposition` of `"planned"` derived from `moodCode` (never read as performed). Empty when the document carries no Plan of Treatment entries. **Two bounds, because a quiet result here is not a complete one.** This returns **seven** entry templates and the Plan of Treatment Section admits **eleven**: Instruction (`…22.4.20`), Handoff Communication Participants (`…22.4.141`), Nutrition Recommendation (`…22.4.130`) and Goal Observation (`…22.4.121`) are not acts planned for the patient and are not returned, and nothing is raised about them. A Goal Observation is the clearest of the four: it is `moodCode="GOL"`, which this parser calls neither performed nor planned. And a planned entry is reached as a direct `` act or nested inside a **Planned Intervention Act** (`…22.4.146`), the one container R2.1 lets hold all seven inline. **Nesting is not solved in general**: R2.1 also puts planned acts inside a Nutrition Recommendation (`…22.4.130`, six of the seven) and a Planned Intervention Act inside an Intervention Act (`…22.4.131`), and a planned entry in either is still not reached. All of it re-serializes faithfully, so `toString()` is the fallback. ###### Returns readonly [`PlannedItem`](#planneditem)[] ###### Example ```ts const orders = doc.getPlannedItems().filter((p) => p.kind === "medicationActivity"); console.log(orders[0]?.code?.code); ``` ##### getProblems() > **getProblems**(): readonly [`ProblemConcern`](#problemconcern)[] The patient's Problem Concern Acts, each wrapping one or more coded problems with an active/resolved status. Empty when the document carries no Problems entries. ###### Returns readonly [`ProblemConcern`](#problemconcern)[] ###### Example ```ts const active = doc.getProblems().filter((c) => c.status === "active"); console.log(active[0]?.problems[0]?.value?.code); ``` ##### getProcedures() > **getProcedures**(): readonly [`Procedure`](#procedure)[] The patient's procedures, each carrying its procedure `code`, status, and a `disposition` of `"performed"` vs `"planned"` derived from `moodCode` (never conflated). Empty when the document carries no Procedures entries. ###### Returns readonly [`Procedure`](#procedure)[] ###### Example ```ts const performed = doc.getProcedures().filter((p) => p.disposition === "performed"); console.log(performed[0]?.code?.code); ``` ##### getResults() > **getResults**(): readonly [`ResultOrganizer`](#resultorganizer)[] The patient's Result Organizers, lab/diagnostic panels, each carrying its member Result Observations with UCUM-checked values and reference ranges. Empty when the document carries no Results entries. ###### Returns readonly [`ResultOrganizer`](#resultorganizer)[] ###### Example ```ts for (const panel of doc.getResults()) for (const r of panel.results) console.log(r.code?.code, r.interpretation?.code); ``` ##### getSmokingStatus() > **getSmokingStatus**(): readonly [`SmokingStatus`](#smokingstatus-4)[] The patient's Smoking Status observations (from Social History), each carrying the SNOMED smoking-status `value` and an `unknown` flag for the explicitly-unknown form. Empty when the document records no smoking status. ###### Returns readonly [`SmokingStatus`](#smokingstatus-4)[] ###### Example ```ts const known = doc.getSmokingStatus().filter((s) => !s.unknown); console.log(known[0]?.value?.code); ``` ##### getVitals() > **getVitals**(): readonly [`VitalSignsOrganizer`](#vitalsignsorganizer)[] The patient's Vital Signs Organizers, reading clusters, each carrying its member Vital Sign Observations with UCUM-checked `PQ` values. Empty when the document carries no Vital Signs entries. ###### Returns readonly [`VitalSignsOrganizer`](#vitalsignsorganizer)[] ###### Example ```ts for (const cluster of doc.getVitals()) for (const v of cluster.vitals) console.log(v.code?.code, v.value); ``` ##### toString() > **toString**(): `string` Serialize this document back to spec-clean C-CDA XML, the conservative *emit* half of the Postel's-Law contract. Returns the faithful re-emission of the source the parser read (no silent loss of unmodeled content), with a guaranteed XML declaration. Serialization is a fixed point: `parseCcda(doc.toString()).toString() === doc.toString()`. ###### Returns `string` The spec-clean XML text. ###### Throws If this document was hand-constructed (not produced by [parseCcda](#parseccda)) and so retains no source document to emit. ###### Example ```ts const doc = parseCcda(xml); const xmlOut = doc.toString(); ``` ##### withWarnings() > **withWarnings**(`additional`): [`CcdaDocument`](#ccdadocument) Return a **new** `CcdaDocument` with `additional` warnings appended, structurally sharing every parsed field (header, sections, entries, and the serialized snapshot) with this instance by reference. The original is never mutated. A downstream pass (e.g. profile-aware validation) uses this to annotate a document without re-parsing. ###### Parameters ###### additional readonly [`CcdaWarning`](#ccdawarning)[] Warnings to append after the existing ones. ###### Returns [`CcdaDocument`](#ccdadocument) A new document; this instance is unchanged. ###### Example ```ts const annotated = doc.withWarnings([ { code: "SECTION_PLACEMENT_SUSPECT", message: "...", position: {} }, ]); // doc.warnings is unchanged; annotated.warnings has the extra entry. ``` *** ### CcdaEditError The typed error [editCcda](#editccda) throws when an edit cannot be applied safely , a hand-constructed source with no XML to edit, a structured-body-less document, an add/replace precondition violation, an edit that would drop a per-document-type SHALL required section, or a revision of a source whose `ClinicalDocument.id` is absent or marked `nullFlavor` (the RPLC link has no prior version it can truthfully name). Consumers narrow via `code`. #### Example ```ts import { editCcda, CcdaEditError } from "@cosyte/ccda"; try { editCcda(doc, { sections: [{ kind: "problems", mode: "add", content: [] }] }); } catch (err) { if (err instanceof CcdaEditError && err.code === "SECTION_ALREADY_PRESENT") { // the document already has a Problems section, use "replace" or "upsert" } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new CcdaEditError**(`code`, `message`): [`CcdaEditError`](#ccdaediterror) **`Internal`** Construct a new `CcdaEditError`. ###### Parameters ###### code [`CcdaEditErrorCode`](#ccdaediterrorcode-1) The stable failure code. ###### message `string` A human-readable, PHI-free explanation. ###### Returns [`CcdaEditError`](#ccdaediterror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`CcdaEditErrorCode`](#ccdaediterrorcode-1) The stable [CcdaEditErrorCode](#ccdaediterrorcode-1) discriminant. *** ### CcdaParseError Thrown by `parseCcda` (and the secure XML substrate it calls) when the input violates one of the seven unrecoverable Tier-3 rules, a declared DTD/external entity, entity-expansion or size/depth/node-count limits, malformed XML, or a well-formed document whose root is not `ClinicalDocument`. Carries a bounded structural [CcdaPosition](#ccdaposition) and a `message` taken whole from FATAL\_MESSAGES; unlike some sibling parsers it retains no raw input snippet, precisely because C-CDA payloads are clinical documents and any snippet would risk leaking PHI. Nothing the document said reaches `err.message` or, under `{ strict: true }`, `err.stack`. #### Example ```ts import { parseCcda, CcdaParseError } from "@cosyte/ccda"; try { parseCcda(raw); } catch (err) { if (err instanceof CcdaParseError && err.code === "NOT_A_CLINICAL_DOCUMENT") { // err.position, err.code available, no PHI in either } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new CcdaParseError**(`code`, `message`, `position`): [`CcdaParseError`](#ccdaparseerror) **`Internal`** Construct a new `CcdaParseError`. All three fields are required so every thrower populates a code, a message (which must come from FATAL\_MESSAGES, never be built from the input), and a structural position. ###### Parameters ###### code [`FatalCode`](#fatalcode) ###### message `string` ###### position [`CcdaPosition`](#ccdaposition) ###### Returns [`CcdaParseError`](#ccdaparseerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`FatalCode`](#fatalcode) ##### position > `readonly` **position**: [`CcdaPosition`](#ccdaposition) *** ### CcdaProfileDefinitionError Thrown by `defineCcdaProfile` when a profile definition is structurally invalid, a missing/empty `name`, an unknown option key, a `tolerate` entry whose `code` is not a real [WarningCode](#warningcode), a missing `rationale`, or, the load-bearing safety rule, an attempt to tolerate a **safety-critical** warning code. Distinct from [CcdaParseError](#ccdaparseerror) (which is about a *document*): this is about a malformed *profile*, so it is never part of the lenient parse path and carries no `CcdaPosition`. #### Example ```ts import { defineCcdaProfile, CcdaProfileDefinitionError } from "@cosyte/ccda"; try { defineCcdaProfile({ name: "bad", tolerate: [{ code: "MISSING_DOSE_QUANTITY", rationale: "x" }] }); } catch (err) { if (err instanceof CcdaProfileDefinitionError) { // a profile may never tolerate a safety-critical code } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new CcdaProfileDefinitionError**(`message`, `profileName?`): [`CcdaProfileDefinitionError`](#ccdaprofiledefinitionerror) **`Internal`** Construct a new `CcdaProfileDefinitionError`. ###### Parameters ###### message `string` ###### profileName? `string` ###### Returns [`CcdaProfileDefinitionError`](#ccdaprofiledefinitionerror) ###### Overrides `Error.constructor` #### Properties ##### profileName > `readonly` **profileName**: `string` \| `undefined` The offending profile's name, when known at throw time. ## Interfaces ### Allergy An Allergy-Intolerance Observation. `allergen` is the offending substance (RxNorm / UNII / SNOMED) from the playing entity; `type` is the propensity type (the observation `value`). `noKnownAllergy` is the `negationInd="true"` "no known allergies" assertion, **distinct** from `nullFlavor` (substance unknown). `criticality` is the propensity criticality; per-reaction severity lives on each [AllergyReaction](#allergyreaction). #### Example ```ts import type { Allergy } from "@cosyte/ccda"; function allergenCode(a: Allergy): string | undefined { return a.noKnownAllergy ? undefined : a.allergen?.code; } ``` #### Properties ##### allergen? > `readonly` `optional` **allergen?**: [`CD`](#cd) ##### allergenLevelSuspect? > `readonly` `optional` **allergenLevelSuspect?**: `boolean` ##### criticality? > `readonly` `optional` **criticality?**: [`CD`](#cd) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### negated? > `readonly` `optional` **negated?**: `boolean` ##### noKnownAllergy > `readonly` **noKnownAllergy**: `boolean` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### reactions > `readonly` **reactions**: readonly [`AllergyReaction`](#allergyreaction)[] ##### type? > `readonly` `optional` **type?**: [`CD`](#cd) *** ### AllergyConcern An Allergy Concern Act: the concern wrapper around one or more [Allergy](#allergy) observations. `status` is the active/resolved/inactive state from the concern `statusCode`; `effectiveTime` is the concern window. #### Example ```ts import type { AllergyConcern } from "@cosyte/ccda"; function isActive(c: AllergyConcern): boolean { return c.status === "active"; } ``` #### Properties ##### allergies > `readonly` **allergies**: readonly [`Allergy`](#allergy)[] ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### status > `readonly` **status**: [`ConcernStatus`](#concernstatus) *** ### AllergyReaction One reaction (manifestation) of an allergy. `manifestation` is the coded clinical effect (e.g. hives); `severity` is the reaction's nested Severity Observation value, distinct from the propensity's overall criticality. #### Example ```ts import type { AllergyReaction } from "@cosyte/ccda"; const r: AllergyReaction = { manifestation: { code: "247472004", codeSystem: "2.16.840.1.113883.6.96" } }; ``` #### Properties ##### manifestation? > `readonly` `optional` **manifestation?**: [`CD`](#cd) ##### severity? > `readonly` `optional` **severity?**: [`CD`](#cd) *** ### BL Parsed HL7 v3 Boolean. `value` is the parsed boolean (omitted when the `@value` token was neither `"true"` nor `"false"`); `nullFlavor` is set when the element declared one. #### Example ```ts import type { BL } from "@cosyte/ccda"; const flag: BL = { value: true }; ``` #### Properties ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### value? > `readonly` `optional` **value?**: `boolean` *** ### BuildCcdaAllergy An Allergy Concern for the Allergies section. Either an `allergen` (RxNorm at ingredient level by default, or UNII / SNOMED CT) **or** `noKnownAllergy: true` (the `negationInd` "No Known Allergies" assertion) is required, the two are never conflated. `reaction`, `severity`, and `criticality` are optional; severity (of a reaction) and criticality (of the propensity) are distinct axes. #### Example ```ts import type { BuildCcdaAllergy } from "@cosyte/ccda"; const penicillin: BuildCcdaAllergy = { allergen: { code: "7980", displayName: "Penicillin G" }, reaction: { code: "247472004", displayName: "Hives" }, criticality: { code: "CRITH", displayName: "High criticality" }, }; const nka: BuildCcdaAllergy = { noKnownAllergy: true }; ``` #### Properties ##### allergen? > `readonly` `optional` **allergen?**: [`BuildCode`](#buildcode) The offending substance (RxNorm ingredient default, or UNII / SNOMED CT). ##### criticality? > `readonly` `optional` **criticality?**: [`BuildCode`](#buildcode) The propensity criticality (HL7 ObservationValue by default), optional. ##### noKnownAllergy? > `readonly` `optional` **noKnownAllergy?**: `boolean` Assert "No Known Allergies" (`negationInd="true"`), mutually exclusive with `allergen`. ##### onset? > `readonly` `optional` **onset?**: `string` Onset date as an HL7 date string, the Allergy Concern Act `effectiveTime/low` (when the allergy became a tracked concern), optional. When omitted the SHALL `low` is emitted as `nullFlavor="UNK"`, never a fabricated date. ##### reaction? > `readonly` `optional` **reaction?**: [`BuildCode`](#buildcode) The reaction manifestation (SNOMED CT by default), optional. ##### resolution? > `readonly` `optional` **resolution?**: `string` Resolution date as an HL7 date string, the concern `effectiveTime/high` (when the concern was completed). Requires `status: "resolved"`, exactly as a [BuildCcdaProblem](#buildccdaproblem) resolution does; [buildCcda](#buildccda) rejects a resolution date on a non-resolved allergy. A resolved allergy whose date is unknown still emits a `nullFlavor="UNK"` `high`. ##### severity? > `readonly` `optional` **severity?**: [`BuildCode`](#buildcode) The reaction's severity (SNOMED CT by default), optional. ##### status? > `readonly` `optional` **status?**: `"active"` \| `"resolved"` \| `"inactive"` Active / resolved / inactive; defaults to `"active"`. ##### type? > `readonly` `optional` **type?**: [`BuildCode`](#buildcode) The propensity **type**, the Allergy-Intolerance Observation `value` (SNOMED CT by default), from the C-CDA Allergy/Intolerance Type value set: e.g. drug allergy `416098002`, food allergy `414285001`, environmental `426232007`. Defaults to the neutral `419199007` "Allergy to substance", the builder does **not** guess "Drug allergy" for a non-drug allergen. *** ### BuildCcdaEncompassingEncounter The `componentOf/encompassingEncounter` frame a Discharge Summary SHALL carry (CONF:1198-8471, -8472): the inpatient stay the document summarises. Ignored for a document type whose rule does not require the frame, because emitting an encounter a CCD never claimed would be asserting one. **NOTHING HERE IS EVER FABRICATED, AND EVERY SLOT IS REQUIRED BY THE TEMPLATE.** The encounter's `effectiveTime` SHALL carry a `low` and a `high` (CONF:1198-8473, -8475) and the encounter SHALL carry a `dischargeDispositionCode` (CONF:1198-8476), so all three elements are always emitted. A bound or a disposition the caller did not supply is emitted as an **explicit** `nullFlavor="UNK"`, which satisfies the cardinality without stating a clinical fact and which [parseCcda](#parseccda) reads back as absent rather than as a date or a code. An admission date, a discharge date and a discharge disposition are each things a clinician acts on, and a guessed one is a wrong clinical fact on the wire. A supplied bound keeps exactly the precision it was given: a day-precision `"20240102"` stays day-precision and is never completed to a time. #### Example ```ts import type { BuildCcdaEncompassingEncounter } from "@cosyte/ccda"; const stay: BuildCcdaEncompassingEncounter = { period: { low: "20240102", high: "20240108" }, dischargeDisposition: { code: "01", displayName: "Discharged to Home or Self Care" }, }; ``` #### Properties ##### dischargeDisposition? > `readonly` `optional` **dischargeDisposition?**: [`BuildCode`](#buildcode) The discharge disposition. `codeSystem` defaults to the NUBC UB-04 Patient Discharge Status code set, the system every member of the value set the template names carries; supply your own to code it elsewhere, since that binding is a SHOULD. Omitted entirely, the element is emitted `nullFlavor="UNK"`: the builder has no defensible default for where a patient went. ##### period? > `readonly` `optional` **period?**: `object` The encounter period as HL7 date strings (an `IVL_TS` low/high), the admission and discharge times. Either bound may be omitted and an omitted one is emitted `nullFlavor="UNK"`; the element itself is always emitted, because the template requires both bounds to be present. ###### high? > `readonly` `optional` **high?**: `string` ###### low? > `readonly` `optional` **low?**: `string` *** ### BuildCcdaEncounter An Encounter Activity for the Encounters section (`…22.4.49`). `type` is the coded encounter type (CPT by default, or SNOMED CT / HL7 ActEncounterCode) and is required, the template SHALL contain a `code` [1..1]. `period` is the visit/admission window emitted as the SHALL `effectiveTime` [1..1] (an `IVL_TS`); when omitted the SHALL slot is filled with a `nullFlavor="UNK"` `low` rather than a fabricated date. `status` maps to the optional `statusCode`. #### Example ```ts import type { BuildCcdaEncounter } from "@cosyte/ccda"; const visit: BuildCcdaEncounter = { type: { code: "99213", displayName: "Office outpatient visit 15 minutes" }, // CPT status: "completed", period: { low: "20230615", high: "20230615" }, }; ``` #### Properties ##### period? > `readonly` `optional` **period?**: `object` The visit/admission period as HL7 date strings (an `IVL_TS` low/high); either bound optional. Emitted as the SHALL `effectiveTime`; `nullFlavor="UNK"` low when omitted. ###### high? > `readonly` `optional` **high?**: `string` ###### low? > `readonly` `optional` **low?**: `string` ##### status? > `readonly` `optional` **status?**: `string` The encounter `statusCode`; defaults to `"completed"`. ##### type > `readonly` **type**: [`BuildCode`](#buildcode) The coded encounter type (CPT by default, or SNOMED CT / HL7 ActEncounterCode). *** ### BuildCcdaFamilyHistory One Family History Organizer (`…22.4.45`) for the Family History section, a single `relative` plus the `observations` (conditions) recorded for them. The relative's identity is carried once on the organizer (not flattened into each condition), so the parser reads every condition back grouped under its relative. #### Example ```ts import type { BuildCcdaFamilyHistory } from "@cosyte/ccda"; const father: BuildCcdaFamilyHistory = { relative: { relationship: { code: "9947008", displayName: "Father" }, deceased: true }, observations: [ { condition: { code: "22298006", displayName: "Myocardial infarction" }, causeOfDeath: true }, ], }; ``` #### Properties ##### observations > `readonly` **observations**: readonly [`BuildCcdaFamilyHistoryObservation`](#buildccdafamilyhistoryobservation)[] The conditions recorded for the relative; each becomes a Family History Observation. **Must be non-empty**, the organizer SHALL carry at least one observation component; pass `[{}]` (an unknown condition) rather than an empty list, else [buildCcda](#buildccda) throws a `TypeError`. ##### relative > `readonly` **relative**: [`BuildCcdaFamilyMember`](#buildccdafamilymember) The family member this organizer describes. *** ### BuildCcdaFamilyHistoryObservation A single condition recorded for a relative, one Family History Observation (`…22.4.46`). The illness is the coded `condition` (SNOMED CT by default); `ageAtOnset` (whole UCUM years) becomes a nested Age Observation (`…22.4.31`); `causeOfDeath` adds a Family History Death Observation (`…22.4.47`) marking this condition as the relative's cause of death; `effectiveTime` (an HL7 date string) is the SHOULD [0..1] time of the condition. **The condition is never fabricated.** When `condition` is omitted the SHALL `value` is emitted as `nullFlavor="UNK"`, an *explicit* unknown, never a guessed illness. `ageAtOnset`, `causeOfDeath`, and `effectiveTime` are optional , each emitted only when supplied, never invented. #### Example ```ts import type { BuildCcdaFamilyHistoryObservation } from "@cosyte/ccda"; const mi: BuildCcdaFamilyHistoryObservation = { condition: { code: "22298006", displayName: "Myocardial infarction" }, // SNOMED CT ageAtOnset: 57, causeOfDeath: true, }; ``` #### Properties ##### ageAtOnset? > `readonly` `optional` **ageAtOnset?**: `number` The relative's age at onset in whole years, a nested Age Observation, emitted only when supplied. ##### causeOfDeath? > `readonly` `optional` **causeOfDeath?**: `boolean` When `true`, marks this condition as the relative's cause of death (Family History Death Observation). ##### condition? > `readonly` `optional` **condition?**: [`BuildCode`](#buildcode) The coded condition the relative had (SNOMED CT default). Omit for an explicit unknown (`value nullFlavor="UNK"`), never guessed. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The time/date of the condition (HL7 date string); the SHOULD [0..1] effectiveTime, emitted only when supplied. *** ### BuildCcdaFamilyMember The relative a [BuildCcdaFamilyHistory](#buildccdafamilyhistory) organizer describes, the family member whose conditions the organizer records. Emitted as the organizer's `subject/relatedSubject` (a `@classCode="PRS"` personal relationship). **The relationship is never fabricated.** `relationship` is the coded relation of the relative to the patient, SNOMED CT by default (e.g. `72705000` mother, `9947008` father, `394859005`… ), overridable via `codeSystem` (e.g. the HL7 RoleCode `FTH`/`MTH` on `2.16.840.1.113883.5.111`). When omitted, the SHALL `relatedSubject/code` is emitted as `nullFlavor="UNK"`, an *explicit* unknown relation, never guessed. `gender` (an HL7 AdministrativeGender code, e.g. `"M"`/`"F"`), `birthTime` (an HL7 date string), and `deceased` (the `sdtc:deceasedInd` flag) are all optional MAY elements, each emitted only when supplied, never fabricated. #### Example ```ts import type { BuildCcdaFamilyMember } from "@cosyte/ccda"; const mother: BuildCcdaFamilyMember = { relationship: { code: "72705000", displayName: "Mother" }, // SNOMED CT gender: "F", deceased: true, }; ``` #### Properties ##### birthTime? > `readonly` `optional` **birthTime?**: `string` The relative's birth date (HL7 date string); emitted only when supplied. ##### deceased? > `readonly` `optional` **deceased?**: `boolean` Whether the relative is deceased (`sdtc:deceasedInd`); emitted only when supplied. ##### gender? > `readonly` `optional` **gender?**: `string` The relative's HL7 AdministrativeGender code (e.g. `"M"`/`"F"`); emitted only when supplied. ##### relationship? > `readonly` `optional` **relationship?**: [`BuildCode`](#buildcode) The coded relationship of the relative to the patient (SNOMED CT default). Omit for an explicit unknown (`relatedSubject/code nullFlavor="UNK"`), never guessed. *** ### BuildCcdaFunctionalStatus A Functional Status finding for the Functional Status section, a Functional Status Observation (`…22.4.67`, the `2014-06-09` stamp). The observation's `code` is **fixed** to LOINC `54522-8` "Functional status" by the template; the specific finding is the coded `value`. `value` is the SNOMED CT finding (e.g. able to walk `165245003`, dependent on wheelchair `105503008`, self-care `129019007`). **Functional and mental status are never conflated.** This builds only the Functional Status templates (section `…22.2.14`, observation `…22.4.67`), so the parser reads every finding back tagged `domain: "functional"`, a functional finding is never filed under mental status (or vice versa). **Unknown is never defaulted to a finding.** When `value` is omitted the observation's SHALL `value` is emitted as `nullFlavor="UNK"`, an *explicit* unknown, never invented as a real finding. `effectiveTime` is when the status was assessed; the template's SHALL effectiveTime is filled with `nullFlavor="UNK"` when the caller supplies none, never a fabricated date. #### Example ```ts import type { BuildCcdaFunctionalStatus } from "@cosyte/ccda"; const ambulation: BuildCcdaFunctionalStatus = { value: { code: "165245003", displayName: "Able to walk" }, // SNOMED CT effectiveTime: "20240101", }; const unrecorded: BuildCcdaFunctionalStatus = {}; // → value nullFlavor="UNK" ``` #### Properties ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The date the status was assessed (HL7 date string); `nullFlavor="UNK"` when omitted. ##### value? > `readonly` `optional` **value?**: [`BuildCode`](#buildcode) The SNOMED CT functional-status finding (the observation `value`). Omit for an explicit unknown (`value nullFlavor="UNK"`), never defaulted to a real finding. *** ### BuildCcdaFunctionalStatusOrganizer A Functional Status Organizer for the Functional Status section, a Functional Status Organizer (`…22.4.66`, the `2014-06-09` stamp, `@classCode="CLUSTER"`) that **groups** two or more related Functional Status Observations (`…22.4.67`) under one categorization. Use it instead of standalone findings when the assessment is a cluster (e.g. all self-care ADLs recorded together); each grouped observation is otherwise identical to a standalone [BuildCcdaFunctionalStatus](#buildccdafunctionalstatus) and reads back tagged `domain: "functional"`. **`code` is the organizer's categorization, not a finding.** It SHALL be present [1..1] and SHOULD be drawn from ICF (`2.16.840.1.113883.6.254`) or LOINC, pass the ICF chapter/category (e.g. `d5` "Self-care") via `code` with its `codeSystem`. When omitted the SHALL `code` is emitted as `nullFlavor="UNK"`, an *explicit* unknown category, never a fabricated one. `codeSystem` defaults to LOINC when a `code` is supplied without one. **`findings` must be non-empty.** The organizer SHALL contain at least one [1..\*] Functional Status Observation; an empty organizer is a `TypeError` (never an organizer emitted with zero members). The Assessment Scale Observation (`…22.4.69`), a scored scale such as a Barthel index, is a *direct section entry* in C-CDA R2.1, **not** an organizer component; only status observations are grouped here. **`selfCareActivities` decides whether the organizer is emitted at all, and that is a conformance rule rather than a preference.** The R2.1 template SHALL also contain at least one [1..\*] component holding a Self-Care Activities (ADL and IADL) observation (`…22.4.128`, CONF:1098-31432), measured against the normative Schematron this repository pins. When none is supplied there is no conformant organizer to emit and none is fabricated, so the `findings` are emitted as standalone Functional Status Observations instead, which every reader of this section understands, and the built document carries `MISSING_SELF_CARE_ACTIVITY` saying the grouping was dropped and why. Supply one activity and the organizer is emitted with its categorization and its `effectiveTime` intact. #### Example ```ts import type { BuildCcdaFunctionalStatusOrganizer } from "@cosyte/ccda"; const selfCare: BuildCcdaFunctionalStatusOrganizer = { code: { code: "d5", displayName: "Self-care", codeSystem: "2.16.840.1.113883.6.254" }, // ICF effectiveTime: "20240101", findings: [ { value: { code: "129019007", displayName: "Self-care" } }, // SNOMED CT { value: { code: "165245003", displayName: "Able to walk" } }, ], selfCareActivities: [ { code: { code: "54520-2", displayName: "Bathing" }, // LOINC, ADL Result Type value: { code: "371153006", displayName: "Independent" }, // SNOMED CT, Ability }, ], }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`BuildCode`](#buildcode) The organizer's categorization `code` (SHOULD be ICF or LOINC). Omit for an explicit unknown (`code nullFlavor="UNK"`), never a fabricated category. `codeSystem` defaults to LOINC when a `code` is supplied without one. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` When the grouped assessment was performed (HL7 date string); omitted (not fabricated) when absent. ##### findings > `readonly` **findings**: readonly [`BuildCcdaFunctionalStatus`](#buildccdafunctionalstatus)[] The Functional Status Observations grouped by this organizer. **Must be non-empty**, the organizer SHALL contain at least one member. ##### selfCareActivities? > `readonly` `optional` **selfCareActivities?**: readonly [`BuildCcdaSelfCareActivity`](#buildccdaselfcareactivity)[] The Self-Care Activities (ADL and IADL) observations grouped by this organizer. The template SHALL contain at least one; with none supplied the organizer is not emitted and its findings are emitted standalone, reported as `MISSING_SELF_CARE_ACTIVITY`. *** ### BuildCcdaImmunization An Immunization Activity for the Immunizations section. `vaccine` is the CVX coded product (CVX by default). `dose` and `route` are optional and **never guessed**, an omitted one is simply left absent. `refused: true` emits the administration with `negationInd="true"` (a *not-administered* / refused record), which the parser reads back as `refused` and flags `IMMUNIZATION_REFUSED`, the clinically load-bearing refusal is surfaced, never conflated with a `nullFlavor` "unknown". `effectiveTime` is the administration date; when omitted the SHALL slot is filled with `nullFlavor="UNK"`. #### Example ```ts import type { BuildCcdaImmunization } from "@cosyte/ccda"; const flu: BuildCcdaImmunization = { vaccine: { code: "140", displayName: "Influenza, split virus, trivalent, injectable, preservative free" }, // CVX dose: { value: 0.5, unit: "mL" }, route: { code: "C28161", displayName: "Intramuscular" }, // NCI Thesaurus effectiveTime: "20240101", }; const refused: BuildCcdaImmunization = { vaccine: { code: "140", displayName: "Influenza, split virus, trivalent, injectable, preservative free" }, refused: true, }; ``` #### Properties ##### dose? > `readonly` `optional` **dose?**: [`BuildQuantity`](#buildquantity) The amount administered (`doseQuantity`), optional, never defaulted. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The administration date as an HL7 date string; `nullFlavor="UNK"` when omitted. ##### refused? > `readonly` `optional` **refused?**: `boolean` Emit a refused / not-administered record (`negationInd="true"`); parser flags `IMMUNIZATION_REFUSED`. ##### route? > `readonly` `optional` **route?**: [`BuildCode`](#buildcode) The administration route (`routeCode`); NCI Thesaurus by default. ##### status? > `readonly` `optional` **status?**: `string` The `statusCode`; defaults to `"completed"`. ##### vaccine > `readonly` **vaccine**: [`BuildCode`](#buildcode) The CVX-coded vaccine product (CVX by default). *** ### BuildCcdaInit Input to [buildCcda](#buildccda). `patient` is required; each clinical collection backing a CCD SHALL section (`problems`, `allergies`, `medications`, `results`, `vitalSigns`, `smokingStatus`) defaults to empty, in which case its section is emitted as a spec-clean empty `nullFlavor="NI"` section. `immunizations` is optional, its section is emitted only when populated (Immunizations is not a CCD SHALL section). `documentType` is `"ccd"` (the default), `"referralNote"` or `"dischargeSummary"`; the other nine C-CDA R2.1 document types are not implemented. #### Example ```ts import type { BuildCcdaInit } from "@cosyte/ccda"; const init: BuildCcdaInit = { patient: { mrn: "MRN001", given: ["Jane"], family: "Doe", gender: "F" }, problems: [{ problem: { code: "59621000", displayName: "Essential hypertension" } }], allergies: [{ noKnownAllergy: true }], }; ``` #### Properties ##### allergies? > `readonly` `optional` **allergies?**: readonly [`BuildCcdaAllergy`](#buildccdaallergy)[] Allergy Concerns for the Allergies section; empty section when omitted. ##### assessment? > `readonly` `optional` **assessment?**: `string` The Assessment Section narrative (`documentType: "referralNote"` only, a Referral Note SHALL section). Narrative-only, so this is a free-text clinician summary; when omitted the SHALL section is emitted as a spec-clean empty `nullFlavor="NI"` section (never a fabricated assessment). Ignored for a CCD; refused with a `TypeError` for a Discharge Summary, which carries no Assessment Section. ##### confidentiality? > `readonly` `optional` **confidentiality?**: `string` The confidentiality code; defaults to `"N"` (normal). ##### custodianName? > `readonly` `optional` **custodianName?**: `string` The custodian organization name; defaults to a synthetic label. ##### dischargeDiagnoses? > `readonly` `optional` **dischargeDiagnoses?**: readonly [`BuildCcdaProblem`](#buildccdaproblem)[] The discharge diagnoses for the Discharge Diagnosis Section (`documentType: "dischargeSummary"` only, a Discharge Summary SHALL section, CONF:1198-30524). Each is emitted as a Problem Observation (V3) under the single Hospital Discharge Diagnosis act the section's entry requires; when omitted the SHALL section is emitted as an empty `nullFlavor="NI"` section, which is conformant because the section's entry is optional (CONF:1198-15489). Read back through `getProblems` is NOT how these surface: they are diagnoses of the stay, not the concern list. Refused with a `TypeError` for the other document types, rather than dropped. ##### documentId? > `readonly` `optional` **documentId?**: `string` The document `id`'s extension; a synthetic id is generated when omitted. ##### documentType? > `readonly` `optional` **documentType?**: [`BuildableDocumentType`](#buildabledocumenttype) The document type, `"ccd"` (default), `"referralNote"` or `"dischargeSummary"`. Each specializes the US Realm Header (its own document `templateId` + LOINC `code`) and its SHALL section set; the other nine C-CDA R2.1 document types are not emitted, and asking for one throws rather than emitting a document that merely resembles it. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` \| `Date` The document `effectiveTime` (a `Date` is formatted to UTC); defaults to now. ##### encompassingEncounter? > `readonly` `optional` **encompassingEncounter?**: [`BuildCcdaEncompassingEncounter`](#buildccdaencompassingencounter) The `componentOf/encompassingEncounter` frame (`documentType: "dischargeSummary"` only, CONF:1198-8471). Supplies the encounter period's two bounds and the discharge disposition; each slot the caller omits is emitted as an explicit `nullFlavor="UNK"` rather than guessed. When the whole field is omitted the frame is still emitted, with every slot unknown, because the document type's rule requires it. Refused with a `TypeError` for the other document types, whose rules do not carry the frame, rather than dropped. ##### encounters? > `readonly` `optional` **encounters?**: readonly [`BuildCcdaEncounter`](#buildccdaencounter)[] Encounter Activities; the Encounters section is emitted only when non-empty (a CCD SHOULD section). ##### familyHistory? > `readonly` `optional` **familyHistory?**: readonly [`BuildCcdaFamilyHistory`](#buildccdafamilyhistory)[] Family history for the Family History section; the section is emitted only when non-empty (a CCD SHOULD section). Each entry is one relative (a Family History Organizer) carrying that relative's conditions, read back via `getFamilyHistory`, grouped by relative. ##### functionalStatus? > `readonly` `optional` **functionalStatus?**: readonly [`BuildCcdaFunctionalStatus`](#buildccdafunctionalstatus)[] Standalone Functional Status findings; the Functional Status section is emitted when this or [functionalStatusOrganizers](#functionalstatusorganizers) is non-empty (a CCD SHOULD section). ##### functionalStatusOrganizers? > `readonly` `optional` **functionalStatusOrganizers?**: readonly [`BuildCcdaFunctionalStatusOrganizer`](#buildccdafunctionalstatusorganizer)[] Functional Status Organizers (each grouping ≥1 Functional Status Observation under one categorization); emitted into the Functional Status section alongside any standalone [functionalStatus](#functionalstatus-1) findings. The section is emitted when either is non-empty. ##### functionalStatusScales? > `readonly` `optional` **functionalStatusScales?**: readonly `BuildCcdaAssessmentScale`[] Direct-entry Assessment Scale Observations (`…22.4.69`) for the Functional Status section, scored instruments (e.g. a Barthel index, a Glasgow Coma scale). Emitted as direct section entries (the conformant R2.1 placement), read back tagged `assessmentScale: true`, `domain: "functional"`. The Functional Status section is emitted when this, [functionalStatus](#functionalstatus-1), or [functionalStatusOrganizers](#functionalstatusorganizers) is non-empty. ##### hospitalCourse? > `readonly` `optional` **hospitalCourse?**: `string` The Hospital Course Section narrative (`documentType: "dischargeSummary"` only, a Discharge Summary SHALL section, CONF:1198-30522). Narrative-only, so this is the free-text account of the stay; when omitted the SHALL section is emitted as a spec-clean empty `nullFlavor="NI"` section, never an invented course. Refused with a `TypeError` for the other document types, rather than dropped. ##### immunizations? > `readonly` `optional` **immunizations?**: readonly [`BuildCcdaImmunization`](#buildccdaimmunization)[] Immunization Activities; the Immunizations section is emitted only when non-empty. ##### languageCode? > `readonly` `optional` **languageCode?**: `string` The document language; defaults to `"en-US"`. ##### medications? > `readonly` `optional` **medications?**: readonly [`BuildCcdaMedication`](#buildccdamedication)[] Medication Activities for the Medications section; an empty section when omitted for a CCD or a Referral Note, whose SHALL sets include it. A Discharge Summary's does not, so for that type the section is emitted only when this is non-empty, and always as the Medications Section, never as the Discharge Medications Section. ##### mentalStatus? > `readonly` `optional` **mentalStatus?**: readonly [`BuildCcdaMentalStatus`](#buildccdamentalstatus)[] Standalone Mental Status findings; the Mental Status section is emitted when this or [mentalStatusOrganizers](#mentalstatusorganizers) is non-empty (a CCD SHOULD section). ##### mentalStatusOrganizers? > `readonly` `optional` **mentalStatusOrganizers?**: readonly [`BuildCcdaMentalStatusOrganizer`](#buildccdamentalstatusorganizer)[] Mental Status Organizers (each grouping ≥1 Mental Status Observation under one categorization); emitted into the Mental Status section alongside any standalone [mentalStatus](#mentalstatus-1) findings. The section is emitted when either is non-empty. ##### mentalStatusScales? > `readonly` `optional` **mentalStatusScales?**: readonly `BuildCcdaAssessmentScale`[] Direct-entry Assessment Scale Observations (`…22.4.69`) for the Mental Status section, scored instruments (e.g. a PHQ-9 depression screen, a MoCA). Emitted as direct section entries (the conformant R2.1 placement), read back tagged `assessmentScale: true`, `domain: "mental"`. The Mental Status section is emitted when this, [mentalStatus](#mentalstatus-1), or [mentalStatusOrganizers](#mentalstatusorganizers) is non-empty. ##### pastMedicalHistory? > `readonly` `optional` **pastMedicalHistory?**: readonly [`BuildCcdaProblem`](#buildccdaproblem)[] Historical problems for the Past Medical History section; the section is emitted only when non-empty (a CCD MAY section). Each is a bare Problem Observation (not a concern act), read back via `getPastMedicalHistory`, never conflated with the active Problems returned by `getProblems`. ##### patient > `readonly` **patient**: [`BuildCcdaPatient`](#buildccdapatient) The single record-target patient (required). ##### planOfTreatment? > `readonly` `optional` **planOfTreatment?**: readonly [`BuildCcdaPlannedItem`](#buildccdaplanneditem)[] Planned items for the Plan of Treatment section; the section is emitted only when non-empty (a CCD SHOULD section). Every item is future/ordered, read back via `getPlannedItems` with `disposition: "planned"`, never conflated with the performed Procedures/Encounters. ##### problems? > `readonly` `optional` **problems?**: readonly [`BuildCcdaProblem`](#buildccdaproblem)[] Problem Concerns for the Problems section; an empty section when omitted for a CCD or a Referral Note, whose SHALL sets include it. A Discharge Summary's does not, so for that type the section is emitted only when this is non-empty. ##### procedures? > `readonly` `optional` **procedures?**: readonly [`BuildCcdaProcedure`](#buildccdaprocedure)[] Procedures; the Procedures section is emitted only when non-empty (a CCD SHOULD section). ##### reasonForReferral? > `readonly` `optional` **reasonForReferral?**: `string` The Reason for Referral Section narrative (`documentType: "referralNote"` only, a Referral Note SHALL section). Narrative-only free text; when omitted the SHALL section is emitted as an empty `nullFlavor="NI"` section (never a fabricated reason). Ignored for a CCD; refused with a `TypeError` for a Discharge Summary, which carries no Reason for Referral Section. ##### results? > `readonly` `optional` **results?**: readonly [`BuildCcdaResultPanel`](#buildccdaresultpanel)[] Result panels for the Results section; empty section when omitted. ##### smokingStatus? > `readonly` `optional` **smokingStatus?**: readonly [`BuildCcdaSmokingStatus`](#buildccdasmokingstatus)[] Smoking Status observations for the Social History section. Social History is a **CCD SHALL** section (CONF:1198-30688), so for a CCD the section is always emitted, as a `nullFlavor="NI"` shell when this is empty. For document types whose SHALL set excludes it, the section is emitted only when non-empty. ##### title? > `readonly` `optional` **title?**: `string` The document title; defaults to the CCD document-code display name. ##### vitalSigns? > `readonly` `optional` **vitalSigns?**: readonly [`BuildCcdaVitalsPanel`](#buildccdavitalspanel)[] Vital Signs panels for the Vital Signs section; empty section when omitted. *** ### BuildCcdaMedication A Medication Activity for the Medications section. `drug` is the RxNorm coded product (RxNorm by default). `dose`, `route`, `frequency`, and `duration` are all optional and **never guessed**: an omitted `dose`/`route` is emitted as absent, which the parser then flags (`MISSING_DOSE_QUANTITY` / `MISSING_ROUTE_CODE`) rather than being defaulted to a confident-wrong value. `frequency` is the periodic timing (a `PIVL_TS` period, e.g. every 8 hours); `duration` is the therapy window (an `IVL_TS` low/high), the two are emitted as distinct `effectiveTime` siblings, never conflated. #### Example ```ts import type { BuildCcdaMedication } from "@cosyte/ccda"; const lisinopril: BuildCcdaMedication = { drug: { code: "314076", displayName: "Lisinopril 10 MG Oral Tablet" }, dose: { value: 1, unit: "{tablet}" }, route: { code: "C38288", displayName: "Oral" }, frequency: { value: 24, unit: "h" }, }; ``` #### Properties ##### dose? > `readonly` `optional` **dose?**: [`BuildQuantity`](#buildquantity) The dose per administration (`doseQuantity`); absent → parser flags it. ##### drug > `readonly` **drug**: [`BuildCode`](#buildcode) The coded drug product (RxNorm by default, or NDC). ##### duration? > `readonly` `optional` **duration?**: `object` The therapy window (`IVL_TS`) as HL7 date strings; either bound optional. ###### high? > `readonly` `optional` **high?**: `string` ###### low? > `readonly` `optional` **low?**: `string` ##### frequency? > `readonly` `optional` **frequency?**: [`BuildQuantity`](#buildquantity) The periodic dosing frequency, a `PIVL_TS` period (e.g. `{ value: 8, unit: "h" }`). ##### route? > `readonly` `optional` **route?**: [`BuildCode`](#buildcode) The administration route (`routeCode`); NCI Thesaurus by default. ##### status? > `readonly` `optional` **status?**: `"active"` \| `"resolved"` \| `"inactive"` Active / resolved / inactive; maps to the `statusCode`. Defaults to `"active"`. *** ### BuildCcdaMentalStatus A Mental Status finding for the Mental Status section, a Mental Status Observation (`…22.4.74`, the R2.1 `2015-08-01` stamp). The observation's `code` is **fixed** to SNOMED CT `373930000` "Cognitive function finding" by the R2.1 template; the specific cognition/mood finding is the coded `value` (e.g. memory impairment `386807006`, no abnormality detected `281900007`, SNOMED CT). **Mental and functional status are never conflated.** This builds only the Mental Status templates (section `…22.2.56`, observation `…22.4.74`), so the parser reads every finding back tagged `domain: "mental"`, a mental finding is never filed under functional status (or vice versa); the two extractors key off their distinct observation template roots. **Unknown is never defaulted to a finding.** When `value` is omitted the observation's SHALL `value` is emitted as `nullFlavor="UNK"`, an *explicit* unknown, never invented as a real finding. `effectiveTime` is when the status was assessed; the template's SHALL effectiveTime is filled with `nullFlavor="UNK"` when the caller supplies none, never a fabricated date. #### Example ```ts import type { BuildCcdaMentalStatus } from "@cosyte/ccda"; const memory: BuildCcdaMentalStatus = { value: { code: "386807006", displayName: "Memory impairment" }, // SNOMED CT effectiveTime: "20240101", }; const unrecorded: BuildCcdaMentalStatus = {}; // → value nullFlavor="UNK" ``` #### Properties ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The date the status was assessed (HL7 date string); `nullFlavor="UNK"` when omitted. ##### value? > `readonly` `optional` **value?**: [`BuildCode`](#buildcode) The SNOMED CT mental-status finding (the observation `value`). Omit for an explicit unknown (`value nullFlavor="UNK"`), never defaulted to a real finding. *** ### BuildCcdaMentalStatusOrganizer A Mental Status Organizer for the Mental Status section, a Mental Status Organizer (`…22.4.75`, the R2.1 `2015-08-01` stamp, `@classCode="CLUSTER"`) that **groups** two or more related Mental Status Observations (`…22.4.74`) under one categorization. Use it instead of standalone findings when the assessment is a cluster (e.g. an orientation battery); each grouped observation is otherwise identical to a standalone [BuildCcdaMentalStatus](#buildccdamentalstatus) and reads back tagged `domain: "mental"`, **never conflated** with functional status (the two key off distinct organizer/observation roots). **`code` is the organizer's categorization, not a finding.** It SHALL be present [1..1] and SHOULD be drawn from ICF (`2.16.840.1.113883.6.254`) or LOINC. When omitted the SHALL `code` is emitted as `nullFlavor="UNK"`, an *explicit* unknown category, never fabricated. `codeSystem` defaults to LOINC when a `code` is supplied without one. **`findings` must be non-empty.** The organizer SHALL contain at least one [1..\*] Mental Status Observation; an empty organizer is a `TypeError`. The Assessment Scale Observation (`…22.4.69`) is a *direct section entry* in R2.1, **not** an organizer component. #### Example ```ts import type { BuildCcdaMentalStatusOrganizer } from "@cosyte/ccda"; const cognition: BuildCcdaMentalStatusOrganizer = { effectiveTime: "20240101", findings: [ { value: { code: "386807006", displayName: "Memory impairment" } }, // SNOMED CT { value: { code: "247663003", displayName: "Orientation finding" } }, ], }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`BuildCode`](#buildcode) The organizer's categorization `code` (SHOULD be ICF or LOINC). Omit for an explicit unknown (`code nullFlavor="UNK"`), never a fabricated category. `codeSystem` defaults to LOINC when a `code` is supplied without one. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` When the grouped assessment was performed (HL7 date string); omitted (not fabricated) when absent. ##### findings > `readonly` **findings**: readonly [`BuildCcdaMentalStatus`](#buildccdamentalstatus)[] The Mental Status Observations grouped by this organizer. **Must be non-empty**, the organizer SHALL contain at least one member. *** ### BuildCcdaOptions Options for [buildCcda](#buildccda). Every field is optional, `buildCcda(init)` is valid and produces the default behavior. #### Example ```ts import type { BuildCcdaOptions, TerminologyAdapter } from "@cosyte/ccda"; const adapter: TerminologyAdapter = { validateCode: (c) => ({ result: myService.has(c.code) }) }; const opts: BuildCcdaOptions = { terminology: adapter }; ``` #### Properties ##### terminology? > `readonly` `optional` **terminology?**: [`TerminologyAdapter`](#terminologyadapter) An optional consumer-supplied bring-your-own [TerminologyAdapter](#terminologyadapter). Two paths consume it: (1) its `validateCode` is forwarded to the internal re-parse so a built document surfaces `SEMANTIC_CODE_INVALID` for any coded value the adapter rejects; (2) its optional `translate` is consulted at each clinical coded slot (problem value, allergen, medication drug + route, vaccine + route) to emit `` alternate codings **beside** the primary code. The builder never coerces a code to satisfy the adapter, it emits every primary value verbatim, a `` is only ever an *additional* alternate, and an adapter with no `translate` opinion produces byte-identical output. Omit for the default behavior. *** ### BuildCcdaPatient A patient for the document's single `recordTarget`. Every field is optional; an omitted demographic is emitted as a spec-clean `nullFlavor="UNK"` rather than invented. Supply `mrn` to set the patient identifier the parser returns from [CcdaDocument.getMrn](#getmrn). #### Example ```ts import type { BuildCcdaPatient } from "@cosyte/ccda"; const patient: BuildCcdaPatient = { mrn: "MRN001", given: ["Jane"], family: "Doe", gender: "F", birthTime: "19800101", }; ``` #### Properties ##### birthTime? > `readonly` `optional` **birthTime?**: `string` Birth time as an HL7 date/datetime string (e.g. `"19800101"`). ##### family? > `readonly` `optional` **family?**: `string` ##### gender? > `readonly` `optional` **gender?**: `string` Administrative gender code (`M` / `F` / `UN`), HL7 AdministrativeGender. ##### given? > `readonly` `optional` **given?**: readonly `string`[] ##### mrn? > `readonly` `optional` **mrn?**: `string` The medical record number (the `patientRole/id/@extension`). ##### mrnAssigningAuthority? > `readonly` `optional` **mrnAssigningAuthority?**: `string` The assigning-authority name for the MRN; defaults to a synthetic label. ##### mrnRoot? > `readonly` `optional` **mrnRoot?**: `string` The assigning-authority OID for the MRN; defaults to a synthetic root. ##### prefix? > `readonly` `optional` **prefix?**: readonly `string`[] ##### suffix? > `readonly` `optional` **suffix?**: readonly `string`[] *** ### BuildCcdaPlannedAct A planned Act / Encounter / Procedure (`…4.39` / `…4.40` / `…4.41`). `mood` accepts the full Planned moodCode value set including the appointment moods (`APT`/`ARQ`), which are valid on these element domains. Default code system: SNOMED CT for an act/procedure, CPT for an encounter. #### Example ```ts import type { BuildCcdaPlannedAct } from "@cosyte/ccda"; const visit: BuildCcdaPlannedAct = { kind: "encounter", code: { code: "99213", displayName: "Office outpatient visit 15 minutes" }, mood: "APT", }; ``` #### Extends - `BuildCcdaPlannedItemBase` #### Properties ##### code > `readonly` **code**: [`BuildCode`](#buildcode) The planned act/observation/drug code; default code system varies by `kind`. ###### Inherited from `BuildCcdaPlannedItemBase.code` ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The planned time as an HL7 date string; emitted only when supplied, never fabricated. **`[0..1]` is the cardinality on five of the seven templates, not on all of them.** Both `substanceAdministration` variants SHALL carry exactly one: Planned Medication Activity (`…4.42`, CONF:1098-30468) and Planned Immunization Activity (`…4.120`). [BuildCcdaPlannedImmunization](#buildccdaplannedimmunization) redeclares this field as required, which is why omitting it there is a type error; [BuildCcdaPlannedOrder](#buildccdaplannedorder) does not, so `buildCcda` will emit a Planned Medication Activity short that SHALL element if you leave it out. **The field stays optional, and the omission is REPORTED rather than silent BY `buildCcda`.** Making it required would be a breaking change to a published input type, so the decision taken was to keep the type as it is and have [buildCcda](#buildccda) raise `MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME` on the returned document. The document is still emitted, still short that element, and no date is ever fabricated to fill it. **`editCcda` reports it too**, so a planned medication grafted in by an edit is no longer emitted short the SHALL element in silence. Its check is scoped to the sections **that call grafted** and that **survived** into the emitted document: an offending edit a later edit in the same call discarded says nothing (reading the caller's ordered edit list instead reports a violation against a conformant document), and an offending act the source already carried is never re-reported. So the absence of this code from `editCcda(...).warnings` says only that the sections **that call wrote** carry no planned medication short this element; it says nothing about the rest of the document, and nothing about any other conformance rule. ###### Inherited from `BuildCcdaPlannedItemBase.effectiveTime` ##### kind > `readonly` **kind**: `"procedure"` \| `"act"` \| `"encounter"` ##### mood? > `readonly` `optional` **mood?**: [`PlannedActMood`](#plannedactmood) The planned `@moodCode`; defaults to `"INT"`. Appointment moods allowed here. *** ### BuildCcdaPlannedImmunization A planned Immunization Activity (`…4.120`). `mood` excludes the appointment moods, the same `substanceAdministration` domain a [BuildCcdaPlannedOrder](#buildccdaplannedorder) draws on. `code` is the **vaccine**, emitted in the `consumable` (Immunization Medication Information `…4.54`) and defaulting to **CVX**, never RxNorm: the two `substanceAdministration` planned variants have different default systems because their slot bindings differ. **`effectiveTime` is required here because the template requires it**, and the builder is conservative on emit, so the type requires what the template requires rather than emitting a document short a SHALL element or fabricating a date the caller never supplied. C-CDA makes `effectiveTime` `[1..1]` on this template, "the time that the immunization activity should occur". **Do not read that as "this is the only planned template that requires it".** C-CDA makes `effectiveTime` `[1..1]` on **both** `substanceAdministration` planned variants: Planned Medication Activity (`…4.42`) SHALL carry exactly one too (CONF:1098-30468). It is the other **five** (`…4.39`, `…4.40`, `…4.41`, `…4.43`, `…4.44`) that make it `[0..1]`. [BuildCcdaPlannedOrder](#buildccdaplannedorder) still types `effectiveTime` as optional for its `medicationActivity` arm, so `buildCcda` can emit a Planned Medication Activity short that SHALL element. Making the field required on `BuildCcdaPlannedOrder` would be a breaking change to a published type, so the field stands as it is and the omission is **reported** instead (`MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME`, raised by both writers on the document they emit, never by `parseCcda`). No such diagnostic exists for this template, and adding one would be dead code: omitting the field here does not compile. #### Example ```ts import type { BuildCcdaPlannedImmunization } from "@cosyte/ccda"; const dueFluShot: BuildCcdaPlannedImmunization = { kind: "immunizationActivity", code: { code: "140", displayName: "Influenza, split virus, trivalent, injectable, preservative free" }, effectiveTime: "20241001", }; ``` #### Extends - `BuildCcdaPlannedItemBase` #### Properties ##### code > `readonly` **code**: [`BuildCode`](#buildcode) The planned act/observation/drug code; default code system varies by `kind`. ###### Inherited from `BuildCcdaPlannedItemBase.code` ##### effectiveTime > `readonly` **effectiveTime**: `string` When the immunization should occur. **Required**: the template makes it `[1..1]`. ###### Overrides `BuildCcdaPlannedItemBase.effectiveTime` ##### kind > `readonly` **kind**: `"immunizationActivity"` ##### mood? > `readonly` `optional` **mood?**: [`PlannedOrderMood`](#plannedordermood) The planned `@moodCode`; defaults to `"INT"`. Appointment moods not representable. *** ### BuildCcdaPlannedObservation A planned Observation (`…4.44`). `mood` excludes the appointment moods (not in `x_ActMoodDocumentObservation`). `value` is the expected coded result (a goal/target, SNOMED CT by default), emitted only when supplied. Default code system for the observation `code`: LOINC. #### Example ```ts import type { BuildCcdaPlannedObservation } from "@cosyte/ccda"; const cbc: BuildCcdaPlannedObservation = { kind: "observation", code: { code: "58410-2", displayName: "CBC panel" }, // LOINC mood: "RQO", effectiveTime: "20240801", }; ``` #### Extends - `BuildCcdaPlannedItemBase` #### Properties ##### code > `readonly` **code**: [`BuildCode`](#buildcode) The planned act/observation/drug code; default code system varies by `kind`. ###### Inherited from `BuildCcdaPlannedItemBase.code` ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The planned time as an HL7 date string; emitted only when supplied, never fabricated. **`[0..1]` is the cardinality on five of the seven templates, not on all of them.** Both `substanceAdministration` variants SHALL carry exactly one: Planned Medication Activity (`…4.42`, CONF:1098-30468) and Planned Immunization Activity (`…4.120`). [BuildCcdaPlannedImmunization](#buildccdaplannedimmunization) redeclares this field as required, which is why omitting it there is a type error; [BuildCcdaPlannedOrder](#buildccdaplannedorder) does not, so `buildCcda` will emit a Planned Medication Activity short that SHALL element if you leave it out. **The field stays optional, and the omission is REPORTED rather than silent BY `buildCcda`.** Making it required would be a breaking change to a published input type, so the decision taken was to keep the type as it is and have [buildCcda](#buildccda) raise `MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME` on the returned document. The document is still emitted, still short that element, and no date is ever fabricated to fill it. **`editCcda` reports it too**, so a planned medication grafted in by an edit is no longer emitted short the SHALL element in silence. Its check is scoped to the sections **that call grafted** and that **survived** into the emitted document: an offending edit a later edit in the same call discarded says nothing (reading the caller's ordered edit list instead reports a violation against a conformant document), and an offending act the source already carried is never re-reported. So the absence of this code from `editCcda(...).warnings` says only that the sections **that call wrote** carry no planned medication short this element; it says nothing about the rest of the document, and nothing about any other conformance rule. ###### Inherited from `BuildCcdaPlannedItemBase.effectiveTime` ##### kind > `readonly` **kind**: `"observation"` ##### mood? > `readonly` `optional` **mood?**: [`PlannedOrderMood`](#plannedordermood) The planned `@moodCode`; defaults to `"INT"`. Appointment moods not representable. ##### value? > `readonly` `optional` **value?**: [`BuildCode`](#buildcode) The expected coded result `value` (`xsi:type="CD"`, SNOMED CT default), the plan's goal/target. Emitted only when supplied; never fabricated. *** ### BuildCcdaPlannedOrder A planned Medication Activity or Supply (`…4.42` / `…4.43`). `mood` excludes the appointment moods (not in these elements' base mood domain). The drug/supply code defaults to RxNorm (medication, emitted in the `consumable`) / SNOMED CT (supply). #### Example ```ts import type { BuildCcdaPlannedOrder } from "@cosyte/ccda"; const order: BuildCcdaPlannedOrder = { kind: "medicationActivity", code: { code: "314076", displayName: "Lisinopril 10 MG Oral Tablet" }, mood: "RQO", }; ``` #### Extends - `BuildCcdaPlannedItemBase` #### Properties ##### code > `readonly` **code**: [`BuildCode`](#buildcode) The planned act/observation/drug code; default code system varies by `kind`. ###### Inherited from `BuildCcdaPlannedItemBase.code` ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The planned time as an HL7 date string; emitted only when supplied, never fabricated. **`[0..1]` is the cardinality on five of the seven templates, not on all of them.** Both `substanceAdministration` variants SHALL carry exactly one: Planned Medication Activity (`…4.42`, CONF:1098-30468) and Planned Immunization Activity (`…4.120`). [BuildCcdaPlannedImmunization](#buildccdaplannedimmunization) redeclares this field as required, which is why omitting it there is a type error; [BuildCcdaPlannedOrder](#buildccdaplannedorder) does not, so `buildCcda` will emit a Planned Medication Activity short that SHALL element if you leave it out. **The field stays optional, and the omission is REPORTED rather than silent BY `buildCcda`.** Making it required would be a breaking change to a published input type, so the decision taken was to keep the type as it is and have [buildCcda](#buildccda) raise `MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME` on the returned document. The document is still emitted, still short that element, and no date is ever fabricated to fill it. **`editCcda` reports it too**, so a planned medication grafted in by an edit is no longer emitted short the SHALL element in silence. Its check is scoped to the sections **that call grafted** and that **survived** into the emitted document: an offending edit a later edit in the same call discarded says nothing (reading the caller's ordered edit list instead reports a violation against a conformant document), and an offending act the source already carried is never re-reported. So the absence of this code from `editCcda(...).warnings` says only that the sections **that call wrote** carry no planned medication short this element; it says nothing about the rest of the document, and nothing about any other conformance rule. ###### Inherited from `BuildCcdaPlannedItemBase.effectiveTime` ##### kind > `readonly` **kind**: `"medicationActivity"` \| `"supply"` ##### mood? > `readonly` `optional` **mood?**: [`PlannedOrderMood`](#plannedordermood) The planned `@moodCode`; defaults to `"INT"`. Appointment moods not representable. *** ### BuildCcdaProblem A Problem Concern for the Problems section. The coded `problem` (SNOMED CT by default, or ICD-10-CM) is the condition; `status` maps to the concern act's status (active/resolved/inactive) and is never guessed. `onset` is the condition's onset date (the concern/observation `effectiveTime` `low`); `resolution` is its resolution date (the `high`, a.k.a. "resolution date", when the condition became biologically resolved). Per the C-CDA R2.1 Problem Observation (`…22.4.4`) rule the *presence* of a `high`, whether a real date or `nullFlavor="UNK"`, itself asserts the problem is resolved, so a `resolution` is only meaningful on a **resolved** concern: [buildCcda](#buildccda) throws when `resolution` is supplied without `status: "resolved"` rather than emit a completion date on a still-active problem. A resolved problem whose resolution date is unknown still emits a `nullFlavor="UNK"` `high` (the SHALL form), never a fabricated date. #### Example ```ts import type { BuildCcdaProblem } from "@cosyte/ccda"; const p: BuildCcdaProblem = { problem: { code: "59621000", displayName: "Essential hypertension" }, status: "active", onset: "20210101", }; const resolved: BuildCcdaProblem = { problem: { code: "195967001", displayName: "Asthma" }, status: "resolved", onset: "20180301", resolution: "20220615", }; ``` #### Properties ##### onset? > `readonly` `optional` **onset?**: `string` Onset date as an HL7 date string (e.g. `"20210101"`), the `effectiveTime/low`, optional. ##### problem > `readonly` **problem**: [`BuildCode`](#buildcode) The coded condition (SNOMED CT default, or ICD-10-CM). ##### resolution? > `readonly` `optional` **resolution?**: `string` Resolution date as an HL7 date string (the `effectiveTime/high`, a.k.a. "resolution date"). Requires `status: "resolved"`, a resolution date on a non-resolved problem is a contradiction [buildCcda](#buildccda) rejects. Omitting it on a resolved problem still emits a `nullFlavor="UNK"` `high`. ##### status? > `readonly` `optional` **status?**: `"active"` \| `"resolved"` \| `"inactive"` Active / resolved / inactive; defaults to `"active"`. *** ### BuildCcdaProcedure A Procedure for the Procedures section. `kind` selects the C-CDA Procedure Activity variant, an altering/operative `"procedure"` (default, `` `…22.4.14`), a non-altering `"act"` service (`` `…22.4.12`), or an assessment `"observation"` (`` `…22.4.13`). `code` is the coded procedure (SNOMED CT by default, or CPT / ICD-10-PCS / LOINC) and is required (the template SHALL contain a `code`). `disposition` maps to the act's `moodCode`, **performed** (`EVN`) vs **planned** (`INT`), which the parser reads back as its performed-vs-planned disposition; the two are never conflated. `effectiveTime` is emitted only when supplied (the template's effectiveTime is SHOULD [0..1], CONF:1098-7662, not fabricated when unknown). #### Example ```ts import type { BuildCcdaProcedure } from "@cosyte/ccda"; const appendectomy: BuildCcdaProcedure = { code: { code: "80146002", displayName: "Appendectomy" }, disposition: "performed", effectiveTime: "20230615", }; const plannedColonoscopy: BuildCcdaProcedure = { code: { code: "73761001", displayName: "Colonoscopy" }, disposition: "planned", }; ``` #### Properties ##### code > `readonly` **code**: [`BuildCode`](#buildcode) The coded procedure (SNOMED CT by default, or CPT / ICD-10-PCS / LOINC). ##### disposition? > `readonly` `optional` **disposition?**: `"performed"` \| `"planned"` Performed (`EVN`) or planned (`INT`); defaults to `"performed"`. Never conflated. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The procedure date as an HL7 date string; emitted only when supplied (SHOULD [0..1]). ##### kind? > `readonly` `optional` **kind?**: [`ProcedureKind`](#procedurekind-1) The Procedure Activity variant; defaults to `"procedure"` (operative). ##### status? > `readonly` `optional` **status?**: `string` The `statusCode`; defaults per disposition (performed → `"completed"`, planned → `"active"`). SHALL [1..1] on the template, so always emitted. ##### value? > `readonly` `optional` **value?**: [`BuildCode`](#buildcode) The coded result `value` (`xsi:type="CD"`, SNOMED CT default). **Required for the `"observation"` variant**, Procedure Activity Observation (`…22.4.13`) SHALL contain a `value` [1..1], and ignored for the other two variants; [buildCcda](#buildccda) throws if a `"observation"` procedure omits it. *** ### BuildCcdaResult One member observation of a Results panel, a Result Observation. `test` is the LOINC test code. **Exactly one** value form is required: a UCUM `quantity` (`xsi:type="PQ"`), a `codedValue` (`xsi:type="CD"`), or a `stringValue` (`xsi:type="ST"`), the builder throws if none (or more than one) is set, so a result value is never silently dropped or invented. `referenceRange` (when given) is emitted as a structured `IVL_PQ` so it round-trips numerically. #### Example ```ts import type { BuildCcdaResult } from "@cosyte/ccda"; const glucose: BuildCcdaResult = { test: { code: "2345-7", displayName: "Glucose" }, quantity: { value: 95, unit: "mg/dL" }, referenceRange: { low: { value: 70, unit: "mg/dL" }, high: { value: 100, unit: "mg/dL" } }, interpretation: { code: "N", displayName: "Normal" }, }; ``` #### Properties ##### codedValue? > `readonly` `optional` **codedValue?**: [`BuildCode`](#buildcode) A `CD` value. Exactly one of `quantity`/`codedValue`/`stringValue`. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The observation time as an HL7 date string, optional. ##### interpretation? > `readonly` `optional` **interpretation?**: [`BuildCode`](#buildcode) The H/L/N interpretation (HL7 ObservationInterpretation by default), optional. ##### quantity? > `readonly` `optional` **quantity?**: [`BuildQuantity`](#buildquantity) A `PQ` value (UCUM). Exactly one of `quantity`/`codedValue`/`stringValue`. ##### referenceRange? > `readonly` `optional` **referenceRange?**: `object` The normal interval, emitted as a structured `IVL_PQ`; either bound optional. ###### high? > `readonly` `optional` **high?**: [`BuildQuantity`](#buildquantity) ###### low? > `readonly` `optional` **low?**: [`BuildQuantity`](#buildquantity) ##### stringValue? > `readonly` `optional` **stringValue?**: `string` A free-text (`ST`) value. Exactly one of `quantity`/`codedValue`/`stringValue`. ##### test > `readonly` **test**: [`BuildCode`](#buildcode) The LOINC test code. *** ### BuildCcdaResultPanel A Results panel, a Result Organizer (the battery/panel wrapper, e.g. a CBC) around one or more [BuildCcdaResult](#buildccdaresult) member observations. `code` is the panel LOINC; `status` maps to the organizer `statusCode` (default `"completed"`). #### Example ```ts import type { BuildCcdaResultPanel } from "@cosyte/ccda"; const cmp: BuildCcdaResultPanel = { code: { code: "24323-8", displayName: "Comprehensive metabolic panel" }, results: [{ test: { code: "2345-7", displayName: "Glucose" }, quantity: { value: 95, unit: "mg/dL" } }], }; ``` #### Properties ##### code > `readonly` **code**: [`BuildCode`](#buildcode) The panel/battery LOINC code. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The panel's span/collection time as an HL7 date string. Emitted as the organizer's `effectiveTime`; when omitted the SHALL slot is filled with `nullFlavor="UNK"` (the member observations still carry their own times). ##### results > `readonly` **results**: readonly [`BuildCcdaResult`](#buildccdaresult)[] The member Result Observations (at least one for a populated panel). ##### status? > `readonly` `optional` **status?**: `string` The organizer `statusCode`; defaults to `"completed"`. *** ### BuildCcdaSelfCareActivity One Self-Care Activities (ADL and IADL) observation (`…22.4.128`), the activity a Functional Status Organizer SHALL carry at least one of (CONF:1098-31432). `code` names the activity assessed and SHOULD come from the ADL Result Type value set (LOINC, `2.16.840.1.113883.11.20.9.47`); `value` is the ability observed and SHOULD come from the Ability value set (SNOMED CT, `2.16.840.1.113883.11.20.9.46`). **Nothing here is defaulted.** An omitted `code` or `value` is emitted `nullFlavor="UNK"`, an explicit unknown rather than an invented activity or an invented ability, and an omitted `effectiveTime` is the same: the template's SHALL is satisfied without fabricating a date. #### Example ```ts import type { BuildCcdaSelfCareActivity } from "@cosyte/ccda"; const bathing: BuildCcdaSelfCareActivity = { code: { code: "54520-2", displayName: "Bathing" }, // LOINC, ADL Result Type value: { code: "371153006", displayName: "Independent" }, // SNOMED CT, Ability effectiveTime: "20240101", }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`BuildCode`](#buildcode) The activity assessed (SHOULD be LOINC, ADL Result Type). Omit for an explicit unknown (`code nullFlavor="UNK"`), never a fabricated activity. `codeSystem` defaults to LOINC when a `code` is supplied without one. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The date the activity was assessed (HL7 date string); `nullFlavor="UNK"` when omitted. ##### value? > `readonly` `optional` **value?**: [`BuildCode`](#buildcode) The ability observed (SHOULD be SNOMED CT, Ability). Omit for an explicit unknown (`value nullFlavor="UNK"`), never a fabricated ability. *** ### BuildCcdaSmokingStatus A Smoking Status observation for the Social History section, the Smoking Status, Meaningful Use observation (`…22.4.78`), the safety-relevant social-history fact most consumers ask for. `value` is the SNOMED CT concept from the Current Smoking Status value set (`2.16.840.1.113883.11.20.9.38`, e.g. former smoker `8517006`, never smoker `266919005`, current every-day smoker `449868002`). **Unknown is never defaulted to a status.** When `value` is omitted the observation's SHALL `value` is emitted as `nullFlavor="UNK"`, an *explicit* unknown that the parser reads back as `unknown: true` (and flags `SMOKING_STATUS_UNKNOWN`). The builder will **never** invent a "never smoker" (or any other) reading the caller did not supply: absent status ≠ non-smoker. `effectiveTime` is when the status was recorded; `nullFlavor="UNK"` when omitted. #### Example ```ts import type { BuildCcdaSmokingStatus } from "@cosyte/ccda"; const former: BuildCcdaSmokingStatus = { value: { code: "8517006", displayName: "Former smoker" }, // SNOMED CT effectiveTime: "20240101", }; const unrecorded: BuildCcdaSmokingStatus = {}; // → value nullFlavor="UNK" ``` #### Properties ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The date the status was recorded (HL7 date string); `nullFlavor="UNK"` when omitted. ##### status? > `readonly` `optional` **status?**: `string` The observation `statusCode`; defaults to `"completed"`. ##### value? > `readonly` `optional` **value?**: [`BuildCode`](#buildcode) The SNOMED CT smoking-status concept (Current Smoking Status value set). Omit for an explicit unknown (`value nullFlavor="UNK"`), never defaulted to a real status. *** ### BuildCcdaVital One member reading of a Vital Signs panel, a Vital Sign Observation. `code` is the LOINC vital (e.g. `8480-6` systolic BP); `quantity` is the UCUM-checked `PQ` reading (required, a vital sign without a value is not emitted). #### Example ```ts import type { BuildCcdaVital } from "@cosyte/ccda"; const systolic: BuildCcdaVital = { code: { code: "8480-6", displayName: "Systolic blood pressure" }, quantity: { value: 120, unit: "mm[Hg]" }, }; ``` #### Properties ##### code > `readonly` **code**: [`BuildCode`](#buildcode) The LOINC vital-sign code. ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The reading time as an HL7 date string, optional. ##### interpretation? > `readonly` `optional` **interpretation?**: [`BuildCode`](#buildcode) The H/L/N interpretation (HL7 ObservationInterpretation by default), optional. ##### quantity > `readonly` **quantity**: [`BuildQuantity`](#buildquantity) The reading as a UCUM `PQ` (required). *** ### BuildCcdaVitalsPanel A Vital Signs panel, a Vital Signs Organizer clustering the readings taken in one event (e.g. a set of vitals at a single visit). `status` maps to the organizer `statusCode` (default `"completed"`); `vitals` are the member readings. #### Example ```ts import type { BuildCcdaVitalsPanel } from "@cosyte/ccda"; const panel: BuildCcdaVitalsPanel = { vitals: [ { code: { code: "8480-6", displayName: "Systolic blood pressure" }, quantity: { value: 120, unit: "mm[Hg]" } }, { code: { code: "8462-4", displayName: "Diastolic blood pressure" }, quantity: { value: 80, unit: "mm[Hg]" } }, ], }; ``` #### Properties ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: `string` The cluster's reading time as an HL7 date string. Emitted as the organizer's SHALL `effectiveTime`; when omitted the slot is filled with `nullFlavor="UNK"`. ##### status? > `readonly` `optional` **status?**: `string` The organizer `statusCode`; defaults to `"completed"`. ##### vitals > `readonly` **vitals**: readonly [`BuildCcdaVital`](#buildccdavital)[] The member Vital Sign Observations (at least one for a populated panel). *** ### BuildCode A coded value for the builder, the tuple the parser reads back as a `CD`. `codeSystem` defaults per slot (SNOMED CT for a problem, RxNorm for an allergen), so most callers pass only `code` + `displayName`. #### Example ```ts import type { BuildCode } from "@cosyte/ccda"; const hypertension: BuildCode = { code: "59621000", displayName: "Essential hypertension" }; ``` #### Properties ##### code > `readonly` **code**: `string` The code within its system (e.g. a SNOMED CT concept id). ##### codeSystem? > `readonly` `optional` **codeSystem?**: `string` The code system OID; defaults per slot when omitted. ##### codeSystemName? > `readonly` `optional` **codeSystemName?**: `string` The code system's human name (e.g. `"SNOMED CT"`), optional. ##### displayName > `readonly` **displayName**: `string` The human-readable label, regenerated into the narrative so the two agree. *** ### BuildQuantity A dimensioned quantity for the builder, a numeric `value` and a **UCUM** unit (the parser round-trips it as a `PQ`). The unit is emitted verbatim: it is the caller's responsibility that it be valid, case-correct UCUM (`"mg/dL"`, `"mm[Hg]"`, `"10*3/uL"`), because a non-UCUM or case-slipped unit is a real defect the parser is designed to flag (`NON_UCUM_UNIT` / `UCUM_CASE_SUSPECT`), the builder never silently "corrects" a unit to a confident-wrong value. #### Example ```ts import type { BuildQuantity } from "@cosyte/ccda"; const glucose: BuildQuantity = { value: 95, unit: "mg/dL" }; ``` #### Properties ##### unit > `readonly` **unit**: `string` The UCUM unit (emitted verbatim, must be valid, case-correct UCUM). ##### value > `readonly` **value**: `number` The numeric magnitude. *** ### CcdaAuthor A parsed `author` participation: who (or what) authored the content, at the level the participation was written on. `identifiers` are the `assignedAuthor/id` values; exactly one of `person` and `device` is populated on a conforming participation (the CDA R2 `assignedAuthor` choice); `representedOrganization` is the organization the document states the author acted for; `time` is the author time at the precision the document stated, never completed and never defaulted (`clinical-safety` C3). `unidentified` is `true` exactly when the participation carried neither arm of the choice, which the US Realm Header requires one of (CONF:1198-8456). Such an author is still surfaced and still conducts to nested levels, beside an `UNIDENTIFIED_AUTHOR` warning, rather than being dropped: "an author whose identity the document never states" and "no author" are different facts. #### Example ```ts import type { CcdaAuthor } from "@cosyte/ccda"; function who(a: CcdaAuthor): string { if (a.unidentified) return "author present, identity not stated"; return a.person?.text ?? a.device?.softwareName ?? "unnamed author"; } ``` #### Properties ##### device? > `readonly` `optional` **device?**: [`CcdaAuthoringDevice`](#ccdaauthoringdevice) ##### identifiers > `readonly` **identifiers**: readonly [`II`](#ii)[] ##### person? > `readonly` `optional` **person?**: [`HumanName`](#humanname) ##### representedOrganization? > `readonly` `optional` **representedOrganization?**: [`CcdaOrganization`](#ccdaorganization) ##### time? > `readonly` `optional` **time?**: [`TS`](#ts) ##### unidentified > `readonly` **unidentified**: `boolean` *** ### CcdaAuthoringDevice A parsed `assignedAuthoringDevice`, the machine arm of the CDA R2 `assignedAuthor` choice: the software or instrument that authored the content rather than a person. Both fields are plain labels the document supplied, and neither is a person's name. #### Example ```ts import type { CcdaAuthoringDevice } from "@cosyte/ccda"; function device(d: CcdaAuthoringDevice): string { return d.softwareName ?? d.manufacturerModelName ?? "unnamed device"; } ``` #### Properties ##### manufacturerModelName? > `readonly` `optional` **manufacturerModelName?**: `string` ##### softwareName? > `readonly` `optional` **softwareName?**: `string` *** ### CcdaAuthorship The author reading for one level of a document: the document itself, a `
`, or a top-level entry act within a section. `inherited` is the whole point of the type. `false` means this level carried these `author` participations itself. `true` means it carried none and these are the nearest enclosing level's, reported so a consumer is not left with nothing, and marked so a consumer is never told this level's content was authored by that person. Reporting the nearest enclosing author and asserting that this entry's author is that person are different claims, and only the first is one the document supports. The reading is **absent** rather than empty when no level carries an author: nothing is substituted for it, not the record target, not the custodian, not a legal authenticator, not an informant. #### Example ```ts import type { CcdaAuthorship } from "@cosyte/ccda"; function provenance(a: CcdaAuthorship): string { const who = a.authors[0]?.person?.text ?? "an unnamed author"; return a.inherited ? `inherited from an enclosing level: ${who}` : `stated here: ${who}`; } ``` #### Properties ##### authors > `readonly` **authors**: readonly [`CcdaAuthor`](#ccdaauthor)[] ##### inherited > `readonly` **inherited**: `boolean` *** ### CcdaCustodian A parsed `custodian` participation: the organization that holds the document and is responsible for maintaining it. The US Realm Header requires exactly one (CONF:1198-5519). `organization` is omitted when the `custodian` carried no `assignedCustodian/representedCustodianOrganization` to read: the participation is still surfaced, because the document did carry one, and nothing is invented to fill it. #### Example ```ts import type { CcdaCustodian } from "@cosyte/ccda"; function custodianName(c: CcdaCustodian): string { return c.organization?.name ?? "custodian present, organization not stated"; } ``` #### Properties ##### organization? > `readonly` `optional` **organization?**: [`CcdaOrganization`](#ccdaorganization) *** ### CcdaDocumentInit Constructor init for [CcdaDocument](#ccdadocument). Produced by [buildDocument](#builddocument) (the parts) plus the orchestrator's accumulated `warnings`. Optional keys are omitted (never set to `undefined`) per `exactOptionalPropertyTypes`. #### Example ```ts import type { CcdaDocumentInit } from "@cosyte/ccda"; const init: CcdaDocumentInit = { templateIds: [], header: { recordTargets: [], relatedDocuments: [] }, sections: [], problems: [], medications: [], allergies: [], results: [], vitals: [], immunizations: [], procedures: [], encounters: [], smokingStatus: [], plannedItems: [], functionalStatus: [], mentalStatus: [], familyHistory: [], pastMedicalHistory: [], warnings: [], }; ``` #### Properties ##### allergies > `readonly` **allergies**: readonly [`AllergyConcern`](#allergyconcern)[] ##### documentType? > `readonly` `optional` **documentType?**: [`DocumentType`](#documenttype-4) ##### encounters > `readonly` **encounters**: readonly [`Encounter`](#encounter)[] ##### familyHistory > `readonly` **familyHistory**: readonly [`FamilyHistory`](#familyhistory-4)[] ##### functionalStatus > `readonly` **functionalStatus**: readonly [`StatusObservation`](#statusobservation)[] ##### header > `readonly` **header**: [`CcdaHeader`](#ccdaheader) ##### immunizations > `readonly` **immunizations**: readonly [`Immunization`](#immunization)[] ##### medications > `readonly` **medications**: readonly [`Medication`](#medication)[] ##### mentalStatus > `readonly` **mentalStatus**: readonly [`StatusObservation`](#statusobservation)[] ##### nonXmlBody? > `readonly` `optional` **nonXmlBody?**: [`ED`](#ed) ##### pastMedicalHistory > `readonly` **pastMedicalHistory**: readonly [`Problem`](#problem-1)[] ##### plannedItems > `readonly` **plannedItems**: readonly [`PlannedItem`](#planneditem)[] ##### problems > `readonly` **problems**: readonly [`ProblemConcern`](#problemconcern)[] ##### procedures > `readonly` **procedures**: readonly [`Procedure`](#procedure)[] ##### profile? > `readonly` `optional` **profile?**: [`ProfileAttribution`](#profileattribution) The [CcdaProfile](#ccdaprofile) applied at parse time (its `name` + resolved `lineage`), or absent when no profile was active. Attribution only, the profile's effect is already reflected in `warnings` (tolerated deviations re-badged `PROFILE_QUIRK_APPLIED`, flagged `expected`). ##### results > `readonly` **results**: readonly [`ResultOrganizer`](#resultorganizer)[] ##### sections > `readonly` **sections**: readonly [`CcdaSection`](#ccdasection)[] ##### serialized? > `readonly` `optional` **serialized?**: `string` **`Internal`** The spec-clean XML snapshot captured from the source DOM at parse time, returned by [CcdaDocument.toString](#tostring). Populated by `parseCcda`; absent for a hand-constructed document (which therefore cannot be serialized until a builder API lands). ##### smokingStatus > `readonly` **smokingStatus**: readonly [`SmokingStatus`](#smokingstatus-4)[] ##### templateIds > `readonly` **templateIds**: readonly [`II`](#ii)[] ##### vitals > `readonly` **vitals**: readonly [`VitalSignsOrganizer`](#vitalsignsorganizer)[] ##### warnings > `readonly` **warnings**: readonly [`CcdaWarning`](#ccdawarning)[] *** ### CcdaEncompassingEncounter A parsed `componentOf/encompassingEncounter`: the encounter the document summarises, which an inpatient Discharge Summary carries (CONF:1198-8471, -8472). `effectiveTime` is the encounter period as an [IVL\_TS](#ivl_ts), so each bound keeps `raw` at exactly the precision the document stated and a bound declaring a `nullFlavor` carries it rather than resolving to a date. The frame itself is **absent** when the document carries no `componentOf`: its bounds are never derived from the document `effectiveTime`, from a `documentationOf` service event, or from any other date in the document. #### Example ```ts import type { CcdaEncompassingEncounter } from "@cosyte/ccda"; function admitted(e: CcdaEncompassingEncounter): string | undefined { return e.effectiveTime?.low?.raw; } ``` #### Properties ##### dischargeDispositionCode? > `readonly` `optional` **dischargeDispositionCode?**: [`CD`](#cd) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) *** ### CcdaEntryAuthorship The author reading for one top-level entry act within a section, beside the act's own ``s so a consumer can join it to an extracted clinical entry. `CcdaSection.entryAuthorship` holds one of these per top-level entry act in the section, in document order, so the Nth element describes the Nth such act. Every extracted entry family carries an `ids` array read from the same `` children, so `ids` is the join key where the act carries one and document order is the join where it does not. #### Example ```ts import type { CcdaEntryAuthorship } from "@cosyte/ccda"; function isInherited(e: CcdaEntryAuthorship): boolean { return e.authorship?.inherited === true; } ``` #### Properties ##### authorship? > `readonly` `optional` **authorship?**: [`CcdaAuthorship`](#ccdaauthorship) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] *** ### CcdaHeader The parsed US Realm Header. `documentId` + `code` + `title` + `effectiveTime` answer the document's identity; `recordTargets` are the patient(s) (usually exactly one, more than one emits `MULTIPLE_RECORD_TARGETS`). `setId` + `versionNumber` + `relatedDocuments` carry the CDA R2 revision chain, a replacement document (see [editCcda](#editccda)) shares its predecessor's `setId`, bumps `versionNumber`, and names the prior version in a `RPLC` [RelatedDocument](#relateddocument). The three participation readings answer "who wrote it, who holds it, and what encounter it summarises". `authorship` is the document-level author reading (its `inherited` is always `false`: the document is the outermost level, so there is nothing for it to inherit from), `custodian` the organization responsible for the document, and `encompassingEncounter` the `componentOf` encounter frame. Each is absent when the document carries none, and nothing is substituted for an absent one. #### Example ```ts import type { CcdaHeader } from "@cosyte/ccda"; function when(h: CcdaHeader): Date | undefined { return h.effectiveTime?.date; } ``` #### Properties ##### authorship? > `readonly` `optional` **authorship?**: [`CcdaAuthorship`](#ccdaauthorship) ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### confidentialityCode? > `readonly` `optional` **confidentialityCode?**: [`CD`](#cd) ##### custodian? > `readonly` `optional` **custodian?**: [`CcdaCustodian`](#ccdacustodian) ##### documentId? > `readonly` `optional` **documentId?**: [`II`](#ii) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`TS`](#ts) ##### encompassingEncounter? > `readonly` `optional` **encompassingEncounter?**: [`CcdaEncompassingEncounter`](#ccdaencompassingencounter) ##### languageCode? > `readonly` `optional` **languageCode?**: `string` ##### recordTargets > `readonly` **recordTargets**: readonly [`CcdaPatient`](#ccdapatient)[] ##### relatedDocuments > `readonly` **relatedDocuments**: readonly [`RelatedDocument`](#relateddocument)[] ##### setId? > `readonly` `optional` **setId?**: [`II`](#ii) ##### title? > `readonly` `optional` **title?**: `string` ##### versionNumber? > `readonly` `optional` **versionNumber?**: `number` *** ### CcdaOrganization A parsed CDA R2 organization: the `representedOrganization` behind an author, or the `representedCustodianOrganization` behind the custodian. Carries the organization's identifiers (its NPI, its assigning-authority OID) and its name. Every field is omitted when the document does not carry it; nothing here is derived from anywhere else in the document. #### Example ```ts import type { CcdaOrganization } from "@cosyte/ccda"; function label(o: CcdaOrganization): string { return o.name ?? o.identifiers[0]?.root ?? "unnamed organization"; } ``` #### Properties ##### identifiers > `readonly` **identifiers**: readonly [`II`](#ii)[] ##### name? > `readonly` `optional` **name?**: `string` *** ### CcdaParseLimits Hard safety limits applied to every parse before the XML is handed to the DOM. Each cap defends a specific denial-of-service vector for hostile XML (oversized payloads, billion-laughs entity expansion, pathological element nesting). All four have library defaults; callers may tighten, or, at their own risk, loosen, any of them via `ParseCcdaOptions.limits`. #### Example ```ts import type { CcdaParseLimits } from "@cosyte/ccda"; const tight: CcdaParseLimits = { maxInputBytes: 1_000_000, maxDepth: 100 }; ``` #### Properties ##### maxDepth? > `readonly` `optional` **maxDepth?**: `number` Maximum element nesting depth. Exceeding it throws `ELEMENT_DEPTH_LIMIT_EXCEEDED`. ##### maxEntityExpansions? > `readonly` `optional` **maxEntityExpansions?**: `number` Maximum count of `&...;` entity references permitted in the raw input. ##### maxInputBytes? > `readonly` `optional` **maxInputBytes?**: `number` Maximum decoded input size in bytes. Exceeding it throws `INPUT_SIZE_LIMIT_EXCEEDED`. ##### maxNodeCount? > `readonly` `optional` **maxNodeCount?**: `number` Maximum total element-node count. Exceeding it throws `NODE_COUNT_LIMIT_EXCEEDED`. *** ### CcdaPatient A parsed C-CDA patient (`recordTarget/patientRole`). `identifiers` are the patient ids (the MRN lives here); demographics carry the coded gender, birth time, and optional race/ethnicity/marital status. #### Example ```ts import type { CcdaPatient } from "@cosyte/ccda"; function label(p: CcdaPatient): string { return p.name?.text ?? p.identifiers[0]?.extension ?? "unknown"; } ``` #### Properties ##### birthTime? > `readonly` `optional` **birthTime?**: [`TS`](#ts) ##### ethnicGroupCode? > `readonly` `optional` **ethnicGroupCode?**: [`CD`](#cd) ##### genderCode? > `readonly` `optional` **genderCode?**: [`CD`](#cd) ##### identifiers > `readonly` **identifiers**: readonly [`II`](#ii)[] ##### maritalStatusCode? > `readonly` `optional` **maritalStatusCode?**: [`CD`](#cd) ##### name? > `readonly` `optional` **name?**: [`HumanName`](#humanname) ##### raceCode? > `readonly` `optional` **raceCode?**: [`CD`](#cd) *** ### CcdaPosition Structural locator attached to every warning and fatal error. Every field is optional. Together with `code` it is the whole contract for locating a deviation: no diagnostic message names anything the document said. **The three string fields the parser populates are bounded, not copied.** `path` is an element local name, which a sender can make anything, so it is echoed only when it is a member of the CDA vocabulary this parser navigates and is `` otherwise. `sectionCode` is echoed only when it has the shape of a LOINC part number, which matters because `UNKNOWN_SECTION_CODE` fires exactly when the code is unrecognized. `templateId` is echoed only when it has the shape of an HL7 v3 UID, for the same reason: it is a consumer-controlled `II.root`. See `./tokens.ts` for all three, and for why a shape or membership test is used rather than a length cap. `line` and `column` are the XML locator and are never derived from content. **Which codes carry which field is narrow, and worth reading before you key a profile on one.** `sectionCode` is carried by `UNKNOWN_SECTION_CODE`, `SECTION_MATCHED_BY_LOINC_FALLBACK` and `SUBJECT_CONTEXT_OVERRIDE`, which names the enclosing section's own `` on its entry-level instances as well as its section-level one, because a withheld entry is located by which section it sat in. It is the bounded token or `` like any other, and a section that carries no `` at all contributes none, so a warning about an entry in an unrecognized section may carry no `sectionCode`. `templateId` is carried by the first two (the section's first rooted ``) and by the two stamp codes, `TEMPLATE_EXTENSION_ABSENT` and `TEMPLATE_EXTENSION_UNMODELED_RELEASE` (the matched document-type root, which is the one templateId either warning is about). No other code carries either, so a `QuirkMatch` keyed on one narrows those codes and matches nothing on the rest: an entry-level warning such as `DEPRECATED_LOINC` carries neither field today, and neither does `REQUIRED_SECTIONS_NOT_EVALUATED`, whose subject is the document's whole obligation rather than one template. Two document-level codes carry no `templateId` **on purpose**, and it is a decision rather than an omission. `MISSING_TEMPLATE_ID` has no template to name. `UNKNOWN_DOCUMENT_TEMPLATE` has too many: its subject is the templateId set naming no type, and the obvious pick, the first root in document order, is the US Realm Header stamp carried by essentially every real C-CDA, so keying a tolerance on it would read like narrowing while tolerating the code everywhere. The claim that stood here through `0.0.4`, that the fields were PHI-free "by construction" because a `path` carries element names and a `sectionCode` is a LOINC code, was an assumption about the sender rather than a property of the parser. For a top-level fatal like `INPUT_SIZE_LIMIT_EXCEEDED` no field need be populated. #### Remarks With `exactOptionalPropertyTypes: true`, do not pass `line: undefined` explicitly, omit the key instead. #### Example ```ts import type { CcdaPosition } from "@cosyte/ccda"; const pos: CcdaPosition = { path: "/ClinicalDocument/component/structuredBody/component[3]/section", sectionCode: "11450-4", templateId: "2.16.840.1.113883.10.20.22.2.5.1", }; ``` #### Properties ##### column? > `readonly` `optional` **column?**: `number` ##### line? > `readonly` `optional` **line?**: `number` ##### path? > `readonly` `optional` **path?**: `string` ##### sectionCode? > `readonly` `optional` **sectionCode?**: `string` ##### templateId? > `readonly` `optional` **templateId?**: `string` *** ### CcdaProfile A frozen, immutable vendor/conformance profile. Produced by [defineCcdaProfile](#defineccdaprofile); consumers pass it to `parseCcda(raw, { profile })` (or register it as the process default). Hand-authoring the object literal is supported but discouraged, the factory validates the safety rules and attaches `describe()`. #### Example ```ts import { parseCcda, ccdaProfiles } from "@cosyte/ccda"; const doc = parseCcda(xml, { profile: ccdaProfiles.smartScorecard }); console.log(doc.profile?.name); // "smartScorecard" ``` #### Properties ##### describe? > `readonly` `optional` **describe?**: () => `string` Multi-line human-readable summary; always present on factory-built profiles. ###### Returns `string` ##### description? > `readonly` `optional` **description?**: `string` Optional human-readable description. ##### lineage > `readonly` **lineage**: readonly `string`[] Resolved lineage, `[...parents, name]`, first-occurrence deduped. ##### name > `readonly` **name**: `string` The profile's unique name (registry key / attribution label). ##### provenance? > `readonly` `optional` **provenance?**: [`ProfileProvenance`](#profileprovenance) The cited public grounding for this profile's quirks (absent for `default`). ##### tolerate > `readonly` **tolerate**: readonly [`QuirkTolerance`](#quirktolerance)[] The expected, non-safety-critical deviations this profile tolerates. *** ### CcdaReleaseStamp One entry of this package's closed release-stamp table: a document-template `@extension` version stamp and the C-CDA release it names. #### Example ```ts import type { CcdaReleaseStamp } from "@cosyte/ccda"; const r21: CcdaReleaseStamp = { stamp: "2015-08-01", release: "R2.1" }; ``` #### Properties ##### release > `readonly` **release**: `"R2.1"` \| `"R3.0 or later"` The release that stamp names. ##### stamp > `readonly` **stamp**: `string` The `templateId/@extension` value, e.g. `2015-08-01`. *** ### CcdaSection A framed C-CDA section. `key`/`title` carry the recognized identity (when the section matched the catalog); `code` and `templateIds` are the raw signals; `narrativeText` is the human-readable `` block and `narrativeById` indexes its `ID`-bearing nodes; `subsections` holds nested `
` children. `authorship` is the section's author reading and `entryAuthorship` holds one reading per top-level entry act in the section, in document order. Either is the level's own participation where it carries one, and otherwise the nearest enclosing level's marked `inherited`; both are absent when no enclosing level carries an author either. `entryAuthorship` runs over the entry acts a record-target read path may read, so an entry an overriding `` declaration governs is absent from it entirely, exactly as it is absent from every extracted entry family. `entryAuthorship` is optional on the type and always populated by the parser: [buildSection](#buildsection) sets it on every section it frames, empty where the section has no entry act to read. It is optional because this interface is an INPUT surface as well as an output one, reachable through `CcdaDocumentInit.sections`, so requiring it would stop a consumer's existing section literal from compiling. #### Example ```ts import type { CcdaSection } from "@cosyte/ccda"; function summarize(s: CcdaSection): string { return `${s.key ?? "unknown"}: ${s.narrativeText ?? "(no narrative)"}`; } ``` #### Properties ##### authorship? > `readonly` `optional` **authorship?**: [`CcdaAuthorship`](#ccdaauthorship) ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### entryAuthorship? > `readonly` `optional` **entryAuthorship?**: readonly [`CcdaEntryAuthorship`](#ccdaentryauthorship)[] ##### key? > `readonly` `optional` **key?**: `string` ##### narrativeById > `readonly` **narrativeById**: `ReadonlyMap`\<`string`, `string`\> ##### narrativeText? > `readonly` `optional` **narrativeText?**: `string` ##### recognizedBy? > `readonly` `optional` **recognizedBy?**: `"templateId"` \| `"loinc"` ##### subsections > `readonly` **subsections**: readonly [`CcdaSection`](#ccdasection)[] ##### templateIds > `readonly` **templateIds**: readonly [`II`](#ii)[] ##### title? > `readonly` `optional` **title?**: `string` *** ### CcdaWarning Data shape for every Tier-2 warning emitted by the parser. Warnings are plain data (distinct from `CcdaParseError`, which is a thrown `Error` subclass) so they can be safely accumulated into `CcdaDocument.warnings` and passed to `onWarning` callbacks. #### Example ```ts import type { CcdaWarning } from "@cosyte/ccda"; const w: CcdaWarning = { code: "UNKNOWN_SECTION_CODE", message: "The section's LOINC code is not a recognized C-CDA section; retained as narrative-only.", position: { sectionCode: "99999-9" }, }; ``` #### Properties ##### code > `readonly` **code**: [`WarningCode`](#warningcode) ##### expected? > `readonly` `optional` **expected?**: `boolean` `true` when an active [CcdaProfile](#ccdaprofile) *expected* this deviation and downgraded it, the warning is retained (never dropped) but flagged so a consumer can filter known, tolerated noise from novel deviations. In strict mode an `expected` warning does **not** escalate to a thrown error. Absent (not `false`) when no profile touched the warning. ##### message > `readonly` **message**: `string` ##### position > `readonly` **position**: [`CcdaPosition`](#ccdaposition) ##### profile? > `readonly` `optional` **profile?**: `string` The name of the [CcdaProfile](#ccdaprofile) that tolerated this warning, when `expected`. ##### toleratedCode? > `readonly` `optional` **toleratedCode?**: [`WarningCode`](#warningcode) When `code` is [WARNING\_CODES.PROFILE\_QUIRK\_APPLIED](#profile_quirk_applied), the original warning code the profile tolerated, so the specific deviation (`DEPRECATED_LOINC`, `TEMPLATE_EXTENSION_ABSENT`, …) is never lost, only re-badged as expected. ##### valueSet? > `readonly` `optional` **valueSet?**: `"2.16.840.1.113883.3.88.12.3221.7.4"` \| `"2.16.840.1.113762.1.4.1010.4"` \| `"2.16.840.1.113762.1.4.1010.1"` \| `"2.16.840.1.113883.3.88.12.3221.8.7"` \| `"2.16.840.1.113762.1.4.1010.6"` When `code` is [WARNING\_CODES.VALUE\_SET\_BINDING\_VIOLATED](#value_set_binding_violated) or [WARNING\_CODES.VALUE\_SET\_BINDING\_NOT\_EVALUATED](#value_set_binding_not_evaluated), the OID of the value set the slot's C-CDA binding names. It is a member of BOUND\_VALUE\_SETS, a closed list this module owns, never a value read from the document, which is why it can ride on a diagnostic at all: the factories take [BoundValueSet](#boundvalueset), so the compiler holds that bound rather than the next author's memory. It is a field rather than part of the message for the same reason every other identifier here is: a message comes whole from the frozen registry and interpolates nothing. ##### valueSetRelease? > `readonly` `optional` **valueSetRelease?**: `string` When `code` is [WARNING\_CODES.VALUE\_SET\_BINDING\_VIOLATED](#value_set_binding_violated) or [WARNING\_CODES.VALUE\_SET\_BINDING\_NOT\_EVALUATED](#value_set_binding_not_evaluated), the release the supplying [ValueSetSource](#valuesetsource) declared for the package it answered from. C-CDA states that any valid expansion of a value set is conformant against a Required binding, so a non-membership finding is only ever a statement about the expansion that answered. Without this a finding would assert non-conformance it cannot support. The string is the consumer's own package label, supplied with the source and never read from a document. *** ### CD Parsed HL7 v3 coded value. `code` + `codeSystem` form the bound concept; `displayName` is the human label; `originalText` is the source text the code was derived from; `translation` holds alternative codings (e.g. a local code alongside a LOINC/SNOMED standard). #### Example ```ts import type { CD } from "@cosyte/ccda"; const sectionCode: CD = { code: "48765-2", codeSystem: "2.16.840.1.113883.6.1", displayName: "Allergies", }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: `string` ##### codeSystem? > `readonly` `optional` **codeSystem?**: `string` ##### codeSystemName? > `readonly` `optional` **codeSystemName?**: `string` ##### displayName? > `readonly` `optional` **displayName?**: `string` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### originalText? > `readonly` `optional` **originalText?**: `string` ##### translation? > `readonly` `optional` **translation?**: readonly [`CD`](#cd)[] *** ### ClinicalEntries The clinical entries extracted from a document body: the reconciliation triad (Problems, Medications, Allergies), the discrete-data sections (Results, Vital Signs, Immunizations), Procedures, Encounters, Social-History smoking status, and the remaining clinical sections (Plan of Treatment, Functional/Mental Status, Family/Past Medical History). Empty arrays when a body carries none. #### Example ```ts import type { ClinicalEntries } from "@cosyte/ccda"; function summarize(e: ClinicalEntries): number { return e.problems.length + e.medications.length + e.allergies.length; } ``` #### Properties ##### allergies > `readonly` **allergies**: readonly [`AllergyConcern`](#allergyconcern)[] ##### encounters > `readonly` **encounters**: readonly [`Encounter`](#encounter)[] ##### familyHistory > `readonly` **familyHistory**: readonly [`FamilyHistory`](#familyhistory-4)[] ##### functionalStatus > `readonly` **functionalStatus**: readonly [`StatusObservation`](#statusobservation)[] ##### immunizations > `readonly` **immunizations**: readonly [`Immunization`](#immunization)[] ##### medications > `readonly` **medications**: readonly [`Medication`](#medication)[] ##### mentalStatus > `readonly` **mentalStatus**: readonly [`StatusObservation`](#statusobservation)[] ##### pastMedicalHistory > `readonly` **pastMedicalHistory**: readonly [`Problem`](#problem-1)[] ##### plannedItems > `readonly` **plannedItems**: readonly [`PlannedItem`](#planneditem)[] ##### problems > `readonly` **problems**: readonly [`ProblemConcern`](#problemconcern)[] ##### procedures > `readonly` **procedures**: readonly [`Procedure`](#procedure)[] ##### results > `readonly` **results**: readonly [`ResultOrganizer`](#resultorganizer)[] ##### smokingStatus > `readonly` **smokingStatus**: readonly [`SmokingStatus`](#smokingstatus-4)[] ##### vitals > `readonly` **vitals**: readonly [`VitalSignsOrganizer`](#vitalsignsorganizer)[] *** ### CodeTranslationResult The outcome of [TerminologyAdapter.translate](#translate), modeled on the FHIR `$translate` operation and `@cosyte/terminology`'s `translate`. `matches` are the declared target codings, drawn **verbatim** from the consumer's map; an empty array means the source did not map. Per the never-fabricate invariant an adapter must return an empty `matches` for an unmapped source, **never a guessed target**. **Consumed on build.** `@cosyte/ccda` defines this so a consumer can wire a ConceptMap-backed engine in behind [TerminologyAdapter.translate](#translate). `buildCcda` consults it at each clinical coded slot (problem value, allergen, medication drug + route, vaccine + route) and emits any returned coding as a spec-clean CDA R2 `` alternate **beside** the primary code, never replacing it, and never fabricated (an empty `matches` emits nothing). The alternates round-trip through `parseCcda` into `CD.translation`. `translate` stays optional on the interface: a validation-only adapter need not implement it, and its absence yields byte-identical output. #### Example ```ts import type { CodeTranslationResult } from "@cosyte/ccda"; const mapped: CodeTranslationResult = { matches: [{ system: "2.16.840.1.113883.6.90", code: "I10", display: "Essential hypertension" }], }; ``` #### Properties ##### matches > `readonly` **matches**: readonly [`TerminologyCoding`](#terminologycoding)[] The declared target codings, verbatim from the map. Empty ⇒ unmapped (never a fabricated target). *** ### CodeValidationResult The outcome of [TerminologyAdapter.validateCode](#validatecode), modeled on the FHIR `$validate-code` operation's out-parameters. `result` is the verdict: `true` when the adapter confirms the code is a valid, active member of its system; `false` when it confirms the code is **not** (unknown / retired / not a member). `display` and `message` are advisory only, the parser **never** applies `display` back onto the document (that would be a silent coercion); both are surfaced to the consumer, never woven into a PHI-free warning message. #### Example ```ts import type { CodeValidationResult } from "@cosyte/ccda"; const invalid: CodeValidationResult = { result: false, message: "not in SNOMED CT US Edition" }; ``` #### Properties ##### display? > `readonly` `optional` **display?**: `string` The authoritative display the adapter knows for the code, when it has one. Advisory, never applied. ##### message? > `readonly` `optional` **message?**: `string` A human-readable reason (e.g. why a code is invalid). Advisory, never placed in a warning. ##### result > `readonly` **result**: `boolean` `true` when the code is a valid member of its system; `false` when it is not. *** ### DateParts The calendar components a parsed value actually stated, and nothing else. A component the value did not state is **absent**: the key is not present at all, rather than present holding `undefined`, so `Object.keys()` of the result is exactly the set of stated components and the value's precision is recoverable from it. Nothing is zero-filled, `month` is the spec-native 1 to 12 rather than the JS `Date` 0 to 11, and the names are singular. That shape is chosen so that deleting `offsetMinutes` leaves an object `Temporal.PlainDateTime.from` and luxon `DateTime.fromObject` both accept with no key rename and no value adjustment. Neither library is a dependency of this package and neither is imported: the compatibility is a property of the shape, stated here rather than proved by a test that would need one of them installed. #### Example ```ts import { toObject, type DateParts } from "@cosyte/ccda"; const parts: DateParts | undefined = toObject({ raw: "20260628" }); // => { year: 2026, month: 6, day: 28 }, and no other key ``` #### Properties ##### day? > `readonly` `optional` **day?**: `number` ##### hour? > `readonly` `optional` **hour?**: `number` ##### millisecond? > `readonly` `optional` **millisecond?**: `number` ##### minute? > `readonly` `optional` **minute?**: `number` ##### month? > `readonly` `optional` **month?**: `number` ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` Signed minutes east of UTC, present if and only if the value carried an explicit offset. ##### second? > `readonly` `optional` **second?**: `number` ##### year? > `readonly` `optional` **year?**: `number` *** ### DefineCcdaProfileOptions Options accepted by [defineCcdaProfile](#defineccdaprofile). Mirrors the [CcdaProfile](#ccdaprofile) shape minus the derived `lineage`/`describe`, plus the `extends` input key. Every field except `name` is optional. #### Example ```ts import { defineCcdaProfile, type DefineCcdaProfileOptions } from "@cosyte/ccda"; const opts: DefineCcdaProfileOptions = { name: "my-site", extends: ccdaProfiles.smartScorecard, tolerate: [{ code: "UNKNOWN_SECTION_CODE", rationale: "site-local sections" }], }; const p = defineCcdaProfile(opts); ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` ##### extends? > `readonly` `optional` **extends?**: [`CcdaProfile`](#ccdaprofile) \| readonly [`CcdaProfile`](#ccdaprofile)[] ##### name > `readonly` **name**: `string` ##### provenance? > `readonly` `optional` **provenance?**: [`ProfileProvenance`](#profileprovenance) ##### tolerate? > `readonly` `optional` **tolerate?**: readonly [`QuirkTolerance`](#quirktolerance)[] *** ### DocumentIdInit A CDA R2 instance identifier (`root` OID with an optional `extension`) for a caller-supplied document `id` or `setId` in a [RevisionInit](#revisioninit). #### Example ```ts import type { DocumentIdInit } from "@cosyte/ccda"; const id: DocumentIdInit = { root: "2.16.840.1.113883.19.5", extension: "DOC-2" }; ``` #### Properties ##### extension? > `readonly` `optional` **extension?**: `string` ##### root > `readonly` **root**: `string` *** ### ED Parsed HL7 v3 Encapsulated Data. `value` is the inline content (verbatim, never base64-decoded); `reference` is the `` URI pointing at out-of-line content (e.g. a narrative `#id` or an external image). #### Example ```ts import type { ED } from "@cosyte/ccda"; const ref: ED = { mediaType: "image/png", representation: "B64", reference: "#img1" }; ``` #### Properties ##### mediaType? > `readonly` `optional` **mediaType?**: `string` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### reference? > `readonly` `optional` **reference?**: `string` ##### representation? > `readonly` `optional` **representation?**: `string` ##### value? > `readonly` `optional` **value?**: `string` *** ### EditCcdaOptions Options for [editCcda](#editccda): the ordered list of section edits to apply, and the revision behavior. `revision` defaults to an automatic `RPLC` revision; pass a [RevisionInit](#revisioninit) to override its ids/version, or `false` to edit in place without stamping a new document version. #### Example ```ts import type { EditCcdaOptions } from "@cosyte/ccda"; const opts: EditCcdaOptions = { sections: [{ kind: "medications", content: [] }], revision: false, }; ``` #### Properties ##### revision? > `readonly` `optional` **revision?**: `false` \| [`RevisionInit`](#revisioninit) ##### sections? > `readonly` `optional` **sections?**: readonly [`SectionEdit`](#sectionedit)[] ##### terminology? > `readonly` `optional` **terminology?**: [`TerminologyAdapter`](#terminologyadapter) An optional consumer-supplied bring-your-own [TerminologyAdapter](#terminologyadapter), forwarded to the final re-parse of the edited document so it surfaces `SEMANTIC_CODE_INVALID` for any coded value the adapter rejects, the same semantic-validation tier `parseCcda` and `buildCcda` already offer, now reaching the edited output. `editCcda` never coerces a code to satisfy the adapter (it emits every value verbatim, byte-faithful on untouched sections and spec-clean on the one it rebuilds); the adapter can only ever add a flag. Omit for the default recognize-only behavior. `@cosyte/ccda` never imports a terminology library; you supply the adapter. *** ### Encounter A single Encounter Activity. `code` is the encounter type; `statusCode` is the encounter status; `effectiveTime` is the visit/admission period; `narrative` is the resolved `` reference. #### Example ```ts import type { Encounter } from "@cosyte/ccda"; function encounterType(e: Encounter): string | undefined { return e.code?.code; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### moodCode? > `readonly` `optional` **moodCode?**: `string` ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### statusCode? > `readonly` `optional` **statusCode?**: `string` *** ### FamilyHistory A Family History Organizer: one relative plus the conditions recorded for them. `relative` carries the structured family-member identity; `observations` are that relative's conditions. #### Example ```ts import type { FamilyHistory } from "@cosyte/ccda"; function conditionCount(h: FamilyHistory): number { return h.observations.length; } ``` #### Properties ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### observations > `readonly` **observations**: readonly [`FamilyHistoryObservation`](#familyhistoryobservation)[] ##### relative > `readonly` **relative**: [`FamilyMember`](#familymember) *** ### FamilyHistoryObservation A single condition in a relative's history. `condition` is the coded problem (SNOMED CT / ICD-10-CM); `ageAtOnset` is the relative's age (a `PQ` in years) from the nested Age Observation; `causeOfDeath` is `true` when a Family History Death Observation marks this condition as the cause of death. `negated` and `nullFlavor` are kept distinct, never collapsed. #### Example ```ts import type { FamilyHistoryObservation } from "@cosyte/ccda"; function fatal(o: FamilyHistoryObservation): boolean { return o.causeOfDeath === true; } ``` #### Properties ##### ageAtOnset? > `readonly` `optional` **ageAtOnset?**: [`PQ`](#pq) ##### causeOfDeath? > `readonly` `optional` **causeOfDeath?**: `boolean` ##### condition? > `readonly` `optional` **condition?**: [`CD`](#cd) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### negated? > `readonly` `optional` **negated?**: `boolean` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` *** ### FamilyMember The family member a [FamilyHistory](#familyhistory-4) group describes. `relationship` is the coded relation (e.g. SNOMED/HL7 `FTH` father); `gender` the relative's administrative gender; `birthTime` their birth date; `deceased` the `sdtc:deceasedInd` flag. All optional, a document may name only the relation. #### Example ```ts import type { FamilyMember } from "@cosyte/ccda"; const m: FamilyMember = { relationship: { code: "FTH", displayName: "Father" } }; ``` #### Properties ##### birthTime? > `readonly` `optional` **birthTime?**: [`TS`](#ts) ##### deceased? > `readonly` `optional` **deceased?**: `boolean` ##### gender? > `readonly` `optional` **gender?**: [`CD`](#cd) ##### relationship? > `readonly` `optional` **relationship?**: [`CD`](#cd) *** ### HumanName A parsed C-CDA person name (``). Captures the structured parts plus the `text` fallback (the element's full trimmed text) for senders that put the whole name in a single node. #### Example ```ts import type { HumanName } from "@cosyte/ccda"; const n: HumanName = { given: ["Jane"], family: "Doe", text: "Jane Doe" }; ``` #### Properties ##### family? > `readonly` `optional` **family?**: `string` ##### given? > `readonly` `optional` **given?**: readonly `string`[] ##### prefix? > `readonly` `optional` **prefix?**: readonly `string`[] ##### suffix? > `readonly` `optional` **suffix?**: readonly `string`[] ##### text? > `readonly` `optional` **text?**: `string` *** ### II Parsed HL7 v3 Instance Identifier. `root` is the namespace OID/UUID; `extension` is the local identifier within it. For a `templateId`, `root` (and optionally `extension`, the R2.1 version stamp) are the meaningful fields; for a patient id, `root`+`extension` form the full MRN. #### Example ```ts import type { II } from "@cosyte/ccda"; const id: II = { root: "2.16.840.1.113883.19.5", extension: "12345" }; ``` #### Properties ##### assigningAuthorityName? > `readonly` `optional` **assigningAuthorityName?**: `string` ##### extension? > `readonly` `optional` **extension?**: `string` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### root? > `readonly` `optional` **root?**: `string` *** ### Immunization An Immunization Activity. `vaccine` is the CVX coded product; `dose` is the amount administered; `route` is the administration route; `effectiveTime` is when it was given. `refused` is `true` for a `negationInd` not-administered record; `nullFlavor` carries an "unknown" marker, the two are kept distinct. `moodCode` distinguishes an actual administration (`EVN`) from a planned one. #### Example ```ts import type { Immunization } from "@cosyte/ccda"; function cvx(i: Immunization): string | undefined { return i.vaccine?.code; } ``` #### Properties ##### dose? > `readonly` `optional` **dose?**: [`PQ`](#pq) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### moodCode? > `readonly` `optional` **moodCode?**: `string` ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### refused? > `readonly` `optional` **refused?**: `boolean` ##### route? > `readonly` `optional` **route?**: [`CD`](#cd) ##### statusCode? > `readonly` `optional` **statusCode?**: `string` ##### vaccine? > `readonly` `optional` **vaccine?**: [`CD`](#cd) *** ### IVL\_PQ Parsed HL7 v3 Interval of Physical Quantity. Any subset of the bound fields may be present; `nullFlavor` is set when the interval element declared one. #### Example ```ts import type { IVL_PQ } from "@cosyte/ccda"; const range: IVL_PQ = { low: { value: 1, unit: "mg" }, high: { value: 2, unit: "mg" } }; ``` #### Properties ##### center? > `readonly` `optional` **center?**: [`PQ`](#pq) ##### high? > `readonly` `optional` **high?**: [`PQ`](#pq) ##### low? > `readonly` `optional` **low?**: [`PQ`](#pq) ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### width? > `readonly` `optional` **width?**: [`PQ`](#pq) *** ### IVL\_TS Parsed HL7 v3 Interval of Point in Time. `low`/`high` are the bounds; `value` captures the degenerate case where the interval element itself carries a `@value` (a point expressed as an interval). `nullFlavor` is set when the element declared one. #### Example ```ts import type { IVL_TS } from "@cosyte/ccda"; const period: IVL_TS = { low: { raw: "20260101" }, high: { raw: "20261231" } }; ``` #### Properties ##### high? > `readonly` `optional` **high?**: [`TS`](#ts) ##### low? > `readonly` `optional` **low?**: [`TS`](#ts) ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### value? > `readonly` `optional` **value?**: [`TS`](#ts) *** ### Medication A Medication Activity. `drug` is the RxNorm coded product; `dose`/`doseRange` is the amount per administration; `route` is the administration route; `duration` is the therapy window (`IVL_TS`) and `frequency` the periodic timing (`PIVL_TS`). `moodCode` distinguishes an actual administration (`EVN`) from a planned/ordered one (`INT`/`RQO`), never conflated. #### Example ```ts import type { Medication } from "@cosyte/ccda"; function rxnorm(m: Medication): string | undefined { return m.drug?.code; } ``` #### Properties ##### dose? > `readonly` `optional` **dose?**: [`PQ`](#pq) ##### doseRange? > `readonly` `optional` **doseRange?**: [`IVL_PQ`](#ivl_pq) ##### drug? > `readonly` `optional` **drug?**: [`CD`](#cd) ##### duration? > `readonly` `optional` **duration?**: [`IVL_TS`](#ivl_ts) ##### frequency? > `readonly` `optional` **frequency?**: [`MedicationFrequency`](#medicationfrequency-1) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### moodCode? > `readonly` `optional` **moodCode?**: `string` ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### negated? > `readonly` `optional` **negated?**: `boolean` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### route? > `readonly` `optional` **route?**: [`CD`](#cd) ##### statusCode? > `readonly` `optional` **statusCode?**: `string` *** ### MedicationFrequency A periodic dosing frequency (HL7 v3 `PIVL_TS`). `period` is the interval between administrations (e.g. 8 hours); `institutionSpecified` marks an institution-defined timing (e.g. "with meals" rather than a fixed clock). #### Example ```ts import type { MedicationFrequency } from "@cosyte/ccda"; const f: MedicationFrequency = { period: { value: 8, unit: "h" } }; ``` #### Properties ##### institutionSpecified? > `readonly` `optional` **institutionSpecified?**: `boolean` ##### period? > `readonly` `optional` **period?**: [`PQ`](#pq) *** ### ParentDocument A CDA R2 `parentDocument`, the earlier document a [RelatedDocument](#relateddocument) points back at. Carries the parent's `id`(s), optional `code`, and the `setId`/`versionNumber` revision pair (the version this document supersedes). #### Example ```ts import type { ParentDocument } from "@cosyte/ccda"; function priorVersion(p: ParentDocument): number | undefined { return p.versionNumber; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### setId? > `readonly` `optional` **setId?**: [`II`](#ii) ##### versionNumber? > `readonly` `optional` **versionNumber?**: `number` *** ### ParseCcdaOptions Options accepted by `parseCcda` to tune lenient/strict behaviour and the security limits. Every field is optional; `parseCcda(raw, {})` is valid and produces the library defaults (lenient parse, default safety caps). #### Remarks With `exactOptionalPropertyTypes: true`, callers cannot pass `{ strict: undefined }`, either omit the key or pass a boolean. #### Example ```ts import { parseCcda, type ParseCcdaOptions } from "@cosyte/ccda"; const opts: ParseCcdaOptions = { strict: false, onWarning: (w) => console.warn(w.code), }; parseCcda(raw, opts); ``` #### Properties ##### limits? > `readonly` `optional` **limits?**: [`CcdaParseLimits`](#ccdaparselimits) Override one or more of the default safety caps applied before DOM construction. ##### onWarning? > `readonly` `optional` **onWarning?**: [`OnWarningCallback`](#onwarningcallback) Inline callback fired for each Tier-2 warning, in emission order (see [OnWarningCallback](#onwarningcallback)). ##### profile? > `readonly` `optional` **profile?**: [`CcdaProfile`](#ccdaprofile) \| `null` The vendor/conformance [CcdaProfile](#ccdaprofile) to apply. A profile downgrades the **non-safety-critical** deviations it expects to `PROFILE_QUIRK_APPLIED` (flagged `expected`), it never changes an extracted value and can never tolerate a safety-critical warning. Omit to consult the process-scoped default ([setDefaultCcdaProfile](#setdefaultccdaprofile)); pass `null` to opt out of that default for this call. ##### strict? > `readonly` `optional` **strict?**: `boolean` When `true`, escalate every Tier-2 deviation to a thrown error instead of a warning. ##### terminology? > `readonly` `optional` **terminology?**: [`TerminologyAdapter`](#terminologyadapter) An optional consumer-supplied bring-your-own [TerminologyAdapter](#terminologyadapter). When present, the parser semantically validates each recognized coded value (a problem, medication, allergen, route, or vaccine code) against it and emits `SEMANTIC_CODE_INVALID` on a negative verdict, the code preserved verbatim, never coerced. Omit it for the default recognize-only behavior. `@cosyte/ccda` never imports a terminology library; you supply the adapter. ##### valueSets? > `readonly` `optional` **valueSets?**: [`ValueSetSource`](#valuesetsource) An optional consumer-supplied bring-your-own [ValueSetSource](#valuesetsource). When present, the parser asks it whether each coded value at a recognized slot is a member of the value set C-CDA R2.1 binds that slot to, and emits `VALUE_SET_BINDING_VIOLATED` where a **Required** binding's value set does not contain the code (a SHALL violation) or `VALUE_SET_BINDING_NOT_EVALUATED` where the source holds no expansion for it. Omit it and nothing is asked: a parse with no source is exactly the parse this library has always produced, and no membership is ever inferred from the code system alone. This is a different question from [ParseCcdaOptions.terminology](#terminology-2), which asks whether a code is a real member of its CODE SYSTEM. A value can pass that and still sit outside the VALUE SET its template binds. `@cosyte/ccda` ships no value set content; you supply the package. *** ### ParseCtx Parse context threaded into every datatype parser so it can surface a Tier-2 warning (e.g. `INVALID_NULL_FLAVOR`, `MALFORMED_DATETIME`) in discovery order. Datatypes that have nothing to warn about simply never call `emit`. #### Example ```ts import { type ParseCtx, parsePq } from "@cosyte/ccda"; const ctx: ParseCtx = { emit: (w) => console.warn(w.code) }; parsePq(el, ctx); ``` #### Properties ##### emit > `readonly` **emit**: (`warning`) => `void` ###### Parameters ###### warning [`CcdaWarning`](#ccdawarning) ###### Returns `void` ##### terminology? > `readonly` `optional` **terminology?**: [`TerminologyAdapter`](#terminologyadapter) The optional consumer-supplied [TerminologyAdapter](#terminologyadapter). When present, the code-system recognition layer ([checkCodeSlot](#checkcodeslot)) calls it to semantically validate coded values; when absent, recognition falls back to structural checks only. `@cosyte/ccda` never constructs one, it only calls the adapter a consumer passed to `parseCcda` / `buildCcda`. ##### valueSets? > `readonly` `optional` **valueSets?**: [`ValueSetSource`](#valuesetsource) The optional consumer-supplied [ValueSetSource](#valuesetsource). When present, the value-set binding layer (`../value-set-bindings.ts`) asks it whether each coded value at a checked slot is a member of the value set C-CDA binds that slot to; when absent, nothing is asked and nothing is emitted. Membership in a VALUE SET is a different question from membership in a CODE SYSTEM, which is what [TerminologyAdapter](#terminologyadapter) answers, and a consumer may supply either, both or neither. *** ### PlannedItem A single planned item from the Plan of Treatment. `kind` is the template variant; `disposition` is the performed-vs-planned reading of `moodCode` (normally `"planned"`, never guessed); `value` carries the expected result for the observation variant. `negated` and `nullFlavor` are kept distinct, never collapsed. `code` is the planned act's own `` for five of the seven variants. For the two `substanceAdministration` variants, `medicationActivity` and `immunizationActivity`, it is the **product** (the drug, or the vaccine), read from `consumable/manufacturedProduct`, and never the `substanceAdministration`'s own ``: that element is an `ActSubstanceAdministrationCode` (the kind of administration act), not the substance. That `[0..1]` cardinality and that binding are **base CDA R2's `SubstanceAdministration` class**, not a C-CDA constraint: R2.1 constrains `code` on neither template, which is precisely why an act `` is legal on both. An act `` present on either variant is not read into this field; it round-trips through `doc.toString()`. **Absence has three shapes there and only two of them are `undefined`, so do not test this field for truthiness and stop.** It is `undefined` when no arm carries a `` at all (beside `MISSING_PRODUCT_CODE`) and when the arms name different drugs (beside `MEDICATION_PRODUCT_ARM_CONFLICT`). But an arm whose `` asserts neither a symbol nor a `nullFlavor` yields a **truthy but empty `CD`**, and a `nullFlavor`-only one yields a `CD` carrying just that marking. Neither is `MISSING_PRODUCT_CODE`, an arm did carry a ``. Read `code?.code`, not `code`. On the two `substanceAdministration` variants that empty shape is **no longer silent**: the field is slot-checked exactly as its performed twin's product is, so it draws `MISSING_CODE_VALUE`. `medicationActivity` is checked at the `medication` binding (RxNorm/NDC, as a performed Medication Activity's `drug` is) and `immunizationActivity` at the `vaccine` binding (**CVX only**, as an Immunization Activity's `vaccine` is), so a product code carrying no `@codeSystem` or one outside its binding draws `MISSING_CODE_SYSTEM` or `UNEXPECTED_CODE_SYSTEM`. The two bindings differ: **NDC is expected on a drug and unexpected on a vaccine**, so an NDC-coded planned vaccine draws `UNEXPECTED_CODE_SYSTEM` where an NDC-coded planned drug does not, matching each variant's performed twin rather than each other. A `nullFlavor`-only `CD` is a complete statement and stays silent, as it does everywhere else. The other five variants are **not** slot-checked: their `code` is the planned act, which is not one of the five bound `CodeSlot`s. #### Example ```ts import type { PlannedItem } from "@cosyte/ccda"; function isPlanned(p: PlannedItem): boolean { return p.disposition === "planned"; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### disposition? > `readonly` `optional` **disposition?**: [`EventDisposition`](#eventdisposition) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### kind > `readonly` **kind**: [`PlannedItemKind`](#planneditemkind-1) ##### moodCode? > `readonly` `optional` **moodCode?**: `string` ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### negated? > `readonly` `optional` **negated?**: `boolean` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### statusCode? > `readonly` `optional` **statusCode?**: `string` ##### value? > `readonly` `optional` **value?**: [`ObservationValue`](#observationvalue) *** ### PQ Parsed HL7 v3 Physical Quantity. `value` is the parsed number (omitted when `@value` was non-numeric), `raw` is the verbatim `@value` string, and `unit` is the UCUM unit when present. #### Example ```ts import type { PQ } from "@cosyte/ccda"; const dose: PQ = { value: 81, raw: "81", unit: "mg" }; ``` #### Properties ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### raw? > `readonly` `optional` **raw?**: `string` ##### unit? > `readonly` `optional` **unit?**: `string` ##### value? > `readonly` `optional` **value?**: `number` *** ### Problem A single Problem Observation. `value` is the coded condition (SNOMED CT / ICD-10-CM), the field most consumers want; `code` is the observation's problem-type code. `negated` (from `@negationInd`) and `nullFlavor` are **distinct**, a negated problem ("no chest pain") is not an unknown one. #### Example ```ts import type { Problem } from "@cosyte/ccda"; const p: Problem = { ids: [], value: { code: "59621000", codeSystem: "2.16.840.1.113883.6.96" } }; ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### negated? > `readonly` `optional` **negated?**: `boolean` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### value? > `readonly` `optional` **value?**: [`CD`](#cd) *** ### ProblemConcern A Problem Concern Act: the clinical-concern wrapper around one or more [Problem](#problem-1) observations. `status` is the resolved active/resolved/ inactive state (from the concern `statusCode`); `effectiveTime` is the concern window (onset → resolution). #### Example ```ts import type { ProblemConcern } from "@cosyte/ccda"; function isActive(c: ProblemConcern): boolean { return c.status === "active"; } ``` #### Properties ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### problems > `readonly` **problems**: readonly [`Problem`](#problem-1)[] ##### status > `readonly` **status**: [`ConcernStatus`](#concernstatus) *** ### Procedure A single procedure. `kind` is the template variant; `code` is the procedure code (SNOMED CT / CPT / ICD-10-PCS / LOINC); `disposition` is the performed-vs-planned reading of `moodCode`; `value` carries the result for the observation variant. `negated` (a `negationInd` "did not happen") and `nullFlavor` ("unknown") are kept distinct, never collapsed. #### Example ```ts import type { Procedure } from "@cosyte/ccda"; function wasPerformed(p: Procedure): boolean { return p.disposition === "performed"; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### disposition? > `readonly` `optional` **disposition?**: [`EventDisposition`](#eventdisposition) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### kind > `readonly` **kind**: [`ProcedureKind`](#procedurekind-1) ##### moodCode? > `readonly` `optional` **moodCode?**: `string` ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### negated? > `readonly` `optional` **negated?**: `boolean` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### statusCode? > `readonly` `optional` **statusCode?**: `string` ##### value? > `readonly` `optional` **value?**: [`ObservationValue`](#observationvalue) *** ### ProfileAttribution PHI-free attribution for the [CcdaProfile](#ccdaprofile) a document was parsed under, just the profile's `name` and resolved `lineage`, not the whole profile object. Mirrors the sibling `@cosyte/hl7` `msg.profile` shape. #### Example ```ts import { parseCcda, ccdaProfiles } from "@cosyte/ccda"; const doc = parseCcda(xml, { profile: ccdaProfiles.smartScorecard }); console.log(doc.profile?.name); // "smartScorecard" ``` #### Properties ##### lineage > `readonly` **lineage**: readonly `string`[] ##### name > `readonly` **name**: `string` *** ### ProfileProvenance Provenance for a [CcdaProfile](#ccdaprofile), the **real, cited public artifact** a profile's quirks are grounded in. A quirk is encoded only when a real document (including a public HL7/ONC/IHE sample or a published conformance study) grounds it; this record is where that grounding is stated, so a reviewer can trace every tolerated deviation back to evidence rather than invention. #### Example ```ts import type { ProfileProvenance } from "@cosyte/ccda"; const prov: ProfileProvenance = { source: "SMART C-CDA Scorecard", reference: "https://ccda-scorecard.smarthealthit.org/", retrieved: "2026-07-18", }; ``` #### Properties ##### note? > `readonly` `optional` **note?**: `string` Optional clarifying note about what in the source grounds the quirks. ##### reference > `readonly` **reference**: `string` A citation the grounding can be traced to, a URL, DOI, or repo+commit. ##### retrieved? > `readonly` `optional` **retrieved?**: `string` When the grounding was last verified (ISO date) or the pinned commit SHA. ##### source > `readonly` **source**: `string` Short human-readable name of the grounding source (corpus, study, or guide). *** ### QuirkMatch Optional structural narrowing for a [QuirkTolerance](#quirktolerance). When present, the tolerance applies only to warnings whose PHI-free [CcdaPosition](#ccdaposition) matches every provided field. Matching is on **structural identifiers only** (LOINC section code, template OID); there is no matching on clinical values, by construction. **Read which codes carry which field before keying on one.** A position field a warning does not carry can never equal the value you match on, so the tolerance is inert rather than broad. `sectionCode` is carried by `UNKNOWN_SECTION_CODE`, `SECTION_MATCHED_BY_LOINC_FALLBACK` and `SUBJECT_CONTEXT_OVERRIDE` (the enclosing section's own ``, bounded on the LOINC shape or replaced by the withheld placeholder, and absent from the position when that section carries no `` at all, as an unrecognized section routinely does); `templateId` is carried by the first two plus the two document-level stamp codes, `TEMPLATE_EXTENSION_ABSENT` and `TEMPLATE_EXTENSION_UNMODELED_RELEASE`. Those five are the deviations a structural identifier can locate today, and `SUBJECT_CONTEXT_OVERRIDE` is in `SAFETY_CRITICAL_CODES`, so narrowing it is refused at definition time however precise the `match` is. Entry-level codes (`DEPRECATED_LOINC`, `UNEXPECTED_CODE_SYSTEM`, the medication-product codes) carry neither, and so do `MISSING_TEMPLATE_ID` and `UNKNOWN_DOCUMENT_TEMPLATE`, so narrowing any of those to a section or a template matches nothing at all. Omit `match` to tolerate a code wherever it fires, which is what every profile shipped in `ccdaProfiles` does. #### Example ```ts import type { QuirkMatch } from "@cosyte/ccda"; // Tolerate the missing R2.1 version stamp on the CCD root template only. const ccdRootOnly: QuirkMatch = { templateId: "2.16.840.1.113883.10.20.22.1.2" }; ``` #### Properties ##### sectionCode? > `readonly` `optional` **sectionCode?**: `string` Match only warnings carrying this section LOINC code in their position. Carried by `UNKNOWN_SECTION_CODE`, `SECTION_MATCHED_BY_LOINC_FALLBACK` and `SUBJECT_CONTEXT_OVERRIDE` (which no profile may tolerate). ##### templateId? > `readonly` `optional` **templateId?**: `string` Match only warnings carrying this template OID in their position. Carried by `UNKNOWN_SECTION_CODE`, `SECTION_MATCHED_BY_LOINC_FALLBACK`, `TEMPLATE_EXTENSION_ABSENT` and `TEMPLATE_EXTENSION_UNMODELED_RELEASE`. *** ### QuirkTolerance One expected deviation declared by a profile. `code` names an **existing, non-safety-critical** [WarningCode](#warningcode) the profile expects; `rationale` documents why (grounded in the profile's [ProfileProvenance](#profileprovenance)); optional `match` narrows it to a structural location. `defineCcdaProfile` throws if `code` is safety-critical or not a real warning code. #### Example ```ts import type { QuirkTolerance } from "@cosyte/ccda"; const t: QuirkTolerance = { code: "DEPRECATED_LOINC", rationale: "Scorecard-documented deprecated BMI LOINC 41909-3 in real docs.", }; ``` #### Properties ##### code > `readonly` **code**: [`WarningCode`](#warningcode) The existing, non-safety-critical warning code this profile expects. ##### match? > `readonly` `optional` **match?**: [`QuirkMatch`](#quirkmatch) Optional structural narrowing (section code / template OID). ##### rationale > `readonly` **rationale**: `string` Why the profile expects this deviation, grounded in its provenance. *** ### ReferenceRange A result's reference range. `low`/`high` are the structured numeric bounds (an `IVL_PQ`); `text` is the free-text form (preserved when present). A range with no structured bounds emits `FREE_TEXT_REFERENCE_RANGE`, it cannot be compared numerically against the result value. #### Example ```ts import type { ReferenceRange } from "@cosyte/ccda"; const r: ReferenceRange = { low: { value: 3.5, unit: "g/dL" }, high: { value: 5, unit: "g/dL" } }; ``` #### Properties ##### high? > `readonly` `optional` **high?**: [`PQ`](#pq) ##### low? > `readonly` `optional` **low?**: [`PQ`](#pq) ##### text? > `readonly` `optional` **text?**: `string` *** ### RelatedDocument A CDA R2 `relatedDocument`, the header link that makes one document a revision of another. `typeCode` is the ActRelationshipType (`RPLC` replaces, `APND` appends, `XFRM` transforms); `parentDocument` names the prior version. A replacement (revision) carries `typeCode="RPLC"`, the **same** `setId` as its parent, and an incremented `versionNumber`. #### Example ```ts import type { RelatedDocument } from "@cosyte/ccda"; function replaces(r: RelatedDocument): boolean { return r.typeCode === "RPLC"; } ``` #### Properties ##### parentDocument > `readonly` **parentDocument**: [`ParentDocument`](#parentdocument) ##### typeCode? > `readonly` `optional` **typeCode?**: `string` *** ### RequiredSectionOptions How a required-section lookup should treat version-scoped SHALL constraints. **Two routes, and the newer one is a superset.** `r21Stamped` is the published two-state option and its behaviour is a compatibility contract: it is unchanged, and `requiredSectionKeys("ccd", { r21Stamped: false })` returns exactly what it always returned. `stamp` is the three-state route, added because a boolean cannot express the third state at all (see [TemplateStampReading](#templatestampreading)) and repurposing it would have moved a published behaviour rather than adding one. When both are supplied, `stamp` wins: it is strictly more specific. #### Properties ##### r21Stamped? > `readonly` `optional` **r21Stamped?**: `boolean` Whether the document carries the R2.1 `@extension="2015-08-01"` stamp on its document-level `templateId`. Defaults to `true`, because these tables are written against C-CDA R2.1. Pass `false` for an R1.1-origin document (the condition that raises `TEMPLATE_EXTENSION_ABSENT`) to drop the SHALL keys whose normative constraint is scoped to the R2.1 stamp. **It cannot describe a document from a later release**, which is what [RequiredSectionOptions.stamp](#stamp-1) is for: `false` there means "no stamp at all", and answering `false` for a `2024-05-01` document takes the R1.1-origin reduction on a document that release never wrote. ##### stamp? > `readonly` `optional` **stamp?**: [`TemplateStampReading`](#templatestampreading) The three-state reading of the document-level `templateId`'s version stamp. Supersedes [RequiredSectionOptions.r21Stamped](#r21stamped) when both are given. `unmodeled-release` is the state the boolean cannot hold: the obligation is **not evaluated**, so the key set is empty and [RequiredSectionStatus.evaluation](#evaluation) says which emptiness that is. *** ### RequiredSectionSource The normative artifact a document type's obligation was read from, and that artifact's **own** revision date, so a reviewer holding a later revision can tell a table has gone stale without re-deriving it. `revision` is the artifact's self-reported revision, never the date this package read it: a re-read that changes nothing does not move it, and a newer artifact does move it even if nobody has looked yet. That is the property a staleness check needs. #### Example ```ts import { requiredSectionStatus } from "@cosyte/ccda"; requiredSectionStatus("ccd").source?.revision; // "2025-09-08" ``` #### Properties ##### artifact > `readonly` **artifact**: `string` The normative artifact, named as standards provenance. ##### revision > `readonly` **revision**: `string` That artifact's own revision date, `YYYY-MM-DD`. *** ### RequiredSectionStatus A document type's required-section obligation **and** how much of it was verified against the normative source: the asserted keys a caller already gets from [requiredSectionKeys](#requiredsectionkeys), plus the verification state, the provenance of every key traced here, and every named SHALL section left unasserted. #### Example ```ts import { requiredSectionStatus } from "@cosyte/ccda"; const status = requiredSectionStatus("consultationNote"); status.verification; // "traced-partial" status.keys; // ["historyOfPresentIllness", "allergies", "problems"] status.unasserted.map((u) => u.reason); // ["not-unconditionally-required", ...] status.source?.revision; // "2025-09-08" ``` #### Properties ##### documentType > `readonly` **documentType**: [`DocumentType`](#documenttype-4) The document type this status is about. ##### evaluation > `readonly` **evaluation**: [`RequiredSectionEvaluation`](#requiredsectionevaluation) Whether an obligation was computed for the supplied stamp reading at all. `not-evaluated` says the empty `keys` below mean "nothing was asked", never "nothing is required". ##### keys > `readonly` **keys**: readonly `string`[] The asserted SHALL keys, identical to [requiredSectionKeys](#requiredsectionkeys). ##### source > `readonly` **source**: [`RequiredSectionSource`](#requiredsectionsource) \| `undefined` The normative artifact this type's obligation was read from, and that artifact's own revision date. `undefined` only where `verification` is `untraced`, which nothing reports today: every recognized type names its source. ##### traced > `readonly` **traced**: readonly [`TracedRequiredSection`](#tracedrequiredsection)[] Provenance for each asserted key that was traced, in `keys` order. ##### unasserted > `readonly` **unasserted**: readonly [`UnassertedRequiredSection`](#unassertedrequiredsection)[] Named SHALL sections this package does not assert, with the reason. ##### verification > `readonly` **verification**: [`RequiredSectionVerification`](#requiredsectionverification) How much of this type's obligation was read off the normative source. *** ### ResolvedLimits **`Internal`** Fully-resolved safety caps, every field present (defaults merged in). #### Properties ##### maxDepth > `readonly` **maxDepth**: `number` ##### maxEntityExpansions > `readonly` **maxEntityExpansions**: `number` ##### maxInputBytes > `readonly` **maxInputBytes**: `number` ##### maxNodeCount > `readonly` **maxNodeCount**: `number` *** ### Result A single Result Observation. `code` is the LOINC test; `value` is the typed result (a UCUM-checked quantity, coded value, string, or range); `referenceRange` is the normal interval; `interpretation` is the H/L/N flag. #### Example ```ts import type { Result } from "@cosyte/ccda"; function highFlag(r: Result): boolean { return r.interpretation?.code === "H"; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### interpretation? > `readonly` `optional` **interpretation?**: [`CD`](#cd) ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### referenceRange? > `readonly` `optional` **referenceRange?**: [`ReferenceRange`](#referencerange-1) ##### value? > `readonly` `optional` **value?**: [`ObservationValue`](#observationvalue) *** ### ResultOrganizer A Result Organizer: the panel/battery wrapper around one or more [Result](#result-1) observations. `code` is the panel LOINC; `statusCode` is the organizer status; `results` are the member observations. #### Example ```ts import type { ResultOrganizer } from "@cosyte/ccda"; function panelSize(o: ResultOrganizer): number { return o.results.length; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### results > `readonly` **results**: readonly [`Result`](#result-1)[] ##### statusCode? > `readonly` `optional` **statusCode?**: `string` *** ### RevisionInit Overrides for the CDA R2 revision an edit stamps. All fields are optional: omit `documentId` to mint a fresh id, omit `setId` to keep the source's version-series id (or mint a **labelled synthetic** one when the source has none, see [SYNTHETIC\_SETID\_PREFIX](#synthetic_setid_prefix); a `setId` supplied here is the caller's assertion and is never relabelled), and omit `versionNumber` to increment the prior version by one. Pass `revision: false` on [EditCcdaOptions](#editccdaoptions) instead of a `RevisionInit` to skip revision stamping entirely. #### Example ```ts import type { RevisionInit } from "@cosyte/ccda"; const rev: RevisionInit = { versionNumber: 5 }; ``` #### Properties ##### documentId? > `readonly` `optional` **documentId?**: [`DocumentIdInit`](#documentidinit) ##### setId? > `readonly` `optional` **setId?**: [`DocumentIdInit`](#documentidinit) ##### versionNumber? > `readonly` `optional` **versionNumber?**: `number` *** ### SectionInfo Recognized-section descriptor. `key` is a stable machine name, `title` a human label, `loinc` the section's LOINC code, and `templateRoots` the section `templateId` root OID(s) (entries-optional and entries-required variants) that identify it. #### Example ```ts import type { SectionInfo } from "@cosyte/ccda"; const s: SectionInfo = { key: "allergies", title: "Allergies", loinc: "48765-2", templateRoots: ["2.16.840.1.113883.10.20.22.2.6.1"], }; ``` #### Properties ##### key > `readonly` **key**: `string` ##### loinc > `readonly` **loinc**: `string` ##### templateRoots > `readonly` **templateRoots**: readonly `string`[] ##### title > `readonly` **title**: `string` *** ### SmokingStatus A Smoking Status observation. `value` is the SNOMED CT smoking-status concept; `unknown` is `true` when the status is explicitly unknown (a `nullFlavor` or an "unknown" SNOMED concept), distinct from simply absent. `effectiveTime` is when the status was recorded. #### Example ```ts import type { SmokingStatus } from "@cosyte/ccda"; function isFormerSmoker(s: SmokingStatus): boolean { return s.value?.code === "8517006"; } ``` #### Properties ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### statusCode? > `readonly` `optional` **statusCode?**: `string` ##### unknown > `readonly` **unknown**: `boolean` ##### value? > `readonly` `optional` **value?**: [`CD`](#cd) *** ### ST Parsed HL7 v3 Character String. `value` is the trimmed text content (omitted when empty); `nullFlavor` is set when the element declared one instead of a value. #### Example ```ts import type { ST } from "@cosyte/ccda"; const title: ST = { value: "Allergies, Adverse Reactions, Alerts" }; ``` #### Properties ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### value? > `readonly` `optional` **value?**: `string` *** ### StatusObservation A single Functional/Mental Status finding. `domain` is the section it came from; `assessmentScale` is `true` when the finding is an Assessment Scale Observation (a scored scale) rather than a plain status observation; `code` is the finding code (usually LOINC), `value` the typed result. `negated` and `nullFlavor` are kept distinct, never collapsed. #### Example ```ts import type { StatusObservation } from "@cosyte/ccda"; function isScale(o: StatusObservation): boolean { return o.assessmentScale === true; } ``` #### Properties ##### assessmentScale? > `readonly` `optional` **assessmentScale?**: `boolean` ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### domain > `readonly` **domain**: [`StatusDomain`](#statusdomain) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### negated? > `readonly` `optional` **negated?**: `boolean` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### statusCode? > `readonly` `optional` **statusCode?**: `string` ##### supporting? > `readonly` `optional` **supporting?**: readonly [`SupportingObservation`](#supportingobservation)[] The scored component observations of an Assessment Scale Observation (`…22.4.86`), present only when `assessmentScale` is `true` and the scale carries components (e.g. the individual PHQ-9 questions under the total score). Absent for a plain status observation. ##### value? > `readonly` `optional` **value?**: [`ObservationValue`](#observationvalue) *** ### SupportingObservation One scored component of an Assessment Scale Observation, an Assessment Scale Supporting Observation (`…22.4.86`), such as a single PHQ-9 question or a Glasgow Coma sub-score. `code` is the item code (LOINC/SNOMED), `value` its scored answer (usually an `integer`). Modeled so the scale's detail is never silently dropped on parse. #### Example ```ts import type { SupportingObservation } from "@cosyte/ccda"; function itemScore(o: SupportingObservation): number | undefined { return o.value?.kind === "integer" ? o.value.value : undefined; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### value? > `readonly` `optional` **value?**: [`ObservationValue`](#observationvalue) *** ### TerminologyAdapter The bring-your-own terminology contract a consumer supplies to `parseCcda` (via [ParseCcdaOptions.terminology](#terminology-2)) or `buildCcda` (via [BuildCcdaOptions.terminology](#terminology)). `@cosyte/ccda` never implements or imports one, it only *calls* the one you supply, and only where a coded value carries both a `code` and a `system`. **`validateCode` is the one method the parser consumes.** For each recognized coded slot (problem, medication, allergen, route, vaccine), the parser calls `validateCode` and, on a `result: false`, emits `SEMANTIC_CODE_INVALID` with the code **preserved verbatim**, it never rewrites the value. Returning `undefined` means the adapter has no opinion (e.g. the system is outside its coverage); the parser then stays silent and falls back to recognize-only, so an adapter that only covers some systems adds no noise for the rest. **`translate` is consumed on build and optional.** `buildCcda` calls it at each clinical coded slot and emits any returned coding as a `` alternate beside the primary code (never a substitution), see [CodeTranslationResult](#codetranslationresult). #### Example ```ts import { parseCcda, type TerminologyAdapter } from "@cosyte/ccda"; // A tiny BYO adapter, ccda imports no terminology library; you supply one. const adapter: TerminologyAdapter = { validateCode: (coding) => coding.system === "2.16.840.1.113883.6.96" ? { result: mySnomedService.has(coding.code) } : undefined, // no opinion on other systems }; const doc = parseCcda(xml, { terminology: adapter }); // A structurally-valid but non-member SNOMED code now carries SEMANTIC_CODE_INVALID. ``` #### Properties ##### translate? > `readonly` `optional` **translate?**: (`coding`) => [`CodeTranslationResult`](#codetranslationresult) \| `undefined` Translate `coding` through the consumer's map, returning declared targets verbatim (empty ⇒ unmapped, never fabricated). `buildCcda` emits each returned coding as a `` alternate beside the primary code, never a substitution. Optional, a validation-only adapter may omit it, and its absence yields byte-identical output. ###### Parameters ###### coding [`TerminologyCoding`](#terminologycoding) ###### Returns [`CodeTranslationResult`](#codetranslationresult) \| `undefined` ##### validateCode > `readonly` **validateCode**: (`coding`) => [`CodeValidationResult`](#codevalidationresult) \| `undefined` Validate that `coding` is a real member of its code system. Return `{ result: true }` when it is, `{ result: false }` when it is not, or `undefined` when the adapter cannot judge (the system is out of its scope), `undefined` produces no warning and no change. ###### Parameters ###### coding [`TerminologyCoding`](#terminologycoding) ###### Returns [`CodeValidationResult`](#codevalidationresult) \| `undefined` *** ### TerminologyCoding A single coded value handed to a [TerminologyAdapter](#terminologyadapter). Mirrors FHIR R4 `Coding` (and `@cosyte/terminology`'s `Coding`), **except** `system` is the C-CDA `@codeSystem` **OID** exactly as it appears in the document (e.g. `2.16.840.1.113883.6.96` for SNOMED CT), not a canonical URI, the parser hands the adapter what the wire actually carries and never invents a URI. A consumer bridging to a URI-based engine (such as `@cosyte/terminology`'s `resolveSystem`) performs that OID→URI mapping inside their adapter. #### Example ```ts import type { TerminologyCoding } from "@cosyte/ccda"; const snomed: TerminologyCoding = { system: "2.16.840.1.113883.6.96", code: "38341003", display: "Hypertension", }; ``` #### Properties ##### code > `readonly` **code**: `string` The symbol within the code system (the C-CDA `@code`). Required, a coding without one is meaningless. ##### display? > `readonly` `optional` **display?**: `string` The human-readable label (the C-CDA `@displayName`), when present. Carried verbatim. ##### system? > `readonly` `optional` **system?**: `string` The code system's OID, as carried by the C-CDA `@codeSystem` (never a URI). May be absent. ##### version? > `readonly` `optional` **version?**: `string` The code-system version the code is drawn from, when known. *** ### ToDateOptions Options for [toDate](#todate). `assumeOffsetMinutes` is the caller's declaration of the zone an offset-less value was written in, in signed minutes east of UTC. It is the **only** way to get an instant out of a value that states no offset, and an explicit `0` means "treat this naive value as UTC". A value that carries its own offset ignores it. #### Example ```ts import { toDate, type ToDateOptions } from "@cosyte/ccda"; const eastern: ToDateOptions = { assumeOffsetMinutes: -300 }; toDate({ raw: "20260628" }, eastern); // => 2026-06-28T05:00:00.000Z ``` #### Properties ##### assumeOffsetMinutes? > `readonly` `optional` **assumeOffsetMinutes?**: `number` *** ### TracedRequiredSection One asserted SHALL section key together with the conformance statement it was read from and the source's own name for the section, so a reviewer holding the same artifact can re-check the assertion instead of re-deriving it. #### Example ```ts import { requiredSectionStatus } from "@cosyte/ccda"; requiredSectionStatus("consultationNote").traced[0]; // { key: "historyOfPresentIllness", conformanceId: "CONF:1198-28907", sourceName: "History of Present Illness Section" } ``` #### Properties ##### conformanceId > `readonly` **conformanceId**: `string` The conformance statement the assertion was read from (`CONF:1198-` + digits). ##### key > `readonly` **key**: `string` The recognized catalog section key this package asserts. ##### sourceName > `readonly` **sourceName**: `string` The source's own name for the required section. *** ### TS Parsed HL7 v3 Point in Time. `raw` is the verbatim `@value`; `date` is the resolved JS `Date` (UTC when the value carried no offset), or omitted when the value was malformed. `nullFlavor` is set when the element declared one. #### Example ```ts import type { TS } from "@cosyte/ccda"; const effective: TS = { raw: "20260628", date: new Date("2026-06-28T00:00:00Z") }; ``` #### Properties ##### date? > `readonly` `optional` **date?**: `Date` ##### nullFlavor? > `readonly` `optional` **nullFlavor?**: `string` ##### raw? > `readonly` `optional` **raw?**: `string` *** ### UnassertedRequiredSection A SHALL section the normative source names for a document type that this package deliberately does not assert, named so a caller can enumerate exactly what the parser is not checking, and why. #### Example ```ts import { requiredSectionStatus } from "@cosyte/ccda"; requiredSectionStatus("diagnosticImagingReport").unasserted[0]; // { sourceName: "Findings Section (DIR)", conformanceId: "CONF:1198-30697", reason: "outside-section-catalog" } ``` #### Properties ##### conformanceId > `readonly` **conformanceId**: `string` The conformance statement that requires it. ##### reason > `readonly` **reason**: [`UnassertedSectionReason`](#unassertedsectionreason) Which of the two permitted reasons keeps it out of the asserted set. ##### sourceName > `readonly` **sourceName**: `string` The source's own name for the section. *** ### ValueSetBinding One checked slot's value-set binding as this package declares it: the value set the C-CDA R2.1 template binds the slot to, how strongly it binds it, and the provenance of that reading. Every field is a fixed literal this package owns. No part of a row comes from a parsed document, which is what lets `valueSet` ride on a warning. #### Example ```ts import { valueSetBinding } from "@cosyte/ccda"; const binding = valueSetBinding("allergen"); binding.valueSet; // "2.16.840.1.113762.1.4.1010.1" binding.conformanceId; // "CONF:1098-7419" ``` #### Properties ##### conformanceId > `readonly` **conformanceId**: `string` The conformance statement the binding was read from (`CONF:` + digits). ##### slot > `readonly` **slot**: `"problem"` \| `"medication"` \| `"allergen"` \| `"route"` \| `"vaccine"` The checked slot this row is about. ##### source > `readonly` **source**: [`ValueSetBindingSource`](#valuesetbindingsource-1) The artifact the row was read from, and that artifact's own revision. ##### sourceTemplate > `readonly` **sourceTemplate**: `string` The template whose rule carries that statement, in the source's own words. ##### strength > `readonly` **strength**: [`ValueSetBindingStrength`](#valuesetbindingstrength-1) Required (a SHALL binding) or preferred (a SHOULD or MAY binding). ##### valueSet > `readonly` **valueSet**: `"2.16.840.1.113883.3.88.12.3221.7.4"` \| `"2.16.840.1.113762.1.4.1010.4"` \| `"2.16.840.1.113762.1.4.1010.1"` \| `"2.16.840.1.113883.3.88.12.3221.8.7"` \| `"2.16.840.1.113762.1.4.1010.6"` The bound value set's published OID. An identifier, never its members, and a member of the closed BOUND\_VALUE\_SETS list so that the compiler, rather than review, holds the bound on what can reach a diagnostic. ##### valueSetName > `readonly` **valueSetName**: `string` The source's own name for that value set. *** ### ValueSetBindingSource The normative artifact a binding row was read from, and that artifact's **own** revision, so a reviewer holding a later revision can tell a row has gone stale without re-deriving it. `revision` is the artifact's self-reported revision, never the date this package read it: a re-read that changes nothing does not move it, and a newer artifact does move it even if nobody has looked yet. That is the property a staleness check needs, and it is the same rule [RequiredSectionSource](#requiredsectionsource) follows. #### Example ```ts import { valueSetBinding } from "@cosyte/ccda"; valueSetBinding("vaccine").source.revision; // "2025-09-08" ``` #### Properties ##### artifact > `readonly` **artifact**: `string` The normative artifact, named as standards provenance. ##### revision > `readonly` **revision**: `string` That artifact's own revision date, `YYYY-MM-DD`. *** ### ValueSetMembershipAnswer What a [ValueSetSource](#valuesetsource) can say about one membership question. Three states, and the third is the reason this is not a boolean. - `member`: the code is in the expansion the source holds. Silent. - `not-a-member`: it is not. Against a Required binding this is a SHALL violation and raises `VALUE_SET_BINDING_VIOLATED`. - `no-expansion`: the source holds no expansion for that value set, so it did not evaluate the binding at all. This raises `VALUE_SET_BINDING_NOT_EVALUATED` rather than nothing, because a package that quietly skips a value set it does not hold reads exactly like one that checked and found the code fine. Returning `undefined` instead of an answer is the fourth state and the only silent one: the source declares it has no opinion here, the same "out of my scope" answer [TerminologyAdapter.validateCode](#validatecode) gives. Use it when the source is not the authority for a value set; use `no-expansion` when it is the authority and the package it answers from does not carry the expansion. #### Example ```ts import type { ValueSetMembershipAnswer } from "@cosyte/ccda"; const answer: ValueSetMembershipAnswer = { membership: "not-a-member" }; ``` #### Properties ##### membership > `readonly` **membership**: `"member"` \| `"not-a-member"` \| `"no-expansion"` The verdict, or the declaration that no expansion was available to give one. *** ### ValueSetMembershipQuery One membership question put to a [ValueSetSource](#valuesetsource): is this coded value a member of this value set? `valueSet` is the OID of the value set the slot's C-CDA binding names, taken from this package's own frozen binding table and never from the document. `coding` is the document's value exactly as the wire carried it, the same shape [TerminologyAdapter.validateCode](#validatecode) receives. #### Example ```ts import type { ValueSetMembershipQuery } from "@cosyte/ccda"; const query: ValueSetMembershipQuery = { valueSet: "2.16.840.1.113762.1.4.1010.4", coding: { system: "2.16.840.1.113883.6.88", code: "314076" }, }; ``` #### Properties ##### coding > `readonly` **coding**: [`TerminologyCoding`](#terminologycoding) The coded value to test, carried verbatim from the document. ##### valueSet > `readonly` **valueSet**: `string` The bound value set's OID, from this package's declared binding table. *** ### ValueSetSource The bring-your-own **value-set** contract, the sibling of [TerminologyAdapter](#terminologyadapter) for the question an adapter cannot answer: is this code inside the VALUE SET the C-CDA template binds this slot to? C-CDA binds each checked slot to a value set, and a Required binding makes membership a SHALL. The member codes of those value sets are licensed data (SNOMED CT, RxNorm and CVX expansions published through VSAC), so `@cosyte/ccda` carries the value set OIDs and never their contents: you hold the package, you answer the question, and the parser reports what you say. **`release` is not optional, and it is what makes the finding honest.** C-CDA's own guidance states that for a Required binding any valid expansion of a value set is conformant, so "not a member" is only ever a statement about the expansion that answered. A finding therefore carries the release you declare here beside the value set OID, and asserts non-conformance no wider than that. The string is your own package label; this library never reads it from a document and never puts it in a message. **Fail-safe, exactly like the terminology adapter.** The source can only report: no value is rewritten, refused, reordered or dropped on a negative verdict, and an exception `isMember` raises is not swallowed. #### Example ```ts import { parseCcda, type ValueSetSource } from "@cosyte/ccda"; // Your own package, in process. `@cosyte/ccda` ships no expansion. const expansions = new Map([["2.16.840.1.113762.1.4.1010.4", new Set(["314076"])]]); const valueSets: ValueSetSource = { release: "my-vsac-package-2026.1", isMember: ({ valueSet, coding }) => { const expansion = expansions.get(valueSet); if (expansion === undefined) return { membership: "no-expansion" }; return { membership: expansion.has(coding.code) ? "member" : "not-a-member" }; }, }; const doc = parseCcda(xml, { valueSets }); ``` #### Properties ##### isMember > `readonly` **isMember**: (`query`) => [`ValueSetMembershipAnswer`](#valuesetmembershipanswer) \| `undefined` Answer one membership question, or return `undefined` to declare no opinion. Return `{ membership: "no-expansion" }` when the package holds no expansion for the value set: that is reported as "not evaluated", never as a pass. ###### Parameters ###### query [`ValueSetMembershipQuery`](#valuesetmembershipquery) ###### Returns [`ValueSetMembershipAnswer`](#valuesetmembershipanswer) \| `undefined` ##### release > `readonly` **release**: `string` The release of the value-set package these answers come from, in your own terms (a VSAC package version, a build tag, a date). It rides on every finding this source produces, so a reader can tell which expansion said so. *** ### VitalSign A single Vital Sign Observation. `code` is the LOINC vital; `value` is the typed reading (normally a UCUM-checked quantity); `interpretation` is the H/L/N flag; `effectiveTime` is when the reading was taken. #### Example ```ts import type { VitalSign } from "@cosyte/ccda"; function systolic(v: VitalSign): number | undefined { return v.code?.code === "8480-6" && v.value?.kind === "physicalQuantity" ? v.value.quantity.value : undefined; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### effectiveTime? > `readonly` `optional` **effectiveTime?**: [`IVL_TS`](#ivl_ts) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### interpretation? > `readonly` `optional` **interpretation?**: [`CD`](#cd) ##### narrative? > `readonly` `optional` **narrative?**: `string` ##### value? > `readonly` `optional` **value?**: [`ObservationValue`](#observationvalue) *** ### VitalSignsOrganizer A Vital Signs Organizer: the cluster wrapper around one or more [VitalSign](#vitalsign) observations taken together. `code` is the cluster code; `statusCode` is the organizer status; `vitals` are the member readings. #### Example ```ts import type { VitalSignsOrganizer } from "@cosyte/ccda"; function clusterSize(o: VitalSignsOrganizer): number { return o.vitals.length; } ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`CD`](#cd) ##### ids > `readonly` **ids**: readonly [`II`](#ii)[] ##### statusCode? > `readonly` `optional` **statusCode?**: `string` ##### vitals > `readonly` **vitals**: readonly [`VitalSign`](#vitalsign)[] ## Type Aliases ### BoundValueSet > **BoundValueSet** = *typeof* `BOUND_VALUE_SETS`\[`number`\] A value set OID this parser is allowed to put on a diagnostic. Closed by construction, so naming one names a parser constant rather than document content. #### Example ```ts import type { BoundValueSet } from "@cosyte/ccda"; const vs: BoundValueSet = "2.16.840.1.113762.1.4.1010.4"; ``` *** ### BuildableDocumentType > **BuildableDocumentType** = `Extract`\<[`DocumentType`](#documenttype-4), `"ccd"` \| `"referralNote"` \| `"dischargeSummary"`\> The document types [buildCcda](#buildccda) can emit, three of the twelve [DocumentType](#documenttype-4)s `parseCcda` recognizes. **Declared as a subset of the recognized enumeration rather than as its own union of strings.** `Extract` fails to compile if any member stops being a recognized document type, so the builder's surface cannot drift from the parser's: renaming a key in `DOCUMENT_TYPES` is already a breaking change and this makes it a compile error here too. The other nine are refused at run time by [buildCcda](#buildccda), and the refusal is derived from DOC\_TYPE\_SPECS rather than from a second list, so a thirteenth recognized type is refused the moment it is recognized. *** ### BuildCcdaPlannedItem > **BuildCcdaPlannedItem** = [`BuildCcdaPlannedAct`](#buildccdaplannedact) \| [`BuildCcdaPlannedOrder`](#buildccdaplannedorder) \| [`BuildCcdaPlannedObservation`](#buildccdaplannedobservation) \| [`BuildCcdaPlannedImmunization`](#buildccdaplannedimmunization) A planned item for the Plan of Treatment section (`…22.2.10`), a discriminated union over the seven planned-entry templates the parser returns, split by which `@moodCode` domain each element admits and by which SHALL elements each template carries: [BuildCcdaPlannedAct](#buildccdaplannedact) (act/encounter/procedure, which accept the appointment moods `APT`/`ARQ`), [BuildCcdaPlannedOrder](#buildccdaplannedorder) (medication/supply), [BuildCcdaPlannedObservation](#buildccdaplannedobservation) (observation, which also carries an expected `value`), and [BuildCcdaPlannedImmunization](#buildccdaplannedimmunization) (immunization, whose `effectiveTime` is required rather than optional). **The mood split is correct by construction:** the base CDA R2 mood domains for `substanceAdministration`, `supply`, and `observation` exclude `APT`/`ARQ`, so those appointment moods are simply not representable on those kinds, the type prevents emitting a schema-invalid `@moodCode`, not merely a discouraged one. **Everything here is future/ordered, never performed.** No variant admits the performed `EVN`; each entry's `statusCode` is fixed to `"active"` (the SHALL the planned templates require), never a performed `"completed"`, and the planned `@moodCode` reads back through the parser's `disposition` as `"planned"`; the two dispositions are never conflated. #### Example ```ts import type { BuildCcdaPlannedItem } from "@cosyte/ccda"; const plannedColonoscopy: BuildCcdaPlannedItem = { kind: "procedure", code: { code: "73761001", displayName: "Colonoscopy" }, // SNOMED CT }; ``` *** ### CcdaEditErrorCode > **CcdaEditErrorCode** = `"NO_SOURCE_DOCUMENT"` \| `"NO_STRUCTURED_BODY"` \| `"SECTION_ALREADY_PRESENT"` \| `"SECTION_ABSENT"` \| `"REQUIRED_SECTION_MISSING"` \| `"SOURCE_MISSING_ID"` Stable string codes for every failure [editCcda](#editccda) raises. Consumers narrow on `err.code`. #### Example ```ts import type { CcdaEditErrorCode } from "@cosyte/ccda"; const code: CcdaEditErrorCode = "REQUIRED_SECTION_MISSING"; ``` *** ### CcdaRelease > **CcdaRelease** = *typeof* [`CCDA_RELEASES`](#ccda_releases)\[`number`\] A C-CDA release this package can name. Closed by construction, so putting one in a warning message names a parser constant rather than document content. #### Example ```ts import type { CcdaRelease } from "@cosyte/ccda"; const release: CcdaRelease = "R2.1"; ``` *** ### CodeSlot > **CodeSlot** = *typeof* `CODE_SLOTS`\[`number`\] Which coded slot a code-system warning is about. Closed by construction, so naming it in a message names a parser constant rather than document content. #### Example ```ts import type { CodeSlot } from "@cosyte/ccda"; const slot: CodeSlot = "problem"; ``` *** ### ConcernStatus > **ConcernStatus** = `"active"` \| `"resolved"` \| `"inactive"` \| `"unknown"` The resolved active-vs-resolved state of a Problem/Allergy Concern Act, derived from its `statusCode` (with `effectiveTime` available for refinement). #### Example ```ts import type { ConcernStatus } from "@cosyte/ccda"; const s: ConcernStatus = "active"; ``` *** ### DocumentType > **DocumentType** = `"ccd"` \| `"dischargeSummary"` \| `"referralNote"` \| `"consultationNote"` \| `"historyAndPhysical"` \| `"progressNote"` \| `"procedureNote"` \| `"operativeNote"` \| `"carePlan"` \| `"diagnosticImagingReport"` \| `"unstructuredDocument"` \| `"transferSummary"` Machine keys for the twelve recognized C-CDA R2.1 document types. Stable strings, consumers may branch on `doc.documentType === "ccd"`. Renaming a key is a breaking change. #### Example ```ts import type { DocumentType } from "@cosyte/ccda"; const t: DocumentType = "dischargeSummary"; ``` *** ### EditableSectionKind > **EditableSectionKind** = `"problems"` \| `"allergies"` \| `"medications"` \| `"results"` \| `"vitalSigns"` \| `"immunizations"` \| `"procedures"` \| `"encounters"` \| `"socialHistory"` \| `"pastMedicalHistory"` \| `"planOfTreatment"` \| `"familyHistory"` **`Internal`** The section kinds buildSectionComponent can emit as a standalone ``, the sections `buildCcda` builds from a single content list. The compound Functional/Mental Status sections (three content arrays each) and narrative-only sections are intentionally excluded from edit support. *** ### EventDisposition > **EventDisposition** = `"performed"` \| `"planned"` The performed-vs-planned disposition of a clinical act, derived from its `@moodCode`: `EVN` → `"performed"`; a planned mood (`INT`/`RQO`/`PRMS`/`PRP`/ `APT`/`ARQ`) → `"planned"`. Shared by Procedures and the Plan of Treatment so a planned act is **never** read as performed (and vice versa). #### Example ```ts import type { EventDisposition } from "@cosyte/ccda"; const d: EventDisposition = "planned"; ``` *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] Discriminant type for `CcdaParseError.code`. Narrowing a caught error by this code lets consumers write exhaustive `switch` blocks (enabled by the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free comparison against the `FATAL_CODES` registry. #### Example ```ts import type { FatalCode } from "@cosyte/ccda"; function describe(code: FatalCode): string { switch (code) { case "XXE_OR_DTD_PRESENT": return "document declared a DTD or external entity"; case "ENTITY_EXPANSION_LIMIT": return "too many entity expansions"; case "INPUT_SIZE_LIMIT_EXCEEDED": return "input too large"; case "ELEMENT_DEPTH_LIMIT_EXCEEDED": return "nesting too deep"; case "NODE_COUNT_LIMIT_EXCEEDED": return "too many elements"; case "NOT_WELL_FORMED_XML": return "XML did not parse"; case "NOT_A_CLINICAL_DOCUMENT": return "root element is not ClinicalDocument"; } } ``` *** ### NullFlavor > **NullFlavor** = *typeof* [`NULL_FLAVORS`](#null_flavors)\[`number`\] A valid HL7 v3 NullFlavor token. Datatype `nullFlavor` fields are typed as `string` (a non-conforming value is preserved verbatim and flagged with `INVALID_NULL_FLAVOR`); this union documents the conforming set, which is the whole code system rather than a working subset of it. #### Example ```ts import type { NullFlavor } from "@cosyte/ccda"; const nf: NullFlavor = "ASKU"; ``` *** ### ObservationValue > **ObservationValue** = \{ `kind`: `"physicalQuantity"`; `quantity`: [`PQ`](#pq); \} \| \{ `code`: [`CD`](#cd); `kind`: `"coded"`; \} \| \{ `kind`: `"string"`; `nullFlavor?`: `string`; `value`: `string`; \} \| \{ `kind`: `"integer"`; `nullFlavor?`: `string`; `raw?`: `string`; `value?`: `number`; \} \| \{ `kind`: `"range"`; `range`: [`IVL_PQ`](#ivl_pq); \} \| \{ `kind`: `"unsupported"`; `raw?`: `string`; `xsiType?`: `string`; \} A parsed observation `value`, discriminated on `kind`. `physicalQuantity` carries a UCUM-checked [PQ](#pq); `coded` a [CD](#cd); `string` a free-text value; `integer` a count/score (`xsi:type="INT"`, the type C-CDA prefers for an assessment-scale score, units are not allowed on an `INT`); `range` an [IVL\_PQ](#ivl_pq). `unsupported` preserves an `xsi:type` the model does not specialize (with any raw text) so nothing is ever discarded. `integer` keeps `value` and `nullFlavor` distinct, a scored `INT` never collapses into an unknown one, and vice versa. `integer` and `string` carry a `nullFlavor` beside their content for the same reason every v3 datatype does, and `integer` mirrors `PQ` exactly: `raw` is the verbatim `@value` token and `value` the parsed number, so when a `nullFlavor` contradicts the score the parser withholds `value` and keeps `raw` (see [parsePq](#parsepq)). These two arms are parsed inline here rather than through `src/model/types/`, so they route through the shared contradiction check explicitly. #### Example ```ts import type { ObservationValue } from "@cosyte/ccda"; function numeric(v: ObservationValue): number | undefined { if (v.kind === "physicalQuantity") return v.quantity.value; return v.kind === "integer" ? v.value : undefined; } ``` *** ### OnWarningCallback > **OnWarningCallback** = (`warning`) => `void` Callback invoked inline each time the parser emits a Tier-2 warning. Always fires BEFORE the warning is appended to `CcdaDocument.warnings` so consumers observe warnings in the same order the parser emitted them. That is emission order, and for one code it is deliberately not discovery order: `UNKNOWN_NAMESPACE_PREFIX` is found during the pre-parse DOM walk and replayed **last**, after the model is built. It is a statement about the whole document, and emitting it where it is found would let it take the place of the `NOT_A_CLINICAL_DOCUMENT` fatal, or of the first safety-critical per-element warning, under `{ strict: true }`, where the first warning is the one that throws. Nothing else is reordered. #### Parameters ##### warning [`CcdaWarning`](#ccdawarning) #### Returns `void` #### Example ```ts import { parseCcda, type OnWarningCallback } from "@cosyte/ccda"; const onWarning: OnWarningCallback = (w) => { console.warn(w.code, w.message); }; parseCcda(raw, { onWarning }); ``` *** ### PlannedActMood > **PlannedActMood** = `"INT"` \| `"RQO"` \| `"PRMS"` \| `"PRP"` \| `"APT"` \| `"ARQ"` The planned `@moodCode` for the **act / encounter / procedure** kinds, the Planned moodCode value set (`2.16.840.1.113883.11.20.9.23`): `INT` intent (default), `RQO` request/order, `PRMS` promise, `PRP` proposal, `APT` appointment, `ARQ` appointment request. The appointment moods (`APT`/`ARQ`) are valid **only** on these three element domains (`x_DocumentActMood` / `x_DocumentEncounterMood` / `x_DocumentProcedureMood`). `EVN` (a performed event) is deliberately not a member, the plan carries only future/ordered items, so a performed act can never be emitted into it. #### Example ```ts import type { PlannedActMood } from "@cosyte/ccda"; const appointment: PlannedActMood = "APT"; ``` *** ### PlannedItemKind > **PlannedItemKind** = `"act"` \| `"encounter"` \| `"procedure"` \| `"medicationActivity"` \| `"supply"` \| `"observation"` \| `"immunizationActivity"` Which planned-entry template a [PlannedItem](#planneditem) came from. Preserved so a consumer can tell a planned medication apart from a planned procedure without re-reading the DOM. #### Example ```ts import type { PlannedItemKind } from "@cosyte/ccda"; const k: PlannedItemKind = "procedure"; ``` *** ### PlannedOrderMood > **PlannedOrderMood** = `"INT"` \| `"RQO"` \| `"PRMS"` \| `"PRP"` The planned `@moodCode` for the **medication / supply / observation** kinds. The base CDA R2 mood domains for these elements, `x_DocumentSubstanceMood` (`substanceAdministration`/`supply`) and `x_ActMoodDocumentObservation` (`observation`), **exclude the appointment moods** (`APT`/`ARQ`), so those are not representable here (you cannot "appoint" a drug order or a lab). `EVN` is likewise excluded, the plan is future/ordered, never performed. #### Example ```ts import type { PlannedOrderMood } from "@cosyte/ccda"; const order: PlannedOrderMood = "RQO"; ``` *** ### ProblemStatus > **ProblemStatus** = [`ConcernStatus`](#concernstatus) Resolved active-vs-resolved state of a [ProblemConcern](#problemconcern). *** ### ProcedureDisposition > **ProcedureDisposition** = [`EventDisposition`](#eventdisposition) The performed-vs-planned disposition of a procedure, derived from its `@moodCode`: `EVN` → `"performed"`; a planned mood (`INT`/`RQO`/`PRMS`/`PRP`/ `APT`/`ARQ`) → `"planned"`. Absent when the mood is missing or unrecognized, never guessed, so a planned procedure is never read as performed. #### Example ```ts import type { ProcedureDisposition } from "@cosyte/ccda"; const d: ProcedureDisposition = "performed"; ``` *** ### ProcedureKind > **ProcedureKind** = `"procedure"` \| `"act"` \| `"observation"` Which of the three Procedure Activity templates an extracted procedure came from, `"procedure"` (altering/operative), `"act"` (non-altering service), or `"observation"` (assessment). Preserved so a consumer can tell an operative act apart from a diagnostic observation without re-reading the DOM. #### Example ```ts import type { ProcedureKind } from "@cosyte/ccda"; const k: ProcedureKind = "procedure"; ``` *** ### RequiredSectionEvaluation > **RequiredSectionEvaluation** = `"evaluated"` \| `"not-evaluated"` Whether a [RequiredSectionStatus](#requiredsectionstatus)'s key set was **computed for this document at all**, as opposed to computed and found empty. This is the axis [RequiredSectionVerification](#requiredsectionverification) deliberately does not carry. `verification` is a claim about the *type*: how much of that document type's obligation this package has read off the normative source, which no option moves. `evaluation` is a claim about the *lookup*: whether the version stamp supplied put the document inside the tables at all. - `evaluated`: the keys below are this type's obligation under the supplied stamp reading. An empty set means the type asserts none, and `verification` says which emptiness that is. - `not-evaluated`: **no obligation was computed.** The only route to it today is `{ stamp: "unmodeled-release" }`: the document names a C-CDA release this package has not read, so `keys` is empty because nothing was asked, not because nothing is required. Reducing the set instead would be a confident wrong statement about conformance, which is the failure this state exists to prevent. #### Example ```ts import { requiredSectionStatus, type RequiredSectionEvaluation } from "@cosyte/ccda"; const e: RequiredSectionEvaluation = requiredSectionStatus("ccd", { stamp: "unmodeled-release", }).evaluation; // "not-evaluated" ``` *** ### RequiredSectionVerification > **RequiredSectionVerification** = `"traced-complete"` \| `"traced-partial"` \| `"untraced"` \| `"not-applicable"` How much of a [DocumentType](#documenttype-4)'s required-section obligation this package has actually read off the normative C-CDA R2.1 base implementation guide (its `CONF:1198-` document-level conformance statements and the Schematron HL7 publishes in that release). The four states are exhaustive and mutually exclusive, so an **empty** key set is never ambiguous: the value says which emptiness it is. **A state is about the TYPE, and no option moves it.** The orthogonal question, whether an obligation was computed for the document in front of you at all, is [RequiredSectionEvaluation](#requiredsectionevaluation), which is where a stamp naming an unmodelled release shows up. Reading `traced-complete` beside an empty key set is not a contradiction; it means the type's obligation is fully read and this lookup did not evaluate it. - `traced-complete`: traced, and every SHALL section the source names for the type is asserted. - `traced-partial`: traced, and one or more named SHALL sections are not asserted. Each is named in [RequiredSectionStatus.unasserted](#unasserted) with the reason it is unassertable. - `untraced`: whatever keys the type asserts predate the trace, and no claim is made that they are complete. A citation recorded against a key does not promote the type out of this state, because a traced state is a claim about the source having been read for **that** type, not about provenance existing somewhere. **No recognized document type reports this today**: the value stays in the vocabulary because it is the honest state for a type whose obligation has not been read, and removing it from the union would be a breaking change that buys nothing. - `not-applicable`: the type structurally carries no sections, so it has no section obligation to trace. #### Example ```ts import { requiredSectionStatus, type RequiredSectionVerification } from "@cosyte/ccda"; const state: RequiredSectionVerification = requiredSectionStatus("progressNote").verification; // "traced-partial" ``` *** ### SectionEdit > **SectionEdit** = `SectionInput` & `object` One section add/replace operation: a SectionInput (the section `kind` plus its typed builder `content`) with an optional [SectionEditMode](#sectioneditmode) (default `"upsert"`). The union is discriminated on `kind`, so `content` is typed to exactly that section's `BuildCcda*` shape. #### Type Declaration ##### mode? > `readonly` `optional` **mode?**: [`SectionEditMode`](#sectioneditmode) #### Example ```ts import type { SectionEdit } from "@cosyte/ccda"; const addAProblem: SectionEdit = { kind: "problems", mode: "replace", content: [{ problem: { code: "38341003", displayName: "Hypertension" }, status: "active" }], }; ``` *** ### SectionEditMode > **SectionEditMode** = `"add"` \| `"replace"` \| `"upsert"` How a [SectionEdit](#sectionedit) reconciles with the section already in the document: `"add"` requires the section be **absent** (else [CcdaEditError](#ccdaediterror) `SECTION_ALREADY_PRESENT`), `"replace"` requires it be **present** (else `SECTION_ABSENT`), and `"upsert"` (the default) replaces it when present and adds it when absent. #### Example ```ts import type { SectionEditMode } from "@cosyte/ccda"; const mode: SectionEditMode = "add"; ``` *** ### StatusDomain > **StatusDomain** = `"functional"` \| `"mental"` Which status section a [StatusObservation](#statusobservation) came from, `"functional"` (ADLs, mobility, self-care) or `"mental"` (cognition, mood). Preserved so a consumer can tell the two apart without re-reading the DOM; the two are never conflated. #### Example ```ts import type { StatusDomain } from "@cosyte/ccda"; const d: StatusDomain = "functional"; ``` *** ### TemplateStampReading > **TemplateStampReading** = `"r21-stamped"` \| `"unstamped"` \| `"unmodeled-release"` The three readings a document-level `templateId`'s version stamp can take, and the reason a boolean cannot carry them. A `boolean` says "R2.1-stamped or not", which reads a document from the future as one from the past: it is the distinction between *no stamp at all* (an R1.1-origin document, whose SHALL obligations this package deliberately does not narrow) and *a stamp this package does not model* (a later release, whose obligations this package has not read and therefore must not compute). - `r21-stamped`: the R2.1 stamp is present, so the R2.1-scoped tables apply. - `unstamped`: no `@extension` at all; the pre-R2.1 reading, unchanged. - `unmodeled-release`: an `@extension` that is not the R2.1 stamp, whether or not it is a member of [CCDA\_RELEASE\_STAMPS](#ccda_release_stamps). This package has not read that release's obligations and reports them unevaluated rather than reducing them. #### Example ```ts import type { TemplateStampReading } from "@cosyte/ccda"; const reading: TemplateStampReading = "unmodeled-release"; ``` *** ### UnassertedSectionReason > **UnassertedSectionReason** = `"outside-section-catalog"` \| `"not-unconditionally-required"` \| `"assertion-would-tighten-parse"` Why a SHALL section the source names is **not** asserted. Three reasons exist. The first two are facts about the parser or the source, and neither is a judgement call; the third is a decision, and says so: - `outside-section-catalog`: this parser does not recognize the section at all, so it can neither find it nor honestly report it missing. - `not-unconditionally-required`: the source does not require it unconditionally (a choice such as `SHALL contain A or B`, or a rule context conditioned on something other than the R2.1 `@extension` stamp), and asserting a conditional requirement as unconditional mis-flags conformant documents. - `assertion-would-tighten-parse`: the parser recognizes the section when a document carries it, and the source requires it unconditionally, but asserting it would newly report `REQUIRED_SECTION_MISSING` on documents this parser reads without that warning today (and, under `strict: true`, refuse them). Making the parser stricter is its own change with its own blast radius, so it is not taken as a side effect of recognizing a section. One section carries this reason today: the Discharge Summary's Hospital Course Section. #### Example ```ts import { requiredSectionStatus, type UnassertedSectionReason } from "@cosyte/ccda"; const why: UnassertedSectionReason | undefined = requiredSectionStatus("operativeNote").unasserted[0]?.reason; // "outside-section-catalog" ``` *** ### ValueSetBindingStrength > **ValueSetBindingStrength** = `"required"` \| `"preferred"` How strongly C-CDA binds a value set to a coded slot, in the vocabulary the standard's own general guidance defines: a SHALL binding is `required`, a SHOULD or MAY binding is `preferred`. The distinction is the difference between a conformance failure and a suggestion, which is why it is declared data here rather than assumed: only a `required` row can produce `VALUE_SET_BINDING_VIOLATED`. #### Example ```ts import { valueSetBinding, type ValueSetBindingStrength } from "@cosyte/ccda"; const strength: ValueSetBindingStrength = valueSetBinding("medication").strength; // "required" ``` *** ### WarningCode > **WarningCode** = *typeof* [`WARNING_CODES`](#warning_codes)\[keyof *typeof* [`WARNING_CODES`](#warning_codes)\] Discriminant type for `CcdaWarning.code`. Narrowing a warning by this code lets consumers write exhaustive `switch` blocks (enabled by the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free comparison against the `WARNING_CODES` registry. #### Example ```ts import type { CcdaWarning, WarningCode } from "@cosyte/ccda"; function describe(w: CcdaWarning): string { const code: WarningCode = w.code; switch (code) { case "UNKNOWN_DOCUMENT_TEMPLATE": return "unrecognized document type"; default: return `warning: ${code}`; } } ``` ## Variables ### CCDA\_CONFORMANCE\_RELEASE > `const` **CCDA\_CONFORMANCE\_RELEASE**: [`CcdaRelease`](#ccdarelease) = `"R2.1"` The C-CDA release this package's conformance tables target: the required-section (SHALL) tables, the recognition catalogs and the version stamp every one of them is written against. **This is the answer to "which release does `@cosyte/ccda` validate against", as a value rather than a sentence in a README.** It does not move when a later release is *recognized*: recognizing a stamp is knowing which guide a document was written for, which is not the same as reading it correctly against that guide. #### Example ```ts import { CCDA_CONFORMANCE_RELEASE } from "@cosyte/ccda"; CCDA_CONFORMANCE_RELEASE; // "R2.1" ``` *** ### CCDA\_RELEASE\_STAMPS > `const` **CCDA\_RELEASE\_STAMPS**: readonly [`CcdaReleaseStamp`](#ccdareleasestamp)[] **The closed, package-owned table of C-CDA document-template version stamps.** Two entries today: R2.1's `2015-08-01` and the `2024-05-01` stamp C-CDA introduced at Release 3.0.0 and carried through 4.0.0 and 5.0.0. Every stamp a diagnostic here can report comes from this table. The document's own `@extension` is compared against it and is never echoed: a value that is not a member yields a message that names no stamp at all, which is the same membership discipline `NULL_FLAVORS` and the section catalog use. #### Example ```ts import { CCDA_RELEASE_STAMPS } from "@cosyte/ccda"; CCDA_RELEASE_STAMPS.map((e) => e.stamp); // ["2015-08-01", "2024-05-01"] ``` *** ### CCDA\_RELEASES > `const` **CCDA\_RELEASES**: readonly \[`"R2.1"`, `"R3.0 or later"`\] The C-CDA releases this package can name from a document-template version stamp. **A closed set of literals this package owns**, exactly like [DOCUMENT\_TYPES](#document_types): a diagnostic naming a release names a member of this list, never a value copied out of the document being parsed. `"R3.0 or later"` is one member rather than three because the stamp cannot tell those releases apart: 3.0.0 restamped every document template and 4.0.0 and 5.0.0 left that stamp alone, so a document carrying it may have been written for any of them. Splitting the member would be a claim the stamp does not support. #### Example ```ts import { CCDA_RELEASES } from "@cosyte/ccda"; CCDA_RELEASES.includes("R2.1"); // true ``` *** ### ccdaProfiles > `const` **ccdaProfiles**: `object` Namespace object exposing the built-in profiles: the conservative `default` baseline plus the two evidence-backed conformance profiles (`smartScorecard`, `legacyR11`), each authored via the public `defineCcdaProfile()` API and carrying its cited public provenance. #### Type Declaration ##### default > `readonly` **default**: [`CcdaProfile`](#ccdaprofile) ##### legacyR11 > `readonly` **legacyR11**: [`CcdaProfile`](#ccdaprofile) ##### smartScorecard > `readonly` **smartScorecard**: [`CcdaProfile`](#ccdaprofile) #### Example ```ts import { parseCcda, ccdaProfiles } from "@cosyte/ccda"; const doc = parseCcda(raw, { profile: ccdaProfiles.smartScorecard }); console.log(doc.profile?.name); // "smartScorecard" ``` *** ### CDA\_DOCUMENT\_OID > `const` **CDA\_DOCUMENT\_OID**: `"2.16.840.1.113883.10.20.22.1.1"` = `"2.16.840.1.113883.10.20.22.1.1"` **`Internal`** The CDA R2 ClinicalDocument root class code (`POCD_MT000040`). *** ### CVX > `const` **CVX**: `"2.16.840.1.113883.12.292"` = `"2.16.840.1.113883.12.292"` CVX, CDC vaccine administered code system (immunizations). *** ### DEFAULT\_LIMITS > `const` **DEFAULT\_LIMITS**: [`ResolvedLimits`](#resolvedlimits) Library-default safety caps applied before DOM construction. Tuned to admit real C-CDA documents (which can embed sizeable base64 images) while still bounding hostile input. Callers tighten or loosen via `ParseCcdaOptions.limits`. #### Example ```ts import { DEFAULT_LIMITS } from "@cosyte/ccda"; console.log(DEFAULT_LIMITS.maxInputBytes); // 30000000 ``` *** ### DOCUMENT\_TYPES > `const` **DOCUMENT\_TYPES**: readonly [`DocumentType`](#documenttype-4)[] Every recognized [DocumentType](#documenttype-4), in the recognition table's own order. The runtime enumeration behind the type union: a consumer can walk all twelve without hand-writing the list (and without it going stale when a thirteenth type is recognized). #### Example ```ts import { DOCUMENT_TYPES } from "@cosyte/ccda"; DOCUMENT_TYPES.length; // 12 DOCUMENT_TYPES.includes("carePlan"); // true ``` *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable string codes for every Tier-3 fatal the parser may throw. Consumers narrow on `err.code` to react to specific structural or safety failures. Each code is its own value (`key === value`) so the set survives `Object.values(...)` into a snapshot tripwire. Renaming a code is a **breaking change**. #### Type Declaration ##### ELEMENT\_DEPTH\_LIMIT\_EXCEEDED > `readonly` **ELEMENT\_DEPTH\_LIMIT\_EXCEEDED**: `"ELEMENT_DEPTH_LIMIT_EXCEEDED"` = `"ELEMENT_DEPTH_LIMIT_EXCEEDED"` ##### ENTITY\_EXPANSION\_LIMIT > `readonly` **ENTITY\_EXPANSION\_LIMIT**: `"ENTITY_EXPANSION_LIMIT"` = `"ENTITY_EXPANSION_LIMIT"` ##### INPUT\_SIZE\_LIMIT\_EXCEEDED > `readonly` **INPUT\_SIZE\_LIMIT\_EXCEEDED**: `"INPUT_SIZE_LIMIT_EXCEEDED"` = `"INPUT_SIZE_LIMIT_EXCEEDED"` ##### NODE\_COUNT\_LIMIT\_EXCEEDED > `readonly` **NODE\_COUNT\_LIMIT\_EXCEEDED**: `"NODE_COUNT_LIMIT_EXCEEDED"` = `"NODE_COUNT_LIMIT_EXCEEDED"` ##### NOT\_A\_CLINICAL\_DOCUMENT > `readonly` **NOT\_A\_CLINICAL\_DOCUMENT**: `"NOT_A_CLINICAL_DOCUMENT"` = `"NOT_A_CLINICAL_DOCUMENT"` ##### NOT\_WELL\_FORMED\_XML > `readonly` **NOT\_WELL\_FORMED\_XML**: `"NOT_WELL_FORMED_XML"` = `"NOT_WELL_FORMED_XML"` ##### XXE\_OR\_DTD\_PRESENT > `readonly` **XXE\_OR\_DTD\_PRESENT**: `"XXE_OR_DTD_PRESENT"` = `"XXE_OR_DTD_PRESENT"` #### Example ```ts import { parseCcda, FATAL_CODES, CcdaParseError } from "@cosyte/ccda"; try { parseCcda(hostileXml); } catch (err) { if (err instanceof CcdaParseError && err.code === FATAL_CODES.XXE_OR_DTD_PRESENT) { // reject the document, it declared a DTD / external entity } } ``` *** ### ICD10\_CM > `const` **ICD10\_CM**: `"2.16.840.1.113883.6.90"` = `"2.16.840.1.113883.6.90"` ICD-10-CM, US diagnosis coding. *** ### ICD10\_PCS > `const` **ICD10\_PCS**: `"2.16.840.1.113883.6.4"` = `"2.16.840.1.113883.6.4"` ICD-10-PCS, US inpatient procedure coding. *** ### ICD9\_CM\_DX > `const` **ICD9\_CM\_DX**: `"2.16.840.1.113883.6.103"` = `"2.16.840.1.113883.6.103"` ICD-9-CM diagnosis, **deprecated** in US contexts (replaced by ICD-10-CM). *** ### ICD9\_CM\_PROC > `const` **ICD9\_CM\_PROC**: `"2.16.840.1.113883.6.104"` = `"2.16.840.1.113883.6.104"` ICD-9-CM procedure, **deprecated** (replaced by ICD-10-PCS). *** ### INTERPRETATION > `const` **INTERPRETATION**: `"2.16.840.1.113883.5.83"` = `"2.16.840.1.113883.5.83"` HL7 ObservationInterpretation, the `interpretationCode` value set source. *** ### LOINC > `const` **LOINC**: `"2.16.840.1.113883.6.1"` = `"2.16.840.1.113883.6.1"` LOINC, observations, lab tests, section/document codes. *** ### NCI\_ROUTE > `const` **NCI\_ROUTE**: `"2.16.840.1.113883.3.26.1.1"` = `"2.16.840.1.113883.3.26.1.1"` NCI Thesaurus, the C-CDA `routeCode` value set source. *** ### NDC > `const` **NDC**: `"2.16.840.1.113883.6.69"` = `"2.16.840.1.113883.6.69"` NDC, National Drug Code (packaged products). *** ### NULL\_FLAVORS > `const` **NULL\_FLAVORS**: readonly \[`"NI"`, `"INV"`, `"DER"`, `"OTH"`, `"NINF"`, `"PINF"`, `"UNC"`, `"MSK"`, `"NA"`, `"UNK"`, `"ASKU"`, `"NAV"`, `"NASK"`, `"NAVU"`, `"QS"`, `"TRC"`, `"NP"`\] The HL7 v3 NullFlavor code system (`2.16.840.1.113883.5.1008`), which CDA R2 binds `@nullFlavor` to. All seventeen concepts, transcribed from the published HL7 Terminology `CodeSystem/v3-NullFlavor` (`content: complete`, `caseSensitive: true`, code-system version `4.0.0`, THO release 7.3.0): `NI`, `INV`, `DER`, `OTH`, `NINF`, `PINF`, `UNC`, `MSK`, `NA`, `UNK`, `ASKU`, `NAV`, `NASK`, `NAVU`, `QS`, `TRC`, `NP`. **It is the whole code system, deliberately, and it used to be eight of the seventeen.** A conforming `nullFlavor="PINF"` on a `PQ`, or the `nullFlavor="NP"` a real Plan of Treatment carries on a ``, drew a false `INVALID_NULL_FLAVOR` on the published `0.0.4`, because the list held only the eight tokens the header and the common datatypes use. Membership here is also the bound that decides whether a `templateId`'s or an `ED`'s `nullFlavor` is echoed or `` (`./ii.ts`, `./ed.ts`), and widening does not weaken it: every entry is a fixed literal this package owns, so the bound is still "a member of a closed set", now the same closed set the standard defines. `NP` carries `status: retired` in THO. It is admitted anyway, because it **is** a concept of the code system and `INVALID_NULL_FLAVOR` says a token is not: saying so about a real code is the false positive, and this package has no deprecation signal for `nullFlavor` to say anything narrower with. #### Example ```ts import { NULL_FLAVORS } from "@cosyte/ccda"; console.log(NULL_FLAVORS.includes("UNK")); // true ``` *** ### R21\_EXTENSION > `const` **R21\_EXTENSION**: `"2015-08-01"` = `"2015-08-01"` The C-CDA R2.1 version stamp carried in a recognized `templateId/@extension`. *** ### R30\_EXTENSION > `const` **R30\_EXTENSION**: `"2024-05-01"` = `"2024-05-01"` The document-template version stamp C-CDA introduced at Release 3.0.0, when "all document template ids received a new extension", and which Releases 4.0.0 and 5.0.0 kept: the published guide's own US Realm Header and CCD StructureDefinitions both pattern `@extension` to this value on the roots this package recognizes. **Recognized, not targeted.** This package's conformance tables are C-CDA R2.1 ([CCDA\_CONFORMANCE\_RELEASE](#ccda_conformance_release)) and this constant does not move them. It exists so a document written for a later release is *named* accurately rather than mistaken for one that pre-dates R2.1. #### Example ```ts import { R30_EXTENSION } from "@cosyte/ccda"; R30_EXTENSION; // "2024-05-01" ``` *** ### RXNORM > `const` **RXNORM**: `"2.16.840.1.113883.6.88"` = `"2.16.840.1.113883.6.88"` RxNorm, clinical drugs and ingredients. *** ### SAFETY\_CRITICAL\_CODES > `const` **SAFETY\_CRITICAL\_CODES**: `ReadonlySet`\<[`WarningCode`](#warningcode)\> Warning codes no profile may list in its `tolerate` set. Genuinely immutable: it exposes only the read half of the `Set` surface and is frozen, so a safety-critical code cannot be deleted at runtime to smuggle it past the gate. (`Object.freeze(new Set(...))` would **not** achieve this, `delete` mutates an internal slot a freeze does not reach.) #### Example ```ts import { SAFETY_CRITICAL_CODES } from "@cosyte/ccda"; console.log(SAFETY_CRITICAL_CODES.has("MISSING_DOSE_QUANTITY")); // true ``` *** ### SDTC\_NS > `const` **SDTC\_NS**: `"urn:hl7-org:sdtc"` = `"urn:hl7-org:sdtc"` The HL7 standards-development-organization extension namespace (`sdtc`), used for attributes/elements the base CDA schema does not define but C-CDA permits (e.g. `sdtc:raceCode`, `sdtc:deceasedInd`). #### Example ```ts import { SDTC_NS } from "@cosyte/ccda"; console.log(SDTC_NS); // "urn:hl7-org:sdtc" ``` *** ### SNOMED\_CT > `const` **SNOMED\_CT**: `"2.16.840.1.113883.6.96"` = `"2.16.840.1.113883.6.96"` SNOMED CT, clinical findings, problems, allergen substances. *** ### SYNTHETIC\_SETID\_PREFIX > `const` **SYNTHETIC\_SETID\_PREFIX**: `"SYNTHETIC-SETID"` = `"SYNTHETIC-SETID"` The `@extension` prefix every `setId` [editCcda](#editccda) **mints** carries, so a minted version-series identifier is recognisable as one on sight and in code. A minted `setId` looks like `SYNTHETIC-SETID-e1` under the synthetic assigning-authority root `2.16.840.1.113883.19.5.99999` (an OID in HL7's own example arc, which is not an organisation's real namespace). Together those two are the whole scheme; [isSyntheticSetId](#issyntheticsetid) is the check. **A `setId` the source already carried, or one the caller supplied through `revision.setId`, is never relabelled.** The prefix marks what this library invented, and only that. **The residual, stated rather than implied: nothing forces a receiving system to read the label.** A receiver that ignores both the prefix and the root will treat a minted `setId` exactly as it treats a real one, and this library cannot make it do otherwise. Making it obvious is the whole of what a label can do; it does not make the identifier true. The alternative considered and rejected was to stop minting, which would emit a replacement whose `setId` does not match its own `parentDocument`'s, and CDA R2 requires that it does. #### Example ```ts import { SYNTHETIC_SETID_PREFIX } from "@cosyte/ccda"; console.log(SYNTHETIC_SETID_PREFIX); // "SYNTHETIC-SETID" ``` *** ### UNII > `const` **UNII**: `"2.16.840.1.113883.4.9"` = `"2.16.840.1.113883.4.9"` UNII, FDA Unique Ingredient Identifier (substances/allergens). *** ### V3\_NS > `const` **V3\_NS**: `"urn:hl7-org:v3"` = `"urn:hl7-org:v3"` The HL7 v3 / CDA R2 default namespace. Every structural C-CDA element (`ClinicalDocument`, `recordTarget`, `section`, …) lives here. #### Example ```ts import { V3_NS } from "@cosyte/ccda"; console.log(V3_NS); // "urn:hl7-org:v3" ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, synced with `package.json#version` at build time. #### Example ```ts import { VERSION } from "@cosyte/ccda"; console.log(VERSION); ``` *** ### WARNING\_CODES > `const` **WARNING\_CODES**: `object` Stable string codes for every Tier-2 warning the parser may emit. The registry is frozen via `as const` so TypeScript infers the exact string literal union for `WarningCode`, there is zero runtime cost and no magic-string comparisons for consumers. Each code is its own value (`key === value`) so the set survives `Object.values(...)` into a snapshot tripwire. Renaming a code is a **breaking change**. #### Type Declaration ##### ALLERGEN\_GRANULARITY\_SUSPECT > `readonly` **ALLERGEN\_GRANULARITY\_SUSPECT**: `"ALLERGEN_GRANULARITY_SUSPECT"` = `"ALLERGEN_GRANULARITY_SUSPECT"` ##### CODE\_NARRATIVE\_MISMATCH > `readonly` **CODE\_NARRATIVE\_MISMATCH**: `"CODE_NARRATIVE_MISMATCH"` = `"CODE_NARRATIVE_MISMATCH"` ##### CONTRADICTORY\_NULL\_FLAVOR > `readonly` **CONTRADICTORY\_NULL\_FLAVOR**: `"CONTRADICTORY_NULL_FLAVOR"` = `"CONTRADICTORY_NULL_FLAVOR"` ##### DEPRECATED\_CODE\_SYSTEM > `readonly` **DEPRECATED\_CODE\_SYSTEM**: `"DEPRECATED_CODE_SYSTEM"` = `"DEPRECATED_CODE_SYSTEM"` ##### DEPRECATED\_LOINC > `readonly` **DEPRECATED\_LOINC**: `"DEPRECATED_LOINC"` = `"DEPRECATED_LOINC"` ##### ENCODING\_BOM\_STRIPPED > `readonly` **ENCODING\_BOM\_STRIPPED**: `"ENCODING_BOM_STRIPPED"` = `"ENCODING_BOM_STRIPPED"` ##### FREE\_TEXT\_REFERENCE\_RANGE > `readonly` **FREE\_TEXT\_REFERENCE\_RANGE**: `"FREE_TEXT_REFERENCE_RANGE"` = `"FREE_TEXT_REFERENCE_RANGE"` ##### IMMUNIZATION\_REFUSED > `readonly` **IMMUNIZATION\_REFUSED**: `"IMMUNIZATION_REFUSED"` = `"IMMUNIZATION_REFUSED"` ##### INVALID\_NULL\_FLAVOR > `readonly` **INVALID\_NULL\_FLAVOR**: `"INVALID_NULL_FLAVOR"` = `"INVALID_NULL_FLAVOR"` ##### MALFORMED\_DATETIME > `readonly` **MALFORMED\_DATETIME**: `"MALFORMED_DATETIME"` = `"MALFORMED_DATETIME"` ##### MEDICATION\_PRODUCT\_ARM\_CONFLICT > `readonly` **MEDICATION\_PRODUCT\_ARM\_CONFLICT**: `"MEDICATION_PRODUCT_ARM_CONFLICT"` = `"MEDICATION_PRODUCT_ARM_CONFLICT"` ##### MEDICATION\_PRODUCT\_ARM\_REPEATED > `readonly` **MEDICATION\_PRODUCT\_ARM\_REPEATED**: `"MEDICATION_PRODUCT_ARM_REPEATED"` = `"MEDICATION_PRODUCT_ARM_REPEATED"` ##### MEDICATION\_PRODUCT\_ARM\_UNEXPECTED > `readonly` **MEDICATION\_PRODUCT\_ARM\_UNEXPECTED**: `"MEDICATION_PRODUCT_ARM_UNEXPECTED"` = `"MEDICATION_PRODUCT_ARM_UNEXPECTED"` ##### MEDICATION\_PRODUCT\_CODE\_REPEATED > `readonly` **MEDICATION\_PRODUCT\_CODE\_REPEATED**: `"MEDICATION_PRODUCT_CODE_REPEATED"` = `"MEDICATION_PRODUCT_CODE_REPEATED"` ##### MEDICATION\_PRODUCT\_CODE\_TRANSLATION\_ONLY > `readonly` **MEDICATION\_PRODUCT\_CODE\_TRANSLATION\_ONLY**: `"MEDICATION_PRODUCT_CODE_TRANSLATION_ONLY"` = `"MEDICATION_PRODUCT_CODE_TRANSLATION_ONLY"` ##### MISSING\_ASSIGNING\_AUTHORITY > `readonly` **MISSING\_ASSIGNING\_AUTHORITY**: `"MISSING_ASSIGNING_AUTHORITY"` = `"MISSING_ASSIGNING_AUTHORITY"` ##### MISSING\_CODE\_SYSTEM > `readonly` **MISSING\_CODE\_SYSTEM**: `"MISSING_CODE_SYSTEM"` = `"MISSING_CODE_SYSTEM"` ##### MISSING\_CODE\_VALUE > `readonly` **MISSING\_CODE\_VALUE**: `"MISSING_CODE_VALUE"` = `"MISSING_CODE_VALUE"` ##### MISSING\_DOSE\_QUANTITY > `readonly` **MISSING\_DOSE\_QUANTITY**: `"MISSING_DOSE_QUANTITY"` = `"MISSING_DOSE_QUANTITY"` ##### MISSING\_PLANNED\_MEDICATION\_EFFECTIVE\_TIME > `readonly` **MISSING\_PLANNED\_MEDICATION\_EFFECTIVE\_TIME**: `"MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME"` = `"MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME"` ##### MISSING\_PRODUCT\_CODE > `readonly` **MISSING\_PRODUCT\_CODE**: `"MISSING_PRODUCT_CODE"` = `"MISSING_PRODUCT_CODE"` ##### MISSING\_ROUTE\_CODE > `readonly` **MISSING\_ROUTE\_CODE**: `"MISSING_ROUTE_CODE"` = `"MISSING_ROUTE_CODE"` ##### MISSING\_SELF\_CARE\_ACTIVITY > `readonly` **MISSING\_SELF\_CARE\_ACTIVITY**: `"MISSING_SELF_CARE_ACTIVITY"` = `"MISSING_SELF_CARE_ACTIVITY"` ##### MISSING\_TEMPLATE\_ID > `readonly` **MISSING\_TEMPLATE\_ID**: `"MISSING_TEMPLATE_ID"` = `"MISSING_TEMPLATE_ID"` ##### MISSING\_UNIT\_ON\_PQ > `readonly` **MISSING\_UNIT\_ON\_PQ**: `"MISSING_UNIT_ON_PQ"` = `"MISSING_UNIT_ON_PQ"` ##### MULTIPLE\_EFFECTIVE\_TIMES\_UNRESOLVED > `readonly` **MULTIPLE\_EFFECTIVE\_TIMES\_UNRESOLVED**: `"MULTIPLE_EFFECTIVE_TIMES_UNRESOLVED"` = `"MULTIPLE_EFFECTIVE_TIMES_UNRESOLVED"` ##### MULTIPLE\_RECORD\_TARGETS > `readonly` **MULTIPLE\_RECORD\_TARGETS**: `"MULTIPLE_RECORD_TARGETS"` = `"MULTIPLE_RECORD_TARGETS"` ##### NARRATIVE\_REFERENCE\_BROKEN > `readonly` **NARRATIVE\_REFERENCE\_BROKEN**: `"NARRATIVE_REFERENCE_BROKEN"` = `"NARRATIVE_REFERENCE_BROKEN"` ##### NEGATION\_VS\_NULLFLAVOR\_AMBIGUOUS > `readonly` **NEGATION\_VS\_NULLFLAVOR\_AMBIGUOUS**: `"NEGATION_VS_NULLFLAVOR_AMBIGUOUS"` = `"NEGATION_VS_NULLFLAVOR_AMBIGUOUS"` ##### NON\_UCUM\_UNIT > `readonly` **NON\_UCUM\_UNIT**: `"NON_UCUM_UNIT"` = `"NON_UCUM_UNIT"` ##### PLAN\_ENTRY\_NOT\_MODELED > `readonly` **PLAN\_ENTRY\_NOT\_MODELED**: `"PLAN_ENTRY_NOT_MODELED"` = `"PLAN_ENTRY_NOT_MODELED"` ##### PLANNED\_VS\_PERFORMED\_AMBIGUOUS > `readonly` **PLANNED\_VS\_PERFORMED\_AMBIGUOUS**: `"PLANNED_VS_PERFORMED_AMBIGUOUS"` = `"PLANNED_VS_PERFORMED_AMBIGUOUS"` ##### PROBLEM\_STATUS\_INDETERMINATE > `readonly` **PROBLEM\_STATUS\_INDETERMINATE**: `"PROBLEM_STATUS_INDETERMINATE"` = `"PROBLEM_STATUS_INDETERMINATE"` ##### PROCEDURE\_MOOD\_UNEXPECTED > `readonly` **PROCEDURE\_MOOD\_UNEXPECTED**: `"PROCEDURE_MOOD_UNEXPECTED"` = `"PROCEDURE_MOOD_UNEXPECTED"` ##### PROFILE\_QUIRK\_APPLIED > `readonly` **PROFILE\_QUIRK\_APPLIED**: `"PROFILE_QUIRK_APPLIED"` = `"PROFILE_QUIRK_APPLIED"` ##### REQUIRED\_SECTION\_MISSING > `readonly` **REQUIRED\_SECTION\_MISSING**: `"REQUIRED_SECTION_MISSING"` = `"REQUIRED_SECTION_MISSING"` ##### REQUIRED\_SECTIONS\_NOT\_EVALUATED > `readonly` **REQUIRED\_SECTIONS\_NOT\_EVALUATED**: `"REQUIRED_SECTIONS_NOT_EVALUATED"` = `"REQUIRED_SECTIONS_NOT_EVALUATED"` ##### RESULT\_VALUE\_TYPE\_UNHANDLED > `readonly` **RESULT\_VALUE\_TYPE\_UNHANDLED**: `"RESULT_VALUE_TYPE_UNHANDLED"` = `"RESULT_VALUE_TYPE_UNHANDLED"` ##### SECTION\_MATCHED\_BY\_LOINC\_FALLBACK > `readonly` **SECTION\_MATCHED\_BY\_LOINC\_FALLBACK**: `"SECTION_MATCHED_BY_LOINC_FALLBACK"` = `"SECTION_MATCHED_BY_LOINC_FALLBACK"` ##### SECTION\_PLACEMENT\_SUSPECT > `readonly` **SECTION\_PLACEMENT\_SUSPECT**: `"SECTION_PLACEMENT_SUSPECT"` = `"SECTION_PLACEMENT_SUSPECT"` ##### SEMANTIC\_CODE\_INVALID > `readonly` **SEMANTIC\_CODE\_INVALID**: `"SEMANTIC_CODE_INVALID"` = `"SEMANTIC_CODE_INVALID"` ##### SMOKING\_STATUS\_CODE\_UNRECOGNIZED > `readonly` **SMOKING\_STATUS\_CODE\_UNRECOGNIZED**: `"SMOKING_STATUS_CODE_UNRECOGNIZED"` = `"SMOKING_STATUS_CODE_UNRECOGNIZED"` ##### SMOKING\_STATUS\_UNKNOWN > `readonly` **SMOKING\_STATUS\_UNKNOWN**: `"SMOKING_STATUS_UNKNOWN"` = `"SMOKING_STATUS_UNKNOWN"` ##### SUBJECT\_CONTEXT\_OVERRIDE > `readonly` **SUBJECT\_CONTEXT\_OVERRIDE**: `"SUBJECT_CONTEXT_OVERRIDE"` = `"SUBJECT_CONTEXT_OVERRIDE"` ##### TEMPLATE\_EXTENSION\_ABSENT > `readonly` **TEMPLATE\_EXTENSION\_ABSENT**: `"TEMPLATE_EXTENSION_ABSENT"` = `"TEMPLATE_EXTENSION_ABSENT"` ##### TEMPLATE\_EXTENSION\_UNMODELED\_RELEASE > `readonly` **TEMPLATE\_EXTENSION\_UNMODELED\_RELEASE**: `"TEMPLATE_EXTENSION_UNMODELED_RELEASE"` = `"TEMPLATE_EXTENSION_UNMODELED_RELEASE"` ##### UCUM\_CASE\_SUSPECT > `readonly` **UCUM\_CASE\_SUSPECT**: `"UCUM_CASE_SUSPECT"` = `"UCUM_CASE_SUSPECT"` ##### UNEXPECTED\_CODE\_SYSTEM > `readonly` **UNEXPECTED\_CODE\_SYSTEM**: `"UNEXPECTED_CODE_SYSTEM"` = `"UNEXPECTED_CODE_SYSTEM"` ##### UNIDENTIFIED\_AUTHOR > `readonly` **UNIDENTIFIED\_AUTHOR**: `"UNIDENTIFIED_AUTHOR"` = `"UNIDENTIFIED_AUTHOR"` ##### UNKNOWN\_DOCUMENT\_TEMPLATE > `readonly` **UNKNOWN\_DOCUMENT\_TEMPLATE**: `"UNKNOWN_DOCUMENT_TEMPLATE"` = `"UNKNOWN_DOCUMENT_TEMPLATE"` ##### UNKNOWN\_NAMESPACE\_PREFIX > `readonly` **UNKNOWN\_NAMESPACE\_PREFIX**: `"UNKNOWN_NAMESPACE_PREFIX"` = `"UNKNOWN_NAMESPACE_PREFIX"` ##### UNKNOWN\_SECTION\_CODE > `readonly` **UNKNOWN\_SECTION\_CODE**: `"UNKNOWN_SECTION_CODE"` = `"UNKNOWN_SECTION_CODE"` ##### VALUE\_SET\_BINDING\_NOT\_EVALUATED > `readonly` **VALUE\_SET\_BINDING\_NOT\_EVALUATED**: `"VALUE_SET_BINDING_NOT_EVALUATED"` = `"VALUE_SET_BINDING_NOT_EVALUATED"` ##### VALUE\_SET\_BINDING\_VIOLATED > `readonly` **VALUE\_SET\_BINDING\_VIOLATED**: `"VALUE_SET_BINDING_VIOLATED"` = `"VALUE_SET_BINDING_VIOLATED"` #### Example ```ts import { parseCcda, WARNING_CODES } from "@cosyte/ccda"; const doc = parseCcda(raw); if (doc.warnings.some((w) => w.code === WARNING_CODES.UNKNOWN_DOCUMENT_TEMPLATE)) { // handle an unrecognized document type } ``` *** ### XSI\_NS > `const` **XSI\_NS**: `"http://www.w3.org/2001/XMLSchema-instance"` = `"http://www.w3.org/2001/XMLSchema-instance"` The XML Schema Instance namespace. Carries the `xsi:type` attribute that selects a concrete HL7 v3 datatype for a polymorphic element (e.g. `value xsi:type="PQ"`). #### Example ```ts import { XSI_NS } from "@cosyte/ccda"; console.log(XSI_NS); // "http://www.w3.org/2001/XMLSchema-instance" ``` ## Functions ### applyProfile() > **applyProfile**(`profile`, `warning`): [`CcdaWarning`](#ccdawarning) Apply a profile to a single warning. Returns a downgraded `PROFILE_QUIRK_APPLIED` warning when the profile expects this deviation; otherwise returns the original warning unchanged (referential identity preserved, so an un-tolerated warning is never reallocated). A warning that is already `expected` (e.g. re-processed) is passed through untouched. #### Parameters ##### profile [`CcdaProfile`](#ccdaprofile) ##### warning [`CcdaWarning`](#ccdawarning) #### Returns [`CcdaWarning`](#ccdawarning) #### Example ```ts import { applyProfile, ccdaProfiles, parseCcda } from "@cosyte/ccda"; const w = parseCcda(raw).warnings[0]!; const out = applyProfile(ccdaProfiles.smartScorecard, w); console.log(out.code); // "PROFILE_QUIRK_APPLIED" console.log(out.toleratedCode); // "DEPRECATED_LOINC" ``` *** ### attr() > **attr**(`el`, `name`): `string` \| `undefined` Read an unprefixed attribute, returning `undefined` (never `null` or `""`) when the attribute is absent or empty, matches the `exactOptionalPropertyTypes` "omit, don't set undefined" discipline used throughout the model. #### Parameters ##### el `Element` ##### name `string` #### Returns `string` \| `undefined` #### Example ```ts import { attr } from "@cosyte/ccda"; const root = attr(el, "root"); // string | undefined ``` *** ### buildCcda() > **buildCcda**(`init`, `options?`): [`CcdaDocument`](#ccdadocument) Build a spec-clean C-CDA R2.1 document from structured input and return the parsed [CcdaDocument](#ccdadocument). Emits a **CCD** by default, a **Referral Note** when `documentType: "referralNote"`, or a **Discharge Summary** when `documentType: "dischargeSummary"`, each with its own US Realm Header specialization (document `templateId` + LOINC `code`) and SHALL section set. The emitted document round-trips through [parseCcda](#parseccda) by construction (see the module doc); a clean build carries zero warnings. #### Parameters ##### init [`BuildCcdaInit`](#buildccdainit) The document content; see [BuildCcdaInit](#buildccdainit). `patient` is required. ##### options? [`BuildCcdaOptions`](#buildccdaoptions) = `{}` Optional [BuildCcdaOptions](#buildccdaoptions); pass a bring-your-own `terminology` adapter to have the returned document flag adapter-rejected codes (`SEMANTIC_CODE_INVALID`) and to emit `` alternate codings from its optional `translate`. The builder never coerces a code to satisfy the adapter, every primary value is emitted verbatim and a `` is only ever an additional alternate coding. #### Returns [`CcdaDocument`](#ccdadocument) The parsed document, the parse of the spec-clean XML just emitted, carrying the re-parse's warnings plus any build-time diagnostic about the input (today: `MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME`, appended last). #### Throws When `documentType` is anything other than a [BuildableDocumentType](#buildabledocumenttype) (the three types this builder supports; only an omitted, `undefined` one defaults to a CCD, and `null` or a non-string is refused), when a type-specific input is supplied to a type that does not carry it (`hospitalCourse`, `dischargeDiagnoses` or `encompassingEncounter` on anything but a Discharge Summary; `assessment` or `reasonForReferral` on a Discharge Summary), when an allergy is neither an `allergen` nor `noKnownAllergy`, when a result does not carry exactly one value form (`quantity` / `codedValue` / `stringValue`), when a `"observation"`-variant procedure omits its SHALL `value`, when a family-history entry carries an empty `observations` list, or when a problem or allergy supplies a `resolution` date without `status: "resolved"`. #### Example ```ts import { buildCcda, serializeCcda } from "@cosyte/ccda"; const doc = buildCcda({ patient: { mrn: "MRN001", given: ["Jane"], family: "Doe", gender: "F" }, problems: [{ problem: { code: "59621000", displayName: "Essential hypertension" } }], allergies: [{ allergen: { code: "7980", displayName: "Penicillin G" } }], }); console.log(doc.getMrn()); // "MRN001" console.log(doc.getProblems().length); // 1 const xml = serializeCcda(doc); // spec-clean C-CDA R2.1 ``` *** ### buildDocument() > **buildDocument**(`root`, `ctx`): `Omit`\<[`CcdaDocumentInit`](#ccdadocumentinit), `"warnings"`\> Build the [CcdaDocumentInit](#ccdadocumentinit) parts (everything except `warnings`) from a `ClinicalDocument` root element. Recognizes the document type, parses the header, and frames the body, a `structuredBody` yields sections; a `nonXMLBody` yields the quarantined `nonXmlBody` content. Never throws; the orchestrator supplies `warnings` and constructs the [CcdaDocument](#ccdadocument). #### Parameters ##### root `Element` ##### ctx [`ParseCtx`](#parsectx) #### Returns `Omit`\<[`CcdaDocumentInit`](#ccdadocumentinit), `"warnings"`\> #### Example ```ts import { buildDocument, CcdaDocument } from "@cosyte/ccda"; const parts = buildDocument(root, ctx); const doc = new CcdaDocument({ ...parts, warnings: [] }); ``` *** ### buildHeader() > **buildHeader**(`root`, `ctx`): [`CcdaHeader`](#ccdaheader) Extract the [CcdaHeader](#ccdaheader) from a `ClinicalDocument` root element. Never throws; omits any field the document does not carry. Emits `MULTIPLE_RECORD_TARGETS` when more than one `recordTarget` is present, and `UNIDENTIFIED_AUTHOR` once per `author` participation carrying neither arm of the `assignedAuthor` choice. More than one `author` at the document level is read as more than one author, in document order, and draws no warning of its own: the US Realm Header requires **at least** one (CONF:1198-5444), so several is conforming rather than a deviation. An absent `author` and an absent `custodian` are likewise left to document-level conformance validation; this function reports what the document carries and never fabricates the participation it does not. #### Parameters ##### root `Element` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`CcdaHeader`](#ccdaheader) #### Example ```ts import { buildHeader } from "@cosyte/ccda"; const header = buildHeader(rootEl, { emit: () => {} }); console.log(header.title, header.recordTargets.length); ``` *** ### buildNarrativeIndex() > **buildNarrativeIndex**(`textEl`): `ReadonlyMap`\<`string`, `string`\> Index a narrative `` block by the `ID` attributes carried on its descendant elements, mapping each `ID` to that node's trimmed text. C-CDA entries reference narrative via ``; this index lets the entry layer resolve those references without re-walking the DOM. #### Parameters ##### textEl `Element` #### Returns `ReadonlyMap`\<`string`, `string`\> #### Example ```ts import { buildNarrativeIndex, child } from "@cosyte/ccda"; const index = buildNarrativeIndex(child(sectionEl, "text")!); console.log(index.get("problem1")); ``` *** ### buildSection() > **buildSection**(`el`, `ctx`, `enclosing?`): [`CcdaSection`](#ccdasection) Frame a `
` element into a [CcdaSection](#ccdasection). Recognizes the section by `templateId` root (primary) or LOINC `code` (fallback, emitting `SECTION_MATCHED_BY_LOINC_FALLBACK`); an unrecognized coded section emits `UNKNOWN_SECTION_CODE` and is retained as narrative-only. Recurses into nested `
` subsections. Never throws. `enclosing` is the author reading of the level this section sits inside (the document's for a top-level section, the parent section's for a subsection). It is conducted down to this section and on to its entry acts wherever the level carries no `author` of its own, and marked `inherited` when it is. Omit it and the section is read as having no enclosing author, which is what a caller framing a detached `
` element is actually looking at. #### Parameters ##### el `Element` ##### ctx [`ParseCtx`](#parsectx) ##### enclosing? [`CcdaAuthorship`](#ccdaauthorship) #### Returns [`CcdaSection`](#ccdasection) #### Example ```ts import { buildSection } from "@cosyte/ccda"; const section = buildSection(sectionEl, { emit: () => {} }); console.log(section.key, section.subsections.length); ``` *** ### checkCodeSlot() > **checkCodeSlot**(`code`, `slot`, `position`, `ctx`): `void` Validate a coded value's `@codeSystem` against the terminologies expected for its [CodeSlot](#codeslot). Emits `DEPRECATED_CODE_SYSTEM` for a known-deprecated system (ICD-9) and `UNEXPECTED_CODE_SYSTEM` for any other unexpected OID. A `CD` that asserts a `@code` with **no** `@codeSystem` emits `MISSING_CODE_SYSTEM`: the symbol names no terminology, so it can be neither read nor checked. No system is ever inferred for it (not from the slot's expected list, not from a `@codeSystemName` label), and it is therefore never handed to a [TerminologyAdapter](#terminologyadapter), which validates a `system` + `code` pair. The mirror shape emits `MISSING_CODE_VALUE`: a `CD` that is **present** but asserts no usable `@code` (absent, empty, or whitespace) **and** declares no `@nullFlavor`, e.g. a system-only ``. A code system without a symbol identifies a concept no better than a symbol without a system does. The `nullFlavor` is what separates the two cases: a `nullFlavor`-only `CD` is a *complete* statement ("this concept is unknown") and stays silent, while a `CD` that says nothing at all leaves a reader unable to tell an absent concept from one lost in transformation. An **absent** value stays silent too, there is no element to judge. The value is preserved verbatim in every case, and no code, system, or `nullFlavor` is ever inferred. #### Parameters ##### code [`CD`](#cd) \| `undefined` ##### slot `"problem"` \| `"medication"` \| `"allergen"` \| `"route"` \| `"vaccine"` ##### position ###### column? `number` ###### line? `number` ###### path? `string` ##### ctx [`ParseCtx`](#parsectx) #### Returns `void` #### Example ```ts import { checkCodeSlot } from "@cosyte/ccda"; checkCodeSlot(problemValue, "problem", { path: "value" }, ctx); ``` *** ### checkLoincDeprecation() > **checkLoincDeprecation**(`code`, `position`, `ctx`): `void` Flag a result/vital observation `code` that is a known-deprecated LOINC. Emits `DEPRECATED_LOINC` (code preserved) only when the value is in the LOINC code system and a recognized-deprecated code; otherwise silent. A value with no `code` or a non-LOINC system is left unchecked. #### Parameters ##### code [`CD`](#cd) \| `undefined` ##### position [`CcdaPosition`](#ccdaposition) ##### ctx [`ParseCtx`](#parsectx) #### Returns `void` #### Example ```ts import { checkLoincDeprecation } from "@cosyte/ccda"; checkLoincDeprecation(observationCode, { path: "code" }, ctx); ``` *** ### checkUcumUnit() > **checkUcumUnit**(`quantity`, `position`, `ctx`): `void` Validate a [PQ](#pq)'s `@unit` with the computable UCUM grammar, emitting the appropriate Tier-2 warning: `MISSING_UNIT_ON_PQ` (numeric value, no unit), `UCUM_CASE_SUSPECT` (a letter-case slip of a canonical unit), or `NON_UCUM_UNIT` (not well-formed UCUM). The quantity itself is never mutated, the raw unit is always preserved. #### Parameters ##### quantity [`PQ`](#pq) ##### position [`CcdaPosition`](#ccdaposition) ##### ctx [`ParseCtx`](#parsectx) #### Returns `void` #### Example ```ts import { checkUcumUnit } from "@cosyte/ccda"; checkUcumUnit({ value: 5, unit: "cc" }, { path: "value" }, ctx); ``` *** ### child() > **child**(`el`, `localName`): `any` Return the first direct child element in the HL7 v3 namespace with the given local name, or `undefined`. Only direct children are considered (no descendant search), so structurally distinct same-named elements at deeper levels are not accidentally matched. #### Parameters ##### el `Element` ##### localName `string` #### Returns `any` #### Example ```ts import { child } from "@cosyte/ccda"; const code = child(sectionEl, "code"); // Element | undefined ``` *** ### childElements() > **childElements**(`el`): readonly `Element`[] Return all direct child elements (any namespace), in document order. Used where the caller wants to enumerate structure without filtering by name. #### Parameters ##### el `Element` #### Returns readonly `Element`[] #### Example ```ts import { childElements } from "@cosyte/ccda"; const kids = childElements(el); ``` *** ### children() > **children**(`el`, `localName`): readonly `Element`[] Return all direct child elements in the HL7 v3 namespace with the given local name, in document order. Empty array when none match. #### Parameters ##### el `Element` ##### localName `string` #### Returns readonly `Element`[] #### Example ```ts import { children } from "@cosyte/ccda"; for (const comp of children(bodyEl, "component")) { // ... } ``` *** ### defineCcdaProfile() > **defineCcdaProfile**(`opts`): [`CcdaProfile`](#ccdaprofile) Build a frozen [CcdaProfile](#ccdaprofile) from a validated options object. Throws [CcdaProfileDefinitionError](#ccdaprofiledefinitionerror) on a bad name, an unknown option key, or an invalid `tolerate` entry, including the **safety rule**: a profile may never tolerate a safety-critical warning code. `extends` composes profiles: lineage, `tolerate`, `provenance`, and `description` merge (parents left-to-right, then self; scalars are child-wins). The merged `tolerate` set is re-validated so a safety-critical code cannot sneak in via a hand-crafted parent. #### Parameters ##### opts [`DefineCcdaProfileOptions`](#defineccdaprofileoptions) The profile definition; see [DefineCcdaProfileOptions](#defineccdaprofileoptions). #### Returns [`CcdaProfile`](#ccdaprofile) A frozen, immutable profile with `describe()` attached. #### Throws [CcdaProfileDefinitionError](#ccdaprofiledefinitionerror) on any invalid definition. #### Example ```ts import { defineCcdaProfile } from "@cosyte/ccda"; const site = defineCcdaProfile({ name: "acme-hospital", description: "Acme's inbound-CCD tolerances", tolerate: [ { code: "TEMPLATE_EXTENSION_ABSENT", rationale: "receives R1.1-origin CCDs" }, ], provenance: { source: "Acme integration corpus", reference: "internal-2026" }, }); console.log(site.lineage); // ["acme-hospital"] console.log(site.describe?.()); ``` *** ### documentTypeForOid() > **documentTypeForOid**(`oid`): [`DocumentType`](#documenttype-4) \| `undefined` Resolve a document-template OID to its [DocumentType](#documenttype-4), or `undefined` when the OID is not one of the twelve recognized C-CDA R2.1 types. #### Parameters ##### oid `string` #### Returns [`DocumentType`](#documenttype-4) \| `undefined` #### Example ```ts import { documentTypeForOid } from "@cosyte/ccda"; documentTypeForOid("2.16.840.1.113883.10.20.22.1.2"); // "ccd" documentTypeForOid("1.2.3"); // undefined ``` *** ### editCcda() > **editCcda**(`source`, `options?`): [`CcdaDocument`](#ccdadocument) Re-emit a parsed C-CDA document with sections added or replaced, preserving every unedited section byte-faithfully and (by default) stamping a CDA R2 revision that supersedes the source. See the module overview for the full contract. #### Parameters ##### source [`CcdaDocument`](#ccdadocument) A document produced by [parseCcda](#parseccda) (it must retain its source XML, a hand-constructed document throws `NO_SOURCE_DOCUMENT`). ##### options? [`EditCcdaOptions`](#editccdaoptions) = `{}` The section edits to apply, the revision behavior, and an optional bring-your-own `terminology` adapter forwarded to the final re-parse so the edited document flags adapter-rejected codes (`SEMANTIC_CODE_INVALID`). #### Returns [`CcdaDocument`](#ccdadocument) A new [CcdaDocument](#ccdadocument), the re-parse of the edited XML, plus any emit-time diagnostic about what this call grafted (today only `MISSING_PLANNED_MEDICATION_EFFECTIVE_TIME`, appended last). The `source` is never mutated. #### Throws [CcdaEditError](#ccdaediterror) when the source has no retained XML, has no `structuredBody` to edit, violates an add/replace precondition, would drop a SHALL required section, or (when stamping a revision) has no usable `ClinicalDocument.id` for the RPLC link to name, absent or `nullFlavor`-marked (`SOURCE_MISSING_ID`). #### Throws when a section's content violates a builder guard (an invalid timestamp, a resolved problem without a resolution date, …). #### Example ```ts import { parseCcda, editCcda } from "@cosyte/ccda"; const doc = parseCcda(xml); const revised = editCcda(doc, { sections: [ { kind: "problems", content: [{ problem: { code: "38341003", displayName: "Hypertension" }, status: "active" }], }, ], }); console.log(revised.header.versionNumber); // incremented from the source ``` *** ### extractAllergies() > **extractAllergies**(`sectionEl`, `narrativeById`, `ctx`): readonly [`AllergyConcern`](#allergyconcern)[] Extract every Allergy Concern Act from an Allergies `
` element. Each `` whose act carries the Allergy Concern Act template becomes an [AllergyConcern](#allergyconcern); the nested Allergy-Intolerance Observations become [Allergy](#allergy)s (including the "No Known Allergies" negated form). Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`AllergyConcern`](#allergyconcern)[] #### Example ```ts import { extractAllergies } from "@cosyte/ccda"; const concerns = extractAllergies(sectionEl, section.narrativeById, ctx); ``` *** ### extractClinical() > **extractClinical**(`structuredBody`, `ctx`): [`ClinicalEntries`](#clinicalentries) Extract the reconciliation triad from a `structuredBody` element. Walks every section, runs each triad extractor, and flags misplaced entries. Never throws; an unstructured document (no `structuredBody`) is handled by the caller and never reaches here. #### Parameters ##### structuredBody `Element` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`ClinicalEntries`](#clinicalentries) #### Example ```ts import { extractClinical } from "@cosyte/ccda"; const entries = extractClinical(structuredBodyEl, ctx); console.log(entries.problems.length); ``` *** ### extractEncounters() > **extractEncounters**(`sectionEl`, `narrativeById`, `ctx`): readonly [`Encounter`](#encounter)[] Extract every Encounter Activity from an Encounters `
` element. Each `` whose `encounter` carries the Encounter Activity template becomes an [Encounter](#encounter). Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`Encounter`](#encounter)[] #### Example ```ts import { extractEncounters } from "@cosyte/ccda"; const encounters = extractEncounters(sectionEl, section.narrativeById, ctx); ``` *** ### extractFamilyHistory() > **extractFamilyHistory**(`sectionEl`, `narrativeById`, `ctx`): readonly [`FamilyHistory`](#familyhistory-4)[] Extract every Family History Organizer from a Family History `
` element. Each `` whose `organizer` carries the Family History Organizer template becomes a [FamilyHistory](#familyhistory-4); its `component/observation` members become [FamilyHistoryObservation](#familyhistoryobservation)s. Never throws. **This family's CONTENTS are carved out of the subject-override rule and read every entry the section carries** (`childEntries`, not `readableEntries`): its contract is a relative's data, so withholding never removes anything from it, in any document shape. **The report is not carved out.** A section that declares a subject still declares it, and a consumer whose only call is this one would otherwise hear nothing about it, so the section's overrides are reported on the caller's `ctx` under the same arithmetic every other extractor uses. Memoized per (context, section), so a whole-document parse counts exactly what it counted before: the record-target extractors reach the section first. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`FamilyHistory`](#familyhistory-4)[] #### Example ```ts import { extractFamilyHistory } from "@cosyte/ccda"; const history = extractFamilyHistory(sectionEl, section.narrativeById, ctx); ``` *** ### extractFunctionalStatus() > **extractFunctionalStatus**(`sectionEl`, `narrativeById`, `ctx`): readonly [`StatusObservation`](#statusobservation)[] Extract every Functional Status finding from a Functional Status `
` element, standalone Functional Status Observations plus the members (observations and assessment scales) of any Functional Status Organizer. Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`StatusObservation`](#statusobservation)[] #### Example ```ts import { extractFunctionalStatus } from "@cosyte/ccda"; const findings = extractFunctionalStatus(sectionEl, section.narrativeById, ctx); ``` *** ### extractImmunizations() > **extractImmunizations**(`sectionEl`, `narrativeById`, `ctx`): readonly [`Immunization`](#immunization)[] Extract every Immunization Activity from an Immunizations `
` element. Each `` whose `substanceAdministration` carries the Immunization Activity template becomes an [Immunization](#immunization). A `negationInd` record is flagged `IMMUNIZATION_REFUSED`. Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`Immunization`](#immunization)[] #### Example ```ts import { extractImmunizations } from "@cosyte/ccda"; const shots = extractImmunizations(sectionEl, section.narrativeById, ctx); ``` *** ### extractMedications() > **extractMedications**(`sectionEl`, `narrativeById`, `ctx`): readonly [`Medication`](#medication)[] Extract every Medication Activity from a Medications `
` element. Each `` whose `substanceAdministration` carries the Medication Activity template becomes a [Medication](#medication). Flags a missing `doseQuantity` / `routeCode` and an unclassifiable `effectiveTime`. Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`Medication`](#medication)[] #### Example ```ts import { extractMedications } from "@cosyte/ccda"; const meds = extractMedications(sectionEl, section.narrativeById, ctx); ``` *** ### extractMentalStatus() > **extractMentalStatus**(`sectionEl`, `narrativeById`, `ctx`): readonly [`StatusObservation`](#statusobservation)[] Extract every Mental Status finding from a Mental Status `
` element, standalone Mental Status Observations plus the members (observations and assessment scales) of any Mental Status Organizer. Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`StatusObservation`](#statusobservation)[] #### Example ```ts import { extractMentalStatus } from "@cosyte/ccda"; const findings = extractMentalStatus(sectionEl, section.narrativeById, ctx); ``` *** ### extractPastMedicalHistory() > **extractPastMedicalHistory**(`sectionEl`, `narrativeById`, `ctx`): readonly [`Problem`](#problem-1)[] Extract every bare Problem Observation from a Past Medical History `
` element. Each `` whose direct `observation` carries the Problem Observation template becomes a [Problem](#problem-1). Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`Problem`](#problem-1)[] #### Example ```ts import { extractPastMedicalHistory } from "@cosyte/ccda"; const history = extractPastMedicalHistory(sectionEl, section.narrativeById, ctx); ``` *** ### extractPlannedItems() > **extractPlannedItems**(`sectionEl`, `narrativeById`, `ctx`): readonly [`PlannedItem`](#planneditem)[] Extract every planned item from a `
` element: each `` whose act carries one of the seven templates in PLANNED\_VARIANTS, **and** each of the seven nested inside a Planned Intervention Act (PLANNED\_INTERVENTION\_ACT, `…22.4.146`). Never throws. **The container is named rather than inferred, and it is the only one.** R2.1 gives the Planned Intervention Act an `entryRelationship` for every one of the seven, each holding the planned act **inline**, so before this a planned entry sitting there was returned as nothing with nothing said, for all seven kinds. A Planned Intervention Act nested in a Planned Intervention Act is descended into too, because the walk is the same walk; the DOM is a tree, so it terminates. **It is the only container descended into, and R2.1 has others, so this does NOT solve nesting in general.** A Nutrition Recommendation (`…22.4.130`) inline-holds six of the seven by the identical `entryRelationship` pattern (every planned template except `…22.4.120`), and an Intervention Act (`…22.4.131`, the performed sibling and the `SHOULD` entry of an Interventions Section) inline-holds a Planned Intervention Act. A planned entry in either is still returned as nothing with nothing said. That is left as it was found: the scope taken here is the one container, and widening it is a decision with its own base-measured matrix, not a tidy-up. A test pins both as unreached so the bound is measured rather than asserted. **An `entryRelationship` is read for what it CONTAINS, never followed for what it REFERENCES.** The template's `[1..*]` `typeCode="RSON"` relationship holds an Entry Reference (`…22.4.122`) whose own SHALL points at a Goal Observation. That act carries no planned template root, so it is walked past rather than resolved: its target lives elsewhere in the document and is reached on its own terms, and resolving a pointer here would return an item the container does not hold. **Which acts a Planned Intervention Act holds and this still does not return:** the performed ones (`MedicationActivity`, `ImmunizationActivity`, `ProcedureActivityProcedure`, `EncounterActivity`, and the rest, none of which is planned and none of which carries a planned root), and the same three non-item templates the section itself admits (Instruction `…22.4.20`, Handoff Communication Participants `…22.4.141`, Nutrition Recommendation `…22.4.130`). **Those three are now REPORTED at both levels** (`PLAN_ENTRY_NOT_MODELED`), still not returned. The performed acts are not, and must not be: they are modelled elsewhere and reached by their own extractors, so a "not modelled" report on one would be false. **The two levels are scoped differently, and the scope is a BOUND this package chose, not a catalog of which sections C-CDA admits these templates in.** A direct `` is reported in the **two** sections named on PLAN\_ENTRY\_REPORT\_SECTION\_KEYS: `planOfTreatment`, the section this accessor is named for, and `interventions`, the Interventions Section (V3), which admits a Handoff as a direct entry in as many words (CONF:1198-32402 / 1198-32403, quoted on that constant) and is where R2.1 puts the container the nested half already reads. Inside a Planned Intervention Act there is no section condition at all, because the container is what the report is relative to there and it is read wherever it sits. An Instruction sitting in the **Instructions Section** (`…22.2.45`) is deliberately still silent: it is that section's own required entry, and reporting a conformant document's required entry as "not modelled" would be noise rather than a finding. **The residual, stated rather than implied: these three templates appear in more places than the report covers, and an occurrence outside them is still dropped in silence.** Two are measured and pinned by test. A Handoff nested in an **Intervention Act** (`…22.4.131`) is silent, because that act is not a container this package descends into. Any of the three as a direct entry of a section this catalog does not recognize at all is silent, because there is no key to match. Widening further is its own decision with its own base-measured matrix, not a tidy-up. **A returned item does not say whether it was a direct `` or nested.** The Planned Intervention Act is not modelled: this package carries no container type, no goal linkage, and no `nested` flag, so the grouping toward a goal is available only from `doc.toString()`. What `getPlannedItems()` answers is which acts are planned for this patient, and a nested one is planned on exactly the same terms as a direct one. Each item keeps its own `ids`, so a caller that needs the grouping can correlate. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`PlannedItem`](#planneditem)[] #### Example ```ts import { extractPlannedItems } from "@cosyte/ccda"; const planned = extractPlannedItems(sectionEl, section.narrativeById, ctx); ``` *** ### extractProblems() > **extractProblems**(`sectionEl`, `narrativeById`, `ctx`): readonly [`ProblemConcern`](#problemconcern)[] Extract every Problem Concern Act from a Problems `
` element. Each `` whose act carries the Problem Concern Act template becomes a [ProblemConcern](#problemconcern); the nested Problem Observations become [Problem](#problem-1)s. Reconciles each problem's coded value against its narrative reference. Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`ProblemConcern`](#problemconcern)[] #### Example ```ts import { extractProblems } from "@cosyte/ccda"; const concerns = extractProblems(sectionEl, section.narrativeById, ctx); ``` *** ### extractProcedures() > **extractProcedures**(`sectionEl`, `narrativeById`, `ctx`): readonly [`Procedure`](#procedure)[] Extract every procedure from a Procedures `
` element. Each `` whose `procedure`/`act`/`observation` carries one of the three Procedure Activity templates becomes a [Procedure](#procedure). Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`Procedure`](#procedure)[] #### Example ```ts import { extractProcedures } from "@cosyte/ccda"; const procedures = extractProcedures(sectionEl, section.narrativeById, ctx); ``` *** ### extractResults() > **extractResults**(`sectionEl`, `narrativeById`, `ctx`): readonly [`ResultOrganizer`](#resultorganizer)[] Extract every Result Organizer from a Results `
` element. Each `` whose `organizer` carries the Result Organizer template becomes a [ResultOrganizer](#resultorganizer); its `component/observation` members become [Result](#result-1)s. Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`ResultOrganizer`](#resultorganizer)[] #### Example ```ts import { extractResults } from "@cosyte/ccda"; const panels = extractResults(sectionEl, section.narrativeById, ctx); ``` *** ### extractSmokingStatus() > **extractSmokingStatus**(`sectionEl`, `narrativeById`, `ctx`): readonly [`SmokingStatus`](#smokingstatus-4)[] Extract every Smoking Status observation from a Social History `
` element. Each `` whose `observation` carries the Smoking Status, Meaningful Use template becomes a [SmokingStatus](#smokingstatus-4). Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`SmokingStatus`](#smokingstatus-4)[] #### Example ```ts import { extractSmokingStatus } from "@cosyte/ccda"; const statuses = extractSmokingStatus(sectionEl, section.narrativeById, ctx); ``` *** ### extractVitals() > **extractVitals**(`sectionEl`, `narrativeById`, `ctx`): readonly [`VitalSignsOrganizer`](#vitalsignsorganizer)[] Extract every Vital Signs Organizer from a Vital Signs `
` element. Each `` whose `organizer` carries the Vital Signs Organizer template becomes a [VitalSignsOrganizer](#vitalsignsorganizer); its `component/observation` members become [VitalSign](#vitalsign)s. Never throws. #### Parameters ##### sectionEl `Element` ##### narrativeById `ReadonlyMap`\<`string`, `string`\> ##### ctx [`ParseCtx`](#parsectx) #### Returns readonly [`VitalSignsOrganizer`](#vitalsignsorganizer)[] #### Example ```ts import { extractVitals } from "@cosyte/ccda"; const clusters = extractVitals(sectionEl, section.narrativeById, ctx); ``` *** ### getCcdaProfile() > **getCcdaProfile**(`name`): [`CcdaProfile`](#ccdaprofile) \| `undefined` Look up a built-in profile by name. Returns `undefined` when no built-in has that name (a user-defined profile is not in this registry, pass it directly). #### Parameters ##### name `string` #### Returns [`CcdaProfile`](#ccdaprofile) \| `undefined` #### Example ```ts import { getCcdaProfile } from "@cosyte/ccda"; const p = getCcdaProfile("smartScorecard"); console.log(p?.provenance?.source); ``` *** ### getDefaultCcdaProfile() > **getDefaultCcdaProfile**(): [`CcdaProfile`](#ccdaprofile) \| `undefined` Return the current process-scoped default profile, or `undefined` if none is registered. #### Returns [`CcdaProfile`](#ccdaprofile) \| `undefined` #### Example ```ts import { getDefaultCcdaProfile } from "@cosyte/ccda"; const p = getDefaultCcdaProfile(); if (p !== undefined) console.log("default profile:", p.name); ``` *** ### isNullFlavor() > **isNullFlavor**(`value`): value is "NI" \| "INV" \| "DER" \| "OTH" \| "NINF" \| "PINF" \| "UNC" \| "MSK" \| "NA" \| "UNK" \| "ASKU" \| "NAV" \| "NASK" \| "NAVU" \| "QS" \| "TRC" \| "NP" Return `true` when a string is a conforming [NullFlavor](#nullflavor-17) token. #### Parameters ##### value `string` #### Returns value is "NI" \| "INV" \| "DER" \| "OTH" \| "NINF" \| "PINF" \| "UNC" \| "MSK" \| "NA" \| "UNK" \| "ASKU" \| "NAV" \| "NASK" \| "NAVU" \| "QS" \| "TRC" \| "NP" #### Example ```ts import { isNullFlavor } from "@cosyte/ccda"; isNullFlavor("UNK"); // true isNullFlavor("nope"); // false ``` *** ### isRecognizedNamespace() > **isRecognizedNamespace**(`namespaceUri`): `boolean` Return `true` when a namespace URI is one the parser recognizes (`urn:hl7-org:v3`, the XSI namespace, or `urn:hl7-org:sdtc`). A `null` URI, an element with no namespace at all, counts as unrecognized. #### Parameters ##### namespaceUri `string` \| `null` #### Returns `boolean` #### Example ```ts import { isRecognizedNamespace, V3_NS } from "@cosyte/ccda"; isRecognizedNamespace(V3_NS); // true isRecognizedNamespace("urn:vendor"); // false isRecognizedNamespace(null); // false ``` *** ### isSafetyCriticalCode() > **isSafetyCriticalCode**(`code`): `boolean` True when `code` is safety-critical and therefore forbidden in a profile's `tolerate` set. #### Parameters ##### code [`WarningCode`](#warningcode) #### Returns `boolean` #### Example ```ts import { isSafetyCriticalCode } from "@cosyte/ccda"; console.log(isSafetyCriticalCode("DEPRECATED_LOINC")); // false console.log(isSafetyCriticalCode("CODE_NARRATIVE_MISMATCH")); // true ``` *** ### isSyntheticSetId() > **isSyntheticSetId**(`setId`): `boolean` Whether a `setId` was **minted by [editCcda](#editccda)** rather than carried by the source document or supplied by the caller. True only when the identifier matches the whole documented scheme: the synthetic assigning-authority root *and* an `@extension` beginning [SYNTHETIC\_SETID\_PREFIX](#synthetic_setid_prefix). Both, because either alone is something a real document could carry by coincidence. **A `false` is not a promise that the identifier is real.** It says only that this library did not mint it under this scheme: a `setId` minted by some other tool, or by a version of this library before the scheme existed (`setid-e1`, unprefixed), reads `false` here. Use it to recognise a synthetic id, never to certify a real one. #### Parameters ##### setId [`II`](#ii) \| `undefined` The `setId` to test, e.g. `doc.header.setId`. `undefined` is `false` (a document with no `setId` has nothing to recognise). #### Returns `boolean` `true` when the identifier matches the minted scheme. #### Example ```ts import { parseCcda, isSyntheticSetId } from "@cosyte/ccda"; const doc = parseCcda(xml); if (isSyntheticSetId(doc.header.setId)) { // the version series was invented by an editor, not asserted by a source system } ``` *** ### isUcumCaseSuspect() > **isUcumCaseSuspect**(`unit`): `boolean` Detect the case-confusion trap: a unit that is **not** a canonical clinical spelling but whose case-folded form is one, `Mg`/`MG` for `mg`, `ML` for `mL`, `mEq` for `meq`. These are reported as `UCUM_CASE_SUSPECT` (more actionable than `NON_UCUM_UNIT`) because the likely fix is a single letter-case change. A unit already spelled canonically returns `false`. #### Parameters ##### unit `string` #### Returns `boolean` #### Example ```ts import { isUcumCaseSuspect } from "@cosyte/ccda"; isUcumCaseSuspect("ML"); // true (meant mL; ML is megaliter) isUcumCaseSuspect("mEq"); // true (meant meq) isUcumCaseSuspect("mg"); // false (already canonical) ``` *** ### isValidUcumUnit() > **isValidUcumUnit**(`unit`): `boolean` Validate a string as a well-formed UCUM unit (case-sensitive). Returns `true` only when the **entire** string parses, `mg/dL`, `mm[Hg]`, `10*3/uL`, `Cel`, `%`, `/min`, `kg/m2`, `1` are valid; `mcg`, `cc`, `mg//dL` and partial garbage are not. This is a grammatical check: a case slip like `Mg/dL` is well-formed UCUM (megagram/deciliter) so it validates here, [isUcumCaseSuspect](#isucumcasesuspect) catches the clinical case-confusion separately. Never throws. #### Parameters ##### unit `string` #### Returns `boolean` #### Example ```ts import { isValidUcumUnit } from "@cosyte/ccda"; isValidUcumUnit("mg/dL"); // true isValidUcumUnit("mm[Hg]"); // true isValidUcumUnit("cc"); // false ``` *** ### listCcdaProfiles() > **listCcdaProfiles**(): readonly `string`[] The names of every built-in profile, in registration order. #### Returns readonly `string`[] #### Example ```ts import { listCcdaProfiles } from "@cosyte/ccda"; console.log(listCcdaProfiles()); // ["default", "smartScorecard", "legacyR11"] ``` *** ### looksProductLevel() > **looksProductLevel**(`displayName`): `boolean` Heuristic: does an RxNorm `displayName` look like a product/branded concept (carries a dose form or strength) rather than a bare ingredient? Used only to flag `ALLERGEN_GRANULARITY_SUSPECT`, a best-effort signal, never a hard rule. Returns `false` when there is no display text to inspect. #### Parameters ##### displayName `string` \| `undefined` #### Returns `boolean` #### Example ```ts import { looksProductLevel } from "@cosyte/ccda"; looksProductLevel("amoxicillin 500 MG Oral Tablet"); // true looksProductLevel("amoxicillin"); // false ``` *** ### missingRequiredSections() > **missingRequiredSections**(`documentType`, `presentKeys`, `options?`): readonly `string`[] The SHALL section keys a [DocumentType](#documenttype-4) requires that are **absent** from `presentKeys`, preserving the type's declared order. The parser passes the set of recognized section keys it framed; each returned key becomes one `REQUIRED_SECTION_MISSING` warning. Returns an empty array when every required section is present (or the type asserts none). #### Parameters ##### documentType [`DocumentType`](#documenttype-4) ##### presentKeys `ReadonlySet`\<`string`\> ##### options? [`RequiredSectionOptions`](#requiredsectionoptions) #### Returns readonly `string`[] #### Example ```ts import { missingRequiredSections } from "@cosyte/ccda"; missingRequiredSections("ccd", new Set(["allergies", "problems"])); // ["medications", "results", "socialHistory", "vitalSigns"] // The same document without the R2.1 stamp: missingRequiredSections("ccd", new Set(["allergies", "problems"]), { r21Stamped: false }); // ["medications", "results"] // The same document stamped for an unmodelled release: nothing is reported // missing, because nothing was evaluated. See requiredSectionStatus(). missingRequiredSections("ccd", new Set(["allergies", "problems"]), { stamp: "unmodeled-release", }); // [] ``` *** ### parseBl() > **parseBl**(`el`, `ctx`): [`BL`](#bl) \| `undefined` Parse a `BL` element (one carrying a `@value`) into a typed [BL](#bl). Returns `undefined` when the element is absent. Never throws. A `@nullFlavor` declared beside a `@value` emits `CONTRADICTORY_NULL_FLAVOR`. The parsed boolean is **kept**: `BL` retains no verbatim copy of its token, so withholding `value` would delete the assertion rather than decline to embellish it (see [parsePq](#parsepq)). #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`BL`](#bl) \| `undefined` #### Example ```ts import { parseBl } from "@cosyte/ccda"; const bl = parseBl(el, { emit: () => {} }); console.log(bl?.value); ``` *** ### parseBlAttr() > **parseBlAttr**(`el`, `name`): `boolean` \| `undefined` Read a named BL-valued attribute (e.g. `negationInd`) off an element, returning the parsed boolean or `undefined` when absent or non-boolean. This is the distinct path for attributes like `@negationInd` that are booleans on an element rather than a child `BL` element. #### Parameters ##### el `Element` ##### name `string` #### Returns `boolean` \| `undefined` #### Example ```ts import { parseBlAttr } from "@cosyte/ccda"; const negated = parseBlAttr(observationEl, "negationInd"); // boolean | undefined ``` *** ### parseCcda() > **parseCcda**(`raw`, `options?`): [`CcdaDocument`](#ccdadocument) Parse a C-CDA XML payload into an immutable [CcdaDocument](#ccdadocument). **Lenient by default**, real-world, vendor-quirky documents parse rather than throw, accruing [CcdaWarning](#ccdawarning)s on `doc.warnings`. Only unrecoverable structural problems throw a [CcdaParseError](#ccdaparseerror) (a Tier-3 [FatalCode](#fatalcode)): a declared DTD/external entity, a size/entity/depth limit breach, malformed XML, or a root that is not `ClinicalDocument`. With `{ strict: true }`, every Tier-2 deviation is escalated to a thrown `CcdaParseError` instead of a warning. #### Parameters ##### raw `string` The raw C-CDA XML document text. ##### options? [`ParseCcdaOptions`](#parseccdaoptions) = `{}` Parse options; see [ParseCcdaOptions](#parseccdaoptions). Lenient unless `strict` is set. #### Returns [`CcdaDocument`](#ccdadocument) The parsed document plus any recovered Tier-2 warnings. #### Throws [CcdaParseError](#ccdaparseerror) on any Tier-3 fatal, or, when `options.strict` is `true`, on the first Tier-2 deviation. #### Example ```ts import { parseCcda } from "@cosyte/ccda"; const doc = parseCcda(xml); console.log(doc.documentType, doc.getPatient()?.name?.text); for (const w of doc.warnings) console.warn(w.code, w.position); ``` *** ### parseCd() > **parseCd**(`el`, `ctx`): [`CD`](#cd) \| `undefined` Parse a `CD`/`CE` element into a typed [CD](#cd). Returns `undefined` when the element is absent. Resolves a child `` to its trimmed text and parses each `` recursively (translations of a translation are ignored, C-CDA does not nest them). Never throws. A `@nullFlavor` declared beside a populated `@code` emits `CONTRADICTORY_NULL_FLAVOR`: the element says both "this concept is unknown" and "this concept is X". Only `@code` counts as the contradicting assertion. A `nullFlavor` beside `originalText`, a ``, a `displayName` or a bare `@codeSystem` is the documented C-CDA idiom for "not codable in the bound value set, here is the source text or an alternate coding", which is coherent rather than contradictory and stays silent. Unlike `PQ`/`TS`, the `code` is **kept**: it is the document's own text rather than a reading this parser manufactured, and there is no verbatim copy to fall back on, so withholding it would delete what the document said. See [parsePq](#parsepq) for the full argument. #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`CD`](#cd) \| `undefined` #### Example ```ts import { parseCd } from "@cosyte/ccda"; const code = parseCd(codeEl, { emit: () => {} }); console.log(code?.code, code?.displayName); ``` *** ### parseEd() > **parseEd**(`el`, `ctx`): [`ED`](#ed) \| `undefined` Parse an `ED` element into a typed [ED](#ed). Returns `undefined` when the element is absent. Captures inline content verbatim (base64 is not decoded) and resolves a child ``'s `@value`. Never throws. A `@nullFlavor` declared beside inline content or a `` emits `CONTRADICTORY_NULL_FLAVOR`; a `@mediaType` or `@representation` alone describes a null value rather than contradicting it and stays silent. Content and reference are **kept** verbatim (see [parsePq](#parsepq)). #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`ED`](#ed) \| `undefined` #### Example ```ts import { parseEd } from "@cosyte/ccda"; const ed = parseEd(el, { emit: () => {} }); console.log(ed?.reference ?? ed?.value); ``` *** ### parseIi() > **parseIi**(`el`, `ctx`): [`II`](#ii) \| `undefined` Parse an `II` element into a typed [II](#ii). Returns `undefined` when the element itself is absent. Never throws; omits any field the element does not carry. A `@nullFlavor` declared beside an `@extension` emits `CONTRADICTORY_NULL_FLAVOR`: the element says both "this identifier is unknown" and "this identifier is 12345". Only `@extension` counts as the contradicted assertion; a `@root` alone is a namespace without a local identifier, so no identifier value is produced and the shape stays silent. The `@extension` itself is **kept**. It is the document's own text, with no second copy the way `PQ.raw` sits beside `PQ.value`, so withholding it here would delete what the document said rather than decline to embellish it (see [parsePq](#parsepq) for the rule and its limit). Nor is there a derived reading to withhold: at this layer `extension` *is* the datum, not something the parser manufactured from it. **Where the withholding happens instead.** The dangerous act is not reporting an `II` whole, with its `nullFlavor` attached, it is *selecting* one and handing back a naked `string` that no longer carries the marking. This model does that in exactly one place, [pickMrn](#pickmrn) (behind `getMrn()`), and that is where a null-marked identifier is declined. The identity slots, `ClinicalDocument.id`, `setId`, `relatedDocument/parentDocument/id` and every entry-level ``, are only ever reported as the whole datatype beside the warning, so the `nullFlavor` never goes missing and there is nothing to withhold. The emit side is guarded separately: `editCcda` refuses to build an `RPLC` `parentDocument` out of a null-marked source ``, because copying `root`/`extension` forward would launder a disowned identifier into an asserted one. **`templateId` is the stated exception, and it is deliberate.** Document- and section-type recognition *does* derive a reading from `templateId.@root`, so `` still resolves the document type and its required-section SHALL set. That is left alone on purpose: a `templateId` is a conformance assertion about the *document's shape*, not an identifier for a person or a record, so a mis-read costs a spurious or missing `REQUIRED_SECTION_MISSING` rather than a misattributed clinical fact. Declining to recognize would also make the parser less informative, not safer, replacing a working document type with `UNKNOWN_DOCUMENT_TEMPLATE`. Note too that the shape stays silent here by the rule above: `@root` is not the contradicted assertion, only `@extension` is. #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`II`](#ii) \| `undefined` #### Example ```ts import { parseIi } from "@cosyte/ccda"; const id = parseIi(idEl, { emit: () => {} }); console.log(id?.root); ``` *** ### parseIvlPq() > **parseIvlPq**(`el`, `ctx`): [`IVL_PQ`](#ivl_pq) \| `undefined` Parse an `IVL_PQ` element into a typed [IVL\_PQ](#ivl_pq). Returns `undefined` when the element is absent. Never throws; omits any bound the element lacks. A `@nullFlavor` on the **interval itself** beside bounds that carry values is the same contradiction [parsePq](#parsepq) resolves one level down, and gets the same treatment: `CONTRADICTORY_NULL_FLAVOR` is emitted once for the interval, every bound is preserved verbatim, and the derived `value` number is withheld from each of them. Without this a dose *range* declared unknown would still hand `doseRange.low.value` back to a caller, which is the scalar-dose harm by another route. #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`IVL_PQ`](#ivl_pq) \| `undefined` #### Example ```ts import { parseIvlPq } from "@cosyte/ccda"; const range = parseIvlPq(el, { emit: () => {} }); console.log(range?.low?.value, range?.high?.value); ``` *** ### parseIvlTs() > **parseIvlTs**(`el`, `ctx`): [`IVL_TS`](#ivl_ts) \| `undefined` Parse an `IVL_TS` element into a typed [IVL\_TS](#ivl_ts). Returns `undefined` when the element is absent. Handles both the ``/`` bound form and the degenerate `@value` point form. Never throws. A `@nullFlavor` on the interval beside its own `@value` or a bound that carries one is a contradiction: `CONTRADICTORY_NULL_FLAVOR` is emitted once, every `raw` is preserved, and the derived `date` is withheld from the point value and from each bound. See [parsePq](#parsepq) for the rule and its limits. #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`IVL_TS`](#ivl_ts) \| `undefined` #### Example ```ts import { parseIvlTs } from "@cosyte/ccda"; const period = parseIvlTs(effectiveTimeEl, { emit: () => {} }); console.log(period?.low?.date?.toISOString()); ``` *** ### parsePq() > **parsePq**(`el`, `ctx`): [`PQ`](#pq) \| `undefined` Parse a `PQ` element into a typed [PQ](#pq). Returns `undefined` when the element is absent. A non-numeric `@value` is preserved in `raw` with `value` omitted. Never throws. **A `@nullFlavor` beside a populated `@value` is a contradiction, and the parser resolves it against the number.** `` asserts both "this quantity is unknown" and "this quantity is 10 mg". The parser emits `CONTRADICTORY_NULL_FLAVOR`, preserves `raw` (`"10"`), `unit` (`"mg"`) and `nullFlavor` (`"UNK"`) verbatim, and **omits `value`**, so `med.dose.value` is `undefined` rather than `10`. The reasoning, because the choice is not obvious and a warning alone was the alternative. A warning alone keeps the house rule (never coerce, surface verbatim, flag) but leaves the dangerous affordance intact: `dose.value` still hands back `10` to the many consumers who do not read `warnings` on a first integration, and of the two readings the reassuring one is the one that can hurt a patient. Withholding costs nothing, because `value` is not the document's bytes, it is a `number` **this parser manufactured** by interpreting `raw`, and `raw` is still right there. So nothing the document said is lost, and the type now forces a caller to look at what it said. That is also exactly what `MALFORMED_DATETIME` already does one datatype over, `TS` keeps `raw` and drops the parsed `date`, so this is the existing rule applied to a second reason for not trusting an interpretation, not a new one. **The limit, stated rather than implied.** Withholding applies only where a verbatim copy survives beside the derived reading, which in this model is `PQ.value` and `TS.date` and nothing else. On `CD`, `II`, `ST`, `ED` and `BL` the value-bearing field *is* the document's own text (`@code`, `@extension`, the element's content), with no second copy, so withholding it would delete what the document said rather than decline to embellish it. Those datatypes therefore warn and keep the field: a naive consumer reading `allergy.allergen.code` still gets the code, with `nullFlavor` on the same object and `CONTRADICTORY_NULL_FLAVOR` in `warnings`. One consequence worth naming: a contradictory PQ no longer reaches `MISSING_UNIT_ON_PQ` (which keys off a defined `value`). That is not a new silence, the stronger `CONTRADICTORY_NULL_FLAVOR` fires in its place, and a missing unit on a value the document declared null is moot. #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`PQ`](#pq) \| `undefined` #### Example ```ts import { parsePq } from "@cosyte/ccda"; const pq = parsePq(el, { emit: () => {} }); console.log(pq?.value, pq?.unit); ``` *** ### parseSecureXml() > **parseSecureXml**(`raw`, `limits`, `emit`, `reportForeignNamespace?`): `Document` Strip a leading UTF-8 BOM and run the pre-parse safety gauntlet (size, DTD/DOCTYPE, entity-reference cap), then build a DOM with a hardened `@xmldom/xmldom` `DOMParser`, then enforce the depth / node-count caps on the constructed tree. Returns the root `Document`. Emits `ENCODING_BOM_STRIPPED` via `emit` when a BOM was removed. Throws a [CcdaParseError](#ccdaparseerror) carrying a PHI-free [CcdaPosition](#ccdaposition) for any safety violation or malformed XML, never returns a partially-built document. `UNKNOWN_NAMESPACE_PREFIX` goes to `reportForeignNamespace` rather than to `emit`, and that separation is load-bearing rather than tidy. This function runs before `parseCcda`'s root gate and before any clinical parsing, and in strict mode the emitter escalates the **first** warning it is handed. Routing a whole-document observation through `emit` here would therefore let it preempt `NOT_A_CLINICAL_DOCUMENT` on a document that is not a C-CDA at all, and preempt a safety-critical per-element code such as `MISSING_CODE_SYSTEM` on one that is. `parseCcda` passes a collector and replays it **after** the model is built, so those keep their precedence. The parameter defaults to `emit` for a caller using this function on its own, where there is no later stage to defer to. #### Parameters ##### raw `string` ##### limits [`ResolvedLimits`](#resolvedlimits) ##### emit (`warning`) => `void` ##### reportForeignNamespace? (`warning`) => `void` #### Returns `Document` #### Example ```ts import { parseSecureXml, resolveLimits } from "@cosyte/ccda"; const doc = parseSecureXml("", resolveLimits(), () => {}); console.log(doc.documentElement?.localName); // "ClinicalDocument" ``` *** ### parseSt() > **parseSt**(`el`, `ctx`): [`ST`](#st) \| `undefined` Parse an `ST` element into a typed [ST](#st). Returns `undefined` when the element is absent. Never throws. A `@nullFlavor` declared beside non-empty text emits `CONTRADICTORY_NULL_FLAVOR`. The text is **kept**: it is the document's own content with no verbatim copy elsewhere, so withholding it would delete what the document said (see [parsePq](#parsepq)). #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`ST`](#st) \| `undefined` #### Example ```ts import { parseSt } from "@cosyte/ccda"; const title = parseSt(titleEl, { emit: () => {} }); console.log(title?.value); ``` *** ### parseTs() > **parseTs**(`el`, `ctx`): [`TS`](#ts) \| `undefined` Parse a `TS` element into a typed [TS](#ts). Returns `undefined` when the element is absent. Emits `MALFORMED_DATETIME` (and omits `date`) when a non-empty `@value` does not parse. Never throws. A `@nullFlavor` declared beside a populated `@value` is a contradiction: `CONTRADICTORY_NULL_FLAVOR` is emitted, `raw` and `nullFlavor` are preserved verbatim, and the derived `date` is withheld. That is the same treatment `MALFORMED_DATETIME` already gives an unparseable value, and the same rule [parsePq](#parsepq) applies to `value`: the parser declines to manufacture a computable reading it has been told is not the document's value, while never dropping the document's own bytes. #### Parameters ##### el `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`TS`](#ts) \| `undefined` #### Example ```ts import { parseTs } from "@cosyte/ccda"; const ts = parseTs(effectiveTimeEl, { emit: () => {} }); console.log(ts?.date?.toISOString()); ``` *** ### parseV3DateTime() > **parseV3DateTime**(`value`): `Date` \| `undefined` Parse a variable-precision HL7 v3 timestamp string to a JS `Date`. Accepts year through second precision plus optional fractional seconds and an optional `±HHMM` (or `±HH`) timezone offset. Per the CDA R2 / HL7 v3 `TS` literal `YYYYMMDDHHMMSS.UUUU[±ZZzz]`, a fraction or offset is accepted **only on a value that carries the time-of-day** (at least the hour): a fraction or offset hung on a bare `YYYY`/`YYYYMM`/`YYYYMMDD` value, e.g. the dropped-dash `"2026-0721"`, is rejected rather than silently misread. A value with no offset resolves to UTC for determinism; truncated values resolve to the first instant of the stated precision (e.g. `2026` → `2026-01-01T00:00:00Z`). Returns `undefined` when the value does not match the shape or is calendar-invalid, never throws. #### Parameters ##### value `string` #### Returns `Date` \| `undefined` #### Example ```ts import { parseV3DateTime } from "@cosyte/ccda"; parseV3DateTime("20260628")?.toISOString(); // "2026-06-28T00:00:00.000Z" parseV3DateTime("20260628153045-0500")?.toISOString(); parseV3DateTime("not-a-date"); // undefined ``` *** ### pickMrn() > **pickMrn**(`identifiers`): `string` \| `undefined` Pick the MRN string from a list of `patientRole/id` [II](#ii) identifiers. Returns the **first** identifier's `extension`, or `undefined` when the list is empty, when that identifier has no `extension` (a root-only id names an assigning authority, not a patient), or when it carries a `nullFlavor`. **Why a `nullFlavor`-marked `` is withheld rather than read.** In HL7 v3, `nullFlavor` is a property of `ANY`: it marks an *exceptional* value, one with no proper value. An `` therefore says both "this identifier is unknown" and "this identifier is X", and `parseIi` already flags that contradiction as `CONTRADICTORY_NULL_FLAVOR` (safety-critical). Reading `X` out of it anyway is how a record gets filed against the wrong patient, which is silent, persistent, and contaminates everything downstream, so it sits alongside a wrong dose and a wrong code system in this package's harm ordering. **And why it is not skipped past, either.** Falling through to the next `` looks like the helpful move and is a worse one. CDA R2 makes `patientRole/id` `1..*` and neither it nor C-CDA R2.1 says which entry is the MRN, so nothing in the document ranks them: the second id is not "another MRN", it is whatever the sending system listed second, commonly a plan member number, an account number, or the SSN under `2.16.840.1.113883.4.1`. Substituting it would answer the MRN question confidently from a different assigning authority, with no signal naming the substitution, which trades one wrong-identifier failure for a quieter one. The rule this helper is applying declines a manufactured reading; it does not manufacture a replacement. Same slot, same position, reading withheld. **This is the `PQ.value` rule, applied one layer up rather than abandoned.** `parsePq` withholds `value` on a contradicted quantity because `value` is a number the parser *manufactured* from `raw`, and `raw` survives, so nothing the document said is lost. `II.extension` is not like that: it **is** the document's own text, with no second copy, so `parseIi` keeps it and a caller reading `patient.identifiers[0].extension` still gets it with the `nullFlavor` sitting on the same object. What *is* manufactured here is the **selection**: picking one `` out of a list and handing back a bare `string` strips away the `nullFlavor` that qualified it. So the derived act declines, and the verbatim data stays reachable. Both properties, rather than a trade between them. A caller whose assigning authority genuinely carries the MRN on a later ``, or who treats a given `nullFlavor` as tolerable, can walk `patient.identifiers` and branch on each `root`. That decision needs the authority OIDs, which this helper does not have and must not guess. **Provenance:** no normative SHALL is cited. CDA R2 declares `@nullFlavor` and `@extension` on `II` independently, and neither CDA R2 nor C-CDA R2.1 states which `patientRole/id` is the MRN. The rule rests on v3 datatype semantics plus the harm ordering above. #### Parameters ##### identifiers readonly [`II`](#ii)[] #### Returns `string` \| `undefined` #### Example ```ts import { pickMrn } from "@cosyte/ccda"; pickMrn([{ root: "2.16.840.1.113883.19.5", extension: "MRN001" }]); // → "MRN001" pickMrn([{ nullFlavor: "UNK", extension: "MRN001" }]); // → undefined (the document disowned that identifier) // A second id under a different authority is NOT a fallback MRN: pickMrn([{ nullFlavor: "UNK", extension: "MRN001" }, { root: "2.16.840.1.113883.4.1", extension: "SYNTH-9" }]); // → undefined (still the first id, withheld; never the next authority's number) pickMrn([]); // → undefined ``` *** ### positionOf() > **positionOf**(`el`): [`CcdaPosition`](#ccdaposition) Build a structural position for an element: its local name as a path hint plus the locator line/column `@xmldom/xmldom` recorded (when present). Never includes attribute values or text content. **`path` is bounded on membership, not copied.** A local name is an XML NCName, so a sender can call an element anything, and `enforceStructureLimits` in `../parser/secure-xml.ts` positions on arbitrary elements from a hostile document. Only a name in the CDA vocabulary this parser navigates is echoed; anything else becomes `` (see `../parser/tokens.ts`). Line and column still locate it exactly. #### Parameters ##### el `Element` #### Returns [`CcdaPosition`](#ccdaposition) #### Example ```ts import { positionOf } from "@cosyte/ccda"; const pos = positionOf(sectionEl); // { path: "section", line: 42, ... } ``` *** ### profileQuirkApplied() > **profileQuirkApplied**(`original`, `profileName`): [`CcdaWarning`](#ccdawarning) Build a `PROFILE_QUIRK_APPLIED` warning, the downgraded form an active [CcdaProfile](#ccdaprofile) produces from a deviation it *expects*. The original warning is **not dropped**: its code moves to `toleratedCode`, the deviation is re-badged `PROFILE_QUIRK_APPLIED`, `expected` is set, and the tolerating profile is named, so a consumer can filter known, grounded noise while the fact of the deviation, and where it was, survive. A profile can only ever reach this path for a **non-safety-critical** code (enforced at profile- definition time); safety-critical warnings can never be tolerated. **The original `message` is not carried forward, and `profileName` is not interpolated.** Both used to be, which made this the one factory whose output was assembled rather than looked up. What the deviation was is on `toleratedCode`, who tolerated it is on `profile`, and where it was is on `position`, all typed fields. **Say what that costs rather than "nothing is lost".** Two things do not survive the re-badge. The tolerated code's own message is not reachable from the returned warning, because WARNING\_MESSAGES is internal and is not on the package entry point. And where the original carried a per-closed-key variant, the key is not recoverable from `toleratedCode` alone: a tolerated `UNEXPECTED_CODE_SYSTEM` no longer says which [CodeSlot](#codeslot) it was about. The trade is deliberate, and narrow because a profile may only ever tolerate a **non**-safety-critical code, but it is a trade. #### Parameters ##### original [`CcdaWarning`](#ccdawarning) ##### profileName `string` #### Returns [`CcdaWarning`](#ccdawarning) #### Example ```ts import { profileQuirkApplied, deprecatedLoinc } from "@cosyte/ccda"; const original = deprecatedLoinc({ path: "code" }); const w = profileQuirkApplied(original, "smartScorecard"); ``` *** ### readObservationValue() > **readObservationValue**(`valueEl`, `ctx`): [`ObservationValue`](#observationvalue) \| `undefined` Read a polymorphic observation `` into an [ObservationValue](#observationvalue), branching on `xsi:type`. A `PQ` is UCUM-checked; an untyped value with a `@code`/`@value` is treated leniently as coded/quantity; an unrecognized `xsi:type` is preserved as `unsupported` and flagged with `RESULT_VALUE_TYPE_UNHANDLED`. Returns `undefined` when the element is absent. #### Parameters ##### valueEl `any` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`ObservationValue`](#observationvalue) \| `undefined` #### Example ```ts import { readObservationValue } from "@cosyte/ccda"; const v = readObservationValue(child(obs, "value"), ctx); ``` *** ### readReferenceRange() > **readReferenceRange**(`obs`, `ctx`): [`ReferenceRange`](#referencerange-1) \| `undefined` Read a result's reference range from a Result Observation. Prefers the structured `observationRange/value xsi:type="IVL_PQ"` bounds; falls back to the free-text form (emitting `FREE_TEXT_REFERENCE_RANGE`). Returns `undefined` when the observation carries no `referenceRange`. #### Parameters ##### obs `Element` ##### ctx [`ParseCtx`](#parsectx) #### Returns [`ReferenceRange`](#referencerange-1) \| `undefined` #### Example ```ts import { readReferenceRange } from "@cosyte/ccda"; const range = readReferenceRange(resultObs, ctx); ``` *** ### readTemplateStamp() > **readTemplateStamp**(`extension`): [`TemplateStampReading`](#templatestampreading) Read a document-level `templateId/@extension` into a [TemplateStampReading](#templatestampreading). `undefined` (the attribute absent, which is how `attr` reports an empty one too) reads `unstamped`; the R2.1 stamp reads `r21-stamped`; anything else reads `unmodeled-release`. Total and deterministic: every input lands in exactly one of the three. #### Parameters ##### extension `string` \| `undefined` #### Returns [`TemplateStampReading`](#templatestampreading) #### Example ```ts import { readTemplateStamp } from "@cosyte/ccda"; readTemplateStamp(undefined); // "unstamped" readTemplateStamp("2015-08-01"); // "r21-stamped" readTemplateStamp("2024-05-01"); // "unmodeled-release" ``` *** ### releaseForTemplateExtension() > **releaseForTemplateExtension**(`extension`): `"R2.1"` \| `"R3.0 or later"` \| `undefined` The C-CDA release a document-template `@extension` names, or `undefined` when the value is not a member of [CCDA\_RELEASE\_STAMPS](#ccda_release_stamps). A **membership test**, never a shape test: the argument may be any string a sender wrote, and only a member is ever returned. #### Parameters ##### extension `string` #### Returns `"R2.1"` \| `"R3.0 or later"` \| `undefined` #### Example ```ts import { releaseForTemplateExtension } from "@cosyte/ccda"; releaseForTemplateExtension("2024-05-01"); // "R3.0 or later" releaseForTemplateExtension("1999-01-01"); // undefined ``` *** ### requiredSectionKeys() > **requiredSectionKeys**(`documentType`, `options?`): readonly `string`[] The catalog section keys a [DocumentType](#documenttype-4) SHALL contain, in a stable order. Returns an empty array when no unconditional in-catalog SHALL section is asserted for that type (see the module note, empty ≠ "no requirements"). #### Parameters ##### documentType [`DocumentType`](#documenttype-4) ##### options? [`RequiredSectionOptions`](#requiredsectionoptions) #### Returns readonly `string`[] #### Example ```ts import { requiredSectionKeys } from "@cosyte/ccda"; requiredSectionKeys("ccd"); // ["allergies", "medications", "problems", "results", "socialHistory", "vitalSigns"] requiredSectionKeys("progressNote"); // [] // An R1.1-origin CCD (no R2.1 stamp) drops the R2.1-scoped keys: requiredSectionKeys("ccd", { r21Stamped: false }); // ["allergies", "medications", "problems", "results"] // A CCD stamped for a release this package does not model asserts NOTHING, // and it is not the R1.1-origin reduction: requiredSectionKeys("ccd", { stamp: "unmodeled-release" }); // [] ``` *** ### requiredSectionStatus() > **requiredSectionStatus**(`documentType`, `options?`): [`RequiredSectionStatus`](#requiredsectionstatus) The required-section obligation of `documentType` **with its verification state attached**: the same asserted keys [requiredSectionKeys](#requiredsectionkeys) returns under the same options, plus how much of the obligation was read off the normative source and what is deliberately left unasserted. `options` narrows `keys` (and the `traced` rows beside them) exactly as [requiredSectionKeys](#requiredsectionkeys) does, and sets `evaluation`. It does **not** move `verification`, nor `unasserted`, nor `source`: those record what was read about the type, not what a particular document is asserted against. #### Parameters ##### documentType [`DocumentType`](#documenttype-4) ##### options? [`RequiredSectionOptions`](#requiredsectionoptions) #### Returns [`RequiredSectionStatus`](#requiredsectionstatus) #### Example ```ts import { requiredSectionStatus } from "@cosyte/ccda"; requiredSectionStatus("unstructuredDocument").verification; // "not-applicable" requiredSectionStatus("ccd").verification; // "traced-complete" requiredSectionStatus("ccd").source?.revision; // "2025-09-08" requiredSectionStatus("consultationNote", { r21Stamped: false }).keys; // [] requiredSectionStatus("ccd", { stamp: "unmodeled-release" }).evaluation; // "not-evaluated" ``` *** ### requiredSectionStatuses() > **requiredSectionStatuses**(`options?`): readonly [`RequiredSectionStatus`](#requiredsectionstatus)[] Every recognized document type's [RequiredSectionStatus](#requiredsectionstatus), in [DOCUMENT\_TYPES](#document_types) order. The enumeration a consumer uses to see the whole picture at once: twelve entries, each carrying exactly one verification state. #### Parameters ##### options? [`RequiredSectionOptions`](#requiredsectionoptions) #### Returns readonly [`RequiredSectionStatus`](#requiredsectionstatus)[] #### Example ```ts import { requiredSectionStatuses } from "@cosyte/ccda"; requiredSectionStatuses().filter((s) => s.verification === "untraced").length; // 0 ``` *** ### resolveLimits() > **resolveLimits**(`overrides?`): [`ResolvedLimits`](#resolvedlimits) Merge caller-supplied limit overrides over [DEFAULT\_LIMITS](#default_limits). Honors `exactOptionalPropertyTypes`, an omitted override key falls back to the default rather than producing `undefined`. #### Parameters ##### overrides? [`CcdaParseLimits`](#ccdaparselimits) #### Returns [`ResolvedLimits`](#resolvedlimits) #### Example ```ts import { resolveLimits } from "@cosyte/ccda"; const limits = resolveLimits({ maxDepth: 200 }); console.log(limits.maxDepth); // 200 ``` *** ### sectionForLoinc() > **sectionForLoinc**(`loinc`): [`SectionInfo`](#sectioninfo) \| `undefined` Resolve a section LOINC `code` to its [SectionInfo](#sectioninfo), or `undefined` when unrecognized. This is the fallback section-recognition path used when no recognized `templateId` is present. #### Parameters ##### loinc `string` #### Returns [`SectionInfo`](#sectioninfo) \| `undefined` #### Example ```ts import { sectionForLoinc } from "@cosyte/ccda"; sectionForLoinc("11450-4")?.key; // "problems" ``` *** ### sectionForTemplateRoot() > **sectionForTemplateRoot**(`root`): [`SectionInfo`](#sectioninfo) \| `undefined` Resolve a section `templateId` root OID to its [SectionInfo](#sectioninfo), or `undefined` when unrecognized. This is the primary section-recognition path. #### Parameters ##### root `string` #### Returns [`SectionInfo`](#sectioninfo) \| `undefined` #### Example ```ts import { sectionForTemplateRoot } from "@cosyte/ccda"; sectionForTemplateRoot("2.16.840.1.113883.10.20.22.2.6.1")?.key; // "allergies" ``` *** ### semanticCodeInvalid() > **semanticCodeInvalid**(`position`, `slot`): [`CcdaWarning`](#ccdawarning) Build a `SEMANTIC_CODE_INVALID` warning. Emitted only when a consumer-supplied bring-your-own [TerminologyAdapter](#terminologyadapter) reports (via `validateCode`) that a coded value is **not** a valid, active member of its code system, the semantic validation tier structural recognition cannot reach without a licensed terminology. The code is **preserved verbatim** (never coerced to a "corrected" value); this surfaces the adapter's negative verdict so a structurally-valid but wrong code, the highest-severity real-world defect, is not silently trusted. The message names the [CodeSlot](#codeslot) and nothing else. It used to carry the observed `@codeSystem` OID as a "structural identifier"; a sender controls that attribute exactly as it controls the code beside it, so it is gone. The adapter's own `message` has never been interpolated and still is not: it is consumer text about a coded value. #### Parameters ##### position [`CcdaPosition`](#ccdaposition) ##### slot `"problem"` \| `"medication"` \| `"allergen"` \| `"route"` \| `"vaccine"` #### Returns [`CcdaWarning`](#ccdawarning) #### Example ```ts import { semanticCodeInvalid } from "@cosyte/ccda"; const w = semanticCodeInvalid({ path: "value" }, "problem"); ``` *** ### serializeCcda() > **serializeCcda**(`doc`): `string` Serialize a parsed [CcdaDocument](#ccdadocument) back to spec-clean C-CDA XML. The output is the faithful re-emission of the document the parser read, no silent loss of unmodeled content, with a guaranteed XML declaration. Serialization is a fixed point: `parseCcda(serializeCcda(doc))` re-serializes to the identical string. Equivalent to `doc.toString()`. #### Parameters ##### doc [`CcdaDocument`](#ccdadocument) A document produced by [parseCcda](#parseccda). #### Returns `string` The spec-clean XML text. #### Throws If `doc` was hand-constructed (neither produced by `parseCcda` nor by `buildCcda`) and therefore retains no source document to emit. To construct a document from scratch, use `buildCcda`, whose result is the parse of the XML it emitted and so serializes normally. #### Example ```ts import { parseCcda, serializeCcda } from "@cosyte/ccda"; const doc = parseCcda(xml); const xmlOut = serializeCcda(doc); // spec-clean, declaration-prefixed ``` *** ### setDefaultCcdaProfile() > **setDefaultCcdaProfile**(`profile`): `void` Register a process-scoped default profile that `parseCcda(raw)` applies when no explicit `profile` option is passed. Pass `null` (or `undefined`) to clear. An explicit `parseCcda(raw, { profile })` always wins; `{ profile: null }` opts out of the default for a single call. **Test hygiene:** the only mutable module-scoped state here, tests that call this MUST clear it in teardown or default-profile bleed infects later tests. #### Parameters ##### profile [`CcdaProfile`](#ccdaprofile) \| `null` #### Returns `void` #### Example ```ts import { setDefaultCcdaProfile, ccdaProfiles, parseCcda } from "@cosyte/ccda"; setDefaultCcdaProfile(ccdaProfiles.legacyR11); const doc = parseCcda(xml); // uses legacyR11 setDefaultCcdaProfile(null); // clear ``` *** ### text() > **text**(`el`): `string` \| `undefined` Concatenated text content of an element with leading/trailing whitespace trimmed, or `undefined` when there is no non-whitespace text. Does not decode or interpret embedded base64, base64 stays quarantined as inert text. #### Parameters ##### el `Element` #### Returns `string` \| `undefined` #### Example ```ts import { text } from "@cosyte/ccda"; text(titleEl); // "Allergies" | undefined ``` *** ### toDate() > **toDate**(`value`, `options?`): `Date` \| `undefined` The absolute instant the value denotes, **only when the zone is determinate**. - the value carries an explicit offset: the instant from **that** offset, and `options.assumeOffsetMinutes` is ignored; - no offset and `assumeOffsetMinutes` supplied: that offset is applied, an explicit `0` meaning "treat this naive value as UTC"; - no offset and no option: `undefined`. The host timezone is never read, UTC is never assumed, and no `Date` is returned. This is where the surface parts company with `TS.date`, which resolves the same value to UTC. An `assumeOffsetMinutes` that names no usable zone is refused the same way, with `undefined`: `NaN` and the two infinities are not a number of minutes, and a finite offset large enough to push the result outside the range a JS `Date` holds does not denote an instant either. **What never comes back is an `Invalid Date`**, which satisfies the `Date | undefined` return type and defeats its point: a caller cannot tell one from a real `Date` without testing `getTime()` for `NaN`, and `toISOString()` on it throws. Every sibling `@cosyte/*` parser answers `undefined` on exactly these inputs. A **stated** offset is bounded too, and more tightly: a value writing an offset wider than 23 hours 59 minutes is refused whole, by all three functions, because the shared `+HH:MM` slot cannot state it. `TS.date` resolves such a value anyway, so this is a second place the two deliberately part company. Components below the stated precision fill to their lowest legal value (month to 1, day to 1, time to 0) **for instant construction only**: the value's own precision is untouched, and [toObject](#toobject) / [toISO](#toiso) answer exactly as they did before the call. A four-digit year below 100 stays that year, so `"00500101"` is year 50 and never 1950. Never throws. #### Parameters ##### value [`TS`](#ts) \| `null` \| `undefined` ##### options? [`ToDateOptions`](#todateoptions) #### Returns `Date` \| `undefined` #### Example ```ts import { toDate, type TS } from "@cosyte/ccda"; const ts: TS = { raw: "20260628" }; toDate(ts); // => undefined, because the document stated no zone toDate(ts, { assumeOffsetMinutes: 0 }); // => 2026-06-28T00:00:00.000Z toDate(ts, { assumeOffsetMinutes: -300 }); // => 2026-06-28T05:00:00.000Z ``` *** ### toISO() > **toISO**(`value`): `string` \| `undefined` The value rendered as ISO-8601, truncated to the precision it stated and never padded out. A stated offset is appended, `Z` when it is exactly zero and `+HH:MM` / `-HH:MM` otherwise. When the value carried no offset **nothing** is appended: the string is deliberately zone-less and no `Z` is fabricated. Because a zero offset renders `Z`, this is not a byte round-trip of the wire value and is not meant to be; `serializeCcda` remains the round-tripping route. Returns `undefined` on exactly the inputs [toObject](#toobject) does, and never throws. That includes a value stating an offset wider than 23 hours 59 minutes: the string this function hands back is always one an ISO-8601 reader accepts, so a value whose offset would render `+24:00` or `+100:39` gets no string at all rather than one that denotes nothing. #### Parameters ##### value [`TS`](#ts) \| `null` \| `undefined` #### Returns `string` \| `undefined` #### Example ```ts import { toISO } from "@cosyte/ccda"; toISO({ raw: "202606" }); // => "2026-06" toISO({ raw: "20260628" }); // => "2026-06-28", with no trailing Z toISO({ raw: "20260628153045.5-0500" }); // => "2026-06-28T15:30:45.5-05:00" ``` *** ### toObject() > **toObject**(`value`): [`DateParts`](#dateparts) \| `undefined` The calendar components a `TS` stated, as a frozen [DateParts](#dateparts), with nothing filled in and nothing invented. Returns `undefined` for an absent value, a `TS` carrying no `@value`, a `TS` whose `@nullFlavor` contradicts a populated `@value`, and a `@value` this package parses as malformed. It also returns `undefined` for a value stating an offset wider than 23 hours 59 minutes, the widest the `+HH:MM` slot can state: `offsetMinutes` is signed minutes east of UTC, and a count of minutes a whole day or more from UTC names no zone. Never throws, for any input. #### Parameters ##### value [`TS`](#ts) \| `null` \| `undefined` #### Returns [`DateParts`](#dateparts) \| `undefined` #### Example ```ts import { toObject } from "@cosyte/ccda"; toObject({ raw: "20260628" }); // => { year: 2026, month: 6, day: 28 } toObject({ raw: "20260628153045.5-0500" }); // => { year: 2026, month: 6, day: 28, hour: 15, minute: 30, // second: 45, millisecond: 500, offsetMinutes: -300 } ``` *** ### valueSetBinding() > **valueSetBinding**(`slot`): [`ValueSetBinding`](#valuesetbinding) The value-set binding this package declares for one checked slot: the bound value set, how strongly C-CDA R2.1 binds it, and the artifact and artifact revision the row was read from. #### Parameters ##### slot `"problem"` \| `"medication"` \| `"allergen"` \| `"route"` \| `"vaccine"` #### Returns [`ValueSetBinding`](#valuesetbinding) #### Example ```ts import { valueSetBinding } from "@cosyte/ccda"; valueSetBinding("route").strength; // "required" valueSetBinding("problem").strength; // "preferred" ``` *** ### valueSetBindingNotEvaluated() > **valueSetBindingNotEvaluated**(`position`, `slot`, `valueSet`, `release`): [`CcdaWarning`](#ccdawarning) Build a `VALUE_SET_BINDING_NOT_EVALUATED` warning. Emitted when a consumer-supplied `ValueSetSource` answers that it holds **no expansion** for the value set a slot's Required binding names: the binding was not evaluated, so no non-membership is reported and the absence of a violation says nothing. This is the guard against the confident-wrong failure mode in its purest form. A source that skipped a value set it does not hold, in silence, is indistinguishable from one that checked and found the code conformant. Both string parameters are library- or consumer-owned constants, exactly as in [valueSetBindingViolated](#valuesetbindingviolated); no part of the document reaches the warning. #### Parameters ##### position [`CcdaPosition`](#ccdaposition) ##### slot `"problem"` \| `"medication"` \| `"allergen"` \| `"route"` \| `"vaccine"` ##### valueSet `"2.16.840.1.113883.3.88.12.3221.7.4"` \| `"2.16.840.1.113762.1.4.1010.4"` \| `"2.16.840.1.113762.1.4.1010.1"` \| `"2.16.840.1.113883.3.88.12.3221.8.7"` \| `"2.16.840.1.113762.1.4.1010.6"` ##### release `string` #### Returns [`CcdaWarning`](#ccdawarning) #### Example ```ts import { valueSetBindingNotEvaluated } from "@cosyte/ccda"; const w = valueSetBindingNotEvaluated( { path: "routeCode" }, "route", "2.16.840.1.113883.3.88.12.3221.8.7", "my-vsac-package-2026.1", ); ``` *** ### valueSetBindings() > **valueSetBindings**(): readonly [`ValueSetBinding`](#valuesetbinding)[] Every declared binding, in CODE\_SLOTS order: the whole table a consumer needs to see which slots this package will report against and which value sets their own package has to hold to answer for them. #### Returns readonly [`ValueSetBinding`](#valuesetbinding)[] #### Example ```ts import { valueSetBindings } from "@cosyte/ccda"; valueSetBindings().filter((b) => b.strength === "required").length; // 4 ``` *** ### valueSetBindingViolated() > **valueSetBindingViolated**(`position`, `slot`, `valueSet`, `release`): [`CcdaWarning`](#ccdawarning) Build a `VALUE_SET_BINDING_VIOLATED` warning. Emitted only when a consumer-supplied `ValueSetSource` reports that a coded value is **not** a member of the value set C-CDA R2.1 binds its slot to with a **Required** binding, which the standard states as a SHALL. The code is preserved verbatim and never coerced. **Neither string parameter can carry a document value, and that is the whole reason the factory takes any at all.** `valueSet` is a fixed OID literal from this package's own frozen binding table, and `release` is the label the consumer declared on the source they supplied. Neither is read from the document, neither reaches the message (which comes whole from the frozen registry), and the offending code appears nowhere in the warning. #### Parameters ##### position [`CcdaPosition`](#ccdaposition) ##### slot `"problem"` \| `"medication"` \| `"allergen"` \| `"route"` \| `"vaccine"` ##### valueSet `"2.16.840.1.113883.3.88.12.3221.7.4"` \| `"2.16.840.1.113762.1.4.1010.4"` \| `"2.16.840.1.113762.1.4.1010.1"` \| `"2.16.840.1.113883.3.88.12.3221.8.7"` \| `"2.16.840.1.113762.1.4.1010.6"` ##### release `string` #### Returns [`CcdaWarning`](#ccdawarning) #### Example ```ts import { valueSetBindingViolated } from "@cosyte/ccda"; const w = valueSetBindingViolated( { path: "manufacturedMaterial" }, "medication", "2.16.840.1.113762.1.4.1010.4", "my-vsac-package-2026.1", ); ``` *** ### wrapEmitterWithProfile() > **wrapEmitterWithProfile**(`next`, `profile`): (`warning`) => `void` Wrap a downstream warning sink so every warning first passes through `profile`'s tolerance transform. Returned unchanged (`next`) when `profile` is `undefined`, so the no-profile path pays nothing. #### Parameters ##### next (`warning`) => `void` ##### profile [`CcdaProfile`](#ccdaprofile) \| `undefined` #### Returns (`warning`) => `void` #### Example ```ts import { wrapEmitterWithProfile } from "@cosyte/ccda"; const emit = wrapEmitterWithProfile(baseEmit, activeProfile); ``` *** ### xsiType() > **xsiType**(`el`): `string` \| `undefined` Read the `xsi:type` attribute (namespace-qualified) that selects a concrete HL7 v3 datatype for a polymorphic element. Returns `undefined` when absent. A leading namespace prefix (e.g. `hl7:PQ`) is stripped to the local type name. #### Parameters ##### el `Element` #### Returns `string` \| `undefined` #### Example ```ts import { xsiType } from "@cosyte/ccda"; xsiType(valueEl); // "PQ" | "CD" | ... | undefined ``` --- URL: https://docs.cosyte.com/ccda/troubleshooting Markdown: https://docs.cosyte.com/ccda/troubleshooting.md # Troubleshooting & known limitations `@cosyte/ccda` is built to be **correct and honest about its edges** rather than to claim more than it delivers. Mis-reading a dose, an allergy, a code system, or a patient identifier can cause real clinical harm, so this page is the deliberate "do not over-trust" list: the error model, common symptoms, and, critically, the explicit statement of **what the parser, the builder, and the editor do and do not do today**. Everything here is a documented boundary, not a bug: the lenient parser never silently drops or garbles data; where a limitation applies, the raw value is preserved (often with a warning), it is simply not further decoded. ## When does it throw vs warn? Only the **seven** Tier-3 structural/security conditions throw a `CcdaParseError`; everything else is a Tier-2 warning on `doc.warnings`. ```ts runnable throws import { parseCcda } from "@cosyte/ccda"; // Well-formed XML whose root is not a ClinicalDocument: a structural fatal. parseCcda("hello"); // throws CcdaParseError (NOT_A_CLINICAL_DOCUMENT) ``` | Fatal code (throws) | Meaning | | ------------------------------ | ------------------------------------------------------------ | | `XXE_OR_DTD_PRESENT` | The document declared a DTD or an external entity. | | `ENTITY_EXPANSION_LIMIT` | Too many `&…;` entity references (billion-laughs). | | `INPUT_SIZE_LIMIT_EXCEEDED` | Decoded input exceeds the byte cap. | | `ELEMENT_DEPTH_LIMIT_EXCEEDED` | Element nesting too deep. | | `NODE_COUNT_LIMIT_EXCEEDED` | Too many element nodes. | | `NOT_WELL_FORMED_XML` | The bytes did not parse as XML. | | `NOT_A_CLINICAL_DOCUMENT` | Well-formed, but the root element is not `ClinicalDocument`. | Narrow on the caught error via `err instanceof CcdaParseError` and `err.code === FATAL_CODES.*` (see [Tolerance & the warning model](https://docs.cosyte.com/ccda/spec-notes-tolerance.md)). Everything a real-world EHR does short of that (an unknown section code, a missing dose, a code/narrative mismatch, an unexpected code system, a non-UCUM unit) is a warning you triage, not an exception you catch. ## Common symptoms | Symptom | Likely cause | What to do | | ----------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `documentType` is `undefined` | The root `templateId` set contains no recognized document-type OID (or none at all) | Check for `UNKNOWN_DOCUMENT_TEMPLATE` / `MISSING_TEMPLATE_ID`; the document still parsed as a generic `ClinicalDocument`. | | A section has `key: undefined` | Its `templateId` and LOINC `code` matched nothing in the catalog | `UNKNOWN_SECTION_CODE` was raised; the section is retained as narrative-only: read `section.narrativeText`. | | `getMedications()[0].dose` is `undefined` | The Medication Activity carried no `doseQuantity` | `MISSING_DOSE_QUANTITY` was raised; the value is preserved-as-absent, never defaulted. | | A procedure's `disposition` is `undefined` | The entry had no `moodCode`, or an unrecognized one | `PLANNED_VS_PERFORMED_AMBIGUOUS` / `PROCEDURE_MOOD_UNEXPECTED` was raised; performed and planned are never conflated. | | A `CODE_NARRATIVE_MISMATCH` warning | A coded value and its referenced narrative disagree | Both are preserved and no winner is chosen; route the record to human review. | | A `NON_UCUM_UNIT` / `UCUM_CASE_SUSPECT` warning | The `PQ` `@unit` is not well-formed UCUM (or a case slip) | The raw unit is preserved, never normalized; a case slip is usually a single-character fix. | | `doc.toString()` throws | The document was hand-constructed, not produced by `parseCcda` or `buildCcda` | Only parsed/built documents retain source XML to serialize; construct from scratch with `buildCcda`. | | An entry you can see in the XML is not returned | A `` on that entry, on a statement inside it, or on its section | `SUBJECT_CONTEXT_OVERRIDE` was raised, one per withheld entry: it is somebody else's clinical statement and is withheld from every record-target read path. It still round-trips through `toString()`. | ## Keeping PHI out of logs Every warning `message` and every `CcdaParseError` message comes **whole from a frozen registry**. No factory takes a value parameter, so nothing the document said, and nothing a sender chose, can reach a message: not a template OID, not a `nullFlavor` token, not a unit, not an element name. A diagnostic tells you `code` plus `position`, and that is the entire contract. The `position` is bounded too, rather than copied. Its `path` is an element local name echoed only when it is one this parser navigates (`` otherwise), its `sectionCode` is echoed only when it has the shape of a LOINC part number, and its `templateId` only when it has the shape of an HL7 v3 UID. `line` and `column` still locate the element exactly. So you can log the full `.warnings` array, and a thrown `CcdaParseError`'s `message`, `stack` and `position`, without leaking. Keep the same discipline in your own code: log the code and the position, not the field content. A `CcdaParseError` deliberately retains **no raw input snippet**, precisely because a C-CDA payload is a clinical document and any snippet would risk leaking PHI. **This page said the opposite through `0.0.4`**, claiming messages were PHI-free "by construction" while thirteen warning factories and one fatal interpolated a value straight from the document. A 500,000-byte `templateId` root produced a 500,106-byte `.message`, and under `{ strict: true }` it reached `err.stack`. If you shipped `.warnings` to a log aggregator on any version at or before `0.0.4`, treat those log lines as potentially carrying document content. ## What it does, and does not do, today Depth tracks the code, and never leads it. Everything below is a deliberate boundary rather than a bug. Where a boundary is genuinely open, this page says so instead of resolving it in your favor. ### Reading a document - **All twelve document types are recognized; fourteen entry families are decoded.** `parseCcda` resolves every C-CDA R2.1 US Realm document type from the root `templateId`. Problems, Medications, Allergies, Results, Vital Signs, Immunizations, Procedures, Encounters, Social-History smoking status, Plan of Treatment, Functional Status, Mental Status, Family History, and Past Medical History decode to structured entries. Every other section is framed and its narrative retained, but its entries are not modeled, and recognition of those sections has three outcomes. A catalog section matched on its `templateId` (Advance Directives, Medical Equipment, Payers, Instructions, Nutrition, Goals, Interventions, and the rest) gets a stable `key` and no warning. A catalog section matched only by its LOINC code also gets its `key`, but raises `SECTION_MATCHED_BY_LOINC_FALLBACK`; Reason for Visit and Chief Complaint carry no recognized `templateId` at all, so they always take this path. A section the catalog does not recognize (Physical Exam among them) reads back with `key: undefined` and raises `UNKNOWN_SECTION_CODE`, or, if it carries no section `code` to match on, silently. None of the three drops anything: the narrative and the raw structure are preserved, and the document still re-serializes faithfully. The Hospital Course Section is the one exception to the third outcome: a section carrying its root (`1.3.6.1.4.1.19376.1.5.3.1.3.5`) and matching nothing in the catalog gets `key: "hospitalCourse"` and no warning. It is matched by that root only, never by its LOINC code `8648-8`, so a section carrying the code without the root still raises `UNKNOWN_SECTION_CODE`. - **An entry another subject governs is withheld, not attributed.** CDA R2 makes `Section.subject` the "primary target of the entries recorded in a section" and C-CDA admits the same override on a clinical statement, so a document can carry a relative's or a donor's statement inside the patient's document. Wherever a `` governs a top-level `` (its own, one on a statement nested inside it, or one on an enclosing section) that WHOLE entry is withheld from every read path documented as the record target's own data, and `SUBJECT_CONTEXT_OVERRIDE` is raised once for it, naming the entry's bounded locus. The warning is safety-critical, so no profile can quiet it, and in strict mode it throws like any other. **Presence is the trigger**: nothing the declaration names is compared with the patient, so a document that redundantly restates the patient as an entry subject loses those entries too, which is the safe direction of that error. Two things are deliberately unaffected: the entry re-serializes byte for byte through `toString()` (the only way to reach it), and `getFamilyHistory()` returns what it always did, because a Family History Organizer's own `` slot is that template's mechanism for naming the relative rather than a context override. - **Code checks are recognition, not membership, unless you supply an adapter.** `checkCodeSlot` / `checkLoincDeprecation` verify that a code's _system_ is the one expected for its slot (and flag deprecated or unexpected systems); on their own they do **not** verify that a code is a real, active member of SNOMED CT / RxNorm / LOINC. That tier needs a licensed terminology service, so it is bring-your-own: pass a `TerminologyAdapter` to `parseCcda` or `buildCcda` and it is consulted at the five recognized coded slots (`problem`, `medication`, `allergen`, `route`, `vaccine`). A code the adapter rejects is surfaced **verbatim** and flagged `SEMANTIC_CODE_INVALID`, never rewritten to a "corrected" value; an adapter returning `undefined` has no opinion and produces no warning. With no adapter, behavior is recognition-only. A value at one of those slots that asserts a `@code` with **no** `@codeSystem` is flagged `MISSING_CODE_SYSTEM` and is never handed to your adapter (which validates a system + code pair): the parser flags it rather than inferring a system, because a code without its system is not a code. The mirror shape, a slot that is present but asserts no usable `@code` **and** declares no `@nullFlavor`, is flagged `MISSING_CODE_VALUE`. A `nullFlavor`-only value stays silent (a complete statement of "unknown"), and so does an absent element, there is nothing to judge. **Those five slots are the whole of it, so read a silent document carefully.** Every other coded value is never handed to your adapter and therefore can never raise `SEMANTIC_CODE_INVALID`: the Results and Vital Signs LOINC codes, the procedure, encounter, and family-history codes, the planned-item codes for the five variants whose `code` is the planned act (the other two are exceptions: a planned **medication**'s `code` is the drug and a planned **immunization**'s is the vaccine, so those are checked at the `medication` and `vaccine` slots like any other), the smoking-status, functional-status, and mental-status observation values, the allergy propensity type, and the reaction, severity, and criticality observations. Within the five, the checks apply to the slot's **primary** coding; alternate codings carried in `` are preserved and re-serialized but are not themselves slot-checked. A clean run means the five checked slots passed, not that the document's terminology was verified. - **A `nullFlavor` beside a value is a contradiction, and the parser resolves it against the number.** `` asserts both "unknown" and "10 mg". Every v3 datatype flags this `CONTRADICTORY_NULL_FLAVOR` (safety-critical, so no profile can quiet it), including the `INT` and `ST` arms of an observation ``, and on `PQ`, `TS` and an `integer` observation value the derived reading is **withheld**: `dose.value` is `undefined`, while `dose.raw` (`"10"`), `dose.unit` and `dose.nullFlavor` are all preserved. If you were reading `.value` and now see `undefined`, that is the fix, and the document is telling you the value is unknown. On `CD`, `II`, `ST`, `ED` and `BL` the field is the document's own text with no verbatim copy behind it, so it is **kept** and the warning is the signal, read `doc.warnings`. Metadata beside a `nullFlavor` (a `@unit` with no `@value`, an `@root` with no `@extension`, a `CD`'s `originalText` / ``) is coherent, not contradictory, and stays silent, and a `nullFlavor`-only coded slot is still checked for a wrong or deprecated `@codeSystem`. - **`getMrn()` withholds on a `nullFlavor`-marked ``.** If you were getting an MRN and now get `undefined`, the first `patientRole/id` declared itself unknown. That is the one place this model manufactures an identifier out of an `II`: `pickMrn` _selects_ one id from a list and flattens it to a bare string with the marking gone, so the selection declines while the datatype keeps everything. It does **not** fall through to the next ``: nothing in a C-CDA ranks `patientRole/id` entries, so the second one is whatever the sender listed second, often a member number, an account number, or the SSN. Read `doc.getPatient()?.identifiers` for the verbatim `extension` with its `nullFlavor` beside it, and branch on `root` yourself if you know your assigning authority's OIDs. The other identity slots (document `id`, `setId`, `parentDocument/id`, entry-level ids) are unchanged and still report the document verbatim; `templateId` is the stated exception, a null-marked one still resolves the document type, deliberately, since it asserts a document shape rather than a person. On the emit side, `editCcda` refuses to stamp an `RPLC` revision from a null-marked `ClinicalDocument.id` (`CcdaEditError` `SOURCE_MISSING_ID`), because copying `root`/`extension` into the `parentDocument` would drop the marking. - **A medication or vaccine product is read from either `ManufacturedProduct` arm.** `manufacturedMaterial` is silent; `manufacturedLabeledDrug` is read too and flagged `MEDICATION_PRODUCT_ARM_UNEXPECTED`. That one is tolerable by a profile, and the reason is conditional rather than absolute: wherever it fires **alone** a `` element was selected and read exactly as a single-arm document's would have been, and wherever **no product identity comes back** it is **not** alone, because `MEDICATION_PRODUCT_ARM_CONFLICT` (the arms disagreed), `MISSING_PRODUCT_CODE` (no arm carried a `` at all, which is what a name-only `LabeledDrug` produces) or `MISSING_CODE_VALUE` (an element was selected and asserts neither a symbol nor a `nullFlavor`) fires beside it, and all three are safety-critical and unquietable by a profile. The third was missing from this list until `0.0.3`: it covered the shapes where _selection_ failed, not every shape where _identity_ is absent. So tolerating the presence warning can never buy silence about an absent or withheld drug. No arm yielding a code is `MISSING_PRODUCT_CODE` and is safety-critical, because dose, route and timing all survive a missing consumable and would otherwise make the record read as a complete medication with no drug. Both apply at every consumable call site: a performed Medication Activity, an Immunization Activity, a Planned Medication Activity, and a Planned Immunization Activity. A document whose arms name **different** products draws `MEDICATION_PRODUCT_ARM_CONFLICT` (safety-critical) and no product code is selected at all: two drugs on one medication is a contradictory document, nothing in it ranks the arms, and picking one would be the parser inventing an answer. That covers both arms of the choice **and a repeated arm of one kind** (two sibling `manufacturedMaterial`s naming different drugs is the same silent pick). If `med.drug` is `undefined` and you see this code, read `doc.toString()`, every arm survives serialization byte-for-byte. `MISSING_PRODUCT_CODE` is suppressed behind it because "no arm yielded a code" would be false, and so are the code-system checks, since there is no code to check, which is why the conflict warning is safety-critical. Arms naming the **same** product are redundant rather than contradictory, so the material arm is read as before. An arm asserting no symbol at all (a `nullFlavor`-only `` with no ``, or no ``) names no product and never conflicts with one that does, so whichever arm names the drug is read. - **An arm that asserts no `@code` names what its `` alternates name.** If you have a `nullFlavor="OTH"` arm whose `` carries the real RxNorm code, that arm now names a drug: where it disagrees with the other arm you will get `MEDICATION_PRODUCT_ARM_CONFLICT` and `med.drug === undefined` where you previously got the other arm's code in silence. **The translations are a fallback, never an addition**, so two arms that both assert a `@code` are compared on those and nothing else, exactly as before. That direction is deliberate: a translation the two arms happen to share is routinely coarser than either primary (an RxNorm ingredient, a local formulary id, an NDC spanning presentations), so counting it would let two arms naming two strengths of one drug agree, and one strength would be handed back. Reading translations can only make the warning fire more, never less. **Selection is separate and unchanged**: the element handed to the code-system and terminology checks is still chosen on the primary `@code`, and a coding is never lifted out of a `` to stand in as the product, because translations are preserved but not slot-checked. **Where BOTH arms fall back to translations, sharing one coding is not always enough to agree**: that is the one pairing where a shared coarser coding can still hold together two arms naming two products, so they also conflict when each names a coding the other does not and two of those unshared codings are in the same code system under different symbols. An arm that merely offers an extra alternate the other stayed quiet about (an NDC beside the RxNorm concept both share) is elaborating its own concept, not denying the other's, and is not a conflict; codings in different code systems are never compared, because that is terminology work. That last test is a parser's reading rather than something the document asserts, and it deliberately over-fires: two NDC package codes can describe one drug, and an RxNorm branded drug and its clinical equivalent are one product at two granularities, but telling those apart is the terminology work above. Over-firing costs a withheld product beside a loud code; under-firing costs one of two strengths handed back in silence. **On the newly-refused shapes `med.drug` is `undefined` with the conflict code beside it.** If the two fallback arms were the only ones you previously got the first arm's `nullFlavor` `CD` and that arm's translations, and `med.drug?.code` was `undefined` either way. If a **third** arm asserts a primary the two fallback arms disagree behind, you previously got that arm's fully coded `CD`, so a code you were reading does go away: the document names one product in a primary and two others in translations, and the parser now declines to pick. Read `doc.toString()` for every arm verbatim. - **A product named only in a `` now says so (`MEDICATION_PRODUCT_CODE_TRANSLATION_ONLY`, safety-critical).** If no arm asserts a primary `@code` and one of them carries the real coding in a ``, you get this code and `med.drug?.code === undefined`. That is not a change of reading, it is the same reading with the silence removed. **Where to find the coding depends on which arm holds it, and the warning's message and `position` tell you.** Only one arm ever becomes `med.drug`: when that is the arm carrying the translation, search `med.drug?.translation` for it, beside the `nullFlavor` the document wrote; do not just read `[0]`, since a `` may carry several ``s and the first can be `nullFlavor`-marked or in a code system you did not want. When it is not (two arms, neither asserting a primary, the translation on the one that was not selected), `med.drug` is the other arm's `CD` and no product-naming coding is on it, so read `doc.toString()`, which re-emits every arm byte-for-byte. No profile may quiet it: on the `nullFlavor`-marked idiom nothing else fires at all (`MISSING_PRODUCT_CODE` would be false, an arm did carry a ``, and the code-system checks are silent by design on a slot whose only assertion is a `nullFlavor`), and on the variant that asserts neither a symbol nor a `nullFlavor` the companion is `MISSING_CODE_VALUE`, which is itself safety-critical, at every consumable call site alike: a performed medication, an immunization, a planned medication's drug, and a planned vaccination's vaccine (see the Plan of Treatment bullet below). Behind `MEDICATION_PRODUCT_ARM_CONFLICT` it stands down, the same way `MISSING_PRODUCT_CODE` does. **Its precondition is about each arm's LEAD ``, not the arm**: selection reads the first `` on an arm and no other, so an arm whose _second_ `` asserts a primary is still a slot with no selected product and still draws this code, with `MEDICATION_PRODUCT_CODE_REPEATED` beside it. The message said "no arm asserts a primary `@code`" and called the translation the _only_ place the product was named until `0.0.3`; both were false on precisely that shape. - **A repeated arm is reported even when the repeats agree (`MEDICATION_PRODUCT_ARM_REPEATED`).** `ManufacturedProduct` models a choice of one participant, so two `manufacturedMaterial`s (or two `manufacturedLabeledDrug`s) is outside the model whatever they say. Disagreeing repeats are still refused; agreeing repeats used to be reduced to one silently, so a document asserting the same product three times looked exactly like one asserting it once. It is keyed to the arms rather than to their codings, so an arm with no `` counts, and it is tolerable by a profile: where it fires alone a `` was selected and read exactly as any single-arm document's would be, and each state where that would not be enough carries an unquietable companion (`MEDICATION_PRODUCT_ARM_CONFLICT`, `MISSING_PRODUCT_CODE`, `MEDICATION_PRODUCT_CODE_TRANSLATION_ONLY`, or `MISSING_CODE_VALUE` where the selected `` asserts neither a symbol nor a `nullFlavor`, which is exactly what two empty-`` material arms produce). That fourth one was missing from this list until `0.0.3`. - **A repeated `` on ONE arm is reported too, and its second `` is no longer dropped from the comparison (`MEDICATION_PRODUCT_CODE_REPEATED`, safety-critical).** `Material.code` and `LabeledDrug.code` are each at most one in CDA R2, and only the first was ever read, so an arm writing an RxNorm code for Lisinopril and a second one for Aspirin handed back Lisinopril and said nothing at all. Every `` on every arm now reaches the comparison, so that document raises `MEDICATION_PRODUCT_ARM_CONFLICT` and `med.drug` is `undefined`. This code is reported **per arm**, and its `position` names which arm carries the repeat, so two offending arms give you two warnings at two places rather than one that points at only one of them. **What you read does not change**: selection still takes each arm's first `` and nothing else, deliberately, because a newly-visible sibling sits earlier in document order and would displace an equally-symboled but richer coding, taking `CODE_NARRATIVE_MISMATCH`, `MISSING_CODE_VALUE`, or a `` list with it in exchange for a symbol that was already identical. **No profile can quiet this one**, and that is why: on the shape where the arm's first `` asserts a `nullFlavor` and its sibling names the drug, `med.drug?.code` is `undefined` over a document that names the drug one element along, nothing else fires, and this is the only signal. Read `doc.toString()` for the sibling verbatim. On a benign identical repeat it costs you a warning over a document that lost nothing; that over-fire is the price of not letting what the codings say decide whether the cardinality gets named at all. - **A Planned Medication Activity's `code` is the drug, not the `substanceAdministration`'s own ``.** If you read `getPlannedItems()` and a `medicationActivity` entry came back with a code that is not a drug (a SNOMED CT "Administration of drug or medicament", say) you were on `0.0.2` or earlier: the direct `` was preferred whenever it was present, and only a planned medication without one ever read its `consumable`. CDA R2 makes `SubstanceAdministration.code` an `ActSubstanceAdministrationCode`, the **kind of administration act**, while the substance participates through `consumable/manufacturedProduct`, so that code was never a drug and reading it into the drug slot short-circuited the entire product path. **Every product warning on this page was unreachable on that variant**, `MEDICATION_PRODUCT_ARM_CONFLICT` above all, so a planned medication whose two `manufacturedProduct` arms named two different drugs was reported with no warning of any kind, on the section that says what a patient is about to be given. `CODE_NARRATIVE_MISMATCH` was reachable but useless there, comparing an act type's label against a narrative that names a drug, so it fired on well-formed documents and stayed quiet about the structured drug contradicting the narrative. All of that is fixed in `0.0.3`. The act `` is not on the model for this variant, as it is not for a performed Medication Activity or an Immunization Activity; read `doc.toString()` if you need it. **The code-system and terminology checks reach it too, as of `0.0.3`.** Because that `code` is the drug, it is checked against the `medication` binding exactly as a performed Medication Activity's `drug` is, so `MISSING_CODE_VALUE`, `MISSING_CODE_SYSTEM`, `UNEXPECTED_CODE_SYSTEM` and (with a `TerminologyAdapter`) `SEMANTIC_CODE_INVALID` fire on a planned drug, code for code with its performed twin. On `0.0.2` and earlier none of them could: a planned drug asserting a `@code` with no `@codeSystem`, an empty ``, or an OID outside RxNorm/NDC came back read and unremarked. `DEPRECATED_CODE_SYSTEM` is the one that does not apply, and it does not apply to a performed drug either: the `medication` binding declares no deprecated systems, so an ICD-9-CM OID on a drug is `UNEXPECTED_CODE_SYSTEM` in both places. **What that changed for the tolerable product codes.** `MEDICATION_PRODUCT_ARM_UNEXPECTED` and `MEDICATION_PRODUCT_ARM_REPEATED` are profile-tolerable, and their tolerability rests on an unquietable companion firing wherever no `` was selected and read normally. On the empty-`` shape that companion is `MISSING_CODE_VALUE`, which could not fire here, so on `0.0.2` a planned medication whose single arm was `` had no drug identity at all and drew `MEDICATION_PRODUCT_ARM_UNEXPECTED` alone, and two empty-`` material arms drew `MEDICATION_PRODUCT_ARM_REPEATED` alone. Apply a vendor profile, filter the expected noise as this documentation elsewhere suggests, and that document reached you silent. Both shapes now carry `MISSING_CODE_VALUE`, which no profile can quiet. **The other five planned kinds are still not slot-checked.** That is a **choice**, and it used to be stated as a necessity ("binding them would mean inventing a value set"), which is true only of the system checks: `MISSING_CODE_VALUE` and `MISSING_CODE_SYSTEM` are raised before any binding is read, so they could be raised on an act-coded planned `` without citing a value set. They are not, deliberately, and changing that would be its own decision. **So do not treat a quiet `getPlannedItems()` as a checked one**: test `item.code?.code`, not `item.code`. - **A Planned Immunization Activity used to vanish entirely, and that was the sharpest form of this family.** On `0.0.2` and earlier a Plan of Treatment entry carrying the Planned Immunization Activity template (`…22.4.120`) matched nothing: `getPlannedItems()` returned **no item for it at all** and raised **no warning**, so reading that list to answer "what is this patient scheduled to receive" gave you a clean, warning-free answer with a scheduled vaccination missing from it. There was no `undefined` to test for and no code to filter on, and the document round-tripped byte-for-byte through `serializeCcda`, so nothing looked wrong. As of `0.0.3` it comes back as `kind: "immunizationActivity"`, and, exactly like a planned medication, its `code` is the **product from the `consumable`** rather than the `substanceAdministration`'s own ``, so every product warning on this page applies to it. It is slot-checked against the **`vaccine`** binding, which is **CVX only**: an NDC-coded planned vaccine draws `UNEXPECTED_CODE_SYSTEM` where an NDC-coded planned drug does not, because each variant matches its own performed twin rather than the other variant. If you kept a workaround that read these entries out of `doc.toString()`, you can drop it. **What `getPlannedItems()` returns is still not everything a Plan of Treatment section can hold**: the section admits eleven entry templates and this returns seven. The four it does not return are Instruction (`…22.4.20`), Handoff Communication Participants (`…22.4.141`), Nutrition Recommendation (`…22.4.130`) and Goal Observation (`…22.4.121`); a Goal Observation is `moodCode="GOL"`, which this parser calls neither performed nor planned. Their narrative and structure are preserved and they re-serialize faithfully, and they are still not planned items. **Three of the four are no longer silent**: Instruction, Handoff Communication Participants and Nutrition Recommendation each draw `PLAN_ENTRY_NOT_MODELED` where planned items are read, once per matching root, so a consumer has a code to filter on instead of an entry that vanishes **in the places the report covers**. Reporting is not modelling: nothing about the returned list changed. A direct entry is reported in **two** sections, Plan of Treatment and the **Interventions Section (V3)** (`…21.2.3`, which admits a Handoff as a direct entry in as many words, CONF:1198-32402 / 1198-32403). That citation is V3's, while the matching is this library's usual section recognition and so is wider: templateId root first, LOINC `62387-6` as the fallback, no `@extension` check. An Instruction in the Instructions Section (`…22.2.45`), where it is that section's own required entry, still draws nothing. The nested half is reported wherever the Planned Intervention Act sits. **That scope is a bound this library chose, not a catalog of which sections C-CDA admits these templates in: they appear in more places than it covers, and an occurrence outside it is still dropped in silence.** Two such places are measured and pinned: a Handoff nested in an **Intervention Act** (`…22.4.131`) is silent because that act is not a container this library descends into, and a direct entry of a section this catalog recognizes as nothing is silent because there is no key to match. **Goal Observation is deliberately not reported**, because the decision taken on it was to model it rather than warn about it. - **A planned entry nested in a Planned Intervention Act was dropped in silence, for all seven kinds (fixed in `0.0.3`).** `getPlannedItems()` used to read an ``'s own act and go no deeper, so a planned flu shot or drug order hanging off a **Planned Intervention Act** (`…22.4.146`, the act that groups the interventions planned toward a goal) came back as no item and no warning, round-tripping byte-for-byte. That was the same silence as the missing Planned Immunization Activity, one markup layer in and across all seven templates. Those entries are now returned, and they read exactly as the same act reads as a direct ``: same `kind`, same `code`, same slot check, same product warnings. If you kept a workaround that read intervention components out of `doc.toString()`, you can drop it. **Three bounds still apply.** A pointer is never followed: the container's `typeCode="RSON"` relationship holds an Entry Reference (`…22.4.122`) naming a goal recorded elsewhere, and that is stepped over rather than resolved. **It is the only container descended into, and C-CDA has others, so nesting is not solved in general.** A Nutrition Recommendation (`…22.4.130`) inline-holds six of the seven planned templates, and an Intervention Act (`…22.4.131`) inline-holds a Planned Intervention Act; a planned entry in either still comes back as nothing, exactly as it did before. What changed is only that the Nutrition Recommendation **container** now draws `PLAN_ENTRY_NOT_MODELED` (it is one of the three named above); what it holds is still not reached, and the Intervention Act is still silent end to end. Read `doc.toString()` if your senders nest that way. A returned item does not say whether it was direct or nested, either: the intervention itself is not modelled, so the grouping toward the goal is available only from `doc.toString()`. - **UCUM validation is grammatical, on a curated atom subset.** The validator checks well-formed UCUM against the prefixes/atoms that appear in lab Results and Vital Signs, not the full UCUM registry. A valid-but-uncurated atom may read as `NON_UCUM_UNIT`; the raw unit is always preserved. It does not convert or dimension-check units. - **`nonXMLBody` base64 is left inert.** An Unstructured Document's wrapped payload is exposed but never decoded: decoding an arbitrary embedded blob is a needless attack surface and a PHI decision the caller owns. - **Vendor profiles only quiet benign noise.** `parseCcda(xml, { profile })` (or a process default via `setDefaultCcdaProfile`) applies a `CcdaProfile` that downgrades the non-safety-critical deviations it expects to `PROFILE_QUIRK_APPLIED` (flagged `expected`, carrying the original `toleratedCode`). It never drops a warning and never touches an extracted value. A profile **cannot** tolerate a safety-critical warning (dose, unit, allergen, identity, wrong or missing code system, malformed datetime, …); `defineCcdaProfile()` throws if you try. Built-ins: `ccdaProfiles.smartScorecard` (deprecated terminology) and `ccdaProfiles.legacyR11` (R1.1-origin structural tolerance), each with cited provenance, plus `ccdaProfiles.default`, which tolerates nothing and is identical to passing no profile at all. There is no named per-vendor profile: one is added only when a real, de-identified document from that vendor grounds the quirk, never from an invented one. - **The required-section (SHALL) table is conservative, and every type reports how far it was verified.** It asserts only unconditional, in-catalog, high-confidence SHALL constraints; it omits choice constraints (`SHALL contain A OR B`), SHOULD/MAY sections, and SHALL sections outside the recognized catalog. `requiredSectionStatus(documentType)` returns the asserted keys with a `verification` state, so an empty set is never ambiguous: - **`traced-complete`** for CCD and Care Plan: every SHALL section the source names for those two is in this parser's catalog and is asserted, so `status.unasserted` is empty because there is nothing left over. - **`traced-partial`** for the other nine structured types. Their SHALL sections were read off the normative C-CDA R2.1 source, and one or more of them cannot be asserted. Consultation Note asserts History of Present Illness (CONF:1198-28907), Allergies (-28911) and Problems (-28929), each scoped to the R2.1 stamp its constraint carries. Progress Note, Procedure Note, Operative Note and Diagnostic Imaging Report assert **nothing**, and that is a traced result rather than an unread table: every SHALL section their templates name is either outside this parser's recognized catalog (Complications, Procedure Description, Anesthesia, Findings (DIR) and the rest) or a choice. Each one is listed by name in `status.unasserted` with the reason. - **`not-applicable`** for Unstructured Document: its component SHALL be a `nonXMLBody` (CONF:1198-31086), so it carries no section to require and none can be missing. - **`untraced`** is reported by no recognized type. It stays in the vocabulary as the honest state for a type whose obligation has not been read. Every status also names the artifact its obligation was read from and that artifact's own revision date (`status.source`), so a reader holding a later revision can tell the table is stale without re-deriving it. This under-warns rather than over-warns: a document of a `traced-partial` type that is missing a section its type requires can still parse clean, with no `REQUIRED_SECTION_MISSING`. Read an empty asserted set through its state, never as "this type has no requirements," and do not treat a quiet parse as a conformance result. ```ts runnable import { requiredSectionKeys, requiredSectionStatus, requiredSectionStatuses, DOCUMENT_TYPES, } from "@cosyte/ccda"; DOCUMENT_TYPES.length; // => 12 requiredSectionStatuses().length; // => 12 requiredSectionStatuses().filter((s) => s.verification === "untraced").length; // => 0 const consult = requiredSectionStatus("consultationNote"); consult.verification; // => "traced-partial" consult.keys.join(","); // => "historyOfPresentIllness,allergies,problems" consult.traced[0]?.conformanceId; // => "CONF:1198-28907" consult.unasserted[0]?.reason; // => "not-unconditionally-required" // An empty asserted set, two different readings, two different values. requiredSectionStatus("operativeNote").keys.length; // => 0 requiredSectionStatus("operativeNote").verification; // => "traced-partial" requiredSectionStatus("operativeNote").unasserted[0]?.reason; // => "outside-section-catalog" requiredSectionStatus("unstructuredDocument").verification; // => "not-applicable" requiredSectionStatus("ccd").verification; // => "traced-complete" // A Discharge Summary requires a Plan of Treatment section and does NOT require // a Discharge Medications section: the source states that one as a SHOULD. requiredSectionKeys("dischargeSummary").includes("planOfTreatment"); // => true requiredSectionKeys("dischargeSummary").includes("dischargeMedications"); // => false requiredSectionStatus("dischargeSummary").source?.revision; // => "2025-09-08" // Hospital Course is recognized when present but not asserted, and says why. requiredSectionKeys("dischargeSummary").includes("hospitalCourse"); // => false requiredSectionStatus("dischargeSummary").unasserted[0]?.reason; // => "assertion-would-tighten-parse" ``` ### Building a document - **`buildCcda` emits three of the twelve document types.** A **CCD** (the default), a **Referral Note** (`documentType: "referralNote"`) and an inpatient **Discharge Summary** (`documentType: "dischargeSummary"`). Any other value throws a `TypeError` rather than emitting something that merely resembles the type you asked for. The other nine types are not implemented. Only an omitted (`undefined`) `documentType` means a CCD: `null`, and any value that is not a string, is refused. - **A clean Discharge Summary build reparses with zero warnings**, like the other two types. Its SHALL set includes the Hospital Course Section (`1.3.6.1.4.1.19376.1.5.3.1.3.5`, LOINC `8648-8`), an IHE PCC template the parser recognizes by its root, so `findSection("hospitalCourse")` resolves. The parser does **not** assert it as required, though: a reparse of a Discharge Summary that lacks it does not report it missing. `pnpm conformance` is the check on its presence. - **Content you supply is emitted or refused, never dropped.** A Discharge Summary emits the `problems` and `medications` you hand it, the medications in the Medications Section (it never emits the Discharge Medications Section, which would assert they are discharge medications). `hospitalCourse`, `dischargeDiagnoses` and `encompassingEncounter` throw a `TypeError` on a CCD or a Referral Note, and `assessment` and `reasonForReferral` throw on a Discharge Summary. A CCD still ignores `assessment` and `reasonForReferral`, as it always has. - **A Discharge Summary's encounter frame is never guessed.** `componentOf/encompassingEncounter` is always emitted for that type, because its template requires it, and each slot the caller did not supply through `encompassingEncounter` (either period bound, the discharge disposition) is emitted `nullFlavor="UNK"` and reparses as absent. None of the three is ever derived from the document `effectiveTime` or from a `documentationOf` service event. - **Which sections a build can carry.** For a CCD, its six SHALL sections are always emitted, as spec-clean empty `nullFlavor="NI"` sections when you supply no content: Problems, Allergies, Medications, Results, Vital Signs, and Social History (smoking status). Immunizations, Procedures, Encounters, Functional Status, Mental Status, Past Medical History, Plan of Treatment, and Family History are emitted only when populated, so an empty one is never fabricated. A Referral Note instead always emits Problems, Allergies, Medications, Reason for Referral, Assessment, and Plan of Treatment, and demotes Results, Vital Signs and Social History to populated-only. A Discharge Summary always emits Allergies, Hospital Course, Discharge Diagnosis and Plan of Treatment, and forces nothing else: neither Problems nor Medications is in its SHALL set, so both are populated-only there. Any C-CDA section outside that set cannot be built. - **Every coded value that reaches the narrative needs a `displayName`, and a missing one is refused.** Each populated section regenerates its `` narrative from the same `BuildCode.displayName` the coded entry carries, and links the two with a ``, so the narrative is the attested restatement a clinician reads. `displayName` is a required field, but the package ships JavaScript and validates at runtime, so `buildCcda` (and `editCcda`, which writes through the same emitter) throws a `TypeError` naming the field path when the label is absent, empty, or not a string. It does **not** substitute a placeholder: any confident stand-in is a sentence the entry does not support. Until `0.0.11` inclusive the allergy slot substituted **"No known allergies"**, so an allergy asserted **positively** could carry a narrative byte-identical to the negated no-known-allergies form, and seven other slots wrote the literal string `undefined`. A `BuildCode` that reaches only the entry (an allergy `type`, a result `interpretation`, a medication `route`) is not covered: `@displayName` is optional on a v3 `CD`, so omitting the attribute states nothing false. - **Every entry carries the `effectiveTime` its template requires.** The caller's time when supplied, else `nullFlavor="UNK"`: the SHALL cardinality is satisfied without a fabricated clinical timestamp, and the parser reads that back as absent, never as a real `Date`. - **`` alternates are emitted only at the recognized coded slots.** If your adapter implements the optional `translate`, `buildCcda` emits each returned coding as a spec-clean CDA R2 `` **beside** the primary code, never replacing it. The wired slots are the problem value, the allergen, the medication drug and route, and the vaccine and route. The Results and Vital Signs LOINC codes, the reaction/severity/criticality observations, and the procedure, encounter, planned-item, and family-history codes are **not** wired. An adapter without `translate`, or one returning no matches, leaves the output byte-identical: an unmapped code never produces a guessed alternate. - **A built document is validated against the normative artifacts, and the result is published.** `pnpm conformance` fetches the C-CDA R2.1 Schematron, its vocabulary file and the CDA R2 XML schema from pinned immutable references, validates every document `buildCcda` emits against the schema and then the Schematron's error-severity phase, and writes `documentation/conformance-report.md`. The measured set is one document per optional section as well as a minimal and a populated document per document type, so a green run is not a green run over a subset of what the builder can emit. The current result is zero error-severity results. **What that does and does not tell you, stated here rather than left to be assumed.** It covers the three document types the builder emits and says nothing about the other nine, which it does not emit. It covers the error-severity phase; the warning phase is not run. It checks value-set membership only where the Schematron's own vocabulary file checks it, so it is not a terminology verification. It is an assessment against a published artifact, **not a certification**, and no accredited body has reviewed it. If your pipeline requires a validation of record, run your own validator over the output. - **A Functional Status Organizer you asked for is missing, and the document says why.** Its R2.1 template SHALL contain a Self-Care Activities (ADL and IADL) observation as well as a Functional Status Observation. Supply `selfCareActivities` on the organizer and it is written with its categorization code and `effectiveTime`; supply none and the findings are written as standalone Functional Status Observations (all present, all read back unchanged) and the returned document carries `MISSING_SELF_CARE_ACTIVITY`. The builder writes neither a fabricated activity nor an organizer claiming a template it does not satisfy. - **`serializeCcda` / `toString()` need a parsed or built document.** `buildCcda` returns the parse of the XML it just emitted, so a built document serializes. A hand-constructed `CcdaDocument` retains no source XML and throws. ### Editing a document - **`editCcda` adds or replaces a whole section, across twelve section kinds.** `problems`, `allergies`, `medications`, `results`, `vitalSigns`, `immunizations`, `procedures`, `encounters`, `socialHistory`, `pastMedicalHistory`, `planOfTreatment`, and `familyHistory`. Functional Status and Mental Status are **buildable but not editable**: each is assembled from three separate content lists, which the single-list edit shape does not fit. The Referral Note's narrative-only Assessment and Reason for Referral sections are likewise not editable. - **No entry-level append, and no section removal.** Adding one problem to an existing Problems section means a `replace` carrying the full entry set, which rebuilds that section from your typed input: anything in the original section you do not carry over, including detail this library does not model, is absent from the result. Every section you did **not** target is still carried through byte-for-byte. A true append that byte-preserves a section's other entries needs a lossless read-model that does not exist yet, and there is no way to remove a section at all. - **`RPLC` only.** An edit stamps a CDA R2 replacement revision (`relatedDocument typeCode="RPLC"` plus `setId` / `versionNumber`), or no revision at all with `revision: false`. The `APND` (append) and `XFRM` (transform) document relationships are not emitted. - **An edit needs a parsed source, and a section edit needs a `structuredBody`.** A hand-constructed document throws `CcdaEditError` (`NO_SOURCE_DOCUMENT`). A document with no `structuredBody`, such as an Unstructured Document, throws `NO_STRUCTURED_BODY` **only when you actually pass a section edit**: with no `sections`, `editCcda` just stamps the revision and returns, so a revision-only edit of an Unstructured Document succeeds. A revision of a source carrying no `ClinicalDocument.id` throws `SOURCE_MISSING_ID` rather than inventing the id the `RPLC` link has to name, and an edit that would drop a SHALL required section throws `REQUIRED_SECTION_MISSING` instead of emitting a non-conformant document. - **On this path the terminology adapter validates but does not translate.** `editCcda` forwards a supplied adapter to its final re-parse, so a rejected code in the edited document is still flagged `SEMANTIC_CODE_INVALID`. It does not emit `` alternates into the section it rebuilds; only `buildCcda` does that. ## Scope (non-goals) - **C-CDA R2.1, US Realm.** Other CDA templates and realms are out of the current scope. - **A parser + serializer, not a transport or a validator suite.** No MLLP/XDS delivery, no Schematron conformance report: this reads and re-emits documents. - **Below 1.0.** `@cosyte/ccda` is at `0.1`, **published on npm** and **public**. Its exported names, options, return shapes and warning codes are the surface we keep stable, and below 1.0 a breaking change bumps the minor version and is called out in the changelog with its migration. Run `npm view @cosyte/ccda version` for the published version. For the exact fields each accessor decodes, see [Core Concepts](https://docs.cosyte.com/ccda/spec-notes-clinical.md); for every export and its signature, see the API reference, which is generated from the source rather than hand-written, so it cannot drift from the shipped code. --- # @cosyte/ncpdp > NCPDP SCRIPT + Telecom parser for pharmacy and e-prescribing. These are the 13 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/ncpdp/intro Markdown: https://docs.cosyte.com/ncpdp/intro.md # @cosyte/ncpdp Parse real-world, vendor-quirky NCPDP and pull fields out in one line, without reading the (paywalled) spec. `@cosyte/ncpdp` is a TypeScript toolkit following the Cosyte parser archetype: a lenient parser, an immutable model, a spec-clean serializer, and a descriptive profile system for vendor quirks. It mirrors the API shape of the reference parser, [`@cosyte/hl7`](https://github.com/cosyte/hl7). > **Status:** `0.1.0`, published to npm. The public API is settled: while the version is below > `1.0.0`, a breaking change ships in a new minor version, never a patch, and the changelog says so. > The **shipped** surface is the SCRIPT read + serialize/build side, the Telecom read (request + response) + serialize/build side, > the shared value vocabulary, and the trading-partner profile system. This documentation is gated to > that surface. Where the parser does not yet do a thing, this site says so rather than promising it. > Streaming, SIG _generation_, and EPCS are explicit non-goals for v1; see > [Troubleshooting & known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md). ## Two standards, one package NCPDP is two structurally unrelated standards under one brand. Each ships as its own subpath export, so a Telecom-only or SCRIPT-only consumer stays lean: - **`@cosyte/ncpdp/script`**, the **SCRIPT** ePrescribing standard (XML): `parseScript`, the `newRx` and lifecycle projections, the response spine, the structured-SIG decode, plus `serializeScript` / `buildNewRx` / `buildScriptResponse`. - **`@cosyte/ncpdp/telecom`**, the **Telecommunication** claim standard (fixed-field text with FS/GS/RS framing): `parseTelecom`, the `claim` request view, the `adjudication` response view, the compound / COB / DUR / prior-authorization reads, plus `serializeTelecom` / `buildTelecomRequest`. - **`@cosyte/ncpdp/common`**, the shared value vocabulary: NDC, decimal-safe money/quantity, code systems, warning/fatal types. - **`@cosyte/ncpdp/profiles`**, the descriptive trading-partner profile system. The package root (`@cosyte/ncpdp`) re-exports the SCRIPT, Telecom, and common surfaces for convenience; deep subpath imports are equivalent. **Which version of each standard is decoded, and for how long, is one document.** The version, the section of public law that adopts it, the date that adoption ends, and the fact that no third party has tested this package are stated once in the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md) and are not restated on this page. ## Install and smoke-test ```bash npm install @cosyte/ncpdp ``` Confirm the package resolves and its version symbol is present: ```ts runnable import { VERSION } from "@cosyte/ncpdp"; typeof VERSION; // => "string" ``` If that resolves, the install is good. Head to the [Installation](https://docs.cosyte.com/ncpdp/installation.md) page for prerequisites and module-system notes, then the [Quickstart](https://docs.cosyte.com/ncpdp/quickstart.md) for a first useful result. ## The archetype in one line The parser is **lenient by default**: vendor quirks become stable-coded `warnings` with positional context (byte offset for Telecom, XPath for SCRIPT), not failures. The serializer, by contrast, always emits spec-clean output (Postel's Law). Only unrecoverable structural corruption throws a typed error. Safety-critical dispositions are **fail-safe**: a failure is never quietly read as a success, and money is never a float. ## Next - [Installation](https://docs.cosyte.com/ncpdp/installation.md): prerequisites, the XML-parser dependency, module systems. - [Quickstart](https://docs.cosyte.com/ncpdp/quickstart.md): parse a SCRIPT NewRx and a Telecom claim end to end. - [Core Concepts](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md): the implementation notes behind each read. - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): task-oriented recipes for the common reads. - [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md): the emit side, the round-trip contract, and the inputs it refuses. - [Trading-partner profiles](https://docs.cosyte.com/ncpdp/spec-notes-profiles.md): documenting a partner's conventions and triaging warnings against them. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): every diagnostic this library can raise, the fail-safe rules, and the non-goals. - [Conformance](https://docs.cosyte.com/ncpdp/conformance.md): the decoded version per wire format, the section adopting it, the date that adoption ends, and the absence of any third-party conformance record. - The **API Reference** documents every export, generated from source. --- URL: https://docs.cosyte.com/ncpdp/installation Markdown: https://docs.cosyte.com/ncpdp/installation.md # Installation `@cosyte/ncpdp` is a TypeScript NCPDP toolkit for Node.js. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration, and it exposes its two standards as separate subpaths so a Telecom-only or SCRIPT-only consumer never pays for the other. > **Status:** `0.1.0`, published to npm. The install command below is live. ## Prerequisites - **Node.js >= 22.** The whole `@cosyte/*` suite targets ES2023 / Node 22+. - A package manager: `pnpm`, `npm`, or `yarn`. - **One runtime dependency.** The **Telecom** side (fixed-field text) is Node stdlib only. The **SCRIPT** side (XML) uses a single vetted parser, [`fast-xml-parser`](https://github.com/NaturalIntelligence/fast-xml-parser): namespace-aware, and configured XXE-safe (entity resolution disabled). No further runtime dependency is added without the same review. ## Install ```bash npm install @cosyte/ncpdp ``` ## Smoke test Confirm the package resolves and a real entry point is callable. Parse the smallest valid SCRIPT message and read its version back: ```ts runnable import { parseScript } from "@cosyte/ncpdp/script"; const msg = parseScript('
'); msg.header.version; // => "2017071" Array.isArray(msg.warnings); // => true ``` If that resolves and returns, the install is good. Head to the [Quickstart](https://docs.cosyte.com/ncpdp/quickstart.md). ## Module systems and subpaths `@cosyte/ncpdp` is `"type": "module"` and exposes both conditions, so both of these resolve to the right build without extra configuration: ```ts // ESM / TypeScript import { parseScript } from "@cosyte/ncpdp/script"; import { parseTelecom } from "@cosyte/ncpdp/telecom"; ``` ```js // CommonJS const { parseScript } = require("@cosyte/ncpdp/script"); const { parseTelecom } = require("@cosyte/ncpdp/telecom"); ``` The four subpaths (the root `@cosyte/ncpdp`, `@cosyte/ncpdp/script`, `@cosyte/ncpdp/telecom`, `@cosyte/ncpdp/common`, and `@cosyte/ncpdp/profiles`) each publish per-condition types (`.d.ts` for `import`, `.d.cts` for `require`), gated by `attw` on every release, and resolve under both `node16` and legacy `node10` module resolution. Editor IntelliSense matches the build you actually load. ## PHI discipline Every example in this documentation uses **synthetic** fixtures: no real patient names, member IDs, prescriber NPIs, or NDCs. Do the same in your own tests: NCPDP messages carry PHI, and a fixture committed to a repository is a leak the moment it publishes. See [Troubleshooting](https://docs.cosyte.com/ncpdp/troubleshooting.md) for how the parser keeps field content out of its warnings and logs. ## Next - [Quickstart](https://docs.cosyte.com/ncpdp/quickstart.md): a first useful result from each of the two standards. - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): task-oriented recipes once the install is good. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): the error model and the non-goals. --- URL: https://docs.cosyte.com/ncpdp/quickstart Markdown: https://docs.cosyte.com/ncpdp/quickstart.md # Quickstart NCPDP is two standards. This page gives you a first useful result from each: read a **SCRIPT** NewRx (the XML ePrescribing message) and a **Telecom** B1 claim (the fixed-field pharmacy-to-PBM claim). Both readers are lenient (vendor quirks become stable-coded `warnings`, never silent failures) and neither reads a failure as a success. > Every message below is **synthetic**. NCPDP carries PHI; fixtures must never hold a real patient > name, member ID, prescriber NPI, or NDC. ## Read a SCRIPT NewRx `parseScript` reads a SCRIPT XML message into an immutable model without ever throwing on a vendor quirk; `newRx` projects the NewRx body (and returns `undefined` for any other transaction). The coded product carries its own recognized code system, and the free-text SIG is preserved verbatim: ```ts runnable import { parseScript, newRx } from "@cosyte/ncpdp/script"; const xml = `
SYNTH-MSG-0001
DOEAVERY Amoxicillin 500 MG Oral Capsule 00000000001 30 Take 1 capsule by mouth three times daily for 10 days.
`; const msg = parseScript(xml); msg.header.messageId; // => "SYNTH-MSG-0001" const rx = newRx(msg); rx?.patient?.name?.lastName; // => "DOE" rx?.medication?.description; // => "Amoxicillin 500 MG Oral Capsule" rx?.medication?.coded?.productCode?.value; // => "00000000001" rx?.medication?.coded?.productCode?.system; // => "NDC" rx?.medication?.quantity?.value?.source; // => "30" ``` The `system` is **recognized from the wire qualifier** (`ND` → `NDC`), while the raw `value` is kept verbatim; the quantity's `source` is the wire string, never a parsed float. See the [Cookbook](https://docs.cosyte.com/ncpdp/cookbook.md) for the response, structured-SIG, and lifecycle reads. ## Read a Telecom B1 claim The Telecommunication standard is the pharmacy-to-PBM claim protocol: a fixed 56-byte Transaction Header followed by FS/GS/RS control-character-framed, field-id-keyed segments. `parseTelecom` decodes the header and segments; `claim` lifts the safety-relevant B1/B2/B3 request fields. (The frame below is built inline from control bytes so the example is self-contained. In practice the bytes arrive off the wire.) ```ts runnable import { parseTelecom, claim } from "@cosyte/ncpdp/telecom"; const FS = "\x1c"; // Field Separator const pad = (v: string, n: number) => v.padEnd(n).slice(0, n); // A synthetic fixed D.0 Transaction Header (all values fabricated: no real BIN/NDC). const header = pad("999999", 6) + pad("D0", 2) + pad("B1", 2) + pad("PCN0000000", 10) + pad("1", 1) + pad("01", 2) + pad("1234567890", 15) + pad("20260629", 8) + pad("SW00000000", 10); // One Claim segment (id 07): Product/Service ID (D7), Quantity Dispensed (E7), Days Supply (D5), // Rx reference (D2), keyed by NCPDP field id and FS-joined. const claimSegment = [ "AM07", "EM1", "D2RX0000001", "D300", "E103", "D700000000031", "E730000", "D530", "D80", ].join(FS); const t = parseTelecom(header + claimSegment); t.kind; // => "request" t.header.transactionCode; // => "B1" const c = claim(t); c?.product?.id; // => "00000000031" c?.product?.qualifier; // => "03" c?.quantityDispensed?.source; // => "30000" c?.quantityDispensed?.impliedDecimal; // => "30.000" c?.prescriptionReferenceNumber; // => "RX0000001" ``` **Quantity is never a float.** Quantity Dispensed carries an implied 3-place decimal, applied **string-wise** (`"30000"` → `"30.000"`) with the verbatim `source` kept, so binary floating point can never corrupt the value. ## Unrecoverable input throws: everything else is a warning Only structurally unrecoverable input throws a typed fatal (empty input, non-XML, a non-`` root, or a ``/`` payload, the XXE boundary). Vendor quirks never throw; they collect on `.warnings`: ```ts runnable throws import { parseScript } from "@cosyte/ncpdp/script"; // Not XML at all: a structural fatal, not a tolerated quirk. parseScript("this is not an NCPDP message"); // throws NcpdpScriptParseError (NCPDP_SCRIPT_NOT_XML) ``` > **About runnable examples.** The blocks tagged ` ```ts runnable ` above are extracted by the test > suite, executed against the built package, and their `// =>` results asserted, so a documented > example can never silently drift from the code (`docSnippetSuite()`, the documentation analog of the > parser conformance runners). Blocks shown as plain ` ```ts ` are illustrative. ## Next - [Cookbook](https://docs.cosyte.com/ncpdp/cookbook.md): recipes for the SCRIPT response, structured SIG, the Telecom PBM response, compound / COB / DUR reads, and the serializers and builders. - [Core Concepts](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md): the implementation notes behind each read, with the exact fields decoded and the deliberate non-goals. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): fatal codes, every warning code, the fail-safe rules, and what v1 does not do. --- URL: https://docs.cosyte.com/ncpdp/spec-notes-telecom Markdown: https://docs.cosyte.com/ncpdp/spec-notes-telecom.md # Spec notes: Telecom foundation + B1 request read These notes record exactly what the `@cosyte/ncpdp/telecom` reader decodes, where the structural facts come from, and what it deliberately does **not** do. **No NCPDP-copyrighted prose is reproduced here.** Field/segment labels below are our own short paraphrases; the codes and field-number designators are factual identifiers from the NCPDP Telecommunication Standard vD.0 and the NCPDP Data Dictionary (paywalled), recorded against our own paraphrased names. ## What the reader decodes Reads a Telecommunication transmission: the fixed Transaction Header, the control-character-framed variable segments, and a B1/B2/B3 **request** view over the safety-relevant fields. Liberal on parse (quirks become stable-coded warnings with byte-offset context); only structurally unrecoverable input throws a typed Telecom fatal. **Which version that is, and until when, is not restated here.** The version this package decodes, the public section that adopts it and the date that adoption ends are stated once in the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md). What follows is the layout and behaviour of the decode itself. ## Framing | Control char | NCPDP designator | Role | | ------------ | ---------------------- | -------------------------------------------- | | `0x1C` | FS (Field Separator) | separates fields within a segment | | `0x1D` | GS (Group Separator) | separates transactions within a transmission | | `0x1E` | RS (Segment Separator) | separates segments within a transaction | Each decoded transaction, request and response alike, is a `transactions[n]` entry carrying its own segments, its own byte offset in the raw message and the warnings raised decoding it, so a quirk in one transaction cannot discard or re-attribute another's data. `segments` is the first transaction's segments, kept as the one-transaction shorthand. Each transaction is decoded independently, which is what isolates a malformed one: a transaction whose segment carries no `AM` field, or a field token too short to hold an id, still surfaces its own bytes verbatim under its own warnings while the transactions around it decode untouched. Nothing about that is fatal: `parseTelecom` stays lenient. ## Fixed Transaction Header (D.0 request, 56 bytes) Positional, no field separators. Offsets `[name, offset, length]`: | Field | Designator | Offset | Length | | ----------------------------- | ---------- | ------ | ------ | | BIN Number | 101-A1 | 0 | 6 | | Version/Release | 102-A2 | 6 | 2 | | Transaction Code | 103-A3 | 8 | 2 | | Processor Control Number | 104-A4 | 10 | 10 | | Transaction Count | 109-A9 | 20 | 1 | | Service Provider ID Qualifier | 202-B2 | 21 | 2 | | Service Provider ID | 201-B1 | 23 | 15 | | Date of Service | 401-D1 | 38 | 8 | | Software/Certification ID | 110-AK | 46 | 10 | Values are trimmed of pad whitespace; numeric leading zeros are preserved (a BIN/PCN is an identifier, not an arithmetic quantity). Transaction Count (109-A9) is one byte wide here, and it is surfaced **verbatim** on `transactionCount`: never coerced to a number, never defaulted, never reconciled against what the body carried. `decodedTransactionCount` is the number of transactions this reader actually decoded, and the two are separate facts. When they disagree, including when the declared value is empty or is not a number at all, `NCPDP_TELECOM_TRANSACTION_COUNT_MISMATCH` says so and every decoded transaction is still exposed. **No maximum is enforced**: no public artifact establishing one could be read, so this reader reports the disagreement and never calls a count illegal. ## Segments + fields modeled Segment Identification (111-AM) codes paraphrased, every request code this package names: `01` Patient, `02` Pharmacy Provider, `03` Prescriber, `04` Insurance, `05` COB/Other Payments, `07` Claim, `08` DUR/PPS, `10` Compound, `11` Pricing, `12` Prior Authorization, `13` Clinical. A 2-character code outside this set is preserved verbatim on `segment.segmentId` and warned (`NCPDP_TELECOM_UNKNOWN_SEGMENT`); only the paraphrased name is absent. Declared 111-AM code ranges: `01` to `16` on the request side, `20` to `28` on the response side. Those bounds are this package's own claim of coverage and nothing more. **No artifact available to this package establishes them**, so they ship marked unverified (`SEGMENT_CODE_RANGES`, whose `boundsVerified` is `false` on both) and should not be read as a statement about the standard. Codes inside a declared 111-AM range that this package does not name: `06`, `09`, `14`, `15`, `16` and `27`. Each one is published as a record in `SEGMENT_ABSENCES` with the reason `"unsourced"`, which means no publicly readable artifact establishes what the standard defines there, so this package neither names the code nor claims that nothing exists at it. The normative vocabulary is the NCPDP External Code List, a purchased product this package can neither cite nor redistribute (see the licensing note in [`KNOWN-LIMITATIONS.md`](https://github.com/cosyte/ncpdp/blob/main/KNOWN-LIMITATIONS.md)). A hole is therefore an accounting, never a gap in the decode: a transmission carrying one of these codes is read in full, warned, and round-trips byte for byte, exactly like any other unnamed code. 111-AM is 2 characters wide, so an `AM` field whose value is any other length is **not** a segment code. Rather than promote it, the reader leaves `segment.segmentId` empty, keeps the `AM` field in `segment.fields` verbatim so no byte is lost, and warns `NCPDP_TELECOM_MALFORMED_SEGMENT_ID`. In practice that shape means a dropped field separator has run the rest of the segment into the code, so those bytes are claim data. A segment that does not begin with `AM` at all is `NCPDP_TELECOM_MISSING_SEGMENT_ID`, and both round-trip byte for byte. The builder applies the same 2-character rule on emit (`NCPDP_TELECOM_BUILD_INVALID_SEGMENT_ID`). The B1 view (`claim()`) lifts these safety-relevant field ids: `C1` Group ID, `C2` Cardholder ID, `C3` Person Code, `C4` Date of Birth, `C5` Patient Gender Code, `D2` Rx/Service Reference Number, `EM` its qualifier, `D3` Fill Number, `D7` Product/Service ID, `E1` Product/Service ID Qualifier, `E7` Quantity Dispensed, `D5` Days Supply, `D8` Dispense-As-Written, `DB` Prescriber ID, `EZ` Prescriber ID Qualifier. An unmodeled field id is still preserved verbatim. Absence of a name never means the field is dropped. ## Safety-critical handling - **Quantity Dispensed (442-E7)** carries an implied 3-place decimal (`9(7)v999`). It is **never** parsed into a float; the implied decimal is applied **string-wise** (`"30000"` → `"30.000"`) and both the verbatim source and the scaled value are surfaced. - **Version safety.** The fixed offsets above are read for the decoded version and for nothing else; which version that is, and the dates that bound it, are in the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md). The **F6** stamp widens the leading identification field (8-byte IIN vs 6-byte BIN), so it is **recognized but not decoded** (`NCPDP_TELECOM_VF6_NOT_DECODED`) rather than read against the wrong offsets: in a **request** the parse succeeds, the stamp is surfaced on `header.versionRelease`, and every other positional field comes back empty. A **response** leads with its Version/Release, which is not where this reader looks for the stamp, so an F6 response is refused with `NCPDP_TELECOM_UNSUPPORTED_VERSION` rather than warned; the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md) states the outcome per direction. Any unrecognized stamp is `NCPDP_TELECOM_UNSUPPORTED_VERSION`. - **Never guess framing.** A non-empty body with no FS/GS/RS bytes → `NCPDP_TELECOM_INVALID_FRAMING`. - **Product/Service ID Qualifier (436-E1) carries no label.** The qualifier and the product id are both surfaced verbatim and neither is reinterpreted. No table of qualifier meanings ships, because no publicly-available artifact establishing the value set could be obtained: the one carried payer manual states two of this field's values and nothing about the rest, and a one-row table sourced to one payer's billing instructions was not worth the surface. `product` is `{ id, qualifier }` and nothing else. Absence of a meaning never means the value is invalid. Which fields do and do not have a label table, and on what source, is listed in [`KNOWN-LIMITATIONS.md`](https://github.com/cosyte/ncpdp/blob/main/KNOWN-LIMITATIONS.md). ## What the reader does NOT do - No response (paid/rejected) decode here. The request view above covers B1/B2/B3; the payer's answer to any of them, and the E1 eligibility response, are read by the response side and are documented in [Telecom responses](https://docs.cosyte.com/ncpdp/spec-notes-telecom-response.md). - No compound or COB/Other-Payer detail view here. Those reads, the DUR/PPS request depth and the prior-authorization read are documented in [Compound, COB and DUR depth](https://docs.cosyte.com/ncpdp/spec-notes-telecom-compound-cob.md). - No serializer/builder (emit), parse only. What emit writes back, and what it refuses to write, is in [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md). - No multi-transaction **emit**. Every transaction is read; `serializeTelecom` writes one transaction per transmission and refuses a model carrying more (`NCPDP_TELECOM_BUILD_MULTI_TRANSACTION_EMIT`) rather than dropping the rest. - No maximum transaction count. The declared count is reported against the decoded count; a count is never called illegal. ## PHI All fixtures are synthetic. Warnings and fatal errors carry only a stable code, a registry message selected by that code, and a position (byte offset + optional 2-char field id), never a field value (cardholder id, DOB, NDC). ## Next - [Telecom responses](https://docs.cosyte.com/ncpdp/spec-notes-telecom-response.md): the payer's answer to any of the request transactions above. - [Compound, COB and DUR depth](https://docs.cosyte.com/ncpdp/spec-notes-telecom-compound-cob.md): the request-side reads this page leaves out. - [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md): the emit half of the same wire layout. - [Trading-partner profiles](https://docs.cosyte.com/ncpdp/spec-notes-profiles.md): documenting the conventions a partner sends these segments under. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): every warning code named above, with what the reader did when it raised one. --- URL: https://docs.cosyte.com/ncpdp/spec-notes-telecom-response Markdown: https://docs.cosyte.com/ncpdp/spec-notes-telecom-response.md # Spec notes: Telecom responses + B2/B3/E1 These notes record exactly what the `@cosyte/ncpdp/telecom` **response** reader decodes, where the structural facts come from, and what it deliberately does **not** do. **No NCPDP-copyrighted prose is reproduced here.** Field/segment labels below are our own short paraphrases; the codes and field-number designators are factual identifiers from the NCPDP Telecommunication Standard vD.0 and the NCPDP Data Dictionary (paywalled), recorded against our own paraphrased names. ## What the response reader decodes Reads a vD.0 Telecommunication **response** transmission: the fixed Response Transaction Header and the control-character-framed response segments, and lifts an adjudication view (status + disposition, pricing, DUR alerts). The same reader serves a B1 billing-claim response, a **B2** reversal, a **B3** rebill, and an **E1** eligibility response. The response shape is identical; only the echoed Transaction Code differs. Liberal on parse; only structurally unrecoverable input throws a typed fatal. ## Detecting a response vs a request A request header leads with the routing BIN (101-A1) at offset 0 and carries the Version/Release (`"D0"`) at offset 6. A response header leads with the Version/Release at offset 0. The two are told apart by where `"D0"` sits. The request shape is checked first so a request is never mistaken for a response. The fixed header region is then sliced up to the first **structural** framing char (GS/RS); the Field Separator (FS) is excluded because it appears _within_ a segment and never marks the header→body boundary. ## Fixed Response Transaction Header (D.0) Positional, leading fields only. The safety-critical adjudication data lives in the framed segments, not the header, so a mis-sized trailing field can never misread a paid/rejected outcome. Offsets `[name, offset, length]`: | Field | Designator | Offset | Length | | ----------------------------- | ---------- | ------ | ------ | | Version/Release | 102-A2 | 0 | 2 | | Transaction Code | 103-A3 | 2 | 2 | | Transaction Count | 109-A9 | 4 | 1 | | Header Response Status | 501-F1 | 5 | 1 | | Service Provider ID Qualifier | 202-B2 | 6 | 2 | | Service Provider ID | 201-B1 | 8 | 15 | Header Response Status (501-F1) is the **transmission-level** accept/reject flag (`A`/`R`), distinct from the per-claim Transaction Response Status (112-AN) in the Response Status segment. ## Response segments + fields modeled Segment Identification (111-AM) codes paraphrased, every response code this package names: `20` Response Message, `21` Response Status, `22` Response Claim, `23` Response Pricing, `24` Response DUR/PPS, `25` Response Insurance, `26` Response Patient, `28` Response Coordination of Benefits. A code outside this set is preserved verbatim and warned (`NCPDP_TELECOM_UNKNOWN_SEGMENT`). The response range this package declares runs wider than the codes it names, and which codes inside it carry no name, and why, is recorded once for both sides under [Telecom foundation and B1](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md). - **Response Status (21):** `AN` Transaction Response Status (112-AN), `FA` Reject Count (510-FA), `FB` Reject Code (511-FB, repeating), `F3` Authorization Number (503-F3), `FQ` Additional Message Information (526-FQ). - **Response Pricing (23):** `F5` Patient Pay Amount (505-F5), `F9` Total Amount Paid (509-F9), `F6` Ingredient Cost Paid (506-F6), `F7` Dispensing Fee Paid (507-F7), `FM` Basis of Reimbursement Determination (522-FM). - **Response DUR/PPS (24):** `J6` DUR/PPS Response Code Counter (567-J6), `E4` Reason For Service Code (439-E4), `FS` Clinical Significance Code (528-FS), `FU` Previous Date Of Fill (530-FU), `FV` Quantity Of Previous Fill (531-FV), `FY` DUR Free Text Message (544-FY). ## Safety-critical handling - **A reject always wins.** The disposition is a total function over the Transaction Response Status (112-AN) **and** the reject codes. Any reject present ⇒ `"rejected"`, even when the status field claims paid; the conflict is surfaced (`NCPDP_TELECOM_STATUS_CONFLICT`, `statusConflict: true`). An unrecognized status reads `"unknown"`, **never** paid (`NCPDP_TELECOM_UNKNOWN_RESPONSE_STATUS`). A declared positive Reject Count with no codes still resolves to rejected. - **Money is never a float.** Dollar fields carry an implied 2-place decimal (cents) and an optional zoned-decimal **overpunch** sign on the final character (the EBCDIC-derived convention NCPDP inherits: `{`,A to I = +0 to +9; `}`,J to R = −0 to −9). Both are interpreted **string-wise** with the verbatim source kept; a signed zero normalizes to a non-negative `0.00`. Unrecognized input is preserved with `isValid: false` and no interpreted amount. Money is never guessed or recomputed. - **No DUR alert is dropped.** The DUR/PPS segment repeats its fields once per returned alert; the reader splits at each counter (567-J6) **and** at each new Reason For Service (439-E4) so two alerts are never collapsed into one. An unrecognized reject or reason code is preserved verbatim with `known: false`. - **A label ships only where a public artifact establishes it.** Reject Codes (511-FB) and Transaction Response Status values (112-AN) carry **no** label table, so the reject code comes back as the bare wire code with `known: false` and no `description`, and the status carries its `disposition` and no `statusDescription`. What the one carried payer manual does and does not establish for each field, and why a partial table was withdrawn rather than shipped, is recorded in source beside each declaration. Reason For Service codes (439-E4) do carry short labels, from one cited and dated public payer manual, and that citation lives in source beside the table along with the single-source caveat that travels with it. Field by field: [`KNOWN-LIMITATIONS.md`](https://github.com/cosyte/ncpdp/blob/main/KNOWN-LIMITATIONS.md). - **Withdrawing a label changes nothing else on the path.** The code is still verbatim, still in wire order, still never dropped, and still carries its recognition flag. The disposition is derived from the status and the rejects, never from a label, so the fail-safe above is unaffected. ## What the response reader does NOT do - No serializer/builder (emit), parse only. - No COB/Other-Payer adjudication detail beyond preserving the segment verbatim. - The DUR/PPS "other pharmacy / database / other prescriber" indicator fields (`FT`/`FW`/`FX`) are tokenized and preserved verbatim on the raw segment but are **not** lifted onto the `TelecomDurAlert` view. Read them from `segment.fields` if needed. Nothing is dropped at the parse layer. - No multi-transaction **emit**. A response transmission carrying an answer per submitted claim is decoded whole, and these views take a transaction index (`adjudication(t, 1)`, defaulting to the first), so a later claim's disposition is never read as the first one's. `serializeTelecom` still writes one transaction per transmission and refuses a model carrying more. - DUR free text (544-FY) may be operationally PHI-adjacent; it is surfaced verbatim but never logged. ## PHI All fixtures are synthetic. Warnings and fatal errors carry only a stable code, a registry message selected by that code, and a position (byte offset + optional 2-char field id), never a field value. ## Next - [Telecom foundation and B1](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md): the request side these responses answer. - [Compound, COB and DUR depth](https://docs.cosyte.com/ncpdp/spec-notes-telecom-compound-cob.md): the response COB blocks and the DUR depth this page introduces. - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): the adjudication read as a recipe. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): every warning code named above, with what the reader did when it raised one. --- URL: https://docs.cosyte.com/ncpdp/spec-notes-telecom-compound-cob Markdown: https://docs.cosyte.com/ncpdp/spec-notes-telecom-compound-cob.md # Spec notes: Telecom compound + COB + DUR depth + prior-auth These notes record exactly what the `@cosyte/ncpdp/telecom` **request-side depth** readers decode, where the structural facts come from, and what they deliberately do **not** do. **No NCPDP-copyrighted prose is reproduced here.** Field/segment labels below are our own short paraphrases; the codes and field-number designators are factual identifiers from the NCPDP Telecommunication Standard vD.0 and the NCPDP Data Dictionary (paywalled), recorded against our own paraphrased names. ## What these readers do Adds four reads over an already-parsed transaction (three on the request side, one spanning both): - **Compound (segment 10)**, `compound(t)`: the multi-ingredient detail of a compounded claim, every ingredient surfaced and none dropped or merged. - **Coordination of Benefits / Other Payments (request segment 05)**, `cobOtherPayments(t)`: each prior payer with its amount-paid and patient-responsibility money rows. - **Response COB / Other Payers (response segment 28)**, `responseCob(t)`: the next-payer routing blocks the payer returns. - **DUR/PPS request depth (segment 08)**, `requestDur(t)`: the submitted DUR/PPS interactions; plus the response DUR alert (segment 24) gains professional-service / result / level-of-effort depth. - **Prior Authorization (segment 12)**, `priorAuthorization(t)`: presence + submitted type/number, **presence only, never adjudicated**. All are lenient reads over the existing tokenizer; no new fatal conditions are introduced. ## Segments + fields modeled Segment Identification (111-AM) codes paraphrased, only those this page uses: `05` Coordination of Benefits/Other Payments, `08` DUR/PPS, `10` Compound, `12` Prior Authorization, `28` Response COB. A code outside the recognized set is preserved verbatim and warned (`NCPDP_TELECOM_UNKNOWN_SEGMENT`). The full inventory, the ranges this package declares and the codes inside them that carry no name are in [Telecom foundation and B1](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md). - **Compound (10):** `EF` Dosage Form Description Code (450-EF), `EG` Dispensing Unit Form Indicator (451-EG), `EC` Compound Ingredient Component Count (447-EC); then per ingredient, repeating: `RE` Compound Product ID Qualifier (488-RE), `TE` Compound Product ID (489-TE), `ED` Compound Ingredient Quantity (448-ED, implied 3-place decimal), `EE` Compound Ingredient Drug Cost (449-EE, money), `UE` Compound Ingredient Basis of Cost Determination (490-UE). - **Request COB / Other Payments (05):** `4C` Coordination of Benefits/Other Payments Count (337-4C); then per other payer, repeating: `5C` Other Payer Coverage Type (338-5C), `6C` Other Payer ID Qualifier (339-6C), `7C` Other Payer ID (340-7C), `E8` Other Payer Date (443-E8), and the money rows `HC` Other Payer Amount Paid Qualifier (342-HC) / `DV` Other Payer Amount Paid (431-DV), `6E` Other Payer-Patient Responsibility Amount Qualifier (472-6E) / `7E` Other Payer-Patient Responsibility Amount (473-7E). - **Response COB / Other Payers (28):** `NT` Other Payer ID Count (355-NT); then per other payer: `5C` Other Payer Coverage Type (338-5C), `6C` Other Payer ID Qualifier (339-6C), `7C` Other Payer ID (340-7C), `MH` Other Payer Processor Control Number (991-MH), `NU` Other Payer Cardholder ID (356-NU), `MJ` Other Payer Group ID (992-MJ). - **DUR/PPS request (08):** per interaction, splitting on `E4` Reason For Service Code (439-E4): `E5` Professional Service Code (440-E5), `E6` Result of Service Code (441-E6), `8E` DUR/PPS Level of Effort (474-8E), `J9` DUR Co-Agent ID Qualifier (475-J9), `H7` DUR Co-Agent ID (476-H7). - **Response DUR/PPS (24) depth:** the existing alert reader now also lifts `E5` Professional Service Code, `E6` Result of Service Code, and `8E` Level of Effort onto each alert. - **Prior Authorization (12):** `EU` Prior Authorization Type Code (461-EU), `EV` Prior Authorization Number Submitted (462-EV). ## Safety-critical handling - **Every compound ingredient is surfaced, none dropped or merged.** A new ingredient begins at each Compound Product ID Qualifier (488-RE) **or** Compound Product ID (489-TE), so an ingredient is recognized even when a sender omits the qualifier. When the declared component count (447-EC) disagrees with the number of decoded ingredients, the count is **not** trusted to drop or pad data. Every decoded ingredient is kept and the disagreement surfaces as `NCPDP_TELECOM_COMPOUND_COUNT_MISMATCH`. - **Every COB money row is preserved with its amount.** Each other-payer block repeats on the Other Payer Coverage Type (338-5C); the segment-level count field (337-4C / 355-NT) is metadata and does not seed a spurious block. Within a block, amount rows pair a qualifier with the next amount in wire order, so two payments are never collapsed. A declared other-payer count that disagrees with the decoded block count surfaces as `NCPDP_TELECOM_COB_COUNT_MISMATCH`; all decoded blocks are kept. - **Money is never a float.** Compound drug cost (449-EE) and the COB amount fields decode through the same `telecomMoney` path as the response pricing reader: implied 2-place decimal + zoned-decimal overpunch sign, interpreted string-wise with the verbatim source authoritative; unrecognized input is kept with `isValid: false`. Compound ingredient quantity (448-ED) uses the implied 3-place decimal string-wise (leading zeros stripped from the whole part, consistent with the money decode). - **A compound ingredient's product-id qualifier (488-RE) carries no label.** It is surfaced verbatim, like the ingredient's product id. No qualifier label table ships for a product id qualifier; see [`KNOWN-LIMITATIONS.md`](https://github.com/cosyte/ncpdp/blob/main/KNOWN-LIMITATIONS.md). - **An unknown DUR reason is kept, never dropped.** A Reason For Service code (439-E4) outside the recognized set is preserved verbatim with `reasonKnown: false` and surfaces as `NCPDP_TELECOM_UNKNOWN_DUR_REASON`. Professional-service / result codes are preserved verbatim with no bundled description (the standard's code list is paywalled; consumers bring their own). - **Prior authorization is presence, not adjudication.** `priorAuthorization` reports that the segment was submitted and echoes the type/number verbatim; it never decides whether a PA is valid, active, or honored. That is the payer's adjudication, returned in the response, not the request. ## What these readers do NOT do - No serializer/builder (emit), parse only. - No clinical interpretation of DUR/PPS codes beyond the bundled Reason-For-Service descriptions; the professional-service, result-of-service, and level-of-effort code meanings are not bundled. - No cross-segment reconciliation of the COB chain against the response pricing (e.g. verifying that other-payer amounts sum to a coordinated total). - No multi-transaction **emit**. These views take a decoded transaction's index (`compound(t, 1)`, `cobOtherPayments(t, 1)`, defaulting to `transactions[0]`); `serializeTelecom` still writes one transaction per transmission and refuses a model carrying more. ## PHI All fixtures are synthetic: no real BIN/PCN/NDC/cardholder/payer identifiers. Other Payer Cardholder ID (356-NU) is PHI-adjacent and is surfaced verbatim but never logged. Warnings and fatal errors carry only a stable code, a registry message selected by that code, and a position (byte offset + optional 2-char field id), never a field value. ## Next - [Telecom foundation and B1](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md): the framing, header and segment inventory these reads sit on. - [Telecom responses](https://docs.cosyte.com/ncpdp/spec-notes-telecom-response.md): the response DUR alert and the response COB blocks, from the answering side. - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): the same reads as recipes. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): the count-mismatch warnings above, with what the reader did when it raised one. --- URL: https://docs.cosyte.com/ncpdp/spec-notes-structured-sig Markdown: https://docs.cosyte.com/ncpdp/spec-notes-structured-sig.md # Spec notes: structured SIG decode These notes record exactly what the `@cosyte/ncpdp/script` structured-SIG decoder reads, where the mapping comes from, and what it deliberately does **not** do. No NCPDP-copyrighted prose is reproduced here. ## What the decoder does Decodes the SCRIPT `` element into a typed `StructuredSig` of dosing components, surfaced on `medication.sig`. The decode is **best-effort and explicitly lossy**: the free-text `` is the source of truth and is always preserved verbatim (`sig.sigText`), and the structured view is additive. ## Component model `StructuredSig` exposes one uniform `SigField` per component, always present and tagged with a `provenance` of `coded` | `derived` | `absent`: | Field | NCPDP Structured-and-Codified-Sig component | Decodes? | | ------------------------ | ------------------------------------------- | --------------- | | `doseDeliveryMethod` | Dose delivery method (verb) | yes | | `dose` | Dose quantity (numeric amount) | yes | | `doseUnitOfMeasure` | Dose unit of measure | always `absent` | | `route` | Route of administration | yes | | `siteOfAdministration` | Site of administration | yes | | `administrationTiming` | Administration timing | yes | | `duration` | Duration of therapy | always `absent` | | `vehicle` | Vehicle / diluent | always `absent` | | `indication` | Clinical indication | always `absent` | | `maximumDoseRestriction` | Maximum-dose restriction | always `absent` | The last column is the consequence of element-name grounding, below: a component decodes only where this release can trace its element name to a published field label. `provenance` semantics: - `coded`: the structured element carried a `` (with an optional ``/`` or `Qualifier`/`CodeSystem` attribute). The code keeps its source qualifier verbatim; the recognized system is exposed via the shared `codedValue` mapping (SNOMED CT, NCI Thesaurus, NDC, RxNorm, ICD-10, else `UNKNOWN`). - `derived`: a value was read from uncoded structure (a `` child, or the element's own text). - `absent`: the element was missing or empty. **An absent field is never inferred from the free text.** ## Element-name recognition, and what grounds it The precise nesting of the NCPDP Structured and Codified Sig Format is defined in the NCPDP SCRIPT Implementation Guide, which is **membership-gated**, and the nesting varies across the SCRIPT releases this package decodes. Rather than hard-code one rigid XPath that a real trading partner's variant could silently miss, the decoder matches each component by its **recognized local element name as a descendant of ``**. Recognized names are declared in `src/script/sig.ts` (`SIG_COMPONENT_NAMES`), each with a provenance record in `SIG_NAME_PROVENANCE`. **A name is recognized only if a published artifact carries a field label that the name transcribes, and that label denotes the same component the name populates here.** A name that merely looks like the right spelling is not enough: reading the wrong element does not produce a missing field, it produces a confidently coded wrong one, and in a dispensing pipeline that is the expensive failure. A label naming the enclosing **segment** does not ground a component name either, since a segment is the container for the field rather than another spelling of it. The artifact behind every name below is table 1 of Liu H, Burkhart Q, Bell DS, "Evaluation of the NCPDP Structured and Codified Sig Format for e-prescriptions", JAMIA 2011;18(5):645-651 ([https://pmc.ncbi.nlm.nih.gov/articles/PMC3168301/](https://pmc.ncbi.nlm.nih.gov/articles/PMC3168301/)), retrieved 2026-08-25. It is a peer-reviewed inventory of the format's 13 segments and their fields, which is why these labels can be quoted at all while the standard itself stays behind membership and purchase. **The assumption, and its fail-safe.** That inventory gives conceptual field labels in title case and prose; this decoder matches XML element _local names_. Those coincide only insofar as the standard's schema spells each label as its closed-up element name, and nothing public confirms that, because the Implementation Guides are paywalled. If the assumption is wrong for a given name, that name simply never matches: the component reads `absent`, nothing is inferred from the free text, and `sigText` still carries the directions verbatim. **One qualification, worth reading before relying on any of it.** That artifact is a **single source**, and it studies Sig Format v1.0 as implemented in **SCRIPT 10.5**, which is not one of the federally adopted releases this package decodes (they are named in the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md)). NCPDP's own release summary records that the later of those two adopted releases carries modifications to the Structured and Codified Sig structure format. So the inventory is evidence about the format's fields and is **not normative** for the adopted releases. Treat every name below as grounded-but-provisional. ### Recognized element names, per component | Component | Recognized element name(s) | Field label it transcribes | | ------------------------ | -------------------------- | -------------------------------------------- | | `doseDeliveryMethod` | `DoseDeliveryMethod` | "Dose delivery method" (dose segment) | | `dose` | `DoseQuantity` | "Dose quantity" (dose segment) | | `doseUnitOfMeasure` | none | no field label denotes it | | `route` | `Route` | "Route" (route of administration segment) | | `siteOfAdministration` | `Site` | "Site" (site of administration segment) | | `administrationTiming` | `AdministrationTiming` | "Administration timing" (Sig timing segment) | | `duration` | none | no field label denotes it | | `vehicle` | none | no field label denotes it | | `indication` | none | no field label denotes it | | `maximumDoseRestriction` | none | no field label denotes it | **A component with no recognized name always decodes `absent`**, whatever element the message carried. Five of the ten slots are in that position. They stay on the type, tagged `absent`, rather than disappearing: a consumer can tell "this release cannot read that" apart from "that component no longer exists". ### Element names this release removed Each of these was recognized previously and matched nothing in the artifact that denotes the component it populated, so it was removed. **A message using one of these names now decodes that component `absent`.** Nothing was added or re-spelled to replace them; the vocabulary only narrowed. | Removed name | Component it populated | Why it is not grounded | | ------------------------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `Dose` | dose | names the enclosing dose segment, not the quantity field | | `DoseUnitOfMeasure` | doseUnitOfMeasure | no such field label; the same string names an NCI Thesaurus terminology subset, which is a value space rather than a Sig component | | `RouteOfAdministration` | route | names the enclosing route of administration segment | | `SiteOfAdministration` | siteOfAdministration | names the enclosing site of administration segment | | `TimingAndDuration` | administrationTiming | no such label, and it spans two different segments | | `Frequency` | administrationTiming | the frequency fields are neighbours of administration timing, not that component | | `Duration` | duration | names the enclosing duration segment | | `Vehicle` | vehicle | names the enclosing vehicle segment | | `Indication` | indication | names the enclosing indication segment | | `MaximumDoseRestriction` | maximumDoseRestriction | names the enclosing maximum dose restriction segment | The serializer emits each component under its recognized name, so what this library writes is what it reads back. A component with no recognized name is not emitted at all, since it can never hold a value. ## Fail-safe behavior - **Never a confident dose from an ambiguous SIG.** If a dose structure is present but no unambiguous quantity can be read, `dose` is surfaced as `absent` and `NCPDP_SCRIPT_SIG_AMBIGUOUS_DOSE` is raised. The parser does not guess a number. - **Never reconciled.** Structured dosing and the free text are surfaced independently. When they disagree, both are returned as-is; the library does not pick a winner. - **Lossy flag.** Whenever any structured component decodes, `NCPDP_SCRIPT_SIG_STRUCTURED_LOSSY` is raised once, signaling that `SigText` is authoritative. ## Known limitations (cumulative) - **Decode-only.** v1 does not _generate_ a SIG from structure (a future builder emits what it is given). - **No natural-language parsing** of arbitrary free-text directions: only the structured `` is decoded; `` / `` stay verbatim. - **No terminology lookup.** Route/site/unit codes are surfaced with their claimed system (provenance), not validated or expanded against SNOMED/NCI. - **Recognized-name tolerance** (above) is a known approximation of the gated IG nesting. ## Next - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): the structured SIG read as a recipe, beside the rest of the SCRIPT reads. - [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md): what the emit side writes back for each recognized component name. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): the two SIG warning codes above, with what the reader did when it raised one. - [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md): the adopted releases this decode is measured against. --- URL: https://docs.cosyte.com/ncpdp/spec-notes-serialize-build Markdown: https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md # Spec notes: spec-clean serializers + builders + round-trip These notes record exactly what the **emit** side does for both NCPDP standards (the SCRIPT XML serializer + message builder and the Telecom FS/GS/RS serializer + transaction builder), where the structural facts come from, and what they deliberately do **not** do. **No NCPDP-copyrighted prose is reproduced here.** Field/segment/element labels below are our own short paraphrases; the codes and field-number designators are factual identifiers from the NCPDP Telecommunication Standard vD.0 and the SCRIPT Standard (paywalled), recorded against our own paraphrased names. ## What the emit side does Closes the parse↔emit loop. Every parser already produced an immutable model; the emit side turns a model back into spec-clean wire form and lets a caller construct one from scratch: - **`serializeScript(message)` / `ScriptMessage#toString()`**: a parsed (or built) SCRIPT message → canonical SCRIPT XML, or a typed refusal when the body is a transaction this library does not model (see the refusals below). - **`buildNewRx(input)` / `buildScriptResponse(input)`**: construct a SCRIPT NewRx or a ``/``/`` response from a structured input. - **`serializeTelecom(transaction)`**: a parsed (or built) Telecom transaction → canonical vD.0 wire form (fixed header + FS/GS/RS-framed body). - **`buildTelecomRequest(input)`**: construct a vD.0 request transaction (fixed header + segments). This is the conservative (emit) half of Postel's Law: the serializer **never warns** on a valid model, and the builder **refuses** a message that is invalid by construction with a typed error rather than emitting malformed output a downstream system would reject. ## Canonical form, not byte-identity The read is **lossy** by design (only the modeled fields are surfaced) so emit reproduces the _modeled_ content, not the original bytes. The contract is **canonical-form idempotence**: - `serialize(parse(serialize(x)))` is byte-identical to `serialize(x)`. Once a value is in canonical serialized form, re-parsing and re-serializing is a no-op. - `parse(serialize(x))` is structurally equal to `x` (compared by canonical form, since the model has no byte-offset identity to compare). What canonicalization normalizes: - **SCRIPT**: the declared version is emitted as the root `` attribute; wrapper elements the parser flattens (e.g. a ``/`` shell) are not reconstructed; indentation is regular two-space. Only modeled elements are emitted, in a fixed document order. - **Telecom**: fixed-header fields are re-padded (left-justified, space-padded) to their wire widths; segments are re-joined with single FS/GS/RS control characters. Arbitrary input whitespace or duplicate separators are not reproduced. ## XML escaping + the entity boundary (SCRIPT) The serializer escapes the text-significant characters `&`, `<`, `>` in element content and additionally `"` in attribute values. Because the loader is XXE-safe with **entity resolution disabled**, a value carrying a raw `&`, `<`, or `>` round-trips only when it was entity-free to begin with; the synthetic corpus is. The builder additionally **refuses** any value carrying an XML-1.0 control character (`\x00` to `\x08`, `\x0B`, `\x0C`, `\x0E` to `\x1F`), which cannot appear in a well-formed XML 1.0 document, with `NCPDP_SCRIPT_BUILD_INVALID_CHARACTER`. ## Emit refusals (invalid-by-construction, and unmodeled-by-definition) SCRIPT (`NcpdpScriptBuildError`): - `NCPDP_SCRIPT_BUILD_MISSING_MEDICATION`: a NewRx with no prescribed-medication description. - `NCPDP_SCRIPT_BUILD_MISSING_RESPONSE_CODE`: a ``/``/`` with no `` (the one field the parser itself flags as required). - `NCPDP_SCRIPT_BUILD_INVALID_CHARACTER`: any supplied value carrying an XML-illegal control char. - `NCPDP_SCRIPT_BUILD_UNSUPPORTED_TRANSACTION`: a message whose body is a transaction this library does not model (`body.kind === "unsupported"`). This one is raised by the **serializer**, not the builder, and it is the only refusal on that side: nothing under such a transaction is modeled, so the only document emit could write is a well-formed, re-parseable one with the transaction body deleted. It refuses instead, on every publicly reachable route, and returns no string. The first three are the builder refusing input that is invalid by construction. The fourth is emit refusing a model it cannot render honestly, and it applies to `serializeScript(message)` and to `ScriptMessage#toString()` alike. Because `toString()` is the message object's own string conversion, it also runs on implicit coercion, so the refusal can surface from a template literal or a log line where the calling code contains no visible serialize call. Branch on the discriminant first: it is public, typed, and never throws. ```ts runnable import { parseScript } from "@cosyte/ncpdp/script"; const msg = parseScript(''); msg.body.kind; // => "unsupported" // Relay the original bytes for a transaction this library does not model. const wire = msg.body.kind === "unsupported" ? "the original document" : msg.toString(); wire; // => "the original document" ``` ```ts runnable throws import { parseScript, serializeScript } from "@cosyte/ncpdp/script"; // A transaction this library does not model. No string comes back: the only // document emit could write is one with the transaction body deleted. serializeScript(parseScript('')); // throws NcpdpScriptBuildError (NCPDP_SCRIPT_BUILD_UNSUPPORTED_TRANSACTION) ``` All four carry the code and the fixed sentence its frozen registry entry holds, and nothing from the input; they are safe to log whole. Telecom (`NcpdpTelecomBuildError`): - `NCPDP_TELECOM_BUILD_MISSING_TRANSACTION_CODE`: no Transaction Code (103-A3); a request cannot route. - `NCPDP_TELECOM_BUILD_MISSING_SEGMENT_ID`: a segment with no Segment Identification (111-AM) code. - `NCPDP_TELECOM_BUILD_INVALID_FIELD_ID`: a data field whose id is not a 2-character identifier. - `NCPDP_TELECOM_BUILD_EMBEDDED_CONTROL_CHARACTER`: an FS/GS/RS control char inside supplied data, which would corrupt the framing. - `NCPDP_TELECOM_BUILD_FIELD_TOO_LONG`: a fixed-width header field longer than its wire width. ## Wire layout reproduced (Telecom) - **Request**: the 56-byte fixed Transaction Header (BIN 101-A1, Version/Release 102-A2, Transaction Code 103-A3, Processor Control Number 104-A4, transaction count 109-A9, Service Provider ID Qualifier 202-B2, Service Provider ID 201-B1, Date of Service 401-D1, Software/Certification ID 110-AK) immediately followed by the framed body. Each segment is `AM` then FS-joined `` tokens; segments are RS-joined. - **Response**: the fixed Response Transaction Header region, a Group Separator introducing the transaction, then the RS-framed response segments. A parsed segment that lacked its `AM` field (a tolerated quirk) re-emits without a leading `AM`, faithfully round-tripping that shape. ## What the emit side deliberately does not do (known limitations) - **Whole-message only, no streaming.** The builder/serializer construct a complete message in memory; there is no incremental/streaming emit. - **Emit the SIG given, no SIG generation.** The SCRIPT builder emits the structured SIG / SigText it is handed; it does not synthesize a SIG from structured dose/route/timing components. - **Lossy fields are not invented.** A field the parser does not model cannot be reproduced; serialize reflects the model, not the original document. - **Emit an unmodeled transaction.** A transaction outside the modeled set has no body model at all, so there is nothing under it to reproduce and emit refuses (`NCPDP_SCRIPT_BUILD_UNSUPPORTED_TRANSACTION`) rather than writing an element with its children dropped. It also does not carry the transaction's raw markup onto the model to make it round-trip: that would hand every downstream package an unbounded, sender-chosen string. Relay the original bytes instead, and branch on `body.kind === "unsupported"` to know when to. - **Telecom emits one transaction per transmission, and refuses rather than drops.** The model the parser returns can hold more transactions than this serializer can express. It does not write the first and discard the rest: a model carrying more than one decoded transaction is refused with the typed `NcpdpTelecomBuildError` `NCPDP_TELECOM_BUILD_MULTI_TRANSACTION_EMIT`. Serialize one transaction at a time (each `transactions[n]` carries its own segments) if you need wire output for a later one. ## Next - [Telecom foundation and B1](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md): the wire layout this serializer reproduces. - [Structured SIG decode](https://docs.cosyte.com/ncpdp/spec-notes-structured-sig.md): the recognized component names emit writes each slot under. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): the parse-side warning and fatal codes, which are a different set from the refusals above. - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): the reads these builders are the mirror of. --- URL: https://docs.cosyte.com/ncpdp/spec-notes-profiles Markdown: https://docs.cosyte.com/ncpdp/spec-notes-profiles.md # Spec notes: trading-partner profile system These notes record exactly what the **profile system** does, where each built-in quirk's convention is documented, and what the system deliberately does **not** do. **No NCPDP-copyrighted prose is reproduced here.** Every field/segment/element label below is our own short paraphrase; the field-number designators (`303-C3`, `511-FB`, …) are factual identifiers from the NCPDP Telecommunication Standard vD.0 and the SCRIPT Standard (paywalled), recorded against our own paraphrased names. ## What the profile system does A profile is a **descriptive** bundle of trading-partner / companion-guide conventions. v1 profiles attach attribution and drive `partitionWarnings`. They do **not** change how a message parses. - **`defineProfile(spec)`**: validates the spec and returns a frozen `NcpdpProfile` with a structured `describe()`: the `relaxes` / `adds` / `requires` buckets (quirks in merged order), the `standards` the profile touches, and the sorted de-duplicated union of `expectedWarnings`. `extends` composes parents (lineage flattens + dedupes, quirks merge by id with the child winning on collision while non-colliding parent quirks survive, `description` is last-wins). - **`setDefaultProfile` / `getDefaultProfile` / `resolveProfile`**: a single process-scoped default. Precedence: an explicit profile wins; `null` opts out of the default for that one call; `undefined` consults the default. - **`partitionWarnings(warnings, profile)`**: splits a parse's warnings into `expected` (in the profile's `expectedWarnings` union) vs. `unexpected`. The intended workflow: attach the partner's profile, then alert only on the `unexpected` set. - **`profiles`**: the frozen namespace holding the built-ins. NCPDP spans two unrelated standards, so one built-in ships per standard: `profiles.surescripts` (SCRIPT) and `profiles.pbm` (Telecom). ## Descriptive only: a profile never alters the parse v1 is intentionally inert on the parse path. `parseScript(xml, { profile })` and `parseTelecom(raw, { profile })` surface the resolved profile as `msg.profile` / `tx.profile`, but the body, header, segments, and warning list are **byte-identical** to a parse with no profile. This is asserted directly in `test/profiles/builtins.test.ts` (profile-on vs. profile-off divergence). The value of a v1 profile is **documentation + attribution + warning triage**, not stricter validation. ## The locked hard rule: no invented quirks Every quirk MUST cite a Tier-2 `fixture` (a relative path under `test/fixtures/`) that demonstrates its convention. This is enforced three independent ways: 1. **Type**: `fixture` is a required field on `NcpdpProfileQuirk`. 2. **Validation**: `defineProfile()` rejects a missing, absolute, or parent-escaping fixture path. 3. **Demonstrator**: `builtins.test.ts` holds a per-quirk demonstrator keyed `${profile}/${quirk.id}` that loads the cited fixture, parses it on the right standard, and asserts the convention is **actually present**. A quirk with no demonstrator fails the suite, so a real-but-irrelevant fixture cannot satisfy a generic exists-and-parses check. ## Built-in provenance ### `profiles.surescripts` (SCRIPT) | Quirk | Effect | Convention (paraphrased) | Documented in | Fixture | | --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | -------------------------------- | | `routing-identifiers` | adds | Header `To`/`From` carry Surescripts routing identifiers (the prescriber SPI and the receiving pharmacy NCPDP ID) on routed traffic. | Surescripts implementation guide: message routing | `script/surescripts-routing.xml` | A second quirk, `version-stamp-variance`, shipped in earlier `0.0.x` releases and has been removed. It claimed that partners stamp SCRIPT versions beyond the modeled set, and its only demonstrating fixture was stamped `2023011`. That version is federally adopted and is now modeled, so the fixture no longer demonstrates the claim. Keeping the quirk would have meant re-stamping the fixture with a version identifier no public source backs. Note that the fixture rule stated above would technically have been satisfied by such a re-stamp, since it asks only that a quirk cite a demonstrating fixture. What forbids it is the broader convention the rule exists to serve: a trading-partner quirk is encoded only when a real document grounds it, never invented. So the quirk was deleted instead. Nothing about the parse changed: an unrecognized version stamp is still read best-effort and still raises `NCPDP_SCRIPT_UNSUPPORTED_VERSION_TOLERATED`. What changed is that `profiles.surescripts` no longer lists that code in its `expectedWarnings`, so `partitionWarnings` now sorts it into `unexpected` rather than `expected`. ### `profiles.pbm` (Telecom vD.0) | Quirk | Effect | Convention (paraphrased) | Documented in | Fixture | | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------- | | `person-code-required` | requires | Insurance segment carries a Person Code (`303-C3`) distinguishing cardholder from dependent on a family policy. | NCPDP Telecom vD.0: Person Code (`303-C3`); PBM payer sheets commonly require it. | `telecom/pbm-person-code.ncpdp` | | `reject-code-depth` | adds | Response Status returns reject codes (`511-FB`) beyond the modeled core set; a code outside it raises `NCPDP_TELECOM_UNKNOWN_REJECT_CODE` and is preserved verbatim with `known:false`. | NCPDP Telecom vD.0: Reject Code (`511-FB`); PBM payer sheets enumerate partner-specific codes. | `telecom/pbm-reject-unknown.ncpdp` | | `response-dur-segment` | adds | A paid response carries an additional Response DUR/PPS segment (clinical alert) alongside the Response Status. | NCPDP Telecom vD.0: Response DUR/PPS segment; PBMs return clinical alerts on adjudication. | `telecom/pbm-response-dur.ncpdp` | The lenient parser already absorbs every one of these conventions; the profile makes the convention **explicit and documented** rather than relying on silent leniency. `person-code-required` and `response-dur-segment` parse with zero warnings; `reject-code-depth` raises exactly `NCPDP_TELECOM_UNKNOWN_REJECT_CODE`. ## What the profile system does NOT do - It does **not** change leniency, validation strictness, or output for any message. v1 is descriptive. - It does **not** ship a quirk that lacks a demonstrating fixture (the hard rule). - It does **not** reproduce NCPDP-copyrighted spec prose. Labels are paraphrases; `sourceCategory` names where a convention is documented, not its text. - It does **not** ship more than one built-in per standard in v1. ## PHI posture Every cited fixture is synthetic: no real BIN/PCN, no real NDC+patient combination, no real cardholder / member ID, NPI, or SPI. Profile source and tests embed only paraphrased field labels, no message bodies. ## Next - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): the warning codes a profile sorts into expected and unexpected, and what the reader did when it raised one. - [Telecom foundation and B1](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md): the segments the Telecom built-in describes conventions over. - [Telecom responses](https://docs.cosyte.com/ncpdp/spec-notes-telecom-response.md): where the reject-code and response DUR quirks above are read. - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): the reads a profile attaches to. --- URL: https://docs.cosyte.com/ncpdp/cookbook Markdown: https://docs.cosyte.com/ncpdp/cookbook.md # Cookbook Task-oriented recipes for the common reads. Each one is a few lines over a parsed message, using the real subpath exports: `@cosyte/ncpdp/script` for ePrescribing XML, `@cosyte/ncpdp/telecom` for the PBM claim wire, `@cosyte/ncpdp/common` for the shared value wrappers. The rules underneath every recipe are the same: the parser is **lenient** (vendor quirks become stable-coded `warnings`, not failures), the model is **immutable**, and safety-critical dispositions are **fail-safe**: a failure is never quietly read as a success, and money is never a float. > All XML and wire samples below are **synthetic**. Fixtures must never carry real PHI: no real > patient names, member IDs, prescriber NPIs, or NDCs. ## Parse a SCRIPT NewRx Read a NewRx ePrescribing message: header, patient, prescribed medication, and the coded product. `parseScript` never throws on a vendor quirk. It collects `warnings` and reads best-effort. `newRx` projects the NewRx body, or returns `undefined` for any other transaction. ```ts runnable import { parseScript, newRx } from "@cosyte/ncpdp/script"; const xml = `
SYNTH-MSG-0001
DOEJANE Amoxicillin 500 MG Oral Capsule 00000000001 30 Take 1 capsule by mouth three times daily for 10 days.
`; const msg = parseScript(xml); msg.header.messageId; // => "SYNTH-MSG-0001" const rx = newRx(msg); // the NewRx body, or undefined for another transaction rx?.patient?.name?.lastName; // => "DOE" rx?.medication?.description; // => "Amoxicillin 500 MG Oral Capsule" rx?.medication?.coded?.productCode?.value; // => "00000000001" rx?.medication?.coded?.productCode?.system; // => "NDC" rx?.medication?.quantity?.value?.source; // => "30" ``` - **Lenient by default.** Vendor quirks (own-text dates, alternate element shapes, an absent version) become `warnings` with an XPath position, not failures. Only unrecoverable structural corruption throws a typed `NcpdpScriptParseError`: empty input, non-XML, a non-`` root, a ``/`` payload (the XXE boundary), or a pre-XML legacy version. - **Coded product carries its own system.** `coded.productCode.system` is recognized from the wire qualifier (`"NDC"`, `"RXNORM"`, `"SNOMED"`, …); the raw `value` is always kept verbatim. - **A warning is a `code` plus an XPath `position`, by construction.** The message text comes from a frozen registry keyed by the code, and the factory that builds a warning takes no value argument at all, so no part of a document can reach it. The path is assembled only from element names this parser recognizes. Log `.warnings` whole; the parsed model is the part that holds patient data. ## Read a SCRIPT response (Status / Error / Verify) Every SCRIPT transaction is answered. The response spine reads the three acknowledgment transactions and exposes the disposition **without ever reading an `Error` as a success**. ```ts import { parseScript, status, error, verify } from "@cosyte/ncpdp/script"; const responseXml = `
SYNTH-MSG-0002
900Prescriber identifier could not be validated.
`; const msg = parseScript(responseXml); msg.disposition; // "success" (Status) | "error" (Error) | "verify" (Verify) | undefined msg.correlatesTo; // "SYNTH-MSG-0002": the answered request's MessageID () error(msg)?.code; // "900": the Error code, verbatim; never reformatted or looked up status(msg)?.description; // the positive-ack description, verbatim (undefined on an Error) verify(msg)?.code; ``` - **An `Error` never reads as success.** `disposition` is derived only from the response body kind, so a failure cannot be coerced to `"success"`: `status(msg)` is `undefined` on an Error. If a malformed message carries more than one response body, the most conservative disposition (Error first) wins and `NCPDP_SCRIPT_RESPONSE_AMBIGUOUS_DISPOSITION` is raised. - **`correlatesTo` ties the answer to its request.** It reads `` verbatim so you can match the acknowledgment back to the NewRx you sent. - **Codes and descriptions are surfaced verbatim.** ``, ``, and `` are read as-is; the library bundles no NCPDP code→meaning table. ## Read a Telecom PBM response (paid / rejected) The PBM answers a claim with a **response** transmission. `parseTelecom` detects the response shape automatically (it leads with the Version/Release, not the routing BIN); `adjudication` lifts the outcome. The same reader serves B1 billing, B2 reversal, B3 rebill, and E1 eligibility responses. ```ts import { parseTelecom, adjudication } from "@cosyte/ncpdp/telecom"; const t = parseTelecom(rawResponse); // kind: "response" (detected, not configured) const a = adjudication(t); // undefined for a request transmission a?.status?.disposition; // "paid" | "rejected" | "captured" | "approved" | "duplicate" | "unknown" a?.status?.rejectCodes; // every Reject Code (511-FB), verbatim, in wire order (none dropped) a?.status?.statusConflict; // true when the status field claimed paid but a reject was present a?.pricing?.patientPayAmount?.amount; // "10.00": implied 2-place decimal, string-wise (never a float) a?.pricing?.totalAmountPaid?.amount; // "45.00" a?.dur; // every returned DUR/PPS alert: one per occurrence, never collapsed ``` - **A reject always wins.** `disposition` is a total function over the Transaction Response Status (112-AN) **and** the reject codes together. If any reject is present the disposition is `"rejected"` even when the status field claims paid. A consumer is never told a rejected claim was paid. The self-contradiction surfaces via `NCPDP_TELECOM_STATUS_CONFLICT` and `status.statusConflict`. An unrecognized status reads `"unknown"`, never paid. - **Reject codes are verbatim.** Each is kept in wire order; an unrecognized code is preserved with `known: false` (`NCPDP_TELECOM_UNKNOWN_REJECT_CODE`) rather than dropped. No 511-FB label table ships, so today that is every reject code: you get the code, not a sentence about it. Which fields do have a label table, and on what source, is in [`KNOWN-LIMITATIONS.md`](https://github.com/cosyte/ncpdp/blob/main/KNOWN-LIMITATIONS.md). - **Money is never a float.** Every dollar amount carries an implied 2-place decimal (and an optional zoned-decimal overpunch sign); both are interpreted **string-wise** with the verbatim `source` kept, so binary floating point can never corrupt a paid amount. Anything unexpected is preserved with `isValid: false` and no interpreted amount. Money is never guessed. ## Decode the structured SIG (lossy, labeled) A medication's directions can arrive as free text **and** as a structured ``. The structured decode is **best-effort and explicitly lossy**: the free-text `sigText` stays the source of truth and is always preserved verbatim; the structured view is additive and every field is provenance-tagged. ```ts import { parseScript, newRx } from "@cosyte/ncpdp/script"; const sig = newRx(parseScript(xml))?.medication?.sig; sig?.sigText; // the free-text directions, verbatim (ALWAYS authoritative) sig?.hasStructuredData; // false when the carried only free text sig?.route.provenance; // "coded" | "derived" | "absent" sig?.route.code?.system; // "SNOMED" | "NCI" | … when coded sig?.route.text; // verbatim text when present sig?.dose.text; // the dose quantity, string-preserved (never a float, never guessed) ``` - **The free text is authoritative and never overwritten.** When the structured dose and the free text disagree, **both** are surfaced as-is. The library never collapses the disagreement into one answer. - **Per-field provenance.** Every component (`doseDeliveryMethod`, `dose`, `doseUnitOfMeasure`, `route`, `siteOfAdministration`, `administrationTiming`, `duration`, `vehicle`, `indication`, `maximumDoseRestriction`) is tagged `coded` / `derived` / `absent`. An **absent** field is never inferred from the free text. - **A component decodes only where its element name is grounded.** `doseUnitOfMeasure`, `duration`, `vehicle`, `indication` and `maximumDoseRestriction` have no element name this release can trace to a published field label, so they **always read `absent`** whatever the message carried. See [Structured SIG spec notes](https://docs.cosyte.com/ncpdp/spec-notes-structured-sig.md) for the per-component table and the evidence behind each recognized name. - **Ambiguous doses are never guessed.** If a dose structure is present but no unambiguous quantity can be read, `dose` is surfaced as `absent` and `NCPDP_SCRIPT_SIG_AMBIGUOUS_DOSE` is raised. Whenever any structured component decodes, `NCPDP_SCRIPT_SIG_STRUCTURED_LOSSY` flags the additive, lossy view. - **Decode-only.** v1 does not _generate_ a SIG from structure, and does not parse arbitrary natural-language directions. See [Structured SIG spec notes](https://docs.cosyte.com/ncpdp/spec-notes-structured-sig.md). ## Read a Telecom B1 claim The Telecommunication standard is the pharmacy-to-PBM claim protocol: a fixed positional Transaction Header followed by FS/GS/RS control-character-framed, field-id-keyed segments. `parseTelecom` decodes the header and segments; `claim` lifts the safety-relevant B1/B2/B3 request fields. ```ts import { parseTelecom, claim } from "@cosyte/ncpdp/telecom"; const t = parseTelecom(raw); // raw: string | Buffer (latin1 by default) t.header.transactionCode; // "B1" t.warnings; // stable, byte-offset-positioned tolerance warnings, never throws on quirks const c = claim(t); // the B1/B2/B3 request view, or undefined when no segments decoded c?.product?.id; // Product/Service ID (e.g. the NDC), verbatim c?.product?.qualifier; // Product/Service ID Qualifier (436-E1), verbatim: no label ships c?.quantityDispensed?.source; // Quantity Dispensed, verbatim c?.quantityDispensed?.impliedDecimal; // "30.000": implied 3-place decimal, applied string-wise c?.daysSupply?.source; // decimal-safe, never a float c?.prescriptionReferenceNumber; // the Rx reference, verbatim c?.cardholderId; // PHI: synthetic only in fixtures ``` - **Quantity is never a float.** Quantity Dispensed carries an implied 3-place decimal; it is scaled **string-wise** so binary floating point can never corrupt the value, and the verbatim `source` is kept. - **Versions are not guessed.** Which Telecom version this package decodes, which stamp it recognizes without decoding, and the dates those adoptions end are stated once in the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md) and are deliberately not restated here. In a **request**, a stamp that is recognized but not decoded is surfaced via `NCPDP_TELECOM_VF6_NOT_DECODED`; a stamp the reader does not recognize where it looked is refused with `NCPDP_TELECOM_UNSUPPORTED_VERSION`, and that includes the same stamp arriving on a **response**. A non-empty body with no framing bytes is `NCPDP_TELECOM_INVALID_FRAMING`. A separator is never guessed. - **Nothing is dropped.** Unknown segments/fields, a missing `AM`, and malformed tokens are preserved verbatim and warned. Read each decoded transaction at `t.transactions[n]`, or pass the index to a view (`claim(t, 1)`). See [Telecom spec notes](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md). ## Convert a date without inventing a timezone Parsed date fields are verbatim strings and stay that way. `dateValue` decodes one into a value; `toObject`, `toISO` and `toDate` read that value. The three names are the same in every `@cosyte/*` parser, so a consumer importing two of them aliases (`toISO as ncpdpToISO`) or namespaces them. ```ts runnable import { dateValue, toObject, toISO, toDate } from "@cosyte/ncpdp/common"; const dob = dateValue("19850722"); // Date of Birth (304-C4), CCYYMMDD toObject(dob); // => { year: 1985, month: 7, day: 22 } toISO(dob); // => "1985-07-22" toDate(dob); // => undefined toDate(dob, { assumeOffsetMinutes: 0 })?.toISOString(); // => "1985-07-22T00:00:00.000Z" toDate(dob, { assumeOffsetMinutes: -300 })?.toISOString(); // => "1985-07-22T05:00:00.000Z" dateValue("19880732"); // => undefined dateValue("1985-07-22"); // => undefined // A day the calendar does not have is refused on the value route too, not just the wire route. const feb30 = { source: "20240230", year: 2024, month: 2, day: 30 }; toISO(feb30); // => undefined toDate(feb30, { assumeOffsetMinutes: 0 }); // => undefined ``` - **`toDate` returns `undefined` unless you supply the zone.** No form decoded here carries a UTC offset, so the zone is never determinate on its own: the host machine's timezone is never read and UTC is never assumed. Pass `assumeOffsetMinutes` (an explicit `0` means "treat this naive value as UTC") or get nothing back. A date of birth resolved to a guessed zone lands on the previous day in every negative-offset zone, silently. - **One wire form is decoded, `CCYYMMDD`,** the only date form this package declares: Date of Service (401-D1) and Date of Birth (304-C4). Other date-bearing fields (SCRIPT ``, ``, ``; Telecom 443-E8 and 530-FU) are carried verbatim with no form stated, so `dateValue` answers `undefined` for them rather than guessing one. The fields themselves are unchanged and still readable. - **A day the calendar does not have is refused, never rolled over.** `DateValue` is an exported interface, so a value you build or spread yourself reaches the conversions without passing `dateValue`; it is bound by the same 4/100/400 leap rule, so both routes answer alike for the same eight digits. `toISO` never renders `2024-02-30`, which every ISO-8601 reader reads back as 1 March, and `toDate` never returns a day in the following month. - **Nothing is rewritten and nothing is zero-filled.** `toObject` reports only the components the value stated, `toISO` truncates to that precision and appends no `Z`, and the parsed model still carries the wire string. See the [Telecom spec notes](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md) for the header layout that positions Date of Service. ## Next - [Compound, COB and DUR depth](https://docs.cosyte.com/ncpdp/spec-notes-telecom-compound-cob.md): every compound ingredient, every other-payer money row, the submitted DUR interactions, and prior-authorization presence. - [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md): turning a model back into spec-clean wire form, and the inputs emit refuses outright. - [Trading-partner profiles](https://docs.cosyte.com/ncpdp/spec-notes-profiles.md): attaching a partner's conventions so you alert only on the warnings you did not expect. - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): every diagnostic code these recipes can raise, and what v1 does not do. - Read the **API reference** for every export, generated from source. - The [README](https://github.com/cosyte/ncpdp#readme) covers the lifecycle transactions (renewal / change / cancel) as well. --- URL: https://docs.cosyte.com/ncpdp/conformance Markdown: https://docs.cosyte.com/ncpdp/conformance.md # Conformance statement **What `@cosyte/ncpdp` decodes, against which adopted version, and on what date that version stops being the adopted one.** This page is the whole answer. It states, per wire format, the version this package decodes, the section of public law that adopts that version, the date on which that adoption ends, and whether any third party has tested this package. It is also the only place in this repository where the decoded version set is written down as a claim. Every other page defers to it, and `test/conformance-statement.test.ts` fails the build if this page and the shipped constants disagree **in either direction**: a version the code decodes and this page omits is a failure, and so is a version this page names and the code does not decode. Nothing here is a certification. Read [No third-party conformance record](#no-third-party-conformance-record) before treating any of it as one. ## Why these facts are cited to regulation and not to the standard The NCPDP Telecommunication Standard Implementation Guide and the NCPDP SCRIPT Standard Implementation Guide are **purchased products**. This package holds no licence to reproduce them and reproduces none of their prose, in code, in fixtures or here. Every version identifier and every date below is therefore read out of United States federal regulation, which is public law and can be cited freely: - **45 CFR 170.205** for electronic prescribing (SCRIPT). - **45 CFR 162.1102** for the retail pharmacy drug claim (Telecom `B1` / `B2` / `B3`). - **45 CFR 162.1202** for the pharmacy eligibility inquiry (Telecom `E1`). Sections are cited; standard prose is not quoted. Where a version this package decodes has no public section adopting it, the row below reads `no public section found` rather than implying one. Today no row needs that: every version this package decodes has a section. ## What is decoded, and until when | Wire format | Version | Status | Public section | Adoption ends | Third-party record | | ------------- | ------------------------------------------------ | ----------------------- | -------------------------------------------------- | ------------- | ------------------ | | SCRIPT | `2017071` | decoded | 45 CFR 170.205(b)(1) | 2028-01-01 | none | | SCRIPT | `2023011` | decoded | 45 CFR 170.205(b)(2) | none stated | none | | SCRIPT | legacy dotted, for example `10.6` | refused | not adopted by 45 CFR 170.205(b) | n/a | none | | SCRIPT | any other XML-era version, for example `2099001` | tolerated | not adopted by 45 CFR 170.205(b) | n/a | none | | Telecom | `D0` | decoded | 45 CFR 162.1102(b)(2)(i); 45 CFR 162.1202(b)(2)(i) | 2028-04-14 | none | | Telecom | `F6` | recognized, not decoded | 45 CFR 162.1102(e)(2)(i); 45 CFR 162.1202(d)(2)(i) | n/a | none | | Telecom | any other version stamp | refused | n/a | n/a | none | | Telecom Batch | `1.2` and `15` | not decoded | 45 CFR 162.1102(b)(2)(i); 45 CFR 162.1102(e)(2)(i) | n/a | none | **Reading the status column.** - **decoded**: the reader parses a message carrying this version against that version's field layout, and the modelled reads (`claim`, `adjudication`, `newRx`, the lifecycle and response projections) are available over it. - **recognized, not decoded**: the stamp is identified where the reader looks for it, the parse succeeds, and no positional field is read. **This status is direction-dependent and the direction is not a detail**: `F6` is recognized in a request and refused in a response. See [F6: recognized in a request, refused in a response](#f6-recognized-in-a-request-refused-in-a-response). - **tolerated**: the version string is present, is not one of the adopted versions above, and is not a pre-XML dotted version. The document is still parsed, against the same XML-era field model, and `NCPDP_SCRIPT_UNSUPPORTED_VERSION_TOLERATED` is raised. Being lenient about an odd-but-present version string is not a claim that the field model is right for it, and nothing about that version is adopted by 45 CFR 170.205(b). - **refused**: a typed fatal is thrown rather than a wrong field position returned. `NCPDP_SCRIPT_UNSUPPORTED_VERSION` for a pre-XML dotted SCRIPT version, `NCPDP_TELECOM_UNSUPPORTED_VERSION` for a Telecom stamp the reader does not recognize at the offset it read. - **not decoded**: no implementation of that framing exists in this package at all. Batch is the only entry of this kind: `grep -ri batch src/` finds nothing, and that is deliberate rather than pending. ## The dates, and the sections that set them - **SCRIPT `2017071` ends on 2028-01-01.** 45 CFR 170.205(b)(1) adopts that version and states that the Secretary's adoption of it expires on January 1, 2028. After that date the paragraph adopts `2023011` alone. - **SCRIPT `2023011` has no stated end.** 45 CFR 170.205(b)(2) adopts it and sets no expiry, so this page records none rather than inventing one. - **Telecom claims: 2027-08-14, then 2028-04-14.** 45 CFR 162.1102(c) makes the paragraph (b)(2) standards, which include D.0, the adopted retail pharmacy drug claim standards for the period from January 1, 2012 through August 14, 2027. That paragraph excepts one of them, the 1999 Version 5.1 guide at (b)(2)(v)(A), which this package does not decode either. 45 CFR 162.1102(e) makes F6 and Batch 15 permissible alongside them from 2027-08-14 through 2028-04-14. 45 CFR 162.1102(f) leaves only the paragraph (e)(2) standards adopted on and after 2028-04-14. - **Telecom eligibility: the same two dates.** 45 CFR 162.1202(c), 45 CFR 162.1202(d) and 45 CFR 162.1202(e) carry the same ladder for the `E1` eligibility inquiry, without that carve-out: D.0 through 2027-08-14, either option through 2028-04-14, and the paragraph (d)(2) standards alone on and after 2028-04-14. **What that means for a consumer planning ahead.** The Telecom decode in this package has a dated end, and it is not far off. D.0 is the expiring standard, not the standing one. ## F6: recognized in a request, refused in a response A Telecom **request** transmission whose version stamp is `F6` is recognized and **not decoded**. The F6 header widens the leading identification field, so reading it at D.0 offsets would misalign safety-critical fields, and a wrong field position is a wrong dispense. The reader declines to do that. What a **request** carrying `F6` gets instead: the parse succeeds, `header.versionRelease` carries the stamp, every other positional header field comes back empty, and the warning `NCPDP_TELECOM_VF6_NOT_DECODED` is raised at the Version/Release position. **No transaction is decoded**, and that is worth saying separately, because every group-separated transaction of a decoded transmission is: on an `F6` request `decodedTransactionCount` is `0` and `transactions` is empty however many transactions arrived on the wire, so the `segments` alias is empty with them. Nothing is guessed and nothing is dropped; the original bytes are still yours to forward. **An `F6` response transmission is refused today, not warned, and you should plan for that rather than for a graceful degrade.** A response leads with its Version/Release at the first byte, where a request leads with the routing identifier and carries the stamp further in. The reader treats a transmission as a response only when that leading stamp is `D0`; anything else is read at the request offsets, where a stamp sitting at the first byte is not visible. So a response stamped `F6` raises the typed fatal `NCPDP_TELECOM_UNSUPPORTED_VERSION` and returns nothing to forward. That is the same refusal the table gives any unrecognized stamp, and it is stated here because the response leg is the one carrying adjudication money. The gap is dated rather than open ended. 45 CFR 162.1102(e)(2)(i) names Version F6, January 2020 and Batch Version 15, October 2017, and 45 CFR 162.1102(f) leaves those two as the only adopted retail pharmacy drug claim standards on and after **2028-04-14**. 45 CFR 162.1202(e) does the same for the eligibility inquiry. From that date a claim on the wire is an F6 claim, and this package does not decode it. ## No third-party conformance record **For SCRIPT and for Telecom alike, no third party has tested this package.** There is no external conformance record for either wire format. This section states that rather than leaving the absence to be read as an endorsement, and two things that carry the word "certification" are worth being precise about, because neither is a record about this software. - **The NCPDP Certification Program certifies people, not systems.** It is an exam taken by an individual, and what it attests to is that individual. Someone who passes it earns a post-nominal designation for their own name and is sent an embossed certificate, a lapel pin and a congratulatory letter. No registry of certified organizations or certified systems is published ([https://www.ncpdp.org/Resources/NCPDP-Certification-Program-FAQs](https://www.ncpdp.org/Resources/NCPDP-Certification-Program-FAQs)). A search that finds the word "certification" here and stops has found the opposite of a conformance record. - **The one software conformance instrument targets a version this package refuses.** ONC, NIST and NCPDP built an electronic conformance testing tool for the SCRIPT standard, and NCPDP took over its stewardship in August 2018. It tests SCRIPT v10.6 ([https://www.healthit.gov/buzz-blog/health-it/a-new-home-for-the-electronic-prescribing-testing-tool](https://www.healthit.gov/buzz-blog/health-it/a-new-home-for-the-electronic-prescribing-testing-tool)). A dotted legacy version such as `10.6` is exactly the shape this package refuses with a typed fatal, so even were the tool reachable it would test a version this library declines to parse. ## What stands in for an external oracle There is no external oracle here, and these are the substitutes, stated so a reader can weigh them rather than assume something stronger: - A three-tier **synthetic** corpus: spec-clean, vendor-quirk, and round-trip goldens. - The `@cosyte/test-utils` property invariants: lenient never-throw, round-trip, immutability and warning-code stability. - A nightly amplified fuzz job over the Telecom byte tokenizer and the SCRIPT XML entity-expansion surface. - Per-directory coverage gates. There is deliberately **no differential corpus** against a third-party reference implementation, and the reason is the licensing posture above rather than an oversight: such a corpus would require redistributing NCPDP-derived material this package is not licensed to ship. The same reasoning, and the rest of the do-not-over-trust list, is in [KNOWN-LIMITATIONS.md](https://github.com/cosyte/ncpdp/blob/main/KNOWN-LIMITATIONS.md). Do not assume byte-for-byte agreement with any specific vendor or switch implementation. ## When a decoded version is added or retired This page is not maintained by remembering to maintain it. The decoded set in the table above is compared, on every test run, against the constants the package actually ships: `KNOWN_SCRIPT_VERSIONS` for SCRIPT and the version stamps `detectVersion` classifies for Telecom. A change to either without a matching change here fails `test/conformance-statement.test.ts`, and the failure names the version that disagrees. What an `F6` message _does_ is derived the same way rather than described from memory: the test parses one in each direction and requires this page to state the outcome it observed, per direction. It also parses the same two-transaction body under each stamp and requires this page to state how many transactions the `F6` request decoded, against a decoded control that shows the body really carried more than one. A change that started decoding `F6` responses, that changed the code they raise, or that started decoding an `F6` request's transactions, would red here until this page said so. The same test closes the citation set: a citation on this page that is not one of the three CFR sections above, one of the two public URLs above, or a file in this repository fails it. It also rejects the overclaim shapes this package must never make about itself. Each of those is proved by a control in that test file, which is where they are enumerated, rather than written out here. ## Scope of this statement Out of scope here, and named so the silence is not read as coverage: - **Payer sheets, the External Code List, and reject-code vocabularies.** Those rest on documents this package cannot publicly read. What labels ship, and on what artifact, is in [KNOWN-LIMITATIONS.md](https://github.com/cosyte/ncpdp/blob/main/KNOWN-LIMITATIONS.md). - **Any claim of certification, vendor parity or an external oracle.** See above. - **Batch framing.** Named in the table as not decoded, and not added by this statement. ## Next - [Troubleshooting and known limitations](https://docs.cosyte.com/ncpdp/troubleshooting.md): the diagnostic codes the statuses above raise, and the rest of the do-not-over-trust list. - [Telecom foundation and B1](https://docs.cosyte.com/ncpdp/spec-notes-telecom.md): the layout the decoded stamp is read against. - [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md): what emit writes back, and what it refuses to write. - [Getting started](https://docs.cosyte.com/ncpdp/intro.md): the package in one page, if you arrived here first. --- URL: https://docs.cosyte.com/ncpdp/api Markdown: https://docs.cosyte.com/ncpdp/api.md # @cosyte/ncpdp Public entry point for the `@cosyte/ncpdp` package. NCPDP is two structurally unrelated standards under one brand. They ship via subpath exports: `@cosyte/ncpdp/script` (SCRIPT XML ePrescribing), `@cosyte/ncpdp/telecom` (the Telecommunication claim standard), and `@cosyte/ncpdp/common` (shared vocabulary). This root re-exports the implemented SCRIPT and Telecom surfaces plus the shared common vocabulary for convenience; deep imports from the subpaths are equivalent and keep Telecom-only or SCRIPT-only consumers lean. ## Classes ### NcpdpProfileError Thrown by `defineProfile()` and profile-validation code when a profile definition is structurally invalid. Carries the offending profile name (when known) so consumers can pinpoint which definition failed. #### Example ```ts import { defineProfile, NcpdpProfileError } from "@cosyte/ncpdp/profiles"; try { defineProfile({ name: "" }); } catch (err) { if (err instanceof NcpdpProfileError) { console.error(err.message, err.profileName); } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new NcpdpProfileError**(`message`, `profileName?`): [`NcpdpProfileError`](#ncpdpprofileerror) **`Internal`** Construct a new `NcpdpProfileError`. `profileName` is optional so the name validator can throw before a usable name is available. ###### Parameters ###### message `string` ###### profileName? `string` ###### Returns [`NcpdpProfileError`](#ncpdpprofileerror) ###### Overrides `Error.constructor` #### Properties ##### profileName > `readonly` **profileName**: `string` \| `undefined` *** ### NcpdpScriptBuildError Thrown when the SCRIPT emit side is asked for a message it cannot render honestly: an invalid-by-construction message handed to the builder, or a transaction this library does not model handed to the serializer. Carries a stable [ScriptBuildCode](#scriptbuildcode), and its `message` is that code's [SCRIPT\_BUILD\_MESSAGES](#script_build_messages) entry: emit input is caller-supplied and PHI-dense, so none of it is quoted back. The error carries the code and nothing else, so it, including its `stack`, is safe to log whole. #### Example ```ts try { buildScriptResponse({ kind: "Status" }); } catch (err) { if (err instanceof NcpdpScriptBuildError) { err.code; // "NCPDP_SCRIPT_BUILD_MISSING_RESPONSE_CODE" } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new NcpdpScriptBuildError**(`code`): [`NcpdpScriptBuildError`](#ncpdpscriptbuilderror) ###### Parameters ###### code [`ScriptBuildCode`](#scriptbuildcode) The stable build error code, which selects the message. ###### Returns [`NcpdpScriptBuildError`](#ncpdpscriptbuilderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`ScriptBuildCode`](#scriptbuildcode) Stable, machine-readable build error code. *** ### NcpdpScriptParseError Thrown when NCPDP SCRIPT input is structurally unrecoverable. Carries a stable [ScriptFatalCode](#scriptfatalcode) and optional positional context, and **nothing else**. `message` is the [SCRIPT\_FATAL\_MESSAGES](#script_fatal_messages) entry for the code, so the error, including its `stack`, is safe to log and safe to forward to an error reporter. It carries no snippet of the offending input. An earlier version did, capped at 64 characters and documented as a redaction boundary; the cap bounded the length and nothing about the content, and the paths that raised it are exactly the paths where the input is too broken to say where in it those characters came from. `NcpdpScriptBuildError` and both Telecom errors had already refused a snippet for that reason. This one now agrees with them. #### Example ```ts try { parseScript("not xml"); } catch (err) { if (err instanceof NcpdpScriptParseError) { err.code; // "NCPDP_SCRIPT_NOT_XML" } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new NcpdpScriptParseError**(`code`, `opts?`): [`NcpdpScriptParseError`](#ncpdpscriptparseerror) ###### Parameters ###### code [`ScriptFatalCode`](#scriptfatalcode) The stable fatal code, which selects the message. ###### opts? Optional positional context. ###### position? [`ScriptPosition`](#scriptposition) ###### Returns [`NcpdpScriptParseError`](#ncpdpscriptparseerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`ScriptFatalCode`](#scriptfatalcode) Stable, machine-readable fatal code. ##### position? > `readonly` `optional` **position?**: [`ScriptPosition`](#scriptposition) XPath-style location of the failure, when known. *** ### NcpdpTelecomBuildError Thrown when the Telecom builder is asked to construct an invalid-by-construction transaction. Carries a stable [TelecomBuildCode](#telecombuildcode), and its `message` is that code's [TELECOM\_BUILD\_MESSAGES](#telecom_build_messages) entry; like the parse error it never quotes the offending value (Telecom data is PHI-dense). #### Example ```ts try { buildTelecomRequest({ header: {}, segments: [] }); } catch (err) { if (err instanceof NcpdpTelecomBuildError) { err.code; // "NCPDP_TELECOM_BUILD_MISSING_TRANSACTION_CODE" } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new NcpdpTelecomBuildError**(`code`, `headerField?`): [`NcpdpTelecomBuildError`](#ncpdptelecombuilderror) ###### Parameters ###### code [`TelecomBuildCode`](#telecombuildcode) The stable build error code, which selects the message. ###### headerField? keyof TelecomHeader The fixed-header slot at fault, when applicable. ###### Returns [`NcpdpTelecomBuildError`](#ncpdptelecombuilderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`TelecomBuildCode`](#telecombuildcode) Stable, machine-readable build error code. ##### headerField? > `readonly` `optional` **headerField?**: keyof TelecomHeader Which fixed-header field the builder rejected, when the rejection was about one. Typed as `keyof TelecomHeader`, so it can only ever be one of this parser's own nine header field names: it names the **slot**, never anything the caller supplied. Absent for rejections that are not header-field-scoped. *** ### NcpdpTelecomParseError Thrown when NCPDP Telecommunication-standard input is structurally unrecoverable. Carries a stable [TelecomFatalCode](#telecomfatalcode) and optional positional context, and nothing else. `message` is the [TELECOM\_FATAL\_MESSAGES](#telecom_fatal_messages) entry for the code, and it intentionally carries no snippet of the offending bytes: a Telecom message is PHI-dense, so the [TelecomPosition](#telecomposition) (offset + field id, never a value) is the only context. #### Example ```ts try { parseTelecom(""); } catch (err) { if (err instanceof NcpdpTelecomParseError) { err.code; // "EMPTY_INPUT" } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new NcpdpTelecomParseError**(`code`, `opts?`): [`NcpdpTelecomParseError`](#ncpdptelecomparseerror) ###### Parameters ###### code [`TelecomFatalCode`](#telecomfatalcode) The stable fatal code, which selects the message. ###### opts? Optional positional context. ###### position? [`TelecomPosition`](#telecomposition) ###### Returns [`NcpdpTelecomParseError`](#ncpdptelecomparseerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`TelecomFatalCode`](#telecomfatalcode) Stable, machine-readable fatal code. ##### position? > `readonly` `optional` **position?**: [`TelecomPosition`](#telecomposition) Byte-offset context of the failure, when known. *** ### ScriptMessage An immutable parsed SCRIPT message: routing header, the typed body, and any non-fatal warnings raised while parsing. Construct via "./parse".parseScript: instances are deeply frozen. #### Example ```ts const msg = parseScript(xml); msg.header.messageId; msg.asNewRx()?.medication?.description; ``` #### Constructors ##### Constructor > **new ScriptMessage**(`init`): [`ScriptMessage`](#scriptmessage) ###### Parameters ###### init Pre-extracted header, body, warnings, and optional profile. ###### body [`ScriptBody`](#scriptbody) ###### header [`ScriptHeader`](#scriptheader) ###### profile? [`NcpdpProfile`](#ncpdpprofile) ###### warnings readonly [`NcpdpScriptWarning`](#ncpdpscriptwarning)[] ###### Returns [`ScriptMessage`](#scriptmessage) #### Properties ##### body > `readonly` **body**: [`ScriptBody`](#scriptbody) The typed transaction body. ##### header > `readonly` **header**: [`ScriptHeader`](#scriptheader) Routing/correlation header fields. ##### profile? > `readonly` `optional` **profile?**: [`NcpdpProfile`](#ncpdpprofile) The trading-partner profile in effect for this parse: either passed explicitly via `parseScript`'s `options.profile` or resolved from the process-scoped default. Present only when a profile applied; attribution only (v1 profiles never alter the parse). ##### warnings > `readonly` **warnings**: readonly [`NcpdpScriptWarning`](#ncpdpscriptwarning)[] Non-fatal warnings, in the order raised. #### Accessors ##### correlatesTo ###### Get Signature > **get** **correlatesTo**(): `string` \| `undefined` The identifier of the message this one answers (``), or `undefined`. The correlation key that ties a response back to its request. ###### Example ```ts parseScript(responseXml).correlatesTo; // the request's MessageID ``` ###### Returns `string` \| `undefined` The correlated message id, or `undefined`. ##### disposition ###### Get Signature > **get** **disposition**(): [`ResponseDisposition`](#responsedisposition) \| `undefined` The disposition of this message when it is a response transaction (``/``/``), else `undefined`. Derived only from the body kind: an `` is **always** `"error"` and is never read as a success. ###### Example ```ts parseScript(xml).disposition; // "success" | "error" | "verify" | undefined ``` ###### Returns [`ResponseDisposition`](#responsedisposition) \| `undefined` The [ResponseDisposition](#responsedisposition), or `undefined` for a request / unsupported transaction. #### Methods ##### asError() > **asError**(): [`ErrorBody`](#errorbody) \| `undefined` The [ErrorBody](#errorbody) when this message is an `` response, else `undefined`. ###### Returns [`ErrorBody`](#errorbody) \| `undefined` The Error body, or `undefined`. ###### Example ```ts parseScript(xml).asError()?.code; ``` ##### asLifecycleRequest() > **asLifecycleRequest**(): [`LifecycleRequest`](#lifecyclerequest) \| `undefined` The [LifecycleRequest](#lifecyclerequest) body when this message is a renewal/change/cancel **request** (`RxRenewalRequest`/`RxChangeRequest`/`CancelRx`), else `undefined`. ###### Returns [`LifecycleRequest`](#lifecyclerequest) \| `undefined` The lifecycle request body, or `undefined`. ###### Example ```ts parseScript(xml).asLifecycleRequest()?.medicationPrescribed?.description; ``` ##### asLifecycleResponse() > **asLifecycleResponse**(): [`LifecycleResponse`](#lifecycleresponse) \| `undefined` The [LifecycleResponse](#lifecycleresponse) body when this message is a renewal/change/cancel **response** (`RxRenewalResponse`/`RxChangeResponse`/`CancelRxResponse`), else `undefined`. ###### Returns [`LifecycleResponse`](#lifecycleresponse) \| `undefined` The lifecycle response body, or `undefined`. ###### Example ```ts parseScript(xml).asLifecycleResponse()?.outcome; // "approved" | "denied" | … ``` ##### asNewRx() > **asNewRx**(): [`NewRx`](#newrx) \| `undefined` The [NewRx](#newrx) body when this message is a NewRx, else `undefined`. ###### Returns [`NewRx`](#newrx) \| `undefined` The NewRx body, or `undefined`. ###### Example ```ts const rx = parseScript(xml).asNewRx(); rx?.medication?.description; ``` ##### asStatus() > **asStatus**(): [`StatusBody`](#statusbody) \| `undefined` The [StatusBody](#statusbody) when this message is a `` response, else `undefined`. ###### Returns [`StatusBody`](#statusbody) \| `undefined` The Status body, or `undefined`. ###### Example ```ts parseScript(xml).asStatus()?.code; ``` ##### asVerify() > **asVerify**(): [`VerifyBody`](#verifybody) \| `undefined` The [VerifyBody](#verifybody) when this message is a `` response, else `undefined`. ###### Returns [`VerifyBody`](#verifybody) \| `undefined` The Verify body, or `undefined`. ###### Example ```ts parseScript(xml).asVerify()?.code; ``` ##### toString() > **toString**(): `string` Serialize this message back to canonical NCPDP SCRIPT XML. Equivalent to "./serialize".serializeScript; only the modeled fields are emitted, so the result is canonical (idempotent under re-parse) rather than byte-identical to any original input. Because this is the object's own string conversion, it also runs on implicit coercion (a template literal, string concatenation, a log line). When the body is a transaction this library does not model it **throws** rather than returning a document with the transaction body deleted, so check `message.body.kind !== "unsupported"` before coercing a message you did not build yourself. ###### Returns `string` The canonical SCRIPT XML string. ###### Throws NcpdpScriptBuildError with "../common/errors".SCRIPT\_BUILD\_CODES.UNSUPPORTED\_TRANSACTION when `body.kind` is `"unsupported"`. No string is returned. ###### Example ```ts parseScript(raw).toString(); // canonical XML ``` ## Interfaces ### CancelRx A prescriber-initiated request to retract a prescription (``). #### Extends - [`LifecycleRequestFields`](#lifecyclerequestfields) #### Properties ##### kind > `readonly` **kind**: `"CancelRx"` ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The prescription this request concerns (``). ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`medicationPrescribed`](#medicationprescribed-2) ##### patient? > `readonly` `optional` **patient?**: [`Patient`](#patient-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`patient`](#patient-1) ##### pharmacy? > `readonly` `optional` **pharmacy?**: [`Pharmacy`](#pharmacy-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`pharmacy`](#pharmacy-1) ##### prescriber? > `readonly` `optional` **prescriber?**: [`Prescriber`](#prescriber-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`prescriber`](#prescriber-1) ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number, verbatim (``). ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`requestReferenceNumber`](#requestreferencenumber-2) *** ### CancelRxResponse A pharmacy's confirmation/denial of a cancel request (``). #### Extends - [`LifecycleResponseFields`](#lifecycleresponsefields) #### Properties ##### kind > `readonly` **kind**: `"CancelRxResponse"` ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The medication carried with the response. For an `approvedWithChanges` outcome this is the **changed** medication and is the field a consumer must read to dispense correctly. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`medicationPrescribed`](#medicationprescribed-3) ##### outcome > `readonly` **outcome**: [`ResponseOutcome`](#responseoutcome) The prescriber's decision, detected fail-safe from the `` choice. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`outcome`](#outcome-1) ##### reason? > `readonly` `optional` **reason?**: [`ResponseReason`](#responsereason) The reason carried with the outcome, when present. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`reason`](#reason-1) ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number echoing the request, verbatim (``). ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`requestReferenceNumber`](#requestreferencenumber-3) *** ### CodedValue A coded value: the raw code, its source qualifier, and the normalized system we recognized from that qualifier. The original qualifier is always preserved so a consumer can re-derive the mapping if our table lags the spec. #### Properties ##### qualifier > `readonly` **qualifier**: `string` The source qualifier string that accompanied the code. ##### system > `readonly` **system**: [`CodeSystem`](#codesystem) Normalized code system recognized from [qualifier](#qualifier). ##### value > `readonly` **value**: `string` The code itself, verbatim. *** ### DateParts The calendar components a parsed value actually stated, and nothing else. ONLY STATED COMPONENTS ARE PRESENT. A component the value did not state is absent: the key is not there at all, rather than there holding `undefined`. `Object.keys()` of the result is therefore exactly the set of stated components and the value's precision is recoverable from it. There is no `precision` key, no `source` key and no `isValid` key. This shape is shared verbatim with the sibling `@cosyte/*` parsers, so all eight components are declared here even though this package can populate only three of them. The one wire form decoded is `CCYYMMDD`, a whole calendar day, so `year`, `month` and `day` are always present together and `hour`, `minute`, `second`, `millisecond` and `offsetMinutes` are never present at all. They are declared so that a consumer moving between two `@cosyte/*` parsers reads one declaration rather than two. Deleting `offsetMinutes` leaves an object that `Temporal.PlainDate.from` and luxon's `DateTime.fromObject` accept with no key rename and no value adjustment: `month` is spec-native 1 to 12 and the names are singular. That compatibility is the reason for the shape. Neither library is a dependency of this package and neither is imported here. #### Properties ##### day? > `readonly` `optional` **day?**: `number` Calendar day of month. ##### hour? > `readonly` `optional` **hour?**: `number` Hour of day, 0 to 23. Never populated by this package. ##### millisecond? > `readonly` `optional` **millisecond?**: `number` Millisecond, 0 to 999. Never populated by this package. ##### minute? > `readonly` `optional` **minute?**: `number` Minute of hour, 0 to 59. Never populated by this package. ##### month? > `readonly` `optional` **month?**: `number` Calendar month, 1 to 12: spec-native, never the JS `Date` 0 to 11. ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` Signed minutes east of UTC. Never populated by this package. ##### second? > `readonly` `optional` **second?**: `number` Second of minute, 0 to 59. Never populated by this package. ##### year? > `readonly` `optional` **year?**: `number` Four-digit calendar year, as stated (`0050` is year 50, never 1950). *** ### DateValue A calendar date decoded from an NCPDP wire value, preserving the wire string exactly beside the components it stated. ONE WIRE FORM IS DECODED, AND THAT IS DELIBERATE: `CCYYMMDD`, a fixed run of eight digits naming one whole calendar day. It is the only date form this package declares anywhere, on Date of Service (401-D1) in the Transaction Header and on Date of Birth (304-C4) in the Patient segment. Every other date-bearing or time-bearing field this package surfaces is carried verbatim with no form stated for it: the SCRIPT ``, `` and `` values, and the Telecom Other Payer Date (443-E8) and Previous Date Of Fill (530-FU) values. Those are UNDECODED here rather than guessed, because the documents that would settle their form are purchased products this package cannot cite, and a date decoded from an unread specification is the quiet wrong answer this whole surface exists to refuse. The parsed structures are untouched by any of this. `header.dateOfService`, `claim.dateOfBirth`, `patient.dateOfBirth` and every other date field are still the verbatim strings they have always been; decoding one is opt-in. #### Properties ##### day > `readonly` **day**: `number` Calendar day of month, 1 to the length of [month](#month-1) in [year](#year-1). ##### month > `readonly` **month**: `number` Calendar month, 1 to 12: spec-native, never the JS `Date` 0 to 11. ##### source > `readonly` **source**: `string` The date exactly as it appeared on the wire. ##### year > `readonly` **year**: `number` Four-digit calendar year, as stated (`0050` is year 50, never 1950). *** ### DecimalValue A decimal value preserved exactly as it appeared on the wire. NCPDP quantities, strengths, and days-supply are decimal quantities where binary floating point would silently corrupt the value (e.g. `0.1`). We never parse them into a JS `number`; we keep the original source string and a validity flag, leaving any arithmetic to the consumer who can choose a decimal-safe representation. #### Properties ##### isValid > `readonly` **isValid**: `boolean` True when [source](#source-1) matches a plain decimal numeric literal. ##### source > `readonly` **source**: `string` The original textual value, verbatim. *** ### DrugCoded A coded drug product (``). #### Properties ##### drugDbCode? > `readonly` `optional` **drugDbCode?**: [`CodedValue`](#codedvalue) `` with its qualifier resolved to a code system. ##### productCode? > `readonly` `optional` **productCode?**: [`CodedValue`](#codedvalue) `` with its qualifier resolved to a code system. *** ### ErrorBody A SCRIPT ``: a negative acknowledgment. Its [ResponseFields.code](#code-7) and description are surfaced verbatim and it always dispositions as `"error"`: it is never coerced to a success. #### Extends - [`ResponseFields`](#responsefields) #### Properties ##### code? > `readonly` `optional` **code?**: `string` Primary response code, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`code`](#code-7) ##### description? > `readonly` `optional` **description?**: `string` Free-text description, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`description`](#description-5) ##### descriptionCode? > `readonly` `optional` **descriptionCode?**: `string` Secondary description code, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`descriptionCode`](#descriptioncode-1) ##### kind > `readonly` **kind**: `"Error"` *** ### LifecycleRequestFields Fields shared by the lifecycle **request** transactions. #### Extended by - [`RxRenewalRequest`](#rxrenewalrequest) - [`RxChangeRequest`](#rxchangerequest) - [`CancelRx`](#cancelrx) #### Properties ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The prescription this request concerns (``). ##### patient? > `readonly` `optional` **patient?**: [`Patient`](#patient-4) ##### pharmacy? > `readonly` `optional` **pharmacy?**: [`Pharmacy`](#pharmacy-4) ##### prescriber? > `readonly` `optional` **prescriber?**: [`Prescriber`](#prescriber-4) ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number, verbatim (``). *** ### LifecycleResponseFields Fields shared by the lifecycle **response** transactions. #### Extended by - [`RxRenewalResponse`](#rxrenewalresponse) - [`RxChangeResponse`](#rxchangeresponse) - [`CancelRxResponse`](#cancelrxresponse) #### Properties ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The medication carried with the response. For an `approvedWithChanges` outcome this is the **changed** medication and is the field a consumer must read to dispense correctly. ##### outcome > `readonly` **outcome**: [`ResponseOutcome`](#responseoutcome) The prescriber's decision, detected fail-safe from the `` choice. ##### reason? > `readonly` `optional` **reason?**: [`ResponseReason`](#responsereason) The reason carried with the outcome, when present. ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number echoing the request, verbatim (``). *** ### MedicationPrescribed The prescribed medication (``). #### Properties ##### coded? > `readonly` `optional` **coded?**: [`DrugCoded`](#drugcoded) ##### daysSupply? > `readonly` `optional` **daysSupply?**: [`DecimalValue`](#decimalvalue) ##### description? > `readonly` `optional` **description?**: `string` ##### directions? > `readonly` `optional` **directions?**: `string` Free-text directions (``), verbatim. ##### note? > `readonly` `optional` **note?**: `string` ##### numberOfRefills? > `readonly` `optional` **numberOfRefills?**: `string` ##### quantity? > `readonly` `optional` **quantity?**: [`Quantity`](#quantity-1) ##### sig? > `readonly` `optional` **sig?**: [`StructuredSig`](#structuredsig) Best-effort, **lossy** decode of the structured ``, present only when a `` element exists. The free-text [sigText](#sigtext) stays the source of truth; this structured view is additive and per-field provenance-tagged. ##### sigText? > `readonly` `optional` **sigText?**: `string` Structured SIG free text (``), verbatim. ##### strength? > `readonly` `optional` **strength?**: [`Strength`](#strength-1) ##### substitutions? > `readonly` `optional` **substitutions?**: `string` ##### writtenDate? > `readonly` `optional` **writtenDate?**: `string` Written date, verbatim. *** ### NcpdpProfile A readonly, frozen profile produced by `defineProfile()`. Mirrors the locked x12/hl7 shape (name / description / lineage) plus NCPDP's `standard`-tagged `quirks` axis and a structured `describe()`. #### Example ```ts import { parseTelecom } from "@cosyte/ncpdp/telecom"; import { profiles } from "@cosyte/ncpdp/profiles"; const tx = parseTelecom(raw, { profile: profiles.pbm }); tx.profile?.name; // "pbm" tx.profile?.describe().requires.length; ``` #### Properties ##### describe > `readonly` **describe**: () => [`NcpdpProfileDescription`](#ncpdpprofiledescription-1) ###### Returns [`NcpdpProfileDescription`](#ncpdpprofiledescription-1) ##### description? > `readonly` `optional` **description?**: `string` ##### lineage > `readonly` **lineage**: readonly `string`[] ##### name > `readonly` **name**: `string` ##### quirks > `readonly` **quirks**: readonly [`NcpdpProfileQuirk`](#ncpdpprofilequirk)[] *** ### NcpdpProfileDescription Structured `describe()` output: the "what this profile relaxes / adds / requires" record published with the package. Returned as DATA (not a formatted string) so downstream tooling: docs generators, the `pathways` engine: can consume it programmatically. #### Example ```ts import { profiles } from "@cosyte/ncpdp/profiles"; const d = profiles.pbm.describe(); d.requires.map((q) => q.id); // ["person-code-required"] d.expectedWarnings; // readonly NcpdpWarningCode[] ``` #### Properties ##### adds > `readonly` **adds**: readonly [`NcpdpProfileQuirk`](#ncpdpprofilequirk)[] ##### description? > `readonly` `optional` **description?**: `string` ##### expectedWarnings > `readonly` **expectedWarnings**: readonly [`NcpdpWarningCode`](#ncpdpwarningcode)[] Sorted, de-duplicated union of every quirk's `expectedWarnings`. ##### lineage > `readonly` **lineage**: readonly `string`[] ##### name > `readonly` **name**: `string` ##### relaxes > `readonly` **relaxes**: readonly [`NcpdpProfileQuirk`](#ncpdpprofilequirk)[] ##### requires > `readonly` **requires**: readonly [`NcpdpProfileQuirk`](#ncpdpprofilequirk)[] ##### standards > `readonly` **standards**: readonly [`NcpdpStandard`](#ncpdpstandard)[] Standards this profile's quirks touch (sorted, de-duplicated). *** ### NcpdpProfileQuirk A single trading-partner convention captured by a profile. Every quirk is fixture-grounded: `fixture` points at a real Tier-2 corpus file that demonstrates the convention, and `sourceCategory` records where it is documented. This is the locked hard rule: a quirk without a demonstrating fixture is forbidden, enforced both by this required field and by the accuracy test. #### Example ```ts import type { NcpdpProfileQuirk } from "@cosyte/ncpdp/profiles"; const quirk: NcpdpProfileQuirk = { id: "person-code-required", standard: "telecom", effect: "requires", summary: "Insurance segment carries a Person Code (303-C3) cardholder/dependent value.", fixture: "telecom/pbm-person-code.ncpdp", sourceCategory: "NCPDP Telecommunication vD.0: Person Code (303-C3); PBM payer sheets", }; ``` #### Properties ##### effect > `readonly` **effect**: [`NcpdpProfileEffect`](#ncpdpprofileeffect) Which `describe()` bucket this quirk renders into. ##### expectedWarnings? > `readonly` `optional` **expectedWarnings?**: readonly [`NcpdpWarningCode`](#ncpdpwarningcode)[] Warning codes this quirk leads a consumer to EXPECT when the convention is present. Drives `partitionWarnings`. Often empty: the lenient parser absorbs most conventions with zero warnings, and that "lossless, no warning" outcome is itself the documented behavior. ##### fixture > `readonly` **fixture**: `string` Path to the Tier-2 fixture demonstrating the convention, relative to `test/fixtures/` (e.g. `"telecom/pbm-person-code.ncpdp"`). REQUIRED: the locked hard rule. The accuracy test parses this file and asserts the claimed convention is present. ##### id > `readonly` **id**: `string` Stable, kebab-case identifier: unique within a profile's quirk set. ##### sourceCategory > `readonly` **sourceCategory**: `string` Where the convention is documented (standard clause / companion guide). ##### standard > `readonly` **standard**: [`NcpdpStandard`](#ncpdpstandard) Which NCPDP standard the quirk (and its cited fixture) belongs to. ##### summary > `readonly` **summary**: `string` One-line human summary. NEVER contains PHI: describes structure only. *** ### NcpdpProfileSpec Input accepted by `defineProfile()`. Every field except `name` is optional; `extends` composes parent profiles (lineage + quirks merge) the same way the x12/hl7 `extends` does. #### Example ```ts import { defineProfile, profiles, type NcpdpProfileSpec } from "@cosyte/ncpdp/profiles"; const spec: NcpdpProfileSpec = { name: "my-regional-pbm", extends: profiles.pbm, quirks: [ { id: "reject-code-depth", standard: "telecom", effect: "adds", summary: "Returns reject codes beyond the modeled core set.", fixture: "telecom/pbm-reject-unknown.ncpdp", sourceCategory: "regional PBM payer sheet: reject-code taxonomy", }, ], }; const profile = defineProfile(spec); ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` ##### extends? > `readonly` `optional` **extends?**: [`NcpdpProfile`](#ncpdpprofile) \| readonly [`NcpdpProfile`](#ncpdpprofile)[] ##### name > `readonly` **name**: `string` ##### quirks? > `readonly` `optional` **quirks?**: readonly [`NcpdpProfileQuirk`](#ncpdpprofilequirk)[] *** ### NcpdpScriptWarning A non-fatal SCRIPT parse warning: a stable code, its registry message, and the XPath-style location where it was raised. **What is and is not guaranteed.** `message` is always the [SCRIPT\_WARNING\_MESSAGES](#script_warning_messages) entry for `code`, byte for byte, so no part of a parsed document can appear in it. `position.path` is assembled only from element names this parser recognizes, so a sender-chosen name never reaches it either. A warning is therefore safe to log whole. That is a property of the construction, not a promise about the document. #### Properties ##### code > `readonly` **code**: [`ScriptWarningCode`](#scriptwarningcode) Stable, machine-readable warning code. ##### message > `readonly` **message**: `string` The [SCRIPT\_WARNING\_MESSAGES](#script_warning_messages) entry for [code](#code-5), verbatim. ##### position > `readonly` **position**: [`ScriptPosition`](#scriptposition) XPath-style location where the condition was detected. *** ### NcpdpTelecomWarning A non-fatal Telecom parse warning: a stable code, its registry message, and the byte-offset location where it was raised. **What is and is not guaranteed.** `message` is always the [TELECOM\_WARNING\_MESSAGES](#telecom_warning_messages) entry for `code`, byte for byte, so no part of a transmission can appear in it. `position` carries a byte offset and, when known, a field identifier this parser named itself. A warning is therefore safe to log whole. That is a property of the construction, not a promise about the transmission: the segment and field **values** on the model are wire data and are exactly as sensitive as the claim they came from. #### Properties ##### code > `readonly` **code**: [`TelecomWarningCode`](#telecomwarningcode) Stable, machine-readable warning code. ##### message > `readonly` **message**: `string` The [TELECOM\_WARNING\_MESSAGES](#telecom_warning_messages) entry for [code](#code-6), verbatim. ##### position > `readonly` **position**: [`TelecomPosition`](#telecomposition) Byte-offset location where the condition was detected. *** ### NcpdpWarningPartition The result of [partitionWarnings](#partitionwarnings): warnings split into those a profile leads you to EXPECT and those it does not. Preserves the input warning type. #### Example ```ts import type { NcpdpWarningPartition } from "@cosyte/ncpdp/profiles"; import type { NcpdpTelecomWarning } from "@cosyte/ncpdp/telecom"; declare const p: NcpdpWarningPartition; p.unexpected.length; // alert only on these ``` #### Type Parameters ##### W `W` *extends* `object` #### Properties ##### expected > `readonly` **expected**: readonly `W`[] ##### unexpected > `readonly` **unexpected**: readonly `W`[] *** ### NdcValue A National Drug Code preserved verbatim, with a best-effort segmentation hint. We do not rewrite or zero-pad the value: the original is authoritative. #### Properties ##### segmentation > `readonly` **segmentation**: [`NdcSegmentation`](#ndcsegmentation) Best-effort segmentation classification of [value](#value-1). ##### value > `readonly` **value**: `string` The NDC exactly as it appeared, including any hyphens. *** ### NewRx A parsed SCRIPT NewRx transaction body. #### Properties ##### kind > `readonly` **kind**: `"NewRx"` ##### medication? > `readonly` `optional` **medication?**: [`MedicationPrescribed`](#medicationprescribed-4) ##### patient? > `readonly` `optional` **patient?**: [`Patient`](#patient-4) ##### pharmacy? > `readonly` `optional` **pharmacy?**: [`Pharmacy`](#pharmacy-4) ##### prescriber? > `readonly` `optional` **prescriber?**: [`Prescriber`](#prescriber-4) *** ### NewRxInput Input to [buildNewRx](#buildnewrx): routing header, optional parties, and the medication. #### Properties ##### header? > `readonly` `optional` **header?**: [`ScriptHeaderInput`](#scriptheaderinput) ##### medication > `readonly` **medication**: [`MedicationPrescribed`](#medicationprescribed-4) The prescribed medication; a `description` is required. ##### patient? > `readonly` `optional` **patient?**: [`Patient`](#patient-4) ##### pharmacy? > `readonly` `optional` **pharmacy?**: [`Pharmacy`](#pharmacy-4) ##### prescriber? > `readonly` `optional` **prescriber?**: [`Prescriber`](#prescriber-4) *** ### ParseScriptOptions Options for [parseScript](#parsescript). #### Properties ##### profile? > `readonly` `optional` **profile?**: [`NcpdpProfile`](#ncpdpprofile) \| `null` Trading-partner profile to attach to the result for attribution (and `partitionWarnings`). An explicit profile ALWAYS wins over any process-scoped default; pass `null` to opt out of the default for this one call; omit (or `undefined`) to consult `getDefaultProfile()`. v1 profiles are DESCRIPTIVE: the profile is surfaced as `msg.profile` but does NOT alter the lenient parse. *** ### PartyIdentification A pharmacy or prescriber identification (``). #### Properties ##### deaNumber? > `readonly` `optional` **deaNumber?**: `string` ##### ncpdpId? > `readonly` `optional` **ncpdpId?**: `string` ##### npi? > `readonly` `optional` **npi?**: `string` *** ### Patient The patient on a NewRx (``). #### Properties ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: `string` Date of birth, verbatim: no reformatting. ##### gender? > `readonly` `optional` **gender?**: `string` ##### name? > `readonly` `optional` **name?**: [`ScriptName`](#scriptname) *** ### Pharmacy The dispensing pharmacy on a NewRx (``). #### Properties ##### businessName? > `readonly` `optional` **businessName?**: `string` ##### identification? > `readonly` `optional` **identification?**: [`PartyIdentification`](#partyidentification) *** ### Prescriber The prescriber on a NewRx (``). #### Properties ##### identification? > `readonly` `optional` **identification?**: [`PartyIdentification`](#partyidentification) ##### name? > `readonly` `optional` **name?**: [`ScriptName`](#scriptname) *** ### Quantity A dispense quantity (``). #### Properties ##### codeListQualifier? > `readonly` `optional` **codeListQualifier?**: `string` ##### unitOfMeasure? > `readonly` `optional` **unitOfMeasure?**: `string` ##### value? > `readonly` `optional` **value?**: [`DecimalValue`](#decimalvalue) *** ### ResponseFields Fields shared by the SCRIPT response transactions. All are optional because a real-world sender may omit them; every value is surfaced **verbatim**: codes and descriptions are never reformatted, looked up, or translated. #### Extended by - [`StatusBody`](#statusbody) - [`ErrorBody`](#errorbody) - [`VerifyBody`](#verifybody) #### Properties ##### code? > `readonly` `optional` **code?**: `string` Primary response code, verbatim (``). ##### description? > `readonly` `optional` **description?**: `string` Free-text description, verbatim (``). ##### descriptionCode? > `readonly` `optional` **descriptionCode?**: `string` Secondary description code, verbatim (``). *** ### ResponseReason The reason carried alongside a lifecycle response outcome. All fields are surfaced **verbatim**: codes and free text are never reformatted, looked up, or translated. #### Properties ##### code? > `readonly` `optional` **code?**: `string` Coded reason, verbatim (``). ##### denialReason? > `readonly` `optional` **denialReason?**: `string` Free-text denial reason, verbatim (``). ##### note? > `readonly` `optional` **note?**: `string` Free-text note, verbatim (``). ##### referenceNumber? > `readonly` `optional` **referenceNumber?**: `string` Reference number tying the reason to a request item, verbatim (``). *** ### RxChangeRequest A pharmacy-initiated request to change a prescription (``). #### Extends - [`LifecycleRequestFields`](#lifecyclerequestfields) #### Properties ##### kind > `readonly` **kind**: `"RxChangeRequest"` ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The prescription this request concerns (``). ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`medicationPrescribed`](#medicationprescribed-2) ##### patient? > `readonly` `optional` **patient?**: [`Patient`](#patient-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`patient`](#patient-1) ##### pharmacy? > `readonly` `optional` **pharmacy?**: [`Pharmacy`](#pharmacy-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`pharmacy`](#pharmacy-1) ##### prescriber? > `readonly` `optional` **prescriber?**: [`Prescriber`](#prescriber-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`prescriber`](#prescriber-1) ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number, verbatim (``). ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`requestReferenceNumber`](#requestreferencenumber-2) *** ### RxChangeResponse A prescriber's answer to a change request (``). #### Extends - [`LifecycleResponseFields`](#lifecycleresponsefields) #### Properties ##### kind > `readonly` **kind**: `"RxChangeResponse"` ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The medication carried with the response. For an `approvedWithChanges` outcome this is the **changed** medication and is the field a consumer must read to dispense correctly. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`medicationPrescribed`](#medicationprescribed-3) ##### outcome > `readonly` **outcome**: [`ResponseOutcome`](#responseoutcome) The prescriber's decision, detected fail-safe from the `` choice. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`outcome`](#outcome-1) ##### reason? > `readonly` `optional` **reason?**: [`ResponseReason`](#responsereason) The reason carried with the outcome, when present. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`reason`](#reason-1) ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number echoing the request, verbatim (``). ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`requestReferenceNumber`](#requestreferencenumber-3) *** ### RxRenewalRequest A pharmacy-initiated request to renew a prescription (``). #### Extends - [`LifecycleRequestFields`](#lifecyclerequestfields) #### Properties ##### kind > `readonly` **kind**: `"RxRenewalRequest"` ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The prescription this request concerns (``). ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`medicationPrescribed`](#medicationprescribed-2) ##### patient? > `readonly` `optional` **patient?**: [`Patient`](#patient-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`patient`](#patient-1) ##### pharmacy? > `readonly` `optional` **pharmacy?**: [`Pharmacy`](#pharmacy-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`pharmacy`](#pharmacy-1) ##### prescriber? > `readonly` `optional` **prescriber?**: [`Prescriber`](#prescriber-4) ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`prescriber`](#prescriber-1) ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number, verbatim (``). ###### Inherited from [`LifecycleRequestFields`](#lifecyclerequestfields).[`requestReferenceNumber`](#requestreferencenumber-2) *** ### RxRenewalResponse A prescriber's answer to a renewal request (``). #### Extends - [`LifecycleResponseFields`](#lifecycleresponsefields) #### Properties ##### kind > `readonly` **kind**: `"RxRenewalResponse"` ##### medicationPrescribed? > `readonly` `optional` **medicationPrescribed?**: [`MedicationPrescribed`](#medicationprescribed-4) The medication carried with the response. For an `approvedWithChanges` outcome this is the **changed** medication and is the field a consumer must read to dispense correctly. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`medicationPrescribed`](#medicationprescribed-3) ##### outcome > `readonly` **outcome**: [`ResponseOutcome`](#responseoutcome) The prescriber's decision, detected fail-safe from the `` choice. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`outcome`](#outcome-1) ##### reason? > `readonly` `optional` **reason?**: [`ResponseReason`](#responsereason) The reason carried with the outcome, when present. ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`reason`](#reason-1) ##### requestReferenceNumber? > `readonly` `optional` **requestReferenceNumber?**: `string` Request reference number echoing the request, verbatim (``). ###### Inherited from [`LifecycleResponseFields`](#lifecycleresponsefields).[`requestReferenceNumber`](#requestreferencenumber-3) *** ### ScriptHeader The SCRIPT `
` fields useful for routing and correlation. Every field is optional because real-world senders omit some; absence is `undefined`, not an error (Postel's Law). #### Properties ##### from? > `readonly` `optional` **from?**: `string` Source identifier (``). ##### messageId? > `readonly` `optional` **messageId?**: `string` Unique message identifier (``). ##### prescriberOrderNumber? > `readonly` `optional` **prescriberOrderNumber?**: `string` Prescriber order number (``), when present. ##### relatesToMessageId? > `readonly` `optional` **relatesToMessageId?**: `string` Correlated prior message identifier (``). ##### sentTime? > `readonly` `optional` **sentTime?**: `string` Sender timestamp, verbatim (``). ##### to? > `readonly` `optional` **to?**: `string` Destination identifier (``). ##### version? > `readonly` `optional` **version?**: `string` Declared SCRIPT version (root `version`/`Version`, or `
`). *** ### ScriptHeaderInput The SCRIPT `
` fields a builder accepts; all optional. #### Properties ##### from? > `readonly` `optional` **from?**: `string` ##### messageId? > `readonly` `optional` **messageId?**: `string` ##### prescriberOrderNumber? > `readonly` `optional` **prescriberOrderNumber?**: `string` ##### relatesToMessageId? > `readonly` `optional` **relatesToMessageId?**: `string` ##### sentTime? > `readonly` `optional` **sentTime?**: `string` ##### to? > `readonly` `optional` **to?**: `string` ##### version? > `readonly` `optional` **version?**: `string` Declared SCRIPT version, emitted as the root `version` attribute. *** ### ScriptName A person's name as carried in SCRIPT (``). #### Properties ##### firstName? > `readonly` `optional` **firstName?**: `string` ##### lastName? > `readonly` `optional` **lastName?**: `string` ##### middleName? > `readonly` `optional` **middleName?**: `string` *** ### ScriptPosition Positional context attached to NCPDP SCRIPT warnings and fatal errors. SCRIPT is XML, so position is an XPath-style location string (e.g. `/Message/Body/NewRx/MedicationPrescribed`): never a field value. This keeps diagnostics PHI-safe: a consumer learns *where* a problem is without the library echoing patient data. #### Properties ##### path > `readonly` **path**: `string` XPath-style location of the element, e.g. `/Message/Body/NewRx`. *** ### ScriptResponseInput Input to [buildScriptResponse](#buildscriptresponse): the response kind, its code, and optional detail. #### Properties ##### code > `readonly` **code**: `string` The primary response ``; required (a response must carry one). ##### description? > `readonly` `optional` **description?**: `string` ##### descriptionCode? > `readonly` `optional` **descriptionCode?**: `string` ##### header? > `readonly` `optional` **header?**: [`ScriptHeaderInput`](#scriptheaderinput) ##### kind > `readonly` **kind**: [`ResponseKind`](#responsekind) Which response transaction to build. *** ### SegmentAbsence The published accounting for one unnamed code inside a declared range. #### Properties ##### reason > `readonly` **reason**: `"unsourced"` Why this code carries no name here. *** ### SegmentCodeRange A run of 111-AM codes this package declares that it covers, and whether an artifact fixes the bounds. #### Properties ##### boundsVerified > `readonly` **boundsVerified**: `boolean` Whether a publicly readable artifact establishes these bounds. `false` on every range declared today: the bounds are this package's own working assumption, carried forward from an unsourced source comment, and are NOT presented as a fact about the standard. ##### first > `readonly` **first**: `string` First code in the range, 2 characters. ##### group > `readonly` **group**: [`SegmentGroup`](#segmentgroup) The half of the transmission this range covers. ##### last > `readonly` **last**: `string` Last code in the range, 2 characters, inclusive. *** ### SigField One decoded component of a structured SIG, always carrying its [SigFieldProvenance](#sigfieldprovenance-1) so a consumer can tell, per field, whether the value was coded, derived from uncoded structure, or absent. A `"coded"` field's code keeps its source qualifier verbatim (SNOMED CT / NCI Thesaurus / etc.) so the provenance is auditable even when our qualifier table lags the spec. #### Properties ##### code? > `readonly` `optional` **code?**: [`CodedValue`](#codedvalue) The code + recognized system, when the structure carried a coded value. ##### provenance > `readonly` **provenance**: [`SigFieldProvenance`](#sigfieldprovenance-1) Whether this field was coded, derived from uncoded structure, or absent. ##### text? > `readonly` `optional` **text?**: `string` Verbatim human-readable text, when the structure carried any. *** ### StatusBody A SCRIPT ``: a positive acknowledgment of a prior transaction. #### Extends - [`ResponseFields`](#responsefields) #### Properties ##### code? > `readonly` `optional` **code?**: `string` Primary response code, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`code`](#code-7) ##### description? > `readonly` `optional` **description?**: `string` Free-text description, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`description`](#description-5) ##### descriptionCode? > `readonly` `optional` **descriptionCode?**: `string` Secondary description code, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`descriptionCode`](#descriptioncode-1) ##### kind > `readonly` **kind**: `"Status"` *** ### Strength Explicit ``. Surfaced independently of any strength implied by a coded product: the two are **never reconciled** (see "../common/warnings".SCRIPT\_WARNING\_CODES.STRENGTH\_CODED\_AND\_EXPLICIT). #### Properties ##### form? > `readonly` `optional` **form?**: `string` ##### unitOfMeasure? > `readonly` `optional` **unitOfMeasure?**: `string` ##### value? > `readonly` `optional` **value?**: `string` *** ### StructuredSig A best-effort, **lossy** decode of a SCRIPT structured `` into typed dosing components. Every component slot is always present and tagged [SigFieldProvenance](#sigfieldprovenance-1), so the surface is uniform and a consumer can see which fields are coded, derived, or absent. **Safety contract.** The free-text [sigText](#sigtext-1) is the source of truth and is preserved **verbatim**; the structured view is **additive** and clearly flagged lossy (see "../common/warnings".SCRIPT\_WARNING\_CODES.SIG\_STRUCTURED\_LOSSY). The two are **never reconciled**: when structured dosing and the free text disagree, both are surfaced as-is. An ambiguous structured dose is never collapsed into a confident value (see "../common/warnings".SCRIPT\_WARNING\_CODES.SIG\_AMBIGUOUS\_DOSE). **A component only decodes when its element name is grounded.** A slot whose name this release cannot trace to a published field label always reads `"absent"`, whatever the message carried: see SIG\_COMPONENT\_NAMES for which slots those are and SIG\_NAME\_PROVENANCE for the evidence behind the rest. #### Properties ##### administrationTiming > `readonly` **administrationTiming**: [`SigField`](#sigfield) Administration timing. ##### dose > `readonly` **dose**: [`SigField`](#sigfield) Dose amount (the numeric quantity), string-preserved; never a confident guess. ##### doseDeliveryMethod > `readonly` **doseDeliveryMethod**: [`SigField`](#sigfield) Method of dose delivery (e.g. "take", "apply"). ##### doseUnitOfMeasure > `readonly` **doseUnitOfMeasure**: [`SigField`](#sigfield) Unit of measure for the dose. **No grounded element name: always `"absent"`.** ##### duration > `readonly` **duration**: [`SigField`](#sigfield) Duration of therapy. **No grounded element name: always `"absent"`.** ##### hasStructuredData > `readonly` **hasStructuredData**: `boolean` True when at least one component decoded to a non-`"absent"` value. ##### indication > `readonly` **indication**: [`SigField`](#sigfield) Clinical indication. **No grounded element name: always `"absent"`.** ##### maximumDoseRestriction > `readonly` **maximumDoseRestriction**: [`SigField`](#sigfield) Maximum-dose restriction. **No grounded element name: always `"absent"`.** ##### route > `readonly` **route**: [`SigField`](#sigfield) Route of administration (SNOMED/NCI when coded). ##### sigText? > `readonly` `optional` **sigText?**: `string` The free-text SIG (``), verbatim: always the source of truth. ##### siteOfAdministration > `readonly` **siteOfAdministration**: [`SigField`](#sigfield) Site of administration (SNOMED/NCI when coded). ##### vehicle > `readonly` **vehicle**: [`SigField`](#sigfield) Vehicle / diluent. **No grounded element name: always `"absent"`.** *** ### TelecomClaim A B1/B2/B3 request view over a decoded Telecom transaction: the safety-relevant fields a biller needs, lifted from their field-id-keyed segments and preserved verbatim. Every field is optional: a missing segment or field yields `undefined` rather than a throw, in keeping with the lenient parse contract. #### Properties ##### cardholderId? > `readonly` `optional` **cardholderId?**: `string` Cardholder ID (302-C2) from the Insurance segment. PHI. ##### dateOfBirth? > `readonly` `optional` **dateOfBirth?**: `string` Date of Birth (304-C4) from the Patient segment, verbatim (`CCYYMMDD`). PHI. ##### daysSupply? > `readonly` `optional` **daysSupply?**: [`DecimalValue`](#decimalvalue) Days Supply (405-D5), preserved as a decimal-safe value. ##### dispenseAsWritten? > `readonly` `optional` **dispenseAsWritten?**: `string` Dispense As Written / Product Selection Code (408-D8), verbatim. ##### fillNumber? > `readonly` `optional` **fillNumber?**: `string` Fill Number (403-D3) from the Claim segment. ##### genderCode? > `readonly` `optional` **genderCode?**: `string` Patient Gender Code (305-C5) from the Patient segment. ##### groupId? > `readonly` `optional` **groupId?**: `string` Group ID (301-C1) from the Insurance segment. ##### personCode? > `readonly` `optional` **personCode?**: `string` Person Code (303-C3) from the Insurance segment. ##### prescriberId? > `readonly` `optional` **prescriberId?**: `string` Prescriber ID (411-DB) from the Prescriber segment. ##### prescriberIdQualifier? > `readonly` `optional` **prescriberIdQualifier?**: `string` Prescriber ID Qualifier (466-EZ) from the Prescriber segment. ##### prescriptionReferenceNumber? > `readonly` `optional` **prescriptionReferenceNumber?**: `string` Prescription/Service Reference Number (402-D2) from the Claim segment. ##### prescriptionReferenceQualifier? > `readonly` `optional` **prescriptionReferenceQualifier?**: `string` Prescription/Service Reference Number Qualifier (455-EM). ##### product? > `readonly` `optional` **product?**: [`TelecomProductCode`](#telecomproductcode) Product/Service ID + qualifier (407-D7 / 436-E1), when present. ##### quantityDispensed? > `readonly` `optional` **quantityDispensed?**: [`TelecomQuantity`](#telecomquantity) Quantity Dispensed (442-E7) with its implied 3-place decimal. ##### transactionCode > `readonly` **transactionCode**: `string` Transaction Code (103-A3) from the header, e.g. `"B1"`. *** ### TelecomDecodedTransaction One group-separated transaction decoded out of a transmission body: its position in the transmission, the absolute byte offset of its first byte, its segments in wire order, and the warnings raised while decoding **it**. A transmission may carry several transactions (the Group Separator is what divides them), and each one is decoded into its own entry with its own warning list. That is what isolates a malformed transaction: a quirk in one cannot discard, reorder or re-attribute the segments of another, and a consumer can tell which transaction a warning belongs to without re-tokenizing the input. #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of this transaction's first byte in the raw message. ##### index > `readonly` **index**: `number` Zero-based position of this transaction within the transmission. ##### segments > `readonly` **segments**: readonly [`TelecomSegment`](#telecomsegment)[] This transaction's segments, in wire order. ##### warnings > `readonly` **warnings**: readonly [`NcpdpTelecomWarning`](#ncpdptelecomwarning)[] The warnings raised decoding this transaction, in the order they were raised. *** ### TelecomField A single decoded field: its 2-character id, verbatim value, and paraphrased name. #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of this field token in the raw message. ##### id > `readonly` **id**: `string` The 2-character field identifier, verbatim (empty for a malformed token). ##### name? > `readonly` `optional` **name?**: `string` Paraphrased field name when [id](#id-1) is recognized. ##### value > `readonly` **value**: `string` The field value exactly as it appeared on the wire. *** ### TelecomFieldInput A single field to build: its 2-character id and verbatim value. #### Properties ##### id > `readonly` **id**: `string` The 2-character field identifier, e.g. `"D7"`. ##### value > `readonly` **value**: `string` The field value, emitted verbatim. *** ### TelecomHeader The fixed-length Transaction Header that opens every NCPDP Telecommunication request. It carries no field separators: each field is positional. Field numbers/designators are from the NCPDP Telecommunication standard vD.0; the names here are our own paraphrases (we do not redistribute NCPDP prose). Values are trimmed of pad whitespace; numeric leading zeros are preserved (a BIN or PCN is an identifier, not an arithmetic quantity). #### Properties ##### binNumber > `readonly` **binNumber**: `string` 101-A1: the routing Bank Identification Number (6 chars on the wire). ##### dateOfService > `readonly` **dateOfService**: `string` 401-D1: Date of Service, verbatim (`CCYYMMDD` on the wire). ##### processorControlNumber > `readonly` **processorControlNumber**: `string` 104-A4: Processor Control Number. ##### serviceProviderId > `readonly` **serviceProviderId**: `string` 201-B1: Service Provider ID (e.g. the pharmacy NPI). ##### serviceProviderIdQualifier > `readonly` **serviceProviderIdQualifier**: `string` 202-B2: Service Provider ID Qualifier. ##### softwareCertificationId > `readonly` **softwareCertificationId**: `string` 110-AK: Software Vendor / Certification ID. ##### transactionCode > `readonly` **transactionCode**: `string` 103-A3: Transaction Code, e.g. `"B1"` (billing), `"B2"`, `"B3"`, `"E1"`. ##### transactionCount > `readonly` **transactionCount**: `string` 109-A9: declared number of transactions in the transmission (1 char). ##### versionRelease > `readonly` **versionRelease**: `string` 102-A2: Version/Release, `"D0"` for the standard this reader decodes. *** ### TelecomHeaderInput The fixed Transaction Header fields to build. Only `transactionCode` is required. #### Properties ##### binNumber? > `readonly` `optional` **binNumber?**: `string` 101-A1: routing BIN. ##### dateOfService? > `readonly` `optional` **dateOfService?**: `string` 401-D1: Date of Service (`CCYYMMDD`). ##### processorControlNumber? > `readonly` `optional` **processorControlNumber?**: `string` 104-A4: Processor Control Number. ##### serviceProviderId? > `readonly` `optional` **serviceProviderId?**: `string` 201-B1: Service Provider ID. ##### serviceProviderIdQualifier? > `readonly` `optional` **serviceProviderIdQualifier?**: `string` 202-B2: Service Provider ID Qualifier. ##### softwareCertificationId? > `readonly` `optional` **softwareCertificationId?**: `string` 110-AK: Software Vendor / Certification ID. ##### transactionCode > `readonly` **transactionCode**: `string` 103-A3: Transaction Code; required (a request cannot route without one). ##### transactionCount? > `readonly` `optional` **transactionCount?**: `string` 109-A9: transaction count; defaults to `"1"`. ##### versionRelease? > `readonly` `optional` **versionRelease?**: `string` 102-A2: Version/Release; defaults to `"D0"`. *** ### TelecomParseOptions Options controlling [parseTelecom](#parsetelecom). #### Properties ##### encoding? > `readonly` `optional` **encoding?**: `BufferEncoding` When the input is a Buffer, the encoding used to decode it to text. The Telecommunication standard is single-byte ASCII; defaults to `"latin1"` so every byte maps to a code point without loss. ##### profile? > `readonly` `optional` **profile?**: [`NcpdpProfile`](#ncpdpprofile) \| `null` Trading-partner profile to attach to the result for attribution (and `partitionWarnings`). An explicit profile ALWAYS wins over any process-scoped default; pass `null` to opt out of the default for this one call; omit (or `undefined`) to consult `getDefaultProfile()`. v1 profiles are DESCRIPTIVE: the profile is surfaced as `tx.profile` but does NOT alter the lenient parse. *** ### TelecomPosition Positional context attached to NCPDP Telecommunication-standard warnings and fatal errors. The Telecommunication standard is a byte-oriented, control-character-framed format, so position is a **byte offset** into the raw message: optionally narrowed to the 2-character field identifier where the condition was detected. It never carries a field *value*: a consumer learns *where* a problem is without the library echoing cardholder, patient, or drug data, keeping diagnostics PHI-safe. #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Zero-based byte offset into the raw message. ##### fieldId? > `readonly` `optional` **fieldId?**: `string` The 2-character field identifier in scope, when known. Never a value. *** ### TelecomProductCode A drug identifier from the Claim segment: the Product/Service ID (407-D7) and its qualifier (436-E1), each preserved verbatim. The qualifier names the code system the id belongs to. This package does not ship a label for it: no public artifact establishing the 436-E1 value set could be obtained, so the qualifier comes back exactly as it appeared on the wire and nothing else. Absence of a meaning never means the value is invalid. #### Properties ##### id > `readonly` **id**: `string` Product/Service ID (407-D7), verbatim: e.g. an 11-digit NDC. ##### qualifier > `readonly` **qualifier**: `string` Product/Service ID Qualifier (436-E1), verbatim. *** ### TelecomQuantity Quantity Dispensed (442-E7). On the wire this is an unsigned integer string with an **implied 3-place decimal** (NCPDP format `9(7)v999`): `"30000"` means `30.000`. We never parse it into a float: binary floating point would corrupt the value, so we keep the verbatim source and, when it is all digits, surface the implied decimal applied **string-wise**. #### Properties ##### impliedDecimal? > `readonly` `optional` **impliedDecimal?**: `string` [source](#source-2) with the implied 3-place decimal inserted, when valid. ##### isValid > `readonly` **isValid**: `boolean` True when [source](#source-2) is a non-empty run of digits. ##### source > `readonly` **source**: `string` The quantity exactly as it appeared on the wire. *** ### TelecomRequestInput The whole request to build: the fixed header and the variable segments. #### Properties ##### header > `readonly` **header**: [`TelecomHeaderInput`](#telecomheaderinput) The fixed Transaction Header fields. ##### segments > `readonly` **segments**: readonly [`TelecomSegmentInput`](#telecomsegmentinput)[] The request segments, in wire order. *** ### TelecomSegment A decoded segment: its identification code, name, and ordered fields. #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of the segment in the raw message. ##### fields > `readonly` **fields**: readonly [`TelecomField`](#telecomfield)[] The segment's data fields, in wire order. The `AM` field is not repeated here once it has been read into [segmentId](#segmentid); when it could not be (`MALFORMED_SEGMENT_ID`), it stays in this list instead of being dropped. ##### name? > `readonly` `optional` **name?**: `string` Paraphrased segment name when [segmentId](#segmentid) is recognized. ##### segmentId > `readonly` **segmentId**: `string` The Segment Identification (111-AM) code, e.g. `"07"`. Always either exactly two characters or empty. It is empty when the segment did not begin with an `AM` field (`MISSING_SEGMENT_ID`) **and** when it did but that field's value was not two characters (`MALFORMED_SEGMENT_ID`); in the second case the `AM` field itself is kept in [fields](#fields), verbatim, so no byte is lost. The bound is what makes this field safe for a consumer to interpolate into its own diagnostics: `fields[].value` is not. *** ### TelecomSegmentInput A segment to build: its Segment Identification code and ordered fields. #### Properties ##### fields > `readonly` **fields**: readonly [`TelecomFieldInput`](#telecomfieldinput)[] The segment's data fields, in wire order. ##### segmentId > `readonly` **segmentId**: `string` The Segment Identification (111-AM) code, e.g. `"07"`. *** ### TelecomTransaction A decoded Telecom transmission: the fixed header, every group-separated transaction it carries (each with its own field-id-keyed segments, in wire order), the header's declared transaction count beside the number actually decoded, and any non-fatal warnings. Everything is frozen. #### Properties ##### decodedTransactionCount > `readonly` **decodedTransactionCount**: `number` The number of transactions actually decoded: `transactions.length`, surfaced beside the declared [transactionCount](#transactioncount-2) so the two are read as separate facts rather than one reconciled guess. ##### header > `readonly` **header**: [`TelecomHeader`](#telecomheader) The decoded fixed Transaction Header. For a response, the overlapping fields (version, transaction code, count, service provider) are lifted from the response header; request-only fields (BIN, PCN, …) are empty. ##### kind > `readonly` **kind**: `"request"` \| `"response"` Whether this is a request transmission (decoded against the 56-byte request header) or a response transmission (decoded against the response header). ##### profile? > `readonly` `optional` **profile?**: [`NcpdpProfile`](#ncpdpprofile) The trading-partner profile in effect for this parse: either passed explicitly via `options.profile` or resolved from the process-scoped default. Present only when a profile applied; attribution only (v1 profiles never alter the parse). ##### responseHeader? > `readonly` `optional` **responseHeader?**: `TelecomResponseHeader` The decoded Response Transaction Header: present only when `kind` is `"response"`. ##### segments > `readonly` **segments**: readonly [`TelecomSegment`](#telecomsegment)[] The **first** decoded transaction's segments, in wire order (empty when nothing decoded). A transmission may carry several transactions, so this is a convenience alias for `transactions[0].segments`, never the whole message: [transactions](#transactions) is what carries them all. ##### transactionCount > `readonly` **transactionCount**: `string` Declared number of transactions (109-A9), **verbatim from the header**: never coerced to a number, never defaulted, and never reconciled against what decoded. Compare it with [decodedTransactionCount](#decodedtransactioncount), which is the count this reader actually decoded; when they disagree, `NCPDP_TELECOM_TRANSACTION_COUNT_MISMATCH` says so and nothing is dropped. ##### transactions > `readonly` **transactions**: readonly [`TelecomDecodedTransaction`](#telecomdecodedtransaction)[] Every decoded transaction, in wire order, each with its own segments, its own byte offset and the warnings raised decoding it. A transmission carrying one transaction has exactly one entry, so this is the uniform read path whatever the message carried. ##### warnings > `readonly` **warnings**: readonly [`NcpdpTelecomWarning`](#ncpdptelecomwarning)[] Non-fatal parse warnings for the whole transmission: stable code + byte offset + field id, never PHI. Every transaction's warnings appear here in wire order; `transactions[n].warnings` is the same set, split per transaction, when a consumer needs to attribute one. *** ### ToDateOptions The one option [toDate](#todate) takes, and the only key it carries. #### Properties ##### assumeOffsetMinutes? > `readonly` `optional` **assumeOffsetMinutes?**: `number` The UTC offset, in signed minutes east of UTC, to assume for a value that states none. An explicit `0` means "treat this naive value as UTC". With no value here there is no determinate zone and [toDate](#todate) answers `undefined`. *** ### UnsupportedBody A SCRIPT transaction body this parser recognizes but does not model. The raw transaction name is surfaced so a consumer can branch, without the parser pretending to understand it. A message carrying one of these **cannot be serialized**: nothing under the transaction was modeled, so emit refuses rather than writing a document with the transaction body deleted. `body.kind === "unsupported"` is the public, non-throwing way to test for that before you emit. #### Properties ##### kind > `readonly` **kind**: `"unsupported"` ##### transaction? > `readonly` `optional` **transaction?**: `string` The SCRIPT transaction element name (e.g. `RxRenewalRequest`), present only when the element the sender used is one of "./versions".SCRIPT\_TRANSACTION\_NAMES. It is **absent** for any other element name, and that is deliberate rather than a gap. A `` child name is chosen by the sender, so echoing it onto the model would hand every downstream package an unbounded, wire-derived string to build its own diagnostics out of. When it is absent, the warning's position and the document itself are how you find the element. *** ### VerifyBody A SCRIPT ``: a prescriber's verification acknowledgment. #### Extends - [`ResponseFields`](#responsefields) #### Properties ##### code? > `readonly` `optional` **code?**: `string` Primary response code, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`code`](#code-7) ##### description? > `readonly` `optional` **description?**: `string` Free-text description, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`description`](#description-5) ##### descriptionCode? > `readonly` `optional` **descriptionCode?**: `string` Secondary description code, verbatim (``). ###### Inherited from [`ResponseFields`](#responsefields).[`descriptionCode`](#descriptioncode-1) ##### kind > `readonly` **kind**: `"Verify"` *** ### XmlElement A namespace-stripped, immutable view of an XML element. This is the only XML shape the rest of the SCRIPT parser sees: the `fast-xml-parser` output is transformed into this tree at load time so downstream code never depends on the parser's representation. #### Properties ##### attrs > `readonly` **attrs**: `Readonly`\<`Record`\<`string`, `string`\>\> Attributes, prefix-stripped, in document order. ##### children > `readonly` **children**: readonly [`XmlElement`](#xmlelement)[] Child elements, in document order. ##### name > `readonly` **name**: `string` Local element name, namespace prefix stripped (e.g. `Message`). ##### text > `readonly` **text**: `string` Concatenated direct text content, verbatim (not trimmed). ## Type Aliases ### CodeSystem > **CodeSystem** = `"NDC"` \| `"RXNORM"` \| `"SNOMED"` \| `"NCI"` \| `"ICD10"` \| `"UNKNOWN"` Drug/clinical code systems recognized in NCPDP SCRIPT product codes. SCRIPT carries a qualifier alongside each coded value; we map common qualifiers to a normalized system and fall back to `"UNKNOWN"` rather than guessing. *** ### KnownScriptVersion > **KnownScriptVersion** = *typeof* [`KNOWN_SCRIPT_VERSIONS`](#known_script_versions)\[`number`\] Union of the explicitly-supported SCRIPT version literals. *** ### LifecycleRequest > **LifecycleRequest** = [`RxRenewalRequest`](#rxrenewalrequest) \| [`RxChangeRequest`](#rxchangerequest) \| [`CancelRx`](#cancelrx) A parsed SCRIPT prescription-lifecycle request body. *** ### LifecycleRequestKind > **LifecycleRequestKind** = `"RxRenewalRequest"` \| `"RxChangeRequest"` \| `"CancelRx"` The SCRIPT prescription-lifecycle **request** transactions this parser models. *** ### LifecycleResponse > **LifecycleResponse** = [`RxRenewalResponse`](#rxrenewalresponse) \| [`RxChangeResponse`](#rxchangeresponse) \| [`CancelRxResponse`](#cancelrxresponse) A parsed SCRIPT prescription-lifecycle response body. *** ### LifecycleResponseKind > **LifecycleResponseKind** = `"RxRenewalResponse"` \| `"RxChangeResponse"` \| `"CancelRxResponse"` The SCRIPT prescription-lifecycle **response** transactions this parser models. *** ### NcpdpProfileEffect > **NcpdpProfileEffect** = `"relaxes"` \| `"adds"` \| `"requires"` The bucket a quirk falls into when rendered by [NcpdpProfile.describe](#describe). Mirrors the roadmap's "what this profile relaxes / adds / requires" framing. - `relaxes`: the partner tolerates / emits a structural variation a strict baseline read would flag (e.g. a SCRIPT version stamp outside the explicitly supported set). - `adds`: the partner emits extra spec-optional content (e.g. an additional Telecom response segment, a deeper reject-code taxonomy) a generic consumer might not expect. - `requires`: the partner mandates a normally-situational element be present (e.g. a PBM that requires Person Code in the Insurance segment). #### Example ```ts import type { NcpdpProfileEffect } from "@cosyte/ncpdp/profiles"; const effect: NcpdpProfileEffect = "adds"; ``` *** ### NcpdpStandard > **NcpdpStandard** = `"script"` \| `"telecom"` Which NCPDP standard a quirk applies to. SCRIPT and Telecom are unrelated formats with disjoint warning registries; a quirk is grounded in exactly one. #### Example ```ts import type { NcpdpStandard } from "@cosyte/ncpdp/profiles"; const std: NcpdpStandard = "telecom"; ``` *** ### NcpdpWarningCode > **NcpdpWarningCode** = [`ScriptWarningCode`](#scriptwarningcode) \| [`TelecomWarningCode`](#telecomwarningcode) The union of every public warning code across both standards. A quirk's `expectedWarnings` is drawn from this set and validated against it. #### Example ```ts import type { NcpdpWarningCode } from "@cosyte/ncpdp/profiles"; const code: NcpdpWarningCode = "NCPDP_TELECOM_UNKNOWN_REJECT_CODE"; ``` *** ### NdcSegmentation > **NdcSegmentation** = `"5-4-2"` \| `"5-4-1"` \| `"5-3-2"` \| `"4-4-2"` \| `"11-digit"` \| `"10-digit"` \| `"unknown"` The labeler/product/package segmentation hint for an NDC. Real-world NDCs arrive in several digit groupings; we surface the detected shape rather than forcing a normalization the consumer may not want. *** ### ResponseApproval > **ResponseApproval** = `"affirmative"` \| `"negative"` \| `"indeterminate"` A coarse, **fail-safe** classification of a [ResponseOutcome](#responseoutcome) for consumers that only need "did the prescriber say yes?". A denial is never `"affirmative"`, and anything that is not a clean yes/no (including `unknown`) is `"indeterminate"` so it is never mistaken for a grant. *** ### ResponseBody > **ResponseBody** = [`StatusBody`](#statusbody) \| [`ErrorBody`](#errorbody) \| [`VerifyBody`](#verifybody) A parsed SCRIPT response transaction body. *** ### ResponseDisposition > **ResponseDisposition** = `"success"` \| `"error"` \| `"verify"` How a SCRIPT response transaction dispositions the message it answers: `success` (a ``), `error` (an ``), or `verify` (a ``). Derived purely from the response body kind so that an `Error` can **never** be read as a success. *** ### ResponseKind > **ResponseKind** = `"Status"` \| `"Error"` \| `"Verify"` The three SCRIPT response-transaction element names this parser models. *** ### ResponseOutcome > **ResponseOutcome** = `"approved"` \| `"approvedWithChanges"` \| `"denied"` \| `"deniedNewToFollow"` \| `"replace"` \| `"validated"` \| `"unknown"` The prescriber's decision on a lifecycle request, detected purely from the `` choice element. The mapping is one-directional and fail-safe: a `` is **always** `"denied"` and is never read as an approval, and a response with no recognized choice is `"unknown"`: never assumed approved. - `approved`: ``: the request is granted as written. - `approvedWithChanges`: ``: granted, but the prescriber altered the medication; the changed medication is carried in [LifecycleResponseFields.medicationPrescribed](#medicationprescribed-3). - `denied`: ``: the request is refused. - `deniedNewToFollow`: `` (renewal): denied, a new prescription will follow separately. - `replace`: ``: the prescriber is replacing the prescription. - `validated`: `` (change): the prescriber validated the request without approving or denying it. - `unknown`: no recognized outcome choice was present. *** ### ScriptBody > **ScriptBody** = [`NewRx`](#newrx) \| [`ResponseBody`](#responsebody) \| [`LifecycleRequest`](#lifecyclerequest) \| [`LifecycleResponse`](#lifecycleresponse) \| [`UnsupportedBody`](#unsupportedbody) The parsed body of a SCRIPT message. *** ### ScriptBuildCode > **ScriptBuildCode** = *typeof* [`SCRIPT_BUILD_CODES`](#script_build_codes)\[keyof *typeof* [`SCRIPT_BUILD_CODES`](#script_build_codes)\] Union of the SCRIPT builder error code string literals. *** ### ScriptFatalCode > **ScriptFatalCode** = *typeof* [`SCRIPT_FATAL_CODES`](#script_fatal_codes)\[keyof *typeof* [`SCRIPT_FATAL_CODES`](#script_fatal_codes)\] Union of the SCRIPT fatal error code string literals. *** ### ScriptTransactionName > **ScriptTransactionName** = *typeof* [`SCRIPT_TRANSACTION_NAMES`](#script_transaction_names)\[`number`\] Union of the SCRIPT transaction element names this parser will name. *** ### ScriptWarningCode > **ScriptWarningCode** = *typeof* [`SCRIPT_WARNING_CODES`](#script_warning_codes)\[keyof *typeof* [`SCRIPT_WARNING_CODES`](#script_warning_codes)\] Union of the SCRIPT warning code string literals. *** ### SegmentAbsenceReason > **SegmentAbsenceReason** = `"unsourced"` Why a code inside a declared 111-AM range carries no name in this package. `"unsourced"`: no publicly readable artifact establishes what the standard defines at this code, so the package neither names it nor claims that nothing exists there. The normative vocabulary is the NCPDP External Code List, a purchased product this package can neither cite nor redistribute. A transmission carrying such a code is still decoded in full and warned, never dropped. *** ### SegmentGroup > **SegmentGroup** = `"request"` \| `"response"` Which half of a transmission a declared 111-AM code range covers. Both tokens are this package's own vocabulary, not a decoded label. *** ### SigFieldProvenance > **SigFieldProvenance** = `"coded"` \| `"derived"` \| `"absent"` Where a decoded structured-SIG field's value came from. - `"coded"`: the structured element carried a code (with an optional system qualifier); a [SigField.code](#code-10) is present. - `"derived"`: a value was read from the structured element but it was not coded; only [SigField.text](#text) is present. - `"absent"`: the structured element was missing or empty; neither a code nor text could be read. The field is **not** inferred from the free text. *** ### TelecomBuildCode > **TelecomBuildCode** = *typeof* [`TELECOM_BUILD_CODES`](#telecom_build_codes)\[keyof *typeof* [`TELECOM_BUILD_CODES`](#telecom_build_codes)\] Union of the Telecom builder error code string literals. *** ### TelecomFatalCode > **TelecomFatalCode** = *typeof* [`TELECOM_FATAL_CODES`](#telecom_fatal_codes)\[keyof *typeof* [`TELECOM_FATAL_CODES`](#telecom_fatal_codes)\] Union of the Telecom fatal error code string literals. *** ### TelecomVersion > **TelecomVersion** = \{ `kind`: `"d0"`; \} \| \{ `kind`: `"f6"`; `stamp`: `string`; \} \| \{ `kind`: `"unsupported"`; `stamp`: `string`; \} Classification of the version stamp peeked from a raw Telecom message. - `"d0"`: the supported D.0 standard; decode the fixed header at D.0 offsets. - `"f6"`: the emerging F6 stamp; recognized but not decoded (different layout). - `"unsupported"`: no recognizable version stamp; the byte layout is untrustworthy. *** ### TelecomWarningCode > **TelecomWarningCode** = *typeof* [`TELECOM_WARNING_CODES`](#telecom_warning_codes)\[keyof *typeof* [`TELECOM_WARNING_CODES`](#telecom_warning_codes)\] Union of the Telecom warning code string literals. *** ### VersionClassification > **VersionClassification** = \{ `kind`: `"known"`; `version`: [`KnownScriptVersion`](#knownscriptversion); \} \| \{ `kind`: `"tolerated"`; `version`: `string`; \} \| \{ `kind`: `"absent"`; \} \| \{ `kind`: `"unsupported"`; `version`: `string`; \} Outcome of classifying a declared SCRIPT version string. ## Variables ### D0\_HEADER\_LENGTH > `const` **D0\_HEADER\_LENGTH**: `56` = `56` Byte length of the fixed vD.0 request Transaction Header. #### Example ```ts import { D0_HEADER_LENGTH } from "@cosyte/ncpdp/telecom"; D0_HEADER_LENGTH; // 56: the fixed-width vD.0 request header ``` *** ### FIELD\_NAMES > `const` **FIELD\_NAMES**: `ReadonlyMap`\<`string`, `string`\> Paraphrased names for the safety-relevant B1 field identifiers this parser surfaces, keyed by their 2-character field id. A field whose id is absent here is still preserved verbatim: absence of a name means only that the parser has not labeled it, never that the field is invalid or droppable. #### Example ```ts import { FIELD_NAMES } from "@cosyte/ncpdp/telecom"; FIELD_NAMES.get("D7"); // "Product / Service ID" FIELD_NAMES.get("ZZ"); // undefined: unlabeled, still preserved verbatim ``` *** ### FIELD\_SEPARATOR > `const` **FIELD\_SEPARATOR**: "\u001c" = "\x1c" Field Separator (NCPDP "FS", ASCII 0x1C): separates fields within a segment. #### Example ```ts import { FIELD_SEPARATOR } from "@cosyte/ncpdp/telecom"; FIELD_SEPARATOR.charCodeAt(0); // 28 (0x1C) ``` *** ### GROUP\_SEPARATOR > `const` **GROUP\_SEPARATOR**: "\u001d" = "\x1d" Group Separator (NCPDP "GS", ASCII 0x1D): separates transactions in a transmission. #### Example ```ts import { GROUP_SEPARATOR } from "@cosyte/ncpdp/telecom"; GROUP_SEPARATOR.charCodeAt(0); // 29 (0x1D) ``` *** ### KNOWN\_SCRIPT\_VERSIONS > `const` **KNOWN\_SCRIPT\_VERSIONS**: readonly \[`"2017071"`, `"2023011"`\] SCRIPT versions this parser explicitly supports. Both are XML-era releases routed through Surescripts. The pair is grounded in US federal regulation, which is public law and therefore the license-clean source for a version identifier (the standard itself is paywalled). Two sections carry it: - **45 CFR 170.205(b)** adopts exactly these two SCRIPT Implementation Guide versions for electronic prescribing, at (b)(1) and (b)(2). It also states that the Secretary's adoption of `2017071` **expires on January 1, 2028**, after which `2023011` is the only SCRIPT version that paragraph adopts. - **42 CFR 423.160** (Medicare Part D electronic prescribing) requires at (b)(1) that a prescription or prescription-related information comply with a standard in 45 CFR 170.205(b), and incorporates both guides by reference at (c)(2) and (c)(3). **Scope, because the list is narrower than it looks.** This is the set those two sections adopt, not the set of SCRIPT versions NCPDP has ever published. A version outside it may well be real; nothing public that this package can cite would establish that, so it is not written down here. Narrowing the list never refuses a message: a present-but-unrecognized stamp is still parsed best-effort and reported as `tolerated`, per [classifyVersion](#classifyversion). #### Example ```ts import { KNOWN_SCRIPT_VERSIONS } from "@cosyte/ncpdp/script"; KNOWN_SCRIPT_VERSIONS.includes("2023011"); // true ``` *** ### profiles > `const` **profiles**: `object` Namespace object exposing the shipped built-in profiles, one per NCPDP standard. Each is authored via the public `defineProfile()` API and grounded in a real Tier-2 fixture. #### Type Declaration ##### pbm > `readonly` **pbm**: [`NcpdpProfile`](#ncpdpprofile) ##### surescripts > `readonly` **surescripts**: [`NcpdpProfile`](#ncpdpprofile) #### Example ```ts import { parseTelecom } from "@cosyte/ncpdp/telecom"; import { profiles } from "@cosyte/ncpdp/profiles"; const tx = parseTelecom(raw, { profile: profiles.pbm }); tx.profile?.name; // "pbm" ``` *** ### SCRIPT\_BUILD\_CODES > `const` **SCRIPT\_BUILD\_CODES**: `object` Stable error codes for the SCRIPT **emit** side: the builder and the serializer. Emit is the conservative half of Postel's Law: it refuses to construct, or to write out, a message it cannot render honestly (with one of these codes) rather than emitting XML a downstream system would reject or, in the unmodeled-transaction case, XML that looks correct and is not. Distinct from the parser's [SCRIPT\_FATAL\_CODES](#script_fatal_codes). #### Type Declaration ##### INVALID\_CHARACTER > `readonly` **INVALID\_CHARACTER**: `"NCPDP_SCRIPT_BUILD_INVALID_CHARACTER"` = `"NCPDP_SCRIPT_BUILD_INVALID_CHARACTER"` A supplied value carries a character that is illegal in XML 1.0 text. ##### MISSING\_MEDICATION > `readonly` **MISSING\_MEDICATION**: `"NCPDP_SCRIPT_BUILD_MISSING_MEDICATION"` = `"NCPDP_SCRIPT_BUILD_MISSING_MEDICATION"` A NewRx was built with no prescribed medication (a drug description is required). ##### MISSING\_RESPONSE\_CODE > `readonly` **MISSING\_RESPONSE\_CODE**: `"NCPDP_SCRIPT_BUILD_MISSING_RESPONSE_CODE"` = `"NCPDP_SCRIPT_BUILD_MISSING_RESPONSE_CODE"` A ``/``/`` response was built without a ``. ##### UNSUPPORTED\_TRANSACTION > `readonly` **UNSUPPORTED\_TRANSACTION**: `"NCPDP_SCRIPT_BUILD_UNSUPPORTED_TRANSACTION"` = `"NCPDP_SCRIPT_BUILD_UNSUPPORTED_TRANSACTION"` Serializing was asked for a transaction this library does not model (an `unsupported` body). Nothing under such a transaction is modeled, so the only document emit could write is one with the transaction body deleted. #### Example ```ts import { SCRIPT_BUILD_CODES } from "@cosyte/ncpdp/common"; SCRIPT_BUILD_CODES.MISSING_RESPONSE_CODE; // "NCPDP_SCRIPT_BUILD_MISSING_RESPONSE_CODE" ``` *** ### SCRIPT\_BUILD\_MESSAGES > `const` **SCRIPT\_BUILD\_MESSAGES**: `Readonly`\<`Record`\<[`ScriptBuildCode`](#scriptbuildcode), `string`\>\> The frozen message registry for SCRIPT builder errors: one fixed sentence per code, and the only source an [NcpdpScriptBuildError](#ncpdpscriptbuilderror)'s `message` can come from. #### Example ```ts import { SCRIPT_BUILD_CODES, SCRIPT_BUILD_MESSAGES } from "@cosyte/ncpdp/common"; SCRIPT_BUILD_MESSAGES[SCRIPT_BUILD_CODES.MISSING_MEDICATION]; ``` *** ### SCRIPT\_FATAL\_CODES > `const` **SCRIPT\_FATAL\_CODES**: `object` Fatal error codes for NCPDP SCRIPT parsing. A fatal is reserved for unrecoverable structural corruption: input that cannot be treated as a SCRIPT message at all. Everything recoverable is a warning instead (see "./warnings".SCRIPT\_WARNING\_CODES). #### Type Declaration ##### EMPTY\_INPUT > `readonly` **EMPTY\_INPUT**: `"EMPTY_INPUT"` = `"EMPTY_INPUT"` Input is empty or whitespace-only. ##### NO\_MESSAGE\_ROOT > `readonly` **NO\_MESSAGE\_ROOT**: `"NCPDP_SCRIPT_NO_MESSAGE_ROOT"` = `"NCPDP_SCRIPT_NO_MESSAGE_ROOT"` Well-formed XML, but the root element is not a SCRIPT ``. ##### NOT\_XML > `readonly` **NOT\_XML**: `"NCPDP_SCRIPT_NOT_XML"` = `"NCPDP_SCRIPT_NOT_XML"` Input is not well-formed XML, or carries a forbidden DOCTYPE/ENTITY. ##### UNSUPPORTED\_VERSION > `readonly` **UNSUPPORTED\_VERSION**: `"NCPDP_SCRIPT_UNSUPPORTED_VERSION"` = `"NCPDP_SCRIPT_UNSUPPORTED_VERSION"` The declared SCRIPT version predates the XML SCRIPT era and is unsupported. #### Example ```ts import { SCRIPT_FATAL_CODES } from "@cosyte/ncpdp/common"; SCRIPT_FATAL_CODES.NOT_XML; // "NCPDP_SCRIPT_NOT_XML" ``` *** ### SCRIPT\_FATAL\_MESSAGES > `const` **SCRIPT\_FATAL\_MESSAGES**: `Readonly`\<`Record`\<[`ScriptFatalCode`](#scriptfatalcode), `string`\>\> The frozen message registry for SCRIPT fatals: one fixed sentence per code, and the only source an [NcpdpScriptParseError](#ncpdpscriptparseerror)'s `message` can come from. #### Example ```ts import { SCRIPT_FATAL_CODES, SCRIPT_FATAL_MESSAGES } from "@cosyte/ncpdp/common"; SCRIPT_FATAL_MESSAGES[SCRIPT_FATAL_CODES.EMPTY_INPUT]; // "SCRIPT input is empty." ``` *** ### SCRIPT\_TRANSACTION\_NAMES > `const` **SCRIPT\_TRANSACTION\_NAMES**: readonly \[`"GetMessage"`, `"Status"`, `"Error"`, `"RxChangeRequest"`, `"RxChangeResponse"`, `"RxRenewalRequest"`, `"RxRenewalResponse"`, `"Resupply"`, `"Verify"`, `"CancelRx"`, `"CancelRxResponse"`, `"RxFill"`, `"DrugAdministration"`, `"NewRxRequest"`, `"NewRx"`, `"NewRxResponseDenied"`, `"RxTransferInitiationRequest"`, `"RxTransfer"`, `"RxTransferConfirm"`, `"RxFillIndicatorChange"`, `"Recertification"`, `"REMSInitiationRequest"`, `"REMSInitiationResponse"`, `"REMSRequest"`, `"REMSResponse"`, `"RxHistoryRequest"`, `"RxHistoryResponse"`, `"PAInitiationRequest"`, `"PAInitiationResponse"`, `"PARequest"`, `"PAResponse"`, `"PAAppealRequest"`, `"PAAppealResponse"`, `"PACancelRequest"`, `"PACancelResponse"`, `"PANotification"`\] SCRIPT transaction element names this parser will **name**, as opposed to model. It is a closed vocabulary the library owns, not something read out of a document, and it exists so that an unmodeled transaction can still be identified without copying a sender-chosen element name onto the model or into a diagnostic. The list is the transaction vocabulary published in **42 CFR 423.160**, the federal e-prescribing standards rule, verbatim and in its order. That source is public law, not the paywalled standard, which is why the names can be written down here at all. Being grounded rather than invented matters: an earlier draft of this list was written from memory and got three transfer names and the recertification names wrong while omitting the whole prior-authorization and REMS families, which would have silently stripped the identity off every ePA message this library saw. **One assumption is worth stating**: the regulation lists transaction *names* in prose, and this list is matched against XML element *local names*. Those are the same artifact only insofar as the regulation transcribed the standard's element names, which the shape of the names strongly suggests but which cannot be confirmed against a public source, since the schemas are paywalled. If one of them turns out not to be the element name, that transaction is surfaced unnamed, which is the same fail-safe path a vendor extension takes. A transaction the standard defines but that regulation does not name is surfaced as unmodeled and **unnamed**; so is a vendor extension. That is the intended default, because the alternative (repeat whatever the element was called) is what put document-derived bytes on a diagnostic surface. A consumer that needs the element name reads the document it already holds. Note that the PHI gate cannot check this list: a closed set passes it by construction. What keeps it safe is that it stays closed and stays sourced, which is a property of review, not of a test. #### Example ```ts import { SCRIPT_TRANSACTION_NAMES } from "@cosyte/ncpdp/script"; SCRIPT_TRANSACTION_NAMES.includes("RxHistoryRequest"); // true ``` *** ### SCRIPT\_WARNING\_CODES > `const` **SCRIPT\_WARNING\_CODES**: `object` Stable warning codes for NCPDP SCRIPT parsing. Per Postel's Law, the parser is lenient: anything recoverable yields a warning with one of these codes rather than throwing. Codes are part of the public contract: renaming one is a breaking change. #### Type Declaration ##### LIFECYCLE\_AMBIGUOUS\_OUTCOME > `readonly` **LIFECYCLE\_AMBIGUOUS\_OUTCOME**: `"NCPDP_SCRIPT_LIFECYCLE_AMBIGUOUS_OUTCOME"` = `"NCPDP_SCRIPT_LIFECYCLE_AMBIGUOUS_OUTCOME"` A lifecycle response (`RxRenewalResponse`/`RxChangeResponse`/`CancelRxResponse`) carried more than one outcome choice (e.g. both `` and ``); the most conservative outcome (a denial before an approval) is reported so an approval can never mask a denial. ##### LIFECYCLE\_OUTCOME\_UNRECOGNIZED > `readonly` **LIFECYCLE\_OUTCOME\_UNRECOGNIZED**: `"NCPDP_SCRIPT_LIFECYCLE_OUTCOME_UNRECOGNIZED"` = `"NCPDP_SCRIPT_LIFECYCLE_OUTCOME_UNRECOGNIZED"` A lifecycle response carried no recognized outcome choice; the outcome is surfaced as `"unknown"` rather than being assumed to be an approval. ##### MISSING\_REQUIRED\_ELEMENT > `readonly` **MISSING\_REQUIRED\_ELEMENT**: `"NCPDP_SCRIPT_MISSING_REQUIRED_ELEMENT"` = `"NCPDP_SCRIPT_MISSING_REQUIRED_ELEMENT"` A required element for the detected transaction was missing; left undefined. ##### RESPONSE\_AMBIGUOUS\_DISPOSITION > `readonly` **RESPONSE\_AMBIGUOUS\_DISPOSITION**: `"NCPDP_SCRIPT_RESPONSE_AMBIGUOUS_DISPOSITION"` = `"NCPDP_SCRIPT_RESPONSE_AMBIGUOUS_DISPOSITION"` More than one response transaction (``/``/``) was present in one body; the most conservative disposition (error first) is reported so a failure is never masked by a co-present success. ##### SIG\_AMBIGUOUS\_DOSE > `readonly` **SIG\_AMBIGUOUS\_DOSE**: `"NCPDP_SCRIPT_SIG_AMBIGUOUS_DOSE"` = `"NCPDP_SCRIPT_SIG_AMBIGUOUS_DOSE"` A structured dose element was present but no unambiguous dose quantity could be derived from it. The dose is surfaced as **absent** (provenance `"absent"`) rather than a guessed value, so an ambiguous SIG never yields a confident dose. ##### SIG\_STRUCTURED\_LOSSY > `readonly` **SIG\_STRUCTURED\_LOSSY**: `"NCPDP_SCRIPT_SIG_STRUCTURED_LOSSY"` = `"NCPDP_SCRIPT_SIG_STRUCTURED_LOSSY"` A structured `` was decoded into typed dosing components. The decode is **best-effort and lossy**; the free-text `SigText` remains the source of truth and is preserved verbatim. Raised once per medication carrying any structured component so a consumer never mistakes the additive structured view for an authoritative one. ##### STRENGTH\_CODED\_AND\_EXPLICIT > `readonly` **STRENGTH\_CODED\_AND\_EXPLICIT**: `"NCPDP_SCRIPT_STRENGTH_CODED_AND_EXPLICIT"` = `"NCPDP_SCRIPT_STRENGTH_CODED_AND_EXPLICIT"` A coded drug and an explicit Strength were both present; both surfaced, never reconciled. ##### UNSUPPORTED\_TRANSACTION > `readonly` **UNSUPPORTED\_TRANSACTION**: `"NCPDP_SCRIPT_UNSUPPORTED_TRANSACTION"` = `"NCPDP_SCRIPT_UNSUPPORTED_TRANSACTION"` The transaction body is a SCRIPT type this parser does not model; surfaced as unsupported. ##### UNSUPPORTED\_VERSION\_TOLERATED > `readonly` **UNSUPPORTED\_VERSION\_TOLERATED**: `"NCPDP_SCRIPT_UNSUPPORTED_VERSION_TOLERATED"` = `"NCPDP_SCRIPT_UNSUPPORTED_VERSION_TOLERATED"` Version is a plausible SCRIPT release we don't explicitly support; tolerated. ##### VERSION\_ABSENT > `readonly` **VERSION\_ABSENT**: `"NCPDP_SCRIPT_VERSION_ABSENT"` = `"NCPDP_SCRIPT_VERSION_ABSENT"` No version could be determined from the message; parsed best-effort. #### Example ```ts import { SCRIPT_WARNING_CODES } from "@cosyte/ncpdp/common"; SCRIPT_WARNING_CODES.VERSION_ABSENT; // "NCPDP_SCRIPT_VERSION_ABSENT" ``` *** ### SCRIPT\_WARNING\_MESSAGES > `const` **SCRIPT\_WARNING\_MESSAGES**: `Readonly`\<`Record`\<[`ScriptWarningCode`](#scriptwarningcode), `string`\>\> The frozen message registry: one fixed sentence per SCRIPT warning code, and the **only** source a warning's `message` can come from. This is the mechanism, not a convention. [scriptWarning](#scriptwarning) takes no value parameter at all, so there is no interpolation site for a document-derived string to reach: a message is a table lookup or it does not exist. Everything a consumer needs to locate the problem travels in the `code` and the [ScriptPosition](#scriptposition), whose path is built from element names this parser recognizes rather than from names a sender chose. #### Example ```ts import { SCRIPT_WARNING_CODES, SCRIPT_WARNING_MESSAGES } from "@cosyte/ncpdp/common"; SCRIPT_WARNING_MESSAGES[SCRIPT_WARNING_CODES.VERSION_ABSENT]; // "No SCRIPT version declared; parsed best-effort." ``` *** ### SEGMENT\_ABSENCES > `const` **SEGMENT\_ABSENCES**: `ReadonlyMap`\<`string`, [`SegmentAbsence`](#segmentabsence)\> Why each unnamed code inside a declared range has no name here, keyed by the 2-character 111-AM code. A code that is absent from this map and from `SEGMENT_NAMES` is simply outside every range this package declares (`99`, say): it decodes identically, and the inventory makes no claim about it either way. Absence of a name is never absence of data. The segment and every one of its fields survive verbatim, in wire order, with `NCPDP_TELECOM_UNKNOWN_SEGMENT`. #### Example ```ts import { SEGMENT_ABSENCES } from "@cosyte/ncpdp/telecom"; SEGMENT_ABSENCES.get("06")?.reason; // "unsourced" SEGMENT_ABSENCES.get("07"); // undefined: 07 is named ``` *** ### SEGMENT\_CODE\_RANGES > `const` **SEGMENT\_CODE\_RANGES**: readonly [`SegmentCodeRange`](#segmentcoderange)[] The 111-AM code ranges this package declares. Neither range's bounds are established by any artifact this package can cite (`boundsVerified` is `false` on both), so read them as the extent of this package's own claim of coverage and not as the extent of the standard. Every code inside a declared range is either named by `SEGMENT_NAMES` or carries a `SEGMENT_ABSENCES` record; `test/telecom/segment-inventory.test.ts` fails when one is neither. #### Example ```ts import { SEGMENT_CODE_RANGES } from "@cosyte/ncpdp/telecom"; SEGMENT_CODE_RANGES[0]?.group; // "request" SEGMENT_CODE_RANGES.every((r) => r.boundsVerified); // false: nothing sources them ``` *** ### SEGMENT\_ID\_LENGTH > `const` **SEGMENT\_ID\_LENGTH**: `2` = `2` Wire width of the Segment Identification (111-AM) code: two characters in the Telecommunication standard, which both halves of this library treat as a structural shape rather than a hint. The parser will not promote an AM value of any other length to [TelecomSegment.segmentId](#segmentid), and the builder refuses to construct one, so the bound holds however a transaction was made. #### Example ```ts import { SEGMENT_ID_LENGTH } from "@cosyte/ncpdp/telecom"; SEGMENT_ID_LENGTH; // 2 ``` *** ### SEGMENT\_NAMES > `const` **SEGMENT\_NAMES**: `ReadonlyMap`\<`string`, `string`\> Segment Identification (111-AM) codes modeled by this parser, mapped to our own paraphrased names. Codes are factual identifiers from the NCPDP Telecommunication standard; the names are ours (no redistributed NCPDP prose). A code outside this set is preserved verbatim with an undefined name. This map is not the whole inventory. `SEGMENT_CODE_RANGES` publishes the code ranges this package declares, and `SEGMENT_ABSENCES` says why each code inside one of them carries no name here, so an unnamed code can be told apart from an unexplained hole. #### Example ```ts import { SEGMENT_NAMES } from "@cosyte/ncpdp/telecom"; SEGMENT_NAMES.get("07"); // "Claim" SEGMENT_NAMES.get("99"); // undefined: preserved verbatim, just not labeled ``` *** ### SEGMENT\_SEPARATOR > `const` **SEGMENT\_SEPARATOR**: "\u001e" = "\x1e" Segment Separator (NCPDP "RS", ASCII 0x1E): separates segments within a transaction. #### Example ```ts import { SEGMENT_SEPARATOR } from "@cosyte/ncpdp/telecom"; SEGMENT_SEPARATOR.charCodeAt(0); // 30 (0x1E) ``` *** ### TELECOM\_BUILD\_CODES > `const` **TELECOM\_BUILD\_CODES**: `object` Stable error codes for the Telecom **builder**. The builder is the conservative (emit) half of Postel's Law: it refuses to construct a message that is invalid by construction, with one of these codes, rather than producing malformed wire output that a downstream system would have to reject. These are distinct from the parser's [TELECOM\_FATAL\_CODES](#telecom_fatal_codes). #### Type Declaration ##### EMBEDDED\_CONTROL\_CHARACTER > `readonly` **EMBEDDED\_CONTROL\_CHARACTER**: `"NCPDP_TELECOM_BUILD_EMBEDDED_CONTROL_CHARACTER"` = `"NCPDP_TELECOM_BUILD_EMBEDDED_CONTROL_CHARACTER"` A field separator / group separator / segment separator appeared inside supplied data. ##### FIELD\_TOO\_LONG > `readonly` **FIELD\_TOO\_LONG**: `"NCPDP_TELECOM_BUILD_FIELD_TOO_LONG"` = `"NCPDP_TELECOM_BUILD_FIELD_TOO_LONG"` A fixed-width header field was supplied with a value longer than its wire width. ##### INVALID\_FIELD\_ID > `readonly` **INVALID\_FIELD\_ID**: `"NCPDP_TELECOM_BUILD_INVALID_FIELD_ID"` = `"NCPDP_TELECOM_BUILD_INVALID_FIELD_ID"` A data field was supplied without a 2-character field identifier. ##### INVALID\_SEGMENT\_ID > `readonly` **INVALID\_SEGMENT\_ID**: `"NCPDP_TELECOM_BUILD_INVALID_SEGMENT_ID"` = `"NCPDP_TELECOM_BUILD_INVALID_SEGMENT_ID"` A segment was supplied with a Segment Identification (111-AM) code that is not exactly 2 characters. The builder refuses it for the same reason the parser refuses to promote one: 111-AM is 2 characters on the wire, and a longer value would put unbounded caller data on `segment.segmentId`, which the model documents as bounded and safe for a consumer to interpolate. ##### MISSING\_SEGMENT\_ID > `readonly` **MISSING\_SEGMENT\_ID**: `"NCPDP_TELECOM_BUILD_MISSING_SEGMENT_ID"` = `"NCPDP_TELECOM_BUILD_MISSING_SEGMENT_ID"` A segment was supplied with no Segment Identification code. ##### MISSING\_TRANSACTION\_CODE > `readonly` **MISSING\_TRANSACTION\_CODE**: `"NCPDP_TELECOM_BUILD_MISSING_TRANSACTION_CODE"` = `"NCPDP_TELECOM_BUILD_MISSING_TRANSACTION_CODE"` No Transaction Code (103-A3) was supplied; a request cannot be routed without one. ##### MULTI\_TRANSACTION\_EMIT > `readonly` **MULTI\_TRANSACTION\_EMIT**: `"NCPDP_TELECOM_BUILD_MULTI_TRANSACTION_EMIT"` = `"NCPDP_TELECOM_BUILD_MULTI_TRANSACTION_EMIT"` A model carrying more than one decoded transaction was handed to the serializer. Emit is whole-message, one transaction per transmission, so the only two honest answers are to write every transaction or to refuse: writing the first and dropping the rest would turn a complete read into a silently partial message. The reader decodes them all; the writer says it cannot yet express them all. #### Example ```ts import { TELECOM_BUILD_CODES } from "@cosyte/ncpdp/telecom"; TELECOM_BUILD_CODES.MISSING_TRANSACTION_CODE; // "NCPDP_TELECOM_BUILD_MISSING_TRANSACTION_CODE" ``` *** ### TELECOM\_BUILD\_MESSAGES > `const` **TELECOM\_BUILD\_MESSAGES**: `Readonly`\<`Record`\<[`TelecomBuildCode`](#telecombuildcode), `string`\>\> The frozen message registry for Telecom builder errors. Every entry names the **rule** that was broken, never the offending value. A caller who supplied the value knows what it was; the library quoting it back is how a cardholder id ends up in a log line. Where the rejection is about one fixed-header slot, the slot travels as [NcpdpTelecomBuildError.headerField](#headerfield), which is typed closed. #### Example ```ts import { TELECOM_BUILD_CODES, TELECOM_BUILD_MESSAGES } from "@cosyte/ncpdp/telecom"; TELECOM_BUILD_MESSAGES[TELECOM_BUILD_CODES.INVALID_FIELD_ID]; ``` *** ### TELECOM\_FATAL\_CODES > `const` **TELECOM\_FATAL\_CODES**: `object` Fatal error codes for NCPDP Telecommunication-standard parsing. A fatal is reserved for structure that cannot be treated as a Telecom transmission at all: input too short to hold the fixed Transaction Header, an unframeable body, or a version whose byte layout this reader cannot trust. Everything recoverable is a warning instead (see "./warnings".TELECOM\_WARNING\_CODES). #### Type Declaration ##### EMPTY\_INPUT > `readonly` **EMPTY\_INPUT**: `"EMPTY_INPUT"` = `"EMPTY_INPUT"` Input is empty or whitespace-only. ##### INVALID\_FRAMING > `readonly` **INVALID\_FRAMING**: `"NCPDP_TELECOM_INVALID_FRAMING"` = `"NCPDP_TELECOM_INVALID_FRAMING"` The message body carries content but none of the framing control characters needed to tokenize it into segments and fields; a separator is never guessed. ##### NO\_HEADER > `readonly` **NO\_HEADER**: `"NCPDP_TELECOM_NO_HEADER"` = `"NCPDP_TELECOM_NO_HEADER"` Input is too short to contain the fixed Transaction Header. ##### UNSUPPORTED\_VERSION > `readonly` **UNSUPPORTED\_VERSION**: `"NCPDP_TELECOM_UNSUPPORTED_VERSION"` = `"NCPDP_TELECOM_UNSUPPORTED_VERSION"` A version stamp is present but is neither the supported D.0 nor a recognized future stamp (e.g. F6); the fixed-header byte layout cannot be trusted, so the message is refused rather than decoded against the wrong offsets. #### Example ```ts import { TELECOM_FATAL_CODES } from "@cosyte/ncpdp/telecom"; TELECOM_FATAL_CODES.NO_HEADER; // "NCPDP_TELECOM_NO_HEADER" ``` *** ### TELECOM\_FATAL\_MESSAGES > `const` **TELECOM\_FATAL\_MESSAGES**: `Readonly`\<`Record`\<[`TelecomFatalCode`](#telecomfatalcode), `string`\>\> The frozen message registry for Telecom fatals: one fixed sentence per code, and the only source an [NcpdpTelecomParseError](#ncpdptelecomparseerror)'s `message` can come from. The two length-related entries name the parser's own fixed header widths rather than the input's length. A byte count is a number and could not carry a value on its own, but it is also nothing a caller cannot measure from the input it just passed in, and dropping it is what lets a test assert that a message equals its registry entry exactly. #### Example ```ts import { TELECOM_FATAL_CODES, TELECOM_FATAL_MESSAGES } from "@cosyte/ncpdp/telecom"; TELECOM_FATAL_MESSAGES[TELECOM_FATAL_CODES.EMPTY_INPUT]; // "Input is empty." ``` *** ### TELECOM\_WARNING\_CODES > `const` **TELECOM\_WARNING\_CODES**: `object` Stable warning codes for NCPDP Telecommunication-standard parsing. Per Postel's Law, the parser is lenient: anything recoverable yields a warning with one of these codes rather than throwing, and the underlying bytes are always preserved verbatim so nothing is silently dropped. Codes are part of the public contract. Renaming one is a breaking change. #### Type Declaration ##### COB\_COUNT\_MISMATCH > `readonly` **COB\_COUNT\_MISMATCH**: `"NCPDP_TELECOM_COB_COUNT_MISMATCH"` = `"NCPDP_TELECOM_COB_COUNT_MISMATCH"` A Coordination-of-Benefits segment declared an other-payer/other-payment count (337-4C request, 355-NT response) that disagrees with the number of other-payer blocks decoded. Every block is still surfaced; this flags a possible mis-read COB chain so secondary-payer money is never silently dropped or duplicated. ##### COMPOUND\_COUNT\_MISMATCH > `readonly` **COMPOUND\_COUNT\_MISMATCH**: `"NCPDP_TELECOM_COMPOUND_COUNT_MISMATCH"` = `"NCPDP_TELECOM_COMPOUND_COUNT_MISMATCH"` A Compound segment declared an ingredient count (447-EC) that disagrees with the number of ingredient occurrences actually decoded. Every ingredient is still surfaced verbatim; this flags a possible truncated/over-stuffed compound so a missing or extra ingredient is never silent (a compound with a missing ingredient is, clinically, a different medication). ##### MALFORMED\_FIELD > `readonly` **MALFORMED\_FIELD**: `"NCPDP_TELECOM_MALFORMED_FIELD"` = `"NCPDP_TELECOM_MALFORMED_FIELD"` A field token was too short to carry a 2-character field identifier. It is preserved verbatim (with an empty id) rather than dropped. ##### MALFORMED\_SEGMENT\_ID > `readonly` **MALFORMED\_SEGMENT\_ID**: `"NCPDP_TELECOM_MALFORMED_SEGMENT_ID"` = `"NCPDP_TELECOM_MALFORMED_SEGMENT_ID"` A segment led with an `AM` field whose value is not a 2-character Segment Identification code. Off-shape bytes there are almost always a framing failure (a dropped field separator runs the rest of the segment into the code), so the value is **not** promoted to `segment.segmentId`: it stays in the segment's `fields` as the `AM` field, verbatim and nothing dropped, and the segment id reads empty. Keeping it out of the id also keeps unbounded wire bytes off a structural identifier a downstream package would build a diagnostic from. ##### MISSING\_SEGMENT\_ID > `readonly` **MISSING\_SEGMENT\_ID**: `"NCPDP_TELECOM_MISSING_SEGMENT_ID"` = `"NCPDP_TELECOM_MISSING_SEGMENT_ID"` A segment's first field was not the Segment Identification (`AM`). The segment is still surfaced with its fields preserved, but its segment id is left empty since it could not be read from the expected position. ##### STATUS\_CONFLICT > `readonly` **STATUS\_CONFLICT**: `"NCPDP_TELECOM_STATUS_CONFLICT"` = `"NCPDP_TELECOM_STATUS_CONFLICT"` A response declared a paid/captured/approved Transaction Response Status (112-AN) **while also carrying one or more reject codes**. The two disagree; the library resolves the disposition to **rejected** (a reject always wins: a consumer must never be told a rejected claim was paid) and raises this so the conflict is visible, never silent. ##### TRANSACTION\_COUNT\_MISMATCH > `readonly` **TRANSACTION\_COUNT\_MISMATCH**: `"NCPDP_TELECOM_TRANSACTION_COUNT_MISMATCH"` = `"NCPDP_TELECOM_TRANSACTION_COUNT_MISMATCH"` The Transaction Count (109-A9) declared in the fixed header disagrees with the number of group-separated transactions actually decoded. Every decoded transaction is still surfaced on `transaction.transactions`; the declared value stays verbatim on `transaction.transactionCount` and the decoded number on `transaction.decodedTransactionCount`, so a consumer reads both and this warning only says they differ. It reports a **disagreement**, never an illegal count: no public artifact establishing a maximum could be read, so this reader enforces none and a transmission declaring any number decodes every transaction it carries. ##### UNKNOWN\_DUR\_REASON > `readonly` **UNKNOWN\_DUR\_REASON**: `"NCPDP_TELECOM_UNKNOWN_DUR_REASON"` = `"NCPDP_TELECOM_UNKNOWN_DUR_REASON"` A request DUR/PPS Reason For Service code (439-E4) is not one this parser recognizes. The code is preserved verbatim and surfaced; only the human-readable description is absent. The interaction is never dropped. ##### UNKNOWN\_REJECT\_CODE > `readonly` **UNKNOWN\_REJECT\_CODE**: `"NCPDP_TELECOM_UNKNOWN_REJECT_CODE"` = `"NCPDP_TELECOM_UNKNOWN_REJECT_CODE"` A Reject Code (511-FB) value is not one this parser recognizes. The code is preserved verbatim and surfaced with `known: false`; only the human-readable description is absent. The reject is never dropped or reinterpreted. ##### UNKNOWN\_RESPONSE\_STATUS > `readonly` **UNKNOWN\_RESPONSE\_STATUS**: `"NCPDP_TELECOM_UNKNOWN_RESPONSE_STATUS"` = `"NCPDP_TELECOM_UNKNOWN_RESPONSE_STATUS"` A Transaction Response Status (112-AN) value is not one this parser models. The status is preserved verbatim and the disposition reads `"unknown"`: never assumed paid, so an unrecognized status can never imply payment. ##### UNKNOWN\_SEGMENT > `readonly` **UNKNOWN\_SEGMENT**: `"NCPDP_TELECOM_UNKNOWN_SEGMENT"` = `"NCPDP_TELECOM_UNKNOWN_SEGMENT"` A segment's identification code is not one this parser models. The segment and its fields are preserved verbatim (keyed by their field ids) and surfaced; only the human-readable segment name is left undefined. ##### VF6\_NOT\_DECODED > `readonly` **VF6\_NOT\_DECODED**: `"NCPDP_TELECOM_VF6_NOT_DECODED"` = `"NCPDP_TELECOM_VF6_NOT_DECODED"` The message declares the emerging F6 version stamp. F6 changes the fixed header layout (an 8-byte IIN replaces the 6-byte BIN, among other changes), so this D.0 reader **recognizes but does not decode** it: the version is surfaced and the body is left untokenized rather than read against the wrong offsets. #### Example ```ts import { TELECOM_WARNING_CODES } from "@cosyte/ncpdp/telecom"; TELECOM_WARNING_CODES.UNKNOWN_SEGMENT; // "NCPDP_TELECOM_UNKNOWN_SEGMENT" ``` *** ### TELECOM\_WARNING\_MESSAGES > `const` **TELECOM\_WARNING\_MESSAGES**: `Readonly`\<`Record`\<[`TelecomWarningCode`](#telecomwarningcode), `string`\>\> The frozen message registry: one fixed sentence per Telecom warning code, and the **only** source a warning's `message` can come from. This is the mechanism, not a convention. [telecomWarning](#telecomwarning) takes no value parameter at all, so there is no interpolation site for wire bytes to reach. A Telecom transmission is PHI-dense in almost every field, and a message that quoted so much as a segment code could quote an NDC or a prescription number instead the moment a field separator went missing. What a consumer needs to locate the problem travels in the `code` and the [TelecomPosition](#telecomposition): a byte offset and, when known, a 2-character field identifier. #### Example ```ts import { TELECOM_WARNING_CODES, TELECOM_WARNING_MESSAGES } from "@cosyte/ncpdp/telecom"; TELECOM_WARNING_MESSAGES[TELECOM_WARNING_CODES.UNKNOWN_SEGMENT]; // "The segment at this offset declares a code this parser does not model; preserved verbatim." ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, synced with `package.json#version` by the build. #### Example ```ts import { VERSION } from "@cosyte/ncpdp"; console.log(VERSION); ``` ## Functions ### approvalOf() > **approvalOf**(`outcome`): [`ResponseApproval`](#responseapproval) Map a [ResponseOutcome](#responseoutcome) to its fail-safe [ResponseApproval](#responseapproval). Total and one-directional: only an outright approval is `"affirmative"`; denials are `"negative"`; everything else (`replace`/`validated`/`unknown`) is `"indeterminate"`. #### Parameters ##### outcome [`ResponseOutcome`](#responseoutcome) The detected response outcome. #### Returns [`ResponseApproval`](#responseapproval) The coarse approval classification. #### Example ```ts approvalOf("denied"); // "negative" approvalOf("approvedWithChanges"); // "affirmative" approvalOf("unknown"); // "indeterminate" ``` *** ### buildNewRx() > **buildNewRx**(`input`): [`ScriptMessage`](#scriptmessage) Build a spec-clean SCRIPT **NewRx** message. The conservative (emit) half of Postel's Law: it refuses to construct a NewRx with no prescribed-medication description ([SCRIPT\_BUILD\_CODES.MISSING\_MEDICATION](#missing_medication)) or carrying an XML-illegal control character ([SCRIPT\_BUILD\_CODES.INVALID\_CHARACTER](#invalid_character)), throwing [NcpdpScriptBuildError](#ncpdpscriptbuilderror) rather than emitting an unusable message. The returned [ScriptMessage](#scriptmessage) serializes (via "./serialize".serializeScript / `toString()`) to XML that re-parses with zero warnings. #### Parameters ##### input [`NewRxInput`](#newrxinput) The header, parties, and medication to build. #### Returns [`ScriptMessage`](#scriptmessage) A frozen [ScriptMessage](#scriptmessage) carrying a NewRx body. #### Throws NcpdpScriptBuildError when the input cannot form a spec-clean NewRx. #### Example ```ts import { buildNewRx } from "@cosyte/ncpdp/script"; const msg = buildNewRx({ header: { version: "2017071", messageId: "SYNTH-1" }, medication: { description: "Amoxicillin 500 MG Oral Capsule" }, }); msg.toString(); // canonical SCRIPT XML ``` *** ### buildScriptResponse() > **buildScriptResponse**(`input`): [`ScriptMessage`](#scriptmessage) Build a spec-clean SCRIPT response (``/``/``) message. Refuses to construct a response with no `` ([SCRIPT\_BUILD\_CODES.MISSING\_RESPONSE\_CODE](#missing_response_code)): the one field the parser itself flags as required, or an XML-illegal control character ([SCRIPT\_BUILD\_CODES.INVALID\_CHARACTER](#invalid_character)). #### Parameters ##### input [`ScriptResponseInput`](#scriptresponseinput) The response kind, code, optional description, and header. #### Returns [`ScriptMessage`](#scriptmessage) A frozen [ScriptMessage](#scriptmessage) carrying the response body. #### Throws NcpdpScriptBuildError when the input cannot form a spec-clean response. #### Example ```ts import { buildScriptResponse } from "@cosyte/ncpdp/script"; const ack = buildScriptResponse({ kind: "Status", code: "010", header: { relatesToMessageId: "SYNTH-1" }, }); ack.disposition; // "success" ``` *** ### buildTelecomRequest() > **buildTelecomRequest**(`input`): [`TelecomTransaction`](#telecomtransaction) Build a spec-clean NCPDP Telecommunication vD.0 **request** transaction from a structured model. The conservative (emit) half of Postel's Law: it refuses to construct a message that is invalid by construction: a missing Transaction Code, a missing or non-2-character Segment Identification, a non-2-character field id, an embedded FS/GS/RS control character, or an over-long fixed-header field: throwing a typed [NcpdpTelecomBuildError](#ncpdptelecombuilderror) rather than producing malformed wire output a downstream processor would have to reject. The returned transaction is frozen and ready for "./serialize".serializeTelecom; the round trip `parseTelecom(serializeTelecom(buildTelecomRequest(input)))` re-parses with zero warnings. #### Parameters ##### input [`TelecomRequestInput`](#telecomrequestinput) The header fields and segments to build. #### Returns [`TelecomTransaction`](#telecomtransaction) A frozen, valid-by-construction [TelecomTransaction](#telecomtransaction). #### Throws NcpdpTelecomBuildError when the input cannot form a spec-clean message. #### Example ```ts import { buildTelecomRequest, serializeTelecom } from "@cosyte/ncpdp/telecom"; const t = buildTelecomRequest({ header: { transactionCode: "B1", binNumber: "999999" }, segments: [{ segmentId: "07", fields: [{ id: "D2", value: "RX0000001" }] }], }); serializeTelecom(t); // canonical wire string ``` *** ### cancelRx() > **cancelRx**(`message`): [`CancelRx`](#cancelrx) \| `undefined` Convenience accessor: the [CancelRx](#cancelrx) body, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`CancelRx`](#cancelrx) \| `undefined` The cancel body, or `undefined`. #### Example ```ts cancelRx(parseScript(xml))?.medicationPrescribed?.description; ``` *** ### cancelRxResponse() > **cancelRxResponse**(`message`): [`CancelRxResponse`](#cancelrxresponse) \| `undefined` Convenience accessor: the [CancelRxResponse](#cancelrxresponse) body, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`CancelRxResponse`](#cancelrxresponse) \| `undefined` The cancel-response body, or `undefined`. #### Example ```ts cancelRxResponse(parseScript(xml))?.outcome; // "approved" | "denied" | … ``` *** ### claim() > **claim**(`transaction`, `index?`): [`TelecomClaim`](#telecomclaim) \| `undefined` Build the B1/B2/B3 request view over one decoded transaction of a parsed Telecom transmission. A transmission may carry several claims for the same patient, so the view is addressed by transaction index: `index` defaults to the first transaction and every decoded transaction is reachable, not just the leading one. An index past the last decoded transaction reads `undefined` rather than throwing. #### Parameters ##### transaction [`TelecomTransaction`](#telecomtransaction) A transmission from [parseTelecom](#parsetelecom). ##### index? `number` = `0` Zero-based transaction index; defaults to the first. #### Returns [`TelecomClaim`](#telecomclaim) \| `undefined` The [TelecomClaim](#telecomclaim) view, or `undefined` when that transaction decoded no segments. #### Example ```ts const t = parseTelecom(rawClaim); claim(t)?.product?.id; // the first transaction's dispensed NDC claim(t, 1)?.product?.id; // the second transaction's, when the message carried one ``` *** ### claimView() > **claimView**(`transactionCode`, `segments`): [`TelecomClaim`](#telecomclaim) \| `undefined` Build the B1/B2/B3 request view over a decoded Telecom transaction. Returns `undefined` only when the transaction carries no segments at all; otherwise it surfaces whatever safety-relevant fields are present, each optional. #### Parameters ##### transactionCode `string` Transaction Code (103-A3) from the header. ##### segments readonly [`TelecomSegment`](#telecomsegment)[] The decoded segments in wire order. #### Returns [`TelecomClaim`](#telecomclaim) \| `undefined` The claim view, or `undefined` when there are no segments to read. #### Example ```ts const t = parseTelecom(raw); claimView(t.header.transactionCode, t.segments)?.product?.id; // the NDC ``` *** ### classifyVersion() > **classifyVersion**(`raw`): [`VersionClassification`](#versionclassification) Classify a declared SCRIPT version string. - A known XML version → `known`. - A legacy dotted major (pre-XML, e.g. `10.6`) → `unsupported` (fatal). - Absent/blank → `absent` (parse best-effort + warn). - Anything else present-but-unrecognized → `tolerated` (parse best-effort + warn), since refusing an odd-but-present version string would violate Postel's Law for a message that is still XML. #### Parameters ##### raw `string` \| `undefined` The version attribute value, or `undefined` when absent. #### Returns [`VersionClassification`](#versionclassification) The [VersionClassification](#versionclassification). #### Example ```ts classifyVersion("2017071"); // { kind: "known", version: "2017071" } classifyVersion("2099001"); // { kind: "tolerated", version: "2099001" } classifyVersion("10.6"); // { kind: "unsupported", version: "10.6" } classifyVersion(undefined); // { kind: "absent" } ``` *** ### codedValue() > **codedValue**(`value`, `qualifier`): [`CodedValue`](#codedvalue) Build a frozen [CodedValue](#codedvalue) from a raw code and qualifier, recognizing the code system in the process. #### Parameters ##### value `string` The code, verbatim. ##### qualifier `string` The accompanying qualifier string. #### Returns [`CodedValue`](#codedvalue) A frozen [CodedValue](#codedvalue). #### Example ```ts codedValue("00002821501", "ND").system; // "NDC" ``` *** ### dateValue() > **dateValue**(`raw`): [`DateValue`](#datevalue) \| `undefined` Decode a verbatim NCPDP date string into a [DateValue](#datevalue), or `undefined` when it is not a value this package decodes. Never throws, for any input, and never returns a half-filled value: a string that is not exactly eight digits, or that names a day the calendar does not have (February 30, month 13, day 0), is `undefined` rather than a value with some components set. `undefined` and `null` are `undefined` too, so a caller can hand it an optional field straight off a parsed model. Returning `undefined` loses nothing. Unlike "./decimal".decimalValue and its siblings, this wrapper never replaces a field on a parsed structure: the wire string stays on the model whatever this answers, so there is no source for the caller to lose. #### Parameters ##### raw `string` \| `null` \| `undefined` The date value exactly as it appeared on the wire. #### Returns [`DateValue`](#datevalue) \| `undefined` A frozen [DateValue](#datevalue), or `undefined` when the string does not match a decoded form. #### Example ```ts dateValue("19880705")?.month; // 7: spec-native, never 6 dateValue("19880732"); // undefined: no such day, nothing partial dateValue("1988-07-05"); // undefined: not a form this package declares ``` *** ### decimalValue() > **decimalValue**(`raw`): [`DecimalValue`](#decimalvalue) Wrap a raw textual value as a [DecimalValue](#decimalvalue) without converting to a float. Invalid input is preserved as-is with `isValid: false`: lenient parse, never a throw. #### Parameters ##### raw `string` The textual value from the message (already trimmed by the caller). #### Returns [`DecimalValue`](#decimalvalue) A frozen [DecimalValue](#decimalvalue). #### Example ```ts decimalValue("0.1").isValid; // true decimalValue("1/2").isValid; // false (still preserved as source) ``` *** ### decodeD0Header() > **decodeD0Header**(`raw`): [`TelecomHeader`](#telecomheader) Decode the fixed-length D.0 Transaction Header from the head of a raw message. The caller guarantees `raw.length >= D0_HEADER_LENGTH`. Each positional field is sliced and trimmed of pad whitespace. #### Parameters ##### raw `string` The raw message text (length already validated by the caller). #### Returns [`TelecomHeader`](#telecomheader) A frozen [TelecomHeader](#telecomheader). #### Example ```ts const h = decodeD0Header("610279D0B1".padEnd(56, " ")); h.binNumber; // "610279" h.transactionCode; // "B1" ``` *** ### deepFreeze() > **deepFreeze**\<`T`\>(`value`): `T` Recursively freeze an object graph so parsed models are immutable by default. Mutation, where allowed, happens only through explicit builder/setter methods that return new instances: never by reaching into a returned model. #### Type Parameters ##### T `T` The value type. #### Parameters ##### value `T` The value to deep-freeze in place. #### Returns `T` The same reference, now deeply frozen. #### Example ```ts const m = deepFreeze({ a: { b: 1 } }); Object.isFrozen(m.a); // true ``` *** ### defineProfile() > **defineProfile**(`opts`): [`NcpdpProfile`](#ncpdpprofile) Build a readonly [NcpdpProfile](#ncpdpprofile) from a validated spec. Invalid input throws "./errors.js".NcpdpProfileError with an actionable message: missing/empty name, unknown option key (with a typo hint), or a quirk that violates the locked hard rule (missing `fixture`, bad `standard`/`effect`, unknown `expectedWarnings` code). `extends` composes parent profiles: lineage flattens + dedupes, quirks merge by id (child wins on collision, non-colliding parent quirks survive), and `description` is last-wins. #### Parameters ##### opts [`NcpdpProfileSpec`](#ncpdpprofilespec) #### Returns [`NcpdpProfile`](#ncpdpprofile) #### Example ```ts import { defineProfile } from "@cosyte/ncpdp/profiles"; const pbm = defineProfile({ name: "pbm", description: "PBM / clearinghouse Telecom claim conventions", quirks: [ { id: "person-code-required", standard: "telecom", effect: "requires", summary: "Insurance segment carries a Person Code (303-C3).", fixture: "telecom/pbm-person-code.ncpdp", sourceCategory: "NCPDP Telecommunication vD.0: Person Code (303-C3)", }, ], }); pbm.name; // "pbm" pbm.lineage; // ["pbm"] pbm.describe().requires; // [{ id: "person-code-required", ... }] ``` *** ### detectVersion() > **detectVersion**(`raw`): [`TelecomVersion`](#telecomversion) Peek the version stamp of a raw Telecom message without trusting the rest of the header layout. D.0 carries `"D0"` in the Version/Release field at offset 6; F6 (which widens the leading identification field) carries `"F6"` at offset 8. Both candidate positions are checked. Anything else is `"unsupported"`: the offsets cannot be trusted, so the caller refuses rather than guesses. #### Parameters ##### raw `string` The raw message text. #### Returns [`TelecomVersion`](#telecomversion) The [TelecomVersion](#telecomversion) classification. #### Example ```ts detectVersion("123456D0B1…").kind; // "d0" ``` *** ### dispositionOf() > **dispositionOf**(`kind`): [`ResponseDisposition`](#responsedisposition) Map a response body kind to its disposition. The mapping is total and one-directional: an [ErrorBody](#errorbody) is **always** `"error"`. #### Parameters ##### kind [`ResponseKind`](#responsekind) The response body kind. #### Returns [`ResponseDisposition`](#responsedisposition) The disposition. #### Example ```ts dispositionOf("Error"); // "error" ``` *** ### error() > **error**(`message`): [`ErrorBody`](#errorbody) \| `undefined` Convenience accessor: the `` (negative-acknowledgment) body of a message, or `undefined`. An Error is never read as a success. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`ErrorBody`](#errorbody) \| `undefined` The Error body, or `undefined` when the message is not an Error. #### Example ```ts error(parseScript(xml))?.code; ``` *** ### extractStructuredSig() > **extractStructuredSig**(`medEl`, `path`, `warnings`): [`StructuredSig`](#structuredsig) \| `undefined` Decode a structured SCRIPT `` element into a [StructuredSig](#structuredsig), or return `undefined` when no `` is present under `medEl`. Lenient and lossy by construction: every component is read independently and tagged with its provenance; unrecoverable ambiguity downgrades a field to `"absent"` and warns rather than guessing. The free-text `` is always preserved verbatim. Raises "../common/warnings".SCRIPT\_WARNING\_CODES.SIG\_STRUCTURED\_LOSSY once when any structured component is decoded. #### Parameters ##### medEl [`XmlElement`](#xmlelement) The `` (or equivalent) element. ##### path `string` XPath-style location of `medEl` (for warning context). ##### warnings [`NcpdpScriptWarning`](#ncpdpscriptwarning)[] Sink that collects non-fatal warnings. #### Returns [`StructuredSig`](#structuredsig) \| `undefined` A frozen [StructuredSig](#structuredsig), or `undefined` when no `` exists. #### Example ```ts const warnings: NcpdpScriptWarning[] = []; const sig = extractStructuredSig(medEl, "/Message/Body/NewRx/MedicationPrescribed", warnings); sig?.sigText; // verbatim free text: the source of truth sig?.route.code?.system; // "SNOMED" when the route was coded ``` *** ### fieldValue() > **fieldValue**(`segment`, `fieldId`): `string` \| `undefined` Read the verbatim value of the first field with a given id within a segment. #### Parameters ##### segment [`TelecomSegment`](#telecomsegment) \| `undefined` The segment to read, or `undefined`. ##### fieldId `string` The 2-character field id, e.g. `"D7"`. #### Returns `string` \| `undefined` The field value, or `undefined` when the segment or field is absent. #### Example ```ts fieldValue(findSegment(t.segments, "07"), "D7"); // the Product/Service ID ``` *** ### findSegment() > **findSegment**(`segments`, `code`): [`TelecomSegment`](#telecomsegment) \| `undefined` Find the first segment with a given identification code. #### Parameters ##### segments readonly [`TelecomSegment`](#telecomsegment)[] The decoded segments. ##### code `string` The 2-character Segment Identification code, e.g. `"07"`. #### Returns [`TelecomSegment`](#telecomsegment) \| `undefined` The first matching segment, or `undefined`. #### Example ```ts findSegment(t.segments, "07")?.name; // "Claim" ``` *** ### getDefaultProfile() > **getDefaultProfile**(): [`NcpdpProfile`](#ncpdpprofile) \| `undefined` Return the current default profile, or `undefined` if none is registered. #### Returns [`NcpdpProfile`](#ncpdpprofile) \| `undefined` #### Example ```ts import { getDefaultProfile } from "@cosyte/ncpdp/profiles"; const p = getDefaultProfile(); if (p !== undefined) console.log("default profile:", p.name); ``` *** ### impliedThreeDecimal() > **impliedThreeDecimal**(`digits`): `string` \| `undefined` Apply the NCPDP implied 3-place decimal to an integer digit string, string-wise (never via float). Returns `undefined` for non-digit input. #### Parameters ##### digits `string` The verbatim integer string from the wire. #### Returns `string` \| `undefined` The value with a 3-place fraction, or `undefined` if not all digits. #### Example ```ts impliedThreeDecimal("30000"); // "30.000" impliedThreeDecimal("5"); // "0.005" ``` *** ### joinPath() > **joinPath**(`parent`, `child`): `string` Append a child step to an XPath-style location string. #### Parameters ##### parent `string` The parent path, e.g. `/Message/Body`. ##### child `string` The child element name, e.g. `NewRx`. #### Returns `string` The joined path, e.g. `/Message/Body/NewRx`. #### Example ```ts joinPath("/Message/Body", "NewRx"); // "/Message/Body/NewRx" ``` *** ### ndcValue() > **ndcValue**(`raw`): [`NdcValue`](#ndcvalue) Classify an NDC string into a [NdcSegmentation](#ndcsegmentation) hint. Hyphenated forms are matched by their digit groups; bare digit strings are classified by total length. Anything else is `"unknown"`. #### Parameters ##### raw `string` The NDC value as it appeared on the wire. #### Returns [`NdcValue`](#ndcvalue) A frozen [NdcValue](#ndcvalue). #### Example ```ts ndcValue("0002-8215-01").segmentation; // "4-4-2" ndcValue("00002821501").segmentation; // "11-digit" ``` *** ### newRx() > **newRx**(`message`): [`NewRx`](#newrx) \| `undefined` Convenience accessor: the [NewRx](#newrx) body of a message, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`NewRx`](#newrx) \| `undefined` The NewRx body, or `undefined` when the message is another transaction. #### Example ```ts const rx = newRx(parseScript(xml)); rx?.patient?.name?.lastName; ``` *** ### parseScript() > **parseScript**(`raw`, `options?`): [`ScriptMessage`](#scriptmessage) Parse a raw NCPDP SCRIPT XML string into an immutable [ScriptMessage](#scriptmessage). Liberal on input (Postel's Law): recoverable anomalies become warnings with stable codes and XPath context. Fatal only for unrecoverable structure: empty input, non-XML / entity-bearing input, a non-`` root, or a pre-XML legacy SCRIPT version. #### Parameters ##### raw `string` The raw SCRIPT XML. ##### options? [`ParseScriptOptions`](#parsescriptoptions) Optional [ParseScriptOptions](#parsescriptoptions). #### Returns [`ScriptMessage`](#scriptmessage) The parsed [ScriptMessage](#scriptmessage). #### Throws On unrecoverable structural problems. #### Example ```ts const msg = parseScript("…"); msg.asNewRx()?.medication?.description; ``` *** ### parseTelecom() > **parseTelecom**(`raw`, `opts?`): [`TelecomTransaction`](#telecomtransaction) Parse a raw NCPDP Telecommunication-standard transmission into a frozen [TelecomTransaction](#telecomtransaction). Lenient by contract: anything recoverable becomes a warning and the underlying bytes are preserved. Only structurally unrecoverable input throws [NcpdpTelecomParseError](#ncpdptelecomparseerror) with a Telecom fatal code. **Every** group-separated transaction is decoded, request and response alike, each into its own entry on `transactions` with its own segments and warnings; a malformed one costs only itself. The declared Transaction Count is surfaced verbatim beside the number decoded and no maximum is enforced. #### Parameters ##### raw `string` \| `Buffer`\<`ArrayBufferLike`\> The raw message as a string or Buffer. ##### opts? [`TelecomParseOptions`](#telecomparseoptions) Optional [TelecomParseOptions](#telecomparseoptions). #### Returns [`TelecomTransaction`](#telecomtransaction) The decoded transmission. #### Throws NcpdpTelecomParseError on empty input, a missing fixed header, unframeable body bytes, or an untrusted version layout. #### Example ```ts const t = parseTelecom(rawClaim); t.header.transactionCode; // "B1" t.transactions.length; // every transaction the transmission carried t.transactionCount; // "3": what the header declared, verbatim t.decodedTransactionCount; // 3: what actually decoded t.transactions[2]?.segments; // the third transaction's segments ``` *** ### partitionWarnings() > **partitionWarnings**\<`W`\>(`warnings`, `profile`): [`NcpdpWarningPartition`](#ncpdpwarningpartition)\<`W`\> Split a parse's warnings against a profile's expected-warning union. A warning whose `code` is in the profile's `expectedWarnings` lands in `expected`; everything else lands in `unexpected`. Order within each bucket preserves the input order. #### Type Parameters ##### W `W` *extends* `object` #### Parameters ##### warnings readonly `W`[] ##### profile [`NcpdpProfile`](#ncpdpprofile) #### Returns [`NcpdpWarningPartition`](#ncpdpwarningpartition)\<`W`\> #### Example ```ts import { parseTelecom } from "@cosyte/ncpdp/telecom"; import { partitionWarnings, profiles } from "@cosyte/ncpdp/profiles"; const tx = parseTelecom(raw, { profile: profiles.pbm }); const { expected, unexpected } = partitionWarnings(tx.warnings, profiles.pbm); if (unexpected.length > 0) flagForReview(unexpected); ``` *** ### recognizeCodeSystem() > **recognizeCodeSystem**(`qualifier`): [`CodeSystem`](#codesystem) Recognize a normalized [CodeSystem](#codesystem) from a SCRIPT code qualifier. Matching is case-insensitive; unrecognized qualifiers yield `"UNKNOWN"` (a lenient default, never a throw). #### Parameters ##### qualifier `string` The qualifier string accompanying a coded value. #### Returns [`CodeSystem`](#codesystem) The normalized code system. #### Example ```ts recognizeCodeSystem("ND"); // "NDC" recognizeCodeSystem("RxCUI"); // "RXNORM" recognizeCodeSystem("zzz"); // "UNKNOWN" ``` *** ### rxChangeRequest() > **rxChangeRequest**(`message`): [`RxChangeRequest`](#rxchangerequest) \| `undefined` Convenience accessor: the [RxChangeRequest](#rxchangerequest) body, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`RxChangeRequest`](#rxchangerequest) \| `undefined` The change-request body, or `undefined`. #### Example ```ts rxChangeRequest(parseScript(xml))?.medicationPrescribed?.description; ``` *** ### rxChangeResponse() > **rxChangeResponse**(`message`): [`RxChangeResponse`](#rxchangeresponse) \| `undefined` Convenience accessor: the [RxChangeResponse](#rxchangeresponse) body, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`RxChangeResponse`](#rxchangeresponse) \| `undefined` The change-response body, or `undefined`. #### Example ```ts rxChangeResponse(parseScript(xml))?.outcome; // "approved" | "denied" | "validated" | … ``` *** ### rxRenewalRequest() > **rxRenewalRequest**(`message`): [`RxRenewalRequest`](#rxrenewalrequest) \| `undefined` Convenience accessor: the [RxRenewalRequest](#rxrenewalrequest) body, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`RxRenewalRequest`](#rxrenewalrequest) \| `undefined` The renewal-request body, or `undefined`. #### Example ```ts rxRenewalRequest(parseScript(xml))?.medicationPrescribed?.description; ``` *** ### rxRenewalResponse() > **rxRenewalResponse**(`message`): [`RxRenewalResponse`](#rxrenewalresponse) \| `undefined` Convenience accessor: the [RxRenewalResponse](#rxrenewalresponse) body, or `undefined`. Read `.outcome` for the prescriber's decision: a denial never reads as an approval. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`RxRenewalResponse`](#rxrenewalresponse) \| `undefined` The renewal-response body, or `undefined`. #### Example ```ts rxRenewalResponse(parseScript(xml))?.outcome; // "approved" | "denied" | … ``` *** ### scriptPosition() > **scriptPosition**(`path`): [`ScriptPosition`](#scriptposition) Build a [ScriptPosition](#scriptposition) from an XPath-style location string. #### Parameters ##### path `string` XPath-style location, e.g. `/Message/Header/To`. #### Returns [`ScriptPosition`](#scriptposition) A frozen positional context. #### Example ```ts const pos = scriptPosition("/Message/Body/NewRx/Patient"); pos.path; // "/Message/Body/NewRx/Patient" ``` *** ### scriptWarning() > **scriptWarning**(`code`, `position`): [`NcpdpScriptWarning`](#ncpdpscriptwarning) Construct a frozen [NcpdpScriptWarning](#ncpdpscriptwarning) from a code and a position. There is deliberately **no value parameter**. That absence is the whole safety property: a factory that accepts a value grows interpolation sites, and every parser in this family that leaked patient data into a log line did so through one. #### Parameters ##### code [`ScriptWarningCode`](#scriptwarningcode) The stable warning code, which selects the message. ##### position [`ScriptPosition`](#scriptposition) XPath-style location of the condition. #### Returns [`NcpdpScriptWarning`](#ncpdpscriptwarning) A frozen warning. #### Example ```ts scriptWarning(SCRIPT_WARNING_CODES.VERSION_ABSENT, scriptPosition("/Message")); ``` *** ### serializeScript() > **serializeScript**(`message`): `string` Serialize a parsed [ScriptMessage](#scriptmessage) back to canonical NCPDP SCRIPT XML. The conservative (emit) half of Postel's Law: it walks the model faithfully and never warns. Only the **modeled** fields are emitted: SCRIPT is a lossy structural read, so a value the parser does not surface cannot be reproduced; this is the honest round-trip contract. The output is **canonical**, not byte-identical to the input: namespace prefixes and wrapper elements the parser flattens (e.g. ``) are dropped, and indentation is normalized. Serializing is idempotent: `serialize(parse(serialize(m)))` equals `serialize(m)`. Values are XML-escaped; because the XXE-safe loader does not resolve entities, a value carrying a raw `<`, `>`, or `&` survives only when it was entity-free to begin with (the synthetic corpus is). A message whose body is a transaction this library does not model (`message.body.kind === "unsupported"`) is **refused**, not emitted: nothing under such a transaction is modeled, so any document written for it would be a well-formed one with the transaction body deleted. Branch on `message.body.kind` before serializing, and relay the original bytes for a transaction this library does not model. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed message from "./parse".parseScript. #### Returns `string` The canonical SCRIPT XML string. #### Throws NcpdpScriptBuildError with "../common/errors".SCRIPT\_BUILD\_CODES.UNSUPPORTED\_TRANSACTION when the body is a transaction this library does not model. No string is returned. #### Example ```ts import { parseScript, serializeScript } from "@cosyte/ncpdp/script"; const xml = serializeScript(parseScript(raw)); parseScript(xml); // re-parses cleanly ``` *** ### serializeTelecom() > **serializeTelecom**(`transaction`): `string` Serialize a [TelecomTransaction](#telecomtransaction) back to its canonical NCPDP Telecommunication vD.0 wire form. The conservative (emit) half of Postel's Law: it walks the model faithfully and never warns: a model produced by "./parse".parseTelecom or "./builder".buildTelecomRequest is trusted as valid by construction. The output is **canonical**, not byte-identical to a quirky input: header fields are re-padded to their fixed widths and segments are re-joined with single FS/GS/RS control characters. Serializing is idempotent: `serialize(parse(serialize(t)))` equals `serialize(t)`, which is the round-trip contract this library guarantees (a normalizing serializer cannot reproduce arbitrary whitespace or duplicate separators). A request emits the 56-byte fixed header immediately followed by the framed body; a response emits the fixed response header, a Group Separator, then the RS-framed segment body. **Emit is one transaction per transmission, and says so.** The reader decodes every group-separated transaction a message carries, but multi-transaction emit is not built: rather than write the first transaction and silently drop the rest, a model carrying more than one decoded transaction is **refused** with `NCPDP_TELECOM_BUILD_MULTI_TRANSACTION_EMIT`. Serialize one transaction at a time (each `transactions[n]` carries its own segments) if you need wire output for a later transaction. #### Parameters ##### transaction [`TelecomTransaction`](#telecomtransaction) A transmission from `parseTelecom` or `buildTelecomRequest`. #### Returns `string` The canonical wire string. #### Throws NcpdpTelecomBuildError when the model carries more than one decoded transaction, which this serializer cannot express without dropping data. #### Example ```ts import { parseTelecom, serializeTelecom } from "@cosyte/ncpdp/telecom"; const wire = serializeTelecom(parseTelecom(raw)); parseTelecom(wire); // re-parses cleanly ``` *** ### setDefaultProfile() > **setDefaultProfile**(`profile`): `void` Register a process-scoped default profile. `parseScript(raw)` / `parseTelecom(raw)` (no explicit profile arg) consult `getDefaultProfile()` and attach the returned profile to the result. Pass `null` (or `undefined`) to clear. Explicit args ALWAYS win: `parseTelecom(raw, { profile: myProfile })` uses `myProfile` regardless of the default; `parseTelecom(raw, { profile: null })` opts out of the default for a single call without changing the registered default. **Test hygiene:** the only mutable module-scoped state in the library. Tests that call this MUST clean up in `afterEach` (`setDefaultProfile(null)`). #### Parameters ##### profile [`NcpdpProfile`](#ncpdpprofile) \| `null` #### Returns `void` #### Example ```ts import { setDefaultProfile, getDefaultProfile, profiles } from "@cosyte/ncpdp/profiles"; import { parseTelecom } from "@cosyte/ncpdp/telecom"; setDefaultProfile(profiles.pbm); const tx = parseTelecom(raw); tx.profile?.name; // "pbm" setDefaultProfile(null); // clear (or in test teardown) ``` *** ### splitWithOffsets() > **splitWithOffsets**(`s`, `sep`, `base`): `Part`[] Split a string on a single-character separator, carrying each piece's absolute byte offset. Empty pieces are retained (the caller decides whether to drop the leading empty that a leading separator produces). #### Parameters ##### s `string` The string to split. ##### sep `string` The single-character separator. ##### base `number` The absolute offset of `s[0]` in the original message. #### Returns `Part`[] The pieces with their absolute offsets. #### Example ```ts splitWithOffsets("\x1cD7123", "\x1c", 56); // [{ text: "", offset: 56 }, { text: "D7123", offset: 57 }] ``` *** ### status() > **status**(`message`): [`StatusBody`](#statusbody) \| `undefined` Convenience accessor: the `` (positive-acknowledgment) body of a message, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`StatusBody`](#statusbody) \| `undefined` The Status body, or `undefined` when the message is not a Status. #### Example ```ts status(parseScript(xml))?.code; ``` *** ### telecomPosition() > **telecomPosition**(`byteOffset`, `fieldId?`): [`TelecomPosition`](#telecomposition) Build a [TelecomPosition](#telecomposition) from a byte offset and an optional field id. #### Parameters ##### byteOffset `number` Zero-based byte offset into the raw message. ##### fieldId? `string` The 2-character field identifier in scope, if any. #### Returns [`TelecomPosition`](#telecomposition) A frozen positional context. #### Example ```ts telecomPosition(56).byteOffset; // 56 telecomPosition(72, "D7").fieldId; // "D7" ``` *** ### telecomQuantity() > **telecomQuantity**(`source`): [`TelecomQuantity`](#telecomquantity) Wrap a verbatim Quantity Dispensed value as a [TelecomQuantity](#telecomquantity), applying the implied 3-place decimal string-wise when the value is all digits. #### Parameters ##### source `string` The quantity exactly as it appeared on the wire. #### Returns [`TelecomQuantity`](#telecomquantity) A frozen [TelecomQuantity](#telecomquantity). #### Example ```ts telecomQuantity("30000").impliedDecimal; // "30.000" ``` *** ### telecomWarning() > **telecomWarning**(`code`, `position`): [`NcpdpTelecomWarning`](#ncpdptelecomwarning) Construct a frozen [NcpdpTelecomWarning](#ncpdptelecomwarning) from a code and a position. There is deliberately **no value parameter**. That absence is the whole safety property: a factory that accepts a value grows interpolation sites, and every parser in this family that leaked patient data into a log line did so through one. #### Parameters ##### code [`TelecomWarningCode`](#telecomwarningcode) The stable warning code, which selects the message. ##### position [`TelecomPosition`](#telecomposition) Byte-offset location of the condition. #### Returns [`NcpdpTelecomWarning`](#ncpdptelecomwarning) A frozen warning. #### Example ```ts telecomWarning(TELECOM_WARNING_CODES.UNKNOWN_SEGMENT, telecomPosition(56, "AM")); ``` *** ### toDate() > **toDate**(`value`, `options?`): `Date` \| `undefined` Turn a parsed date into an absolute instant, ONLY when the caller supplies the zone. No wire form this package decodes carries a UTC offset, so the zone is never determinate on its own: with no `assumeOffsetMinutes` the answer is `undefined`. The host machine's timezone is never read and UTC is never assumed. That is the point of the refusal rather than a limitation of it: a date of birth resolved to a guessed zone is a day out in every negative-offset zone, and nothing throws to say so. The value's own precision is unchanged by the call. The time of day is filled to midnight for instant construction only, and a later [toObject](#toobject) or [toISO](#toiso) on the same value answers exactly what it answered before. Returns `undefined` for `undefined`, for `null`, for a value stating no usable components, for a value whose components do not name a real calendar day, for a non-finite offset, and for an offset that pushes the instant outside the range a `Date` can represent. Never throws, for any input, and never rolls an impossible day over into the following month. Never an `Invalid Date` either: a partial answer wearing the shape of an instant is the one shape a caller cannot tell from a real one. #### Parameters ##### value [`DateValue`](#datevalue) \| `null` \| `undefined` A parsed date from "./date".dateValue. ##### options? [`ToDateOptions`](#todateoptions) The zone to assume; see [ToDateOptions](#todateoptions). #### Returns `Date` \| `undefined` The instant, or `undefined` when no determinate zone was supplied. #### Example ```ts toDate(dateValue("19880705")); // undefined: no zone toDate(dateValue("19880705"), { assumeOffsetMinutes: 0 }); // 1988-07-05T00:00:00.000Z ``` *** ### toISO() > **toISO**(`value`): `string` \| `undefined` Render a parsed date as ISO-8601, truncated to the precision it stated. The one decoded wire form is a whole calendar day, so the rendering is always `YYYY-MM-DD` and NOTHING is appended: the value carries no UTC offset, so no `Z` is fabricated and the string is deliberately zone-less. A year below 1000 is zero-padded to four digits and stays the year it is. Returns `undefined` for `undefined`, for `null`, for a value stating no usable components at all, and for a value whose components do not name a real calendar day: the string this returns always names a day that exists, so it reads back as itself rather than being silently moved. Never throws, for any input. #### Parameters ##### value [`DateValue`](#datevalue) \| `null` \| `undefined` A parsed date from "./date".dateValue. #### Returns `string` \| `undefined` The ISO-8601 rendering, or `undefined`. #### Example ```ts toISO(dateValue("19880705")); // "1988-07-05" toISO(dateValue("00500101")); // "0050-01-01" // Never "2024-02-30", which every ISO-8601 reader reads back as 1 March: toISO({ source: "20240230", year: 2024, month: 2, day: 30 }); // undefined ``` *** ### tokenizeBody() > **tokenizeBody**(`body`, `base`, `warnings`): [`TelecomSegment`](#telecomsegment)[] Tokenize the variable body of a Telecom transmission into the segments of its **first** group-separated transaction, pushing that transaction's warnings into the supplied sink. This is the single-transaction view of [tokenizeTransactions](#tokenizetransactions), kept for callers that hold one transaction's worth of body. A transmission may carry more than one transaction, and this function reads only the first, so `tokenizeTransactions` (or `parseTelecom`, which uses it) is the route that reads them all. It says **nothing** about the transactions it did not read: the sink receives the first transaction's warnings and no others, and no warning is raised to report the ones past it. Hand a multi-transaction body to this function and the answer is a partial one that looks whole, so reach for `tokenizeTransactions` whenever the body might carry more than one. #### Parameters ##### body `string` The raw message body (the portion after the fixed header). ##### base `number` The absolute offset of `body[0]` in the raw message. ##### warnings [`NcpdpTelecomWarning`](#ncpdptelecomwarning)[] Sink that collects non-fatal warnings. #### Returns [`TelecomSegment`](#telecomsegment)[] The decoded segments of the first transaction, in wire order. #### Example ```ts const warnings: NcpdpTelecomWarning[] = []; const segs = tokenizeBody("\x1cAM07\x1cD2RX1", 56, warnings); segs[0]?.segmentId; // "07" ``` *** ### tokenizeTransactions() > **tokenizeTransactions**(`body`, `base`): [`TelecomDecodedTransaction`](#telecomdecodedtransaction)[] Tokenize the variable body of a Telecom transmission (everything after the fixed header) into **every** group-separated transaction it carries, in wire order. Within a transaction, segments are segment-separator delimited and fields are field-separator delimited; the first field of each segment is the Segment Identification (`AM`). Each transaction is decoded independently, into its own segment list and its own warning sink, so a malformed transaction cannot cost the transmission the transactions that decoded around it. No maximum is enforced: this reader has no citable authority for one, so it reports what it decoded and never refuses a count (see "./warnings".TELECOM\_WARNING\_CODES.TRANSACTION\_COUNT\_MISMATCH). #### Parameters ##### body `string` The raw message body (the portion after the fixed header). ##### base `number` The absolute offset of `body[0]` in the raw message. #### Returns [`TelecomDecodedTransaction`](#telecomdecodedtransaction)[] One [TelecomDecodedTransaction](#telecomdecodedtransaction) per transaction, in wire order. #### Example ```ts import { tokenizeTransactions } from "@cosyte/ncpdp/telecom"; const decoded = tokenizeTransactions("AM07\x1cD2RX1\x1dAM04", 56); decoded.length; // 2: both transactions decoded decoded[1]?.segments[0]?.segmentId; // "04" ``` *** ### toObject() > **toObject**(`value`): [`DateParts`](#dateparts) \| `undefined` Read the calendar components a parsed date stated, as a frozen plain object. Returns `undefined` for `undefined`, for `null`, for a value stating no usable components at all, and for a value whose components do not name a real calendar day (February 30, month 13, day 0). Never throws, for any input. #### Parameters ##### value [`DateValue`](#datevalue) \| `null` \| `undefined` A parsed date from "./date".dateValue. #### Returns [`DateParts`](#dateparts) \| `undefined` The stated components, or `undefined`. #### Example ```ts toObject(dateValue("19880705")); // { year: 1988, month: 7, day: 5 } toObject(undefined); // undefined // A hand-built value is bound by the same calendar rule as a wire string: toObject({ source: "20240230", year: 2024, month: 2, day: 30 }); // undefined ``` *** ### undecodedHeader() > **undecodedHeader**(`versionStamp`): [`TelecomHeader`](#telecomheader) A minimal header for a recognized-but-undecoded version (F6): the version stamp is surfaced and every positional field is left empty, since the layout differs from D.0 and decoding it here would misalign safety-critical fields. #### Parameters ##### versionStamp `string` The recognized version stamp, e.g. `"F6"`. #### Returns [`TelecomHeader`](#telecomheader) A frozen [TelecomHeader](#telecomheader) with only `versionRelease` populated. #### Example ```ts undecodedHeader("F6").versionRelease; // "F6" ``` *** ### verify() > **verify**(`message`): [`VerifyBody`](#verifybody) \| `undefined` Convenience accessor: the `` (verification-acknowledgment) body of a message, or `undefined`. #### Parameters ##### message [`ScriptMessage`](#scriptmessage) A parsed [ScriptMessage](#scriptmessage). #### Returns [`VerifyBody`](#verifybody) \| `undefined` The Verify body, or `undefined` when the message is not a Verify. #### Example ```ts verify(parseScript(xml))?.code; ``` --- URL: https://docs.cosyte.com/ncpdp/troubleshooting Markdown: https://docs.cosyte.com/ncpdp/troubleshooting.md # Troubleshooting & known limitations The honest list. A parser that oversells what it reads is how a dose, a code system, or a claim disposition gets mis-read, so this page is a deliverable, not a footnote. It covers the error model, the fail-safe rules, and (just as importantly) what v1 deliberately does **not** do. ## The error model: fatal vs. warning The parser follows Postel's Law. **Only unrecoverable structural corruption throws**; every recoverable vendor quirk is a stable-coded `warning` with positional context (an XPath for SCRIPT, a byte offset for Telecom), collected on the result's `.warnings` and never thrown. **SCRIPT fatals** (`NcpdpScriptParseError.code`): | Code | Symptom | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `EMPTY_INPUT` | The input was empty or whitespace-only. | | `NCPDP_SCRIPT_NOT_XML` | The input did not parse as XML, or carried a ``/`` (the XXE boundary, rejected by design). | | `NCPDP_SCRIPT_NO_MESSAGE_ROOT` | Well-formed XML, but the root element is not ``. | | `NCPDP_SCRIPT_UNSUPPORTED_VERSION` | A declared version that predates the XML-era SCRIPT this package decodes (see the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md)). | **Telecom fatals** (`NcpdpTelecomParseError.code`): | Code | Symptom | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `EMPTY_INPUT` | The input was empty. | | `NCPDP_TELECOM_NO_HEADER` | The transmission is too short to hold the fixed Transaction Header. | | `NCPDP_TELECOM_INVALID_FRAMING` | A non-empty body carried no FS/GS/RS framing bytes. A separator is never guessed. | | `NCPDP_TELECOM_UNSUPPORTED_VERSION` | A version stamp this package does not decode at the offset it read (see the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md), which states the outcome per direction). | Everything else (an absent SCRIPT version, an unknown segment, a malformed field, an unrecognized reject code) is a warning. Catch the two fatal classes at the parse boundary; read `.warnings` afterward for the tolerated deviations. **One throw comes from the emit side, not the parse side.** Serializing a SCRIPT message whose `body.kind` is `"unsupported"` (a transaction this library does not model) raises `NcpdpScriptBuildError` with `NCPDP_SCRIPT_BUILD_UNSUPPORTED_TRANSACTION` and returns no string, because nothing under such a transaction is modeled and the only document it could write is one with the transaction body deleted. It applies to `serializeScript(message)` and to `ScriptMessage#toString()` alike, so it can surface from an implicit coercion (a template literal, a log line). Reading such a message is unaffected. Branch on `body.kind === "unsupported"`, which is public, typed and never throws, and relay the original bytes instead. ```ts runnable throws import { parseTelecom } from "@cosyte/ncpdp/telecom"; const pad = (v: string, n: number) => v.padEnd(n).slice(0, n); // A valid fixed D.0 header (vD.0, B1) followed by a body carrying no FS/GS/RS framing bytes. const header = pad("999999", 6) + pad("D0", 2) + pad("B1", 2) + pad("PCN0000000", 10) + pad("1", 1) + pad("01", 2) + pad("1234567890", 15) + pad("20260629", 8) + pad("SW00000000", 10); parseTelecom(header + "PLAINBODYNOFRAMINGBYTES"); // throws NcpdpTelecomParseError (NCPDP_TELECOM_INVALID_FRAMING): a separator is never guessed ``` ## Every warning this parser raises A fatal is rare. A warning is what you will actually see, and each one is a **stable code**: renaming one is a breaking change, so you can branch on them. The tables below are the complete set, and each entry says what the reader **did** when it raised the code, because that is the part a consumer has to act on. Nothing here is dropped, guessed or reinterpreted: a warning always means the bytes were kept and something about them was worth telling you. **SCRIPT warnings** (`msg.warnings[].code`, each with an XPath `position`): | Code | What the reader did | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `NCPDP_SCRIPT_VERSION_ABSENT` | Found no version declared on the message and parsed it best-effort rather than refusing it. | | `NCPDP_SCRIPT_UNSUPPORTED_VERSION_TOLERATED` | Read a version string it does not model, parsed the document against the same XML-era field model, and surfaced the string verbatim. | | `NCPDP_SCRIPT_UNSUPPORTED_TRANSACTION` | Met a transaction body it does not model and surfaced it as `unsupported`, naming the transaction only when its element name is one it recognizes. | | `NCPDP_SCRIPT_MISSING_REQUIRED_ELEMENT` | Left a required element of the detected transaction undefined rather than inventing a value to fill it. | | `NCPDP_SCRIPT_STRENGTH_CODED_AND_EXPLICIT` | Found a coded drug and an explicit Strength together, surfaced both verbatim, and reconciled neither into a single answer. | | `NCPDP_SCRIPT_RESPONSE_AMBIGUOUS_DISPOSITION` | Found more than one response body and reported the most conservative disposition, so a failure is never masked by a co-present success. | | `NCPDP_SCRIPT_LIFECYCLE_AMBIGUOUS_OUTCOME` | Found more than one outcome on a lifecycle response and reported by fail-safe precedence, so a denial is never masked by a co-present approval. | | `NCPDP_SCRIPT_LIFECYCLE_OUTCOME_UNRECOGNIZED` | Found no recognized outcome on a lifecycle response and surfaced it as unknown rather than assuming an approval. | | `NCPDP_SCRIPT_SIG_STRUCTURED_LOSSY` | Decoded a structured `` into an additive, lossy view while the free-text `SigText` stayed authoritative and verbatim. | | `NCPDP_SCRIPT_SIG_AMBIGUOUS_DOSE` | Read a dose structure it could not resolve to one unambiguous quantity and surfaced the dose as absent rather than guessing a number. | **Telecom warnings** (`tx.warnings[].code`, each with a byte-offset `position`): | Code | What the reader did | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NCPDP_TELECOM_VF6_NOT_DECODED` | Recognized the F6 stamp on a **request**, surfaced it on `header.versionRelease`, and lifted no positional field rather than read one at offsets it cannot trust. | | `NCPDP_TELECOM_UNKNOWN_SEGMENT` | Met a Segment Identification code it does not name, kept the segment and every field verbatim, and left only the paraphrased name absent. | | `NCPDP_TELECOM_MALFORMED_FIELD` | Met a field token too short to carry a 2-character identifier and kept it verbatim with an empty id rather than dropping the bytes. | | `NCPDP_TELECOM_MISSING_SEGMENT_ID` | Met a segment that did not begin with an `AM` field, kept its fields, and left the segment id empty because it could not be read where it belongs. | | `NCPDP_TELECOM_MALFORMED_SEGMENT_ID` | Met an `AM` value that is not 2 characters, kept it in `segment.fields` verbatim, and refused to promote unbounded wire bytes onto `segment.segmentId`. | | `NCPDP_TELECOM_TRANSACTION_COUNT_MISMATCH` | Reported that the declared Transaction Count and the number decoded disagree, kept every decoded transaction, and enforced no maximum. | | `NCPDP_TELECOM_STATUS_CONFLICT` | Found reject codes beside a status claiming payment and resolved the disposition to rejected, because a reject always wins. | | `NCPDP_TELECOM_UNKNOWN_REJECT_CODE` | Kept a Reject Code it does not recognize verbatim, in wire order, with `known: false`, and dropped nothing. | | `NCPDP_TELECOM_UNKNOWN_RESPONSE_STATUS` | Kept a Transaction Response Status it does not model verbatim and read the disposition as unknown, never as paid. | | `NCPDP_TELECOM_COMPOUND_COUNT_MISMATCH` | Reported that the declared ingredient count and the number decoded disagree, and kept every decoded ingredient. | | `NCPDP_TELECOM_COB_COUNT_MISMATCH` | Reported that the declared other-payer count and the number of blocks decoded disagree, and kept every decoded block. | | `NCPDP_TELECOM_UNKNOWN_DUR_REASON` | Kept a Reason For Service code it does not recognize verbatim and never dropped the interaction it belonged to. | The **emit** side has its own refusal codes, one per rule it will not break. They are listed beside the rule each one enforces under [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md), and are not repeated here. ## The fail-safe rules (safety-critical) These are invariants, not best-effort behaviors. They exist because reading a failure as a success can harm someone: - **A reject always wins.** A Telecom response `disposition` is a total function over the Transaction Response Status **and** the reject codes together. If any reject is present the disposition is `"rejected"` even when the status field claims paid; the self-contradiction surfaces as `NCPDP_TELECOM_STATUS_CONFLICT` and `status.statusConflict`. An unrecognized status reads `"unknown"`, never `"paid"`. - **An `Error` never reads as success.** A SCRIPT response `disposition` is derived only from the response body kind, so `status(msg)` is `undefined` on an ``. A message carrying more than one response body resolves to the most conservative disposition (Error first) and raises `NCPDP_SCRIPT_RESPONSE_AMBIGUOUS_DISPOSITION`. - **Money is never a float.** Every dollar amount carries an implied 2-place decimal (and an optional zoned-decimal overpunch sign), interpreted **string-wise** with the verbatim `source` kept. Anything unexpected is preserved with `isValid: false` and no interpreted amount. Money is never guessed. - **Quantities are never floats.** Quantity Dispensed applies its implied 3-place decimal string-wise; the verbatim `source` is always kept. - **The structured SIG never overwrites the free text.** `sig.sigText` is authoritative and preserved verbatim; the structured decode is additive, provenance-tagged, and flagged lossy (`NCPDP_SCRIPT_SIG_STRUCTURED_LOSSY`). An ambiguous dose is surfaced as `absent` with `NCPDP_SCRIPT_SIG_AMBIGUOUS_DOSE`, never guessed. ## Warnings and errors are safe to log; the parsed model is not Every warning is a stable `code`, a `message`, and a `position`. The message is not written at the place the deviation is detected: it is looked up in a frozen registry (`SCRIPT_WARNING_MESSAGES` / `TELECOM_WARNING_MESSAGES`) by code, and the factories (`scriptWarning`, `telecomWarning`) take a position and nothing else. There is no value parameter, so there is nowhere for a document to leak into. The same is true of the typed fatals and the builder errors, and none of them carries a snippet of the input. `w.message === WARNING_MESSAGES[w.code]` is asserted by the test suite, for every code, on both standards. That is a claim about diagnostics only. `tx.segments[].fields[].value`, a drug description, a cardholder ID and an Rx number are on the **model**, verbatim, because reading them is the point of the library. Redact them the way you would redact the message they came from. If you pass this parser's output to another package that builds its own diagnostics, the fields it will reach for are the structural ones, and those are bounded here: `segment.segmentId` is two characters or empty, `field.id` is two characters, and an unmodeled SCRIPT transaction is named only when its element name is one of `SCRIPT_TRANSACTION_NAMES`. ## Known limitations & non-goals (v1) Depth here tracks the parser; where it is thin, it is thin on purpose. - **Whole-message only: no streaming.** Both parsers read a complete message; there is no incremental / streaming API. - **Telecom: which version is decoded is stated once, and the answer has a direction.** An `F6` stamp on a **request** is _recognized but not decoded_ (`NCPDP_TELECOM_VF6_NOT_DECODED`); the bytes are preserved but no positional field is lifted. The same stamp on a **response** is refused (`NCPDP_TELECOM_UNSUPPORTED_VERSION`), because a response leads with its Version/Release and that is not where this reader looks for the stamp. Which version is decoded, the outcome per direction, and the date that adoption ends are in the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md). - **Telecom emit is one transaction per transmission.** Each decoded transaction is at `t.transactions[n]`, with the views taking that index, but `serializeTelecom` writes one, so a model carrying more than one decoded transaction is refused with `NCPDP_TELECOM_BUILD_MULTI_TRANSACTION_EMIT` rather than emitted with the rest dropped. A declared Transaction Count that disagrees with the number decoded raises `NCPDP_TELECOM_TRANSACTION_COUNT_MISMATCH`; no maximum count is enforced. - **SCRIPT decodes the XML-era standard**; pre-XML legacy SCRIPT is a fatal, not a tolerated read. Which XML-era versions, and until when, is in the [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md). - **SIG is decode-only.** v1 reads a structured `` best-effort; it does **not** _generate_ a SIG from structure, and does not parse arbitrary natural-language directions. - **No bundled NCPDP code→meaning table.** Codes and descriptions (``, reject codes, status values) are surfaced **verbatim**; the library ships no lookup of NCPDP-copyrighted descriptions. Recognized _code systems_ (NDC / RxNorm / SNOMED via the wire qualifier) are the exception. Those are widely-known identifiers, not copyrighted prose. - **Profiles are descriptive, not transformative.** Attaching a trading-partner profile surfaces `msg.profile` / `tx.profile` and powers `partitionWarnings`, but it **never alters the parse**: profile-on output is byte-identical to profile-off. - **No `strict` mode yet.** A mode that escalates every tolerated deviation to a thrown error is not shipped; today the model is lenient-with-warnings only. - **EPCS is out of scope.** Electronic Prescribing of Controlled Substances (DEA-regulated digital signatures, HSM integration) belongs in a separate package and is not in v1. - **Not differentially verified against a reference implementation.** NCPDP redistribution limits exclude differential testing against a licensed reference parser; conformance is proven against synthetic and de-identified fixtures and the spec structure, not an oracle. Validate against your actual trading partner before trusting a production interface. ## What the version number promises `@cosyte/ncpdp` is at `0.1.0`, and its public API is settled. While the version is below `1.0.0`, a breaking change ships in a new minor version, never a patch, and the changelog says what broke and what to do instead. The stable **warning codes** and **fatal codes** are public API: renaming one is a breaking change. A caret range such as `^0.1.0` takes fixes and nothing that breaks. Two surfaces are still moving, both additively: the structured SIG view and the wire-code label tables. --- ## The one thing this package exists to prevent **A safety-critical value being read wrong and reported as right**: a rejected claim shown as paid, a dose invented from ambiguous structure, a dollar amount corrupted by floating point. Every fail-safe rule above is a wall around that single failure mode. The rest of the package is the honest parse around it. ## Next - [Conformance statement](https://docs.cosyte.com/ncpdp/conformance.md): what is decoded on each wire format, the section adopting it, and the date that adoption ends. - [Serializers and builders](https://docs.cosyte.com/ncpdp/spec-notes-serialize-build.md): the emit-side refusals named above, each beside the rule it enforces. - [Trading-partner profiles](https://docs.cosyte.com/ncpdp/spec-notes-profiles.md): sorting the warnings above into the ones a partner's conventions predict and the ones worth alerting on. - [Compound, COB and DUR depth](https://docs.cosyte.com/ncpdp/spec-notes-telecom-compound-cob.md): the count-mismatch warnings above, in the readers that raise them. - [Guides](https://docs.cosyte.com/ncpdp/cookbook.md): the same rules applied recipe by recipe. --- # @cosyte/astm > ASTM E1394 / E1381 parser for clinical laboratory analyzer (LIS) interfaces. These are the 11 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/astm/intro Markdown: https://docs.cosyte.com/astm/intro.md # @cosyte/astm Parse real-world, vendor-quirky ASTM and pull fields out in one line, without reading the spec. `@cosyte/astm` is a zero-dependency TypeScript toolkit following the Cosyte parser archetype: a lenient parser, an immutable model, a spec-clean serializer, and a profile system for vendor quirks. It mirrors the API shape of the reference parser, `@cosyte/hl7`. > **Status:** published on npm and public, on a pre-release ladder: the public API is **not yet > stable**. Both layers are **feature-complete**, but exported names, options and warning codes can > still change from one release to the next, so pin the version you install rather than tracking the > latest. Read the version you have with `npm view @cosyte/astm version`. > The **record** layer reads `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` with delimiter self-declaration and > escape decode, safety-critical result semantics, patient/order identity depth, the `C` comment > record, and host-query classification. The E1381/CLSI-LIS01 **framing** layer decodes framed byte > streams with a pure LTP transport reducer, the **spec-clean serializers + builders** round-trip both > layers by construction, the vendor **profile** system tolerates documented quirks without ever > touching a value, and **LIVD**-aware LOINC recognition maps local codes from a bring-your-own > catalog. Read [What this library does, and does not do](https://docs.cosyte.com/astm/limitations.md) before you rely on it: the > promise is narrow and honest. ## Install ```bash npm install @cosyte/astm ``` ## Parse a message ```ts import { parseAstmRecords, results } from "@cosyte/astm"; const msg = parseAstmRecords(raw); results(msg)[0]?.value; // the measured value, surfaced raw msg.warnings; // stable, positional tolerance warnings ``` The parser is **lenient by default**. Vendor quirks become warnings, not failures (Postel's Law), and it refuses to produce a confident wrong value. A `{ strict: true }` mode escalates every tolerated deviation to a thrown error. ## Host-query vs result upload On many analyzers the host-query mode is first-class (on the Roche cobas 4800 it is mandatory): the instrument sends an `H/P/Q/L` **request** and the LIS answers with orders. Misreading a query as a result upload breaks the order flow, so a `Q`-bearing message is classified explicitly, and **never** read as a result set. ```ts import { parseAstmRecords } from "@cosyte/astm"; const request = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r"); request.classification.kind; // => "host-query" request.classification.isHostQueryRequest; // => true ``` `M` (manufacturer) and `S` (scientific) records, vendor-defined QC / calibration / maintenance data, are surfaced **verbatim** and never interpreted into clinical fields; their exact wire bytes are on `record.rawLine`. ## Next - [What it does with patient data](https://docs.cosyte.com/astm/patient-data.md): what this library logs, retains and writes, and what stays your responsibility. Read it before you point lab traffic at it. - [Vendor profiles](https://docs.cosyte.com/astm/guides-vendor-profiles.md): tolerating a documented quirk without turning the parser's judgement off. - Read the **API reference** for every export, generated from source. --- URL: https://docs.cosyte.com/astm/installation Markdown: https://docs.cosyte.com/astm/installation.md # Installation `@cosyte/astm` is a zero-dependency TypeScript package for Node.js. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration. > **Status:** published on npm, on a pre-release ladder: the public API is **not yet stable**. The > exported names, options and warning codes can still change from one release to the next, so pin > the version you install rather than tracking the latest. ## Prerequisites - **Node.js >= 22** (the whole `@cosyte/*` suite targets ES2023 / Node 22+). - A package manager: `pnpm`, `npm`, or `yarn`. ## Install ```bash npm install @cosyte/astm ``` ## Smoke test Confirm the package resolves and its version symbol is present: ```ts import { VERSION } from "@cosyte/astm"; console.log(VERSION); ``` If that prints a version string, the install is good: head to the [Quickstart](https://docs.cosyte.com/astm/quickstart.md). --- URL: https://docs.cosyte.com/astm/quickstart Markdown: https://docs.cosyte.com/astm/quickstart.md # Quickstart Parse an ASTM/CLSI-LIS02 record stream and read a result in a few lines. `@cosyte/astm` is **lenient by default** (Postel's Law): real-world, vendor-quirky input parses into an immutable message plus a list of tolerance **warnings**, rather than throwing, and it never hands you a confident wrong value. ## Parse a result upload ```ts runnable import { parseAstmRecords, results } from "@cosyte/astm"; // A de-framed ASTM record stream: header (declares the delimiters) + patient + order + result + end. const raw = "H|\\^&\rP|1|PRAC|LAB\rO|1|ACC\rR|1|^^^687|28.6|U/L||N||F\rL|1|N\r"; const msg = parseAstmRecords(raw); const first = results(msg)[0]; first?.value; // => "28.6" ``` `parseAstmRecords` reads the four delimiters **from the header** (never hardcoded), decodes embedded escapes before splitting a value, and keeps the practice- and laboratory-assigned patient IDs distinct. Each warning carries a **stable code** you can branch on: ### More than one message in a stream A message runs from its `H` header to its `L` terminator, so a stream can carry several back to back, and each header declares the delimiters for the records that follow it. If a later header declares a **different** set, those records are read with the new set and you get an `ASTM_RECORD_DELIMITERS_REDECLARED` warning pointing at that header; records already read keep the set they were read with. A header that simply restates the delimiters already in use is normal and warns nothing. If a later header's declaration is unusable, the delimiters already in force are kept and you get `ASTM_RECORD_UNREADABLE_REDECLARATION`: no set is ever guessed and no record is dropped. A declaration that names **one character in two roles** is a third case. If the field separator is one of the other three, the declaration is unusable and the two rules above apply. If two of the remaining three agree, the set is read and honored, but the boundary between those two roles is gone from the bytes, and you get `ASTM_RECORD_DELIMITER_ROLE_COLLISION` at that header. Under `H|^^&`, for instance, a field a canonical sender would have written as two repeats of two components reads back as four repeats of one component each. That code is **not** tolerable: every such set is non-canonical, so before it existed the only warning was the tolerable `ASTM_NONSTANDARD_DELIMITERS` and a profile expecting an ordinary vendor set left a strict parse accepting it. Emit refuses the same sets (`ASTM_EMIT_INVALID_DELIMITERS`). Read each header's own set from `header.delimiters`; `msg.delimiters` is the first header's. ### Reading a stream that carries several messages Use `messages()`. It splits a parsed stream into the messages it actually contains, so a patient and a result are only ever paired inside the message that carried both: ```ts runnable import { parseAstmRecords, messages } from "@cosyte/astm"; const two = "H|\\^&\rP|1|PRAC-1\rR|1|^^^687|10.0|U/L||N||F\rL|1|N\r" + "H|\\^&\rP|1|PRAC-2\rR|1|^^^688|99.9|U/L||H||F\rL|1|N\r"; const pairs = messages(parseAstmRecords(two)).map( (m) => `${m.patient?.practiceAssignedId}:${m.results[0]?.value}`, ); pairs.join(" "); // => "PRAC-1:10.0 PRAC-2:99.9" ``` Each entry carries its own `header`, `delimiters`, `records`, `patient`, `patients`, `results`, `orders`, `comments`, and `queries`. `messages()` never throws, and on an ordinary single-message stream it yields exactly one entry. > **The flat accessors refuse a stream they cannot answer for.** `patient()`, `results()`, > `orders()`, `comments()`, and `query()` read the whole stream, so on a multi-message stream they > throw `AstmAmbiguousStreamError` (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) instead of answering across > patients. `patient()` also throws (`ASTM_AMBIGUOUS_MULTI_PATIENT`) when a single message carries > more than one `P`, because "the first `P`" is a guess about whose result it is. **That second one > reaches single-message callers**: a lone message carrying several patients used to answer with the > first of them, and now refuses. A stream that is one message with at most one patient is unchanged, > as is a result-only message with no `P`, which still answers `undefined`. `commentsFor()` works on > any stream: the parent record you hand it already names the message. > **Check for an `ASTM_RECORD_UNKNOWN_TYPE` warning before you trust a split.** Grouping reads each > record's type letter, so a header the reader does not recognize as a header, one carrying a stray > leading byte for instance, opens no message and the messages either side of it merge, so a patient > can end up holding results that arrived under a different header. The parser > reports that record as an unsupported record and warns, and a `{ strict: true }` parse refuses the > stream outright. That warning is the only report the merge produces, so a profile is **not** > allowed to tolerate it: naming it in `tolerate` throws from `defineAstmProfile()`, and a warning > carrying it is not downgraded whatever profile is in force. Match on the code rather than gating > on the warning count, because the records that merged in can raise warnings of their own. Reading > it as cosmetic noise is what puts a patient back next to somebody else's result. ```ts import { parseAstmRecords, WARNING_CODES } from "@cosyte/astm"; const { warnings } = parseAstmRecords(raw); for (const w of warnings) { if (w.code === WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE) { // an unrecognized record, surfaced as an unsupported record and not dropped. // If it was a header, this stream has one fewer message than it looks like. } if (w.code === WARNING_CODES.ASTM_RECORD_FIELDS_UNSEPARATED) { // this record did not split: treat its modeled fields as lost, not as absent. } } ``` > **`ASTM_RECORD_FIELDS_UNSEPARATED` means a record lost its fields, not that it was formatted > oddly.** Delimiters are re-read at each header, so a header the reader did not recognize does not > re-scope them, and the records after it are read with the previous set. When that set's field > separator does not occur in a record at all, the whole line comes back as one field and none of its > modeled fields survive. On a result record that costs the value, the **units** and the **status** > together, and the result then reads as though it simply never carried them. The parser warns once > per affected record, surfaces the raw line intact, and **never** re-splits it on a set no header > declared, because that would invent data. A `{ strict: true }` parse refuses, and no profile may > tolerate the code. This does not need a mangled header to happen: a lone record written in another > set trips it too. > > **The absence of this warning is not a guarantee that a record split correctly**, and that is the > half worth carrying away. It tests one of the four delimiter roles, the **field** separator, and > only in its total form, where no unescaped separator occurs in the line. Two classes of the same > loss sit outside it. A foreign set whose **field** separator happens to appear somewhere in the > line still splits, on the wrong boundaries and silently: one stray `|` inside an otherwise > `*`-separated result loses the value, the units and the status with **no** warning, and this can > happen to one record **inside** a run of these warnings, so a run does not mean every record in it > was checked. And a set differing in the **repeat, component or escape** role usually splits into > fields normally, with the damage varying: a mis-split component can cost a test identity while the > value survives, and an `&X&` sequence whose body is an unrecognized character that is itself a > delimiter in force is an opaque atom, so that > delimiter does not split and the value, units and status can go together. A bare escape character > is no longer in that group (it reads as a literal and raises `ASTM_UNPAIRED_ESCAPE_CHARACTER`); > the atom case remains, and now raises `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, which is not > tolerable, alongside the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. Its mirror, where the leftmost > alignment lets a delimiter split that a competing alignment would have held, gains a boundary > instead of losing one and raises `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, also not tolerable. > Where that gained boundary is a **field** boundary and the reading taken resumes on an escape > character heading no sequence it can interpret, every later field shifts, so a result's > units and status > are read out of slots the other alignment does not put them in and a status can read `final` where > the sender wrote nothing in that slot: `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, not tolerable > either. Where it is a **repeat** boundary nothing shifts, and the field can still be read short, > because its modeled value and components come from its first repeat alone: a gained **first** > boundary truncates a value and costs a Universal Test ID or a patient name the components that sat > after it. That is `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`, not tolerable either. Where it is a > **component** boundary nothing leaves the record and no field number changes, and every component > after it moves along the component list, so a test identity's coding scheme and local code, and a patient's > given and middle names, are read out of positions the other alignment does not put them in. That is > `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS`, not tolerable either, and it completes the set: those > three are the three roles a split is taken on. **All three shift claims have one measured > exception**: where the sequence past the boundary carries the splitting delimiter itself as its > body, the two readings read the same number of segments in different places, so nothing moves > index and what differs is the contents; the codes still fire there, and that class was already > refused by `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`. All are > accepted limits: widening the field-separator check would mean deciding which set a record ought > to have had, which is a guess this parser does not make, and narrowing the escape atom would break > the guarantee it exists for, so the boundary is documented instead. If delimiter drift is a real > risk on your feed, parse with > `{ strict: true }`, which > refuses both an outright collapse and an unrecognized type letter, and treat > `ASTM_RECORD_UNKNOWN_TYPE` as invalidating what follows it rather than expecting this warning to > enumerate the damage. > **About runnable examples.** The first block above is tagged ` ```ts runnable `: the docs > build extracts it, runs it against the package, and asserts the `// =>` result, so a documented > example can never silently drift from the code. ## Read a result safely: status, flag, range A result carries the raw fields **and** a modeled, fail-safe view alongside them. The rule is _never a confident wrong value_: a corrected or cancelled result never reads as active-final, an unrecognized abnormal flag is never coerced to "normal", and an unparseable reference range never fabricates a bound. ```ts runnable import { parseAstmRecords, results } from "@cosyte/astm"; // A correction (status `C`) that supersedes a previously transmitted value. const raw = "H|\\^&\rO|1|ACC\rR|1|^^^687|30.1|U/L|10-40|H||C\rL|1|N\r"; const r = results(parseAstmRecords(raw))[0]; r?.status.meaning; // => "correction" ``` The `status` object is **always present** (an absent status field is typed `unspecified`, never assumed `final`), so `status.isActiveFinal` is a reliable boolean, `true` only for a plain `F`: ```ts import { parseAstmRecords, results } from "@cosyte/astm"; const r = results(parseAstmRecords(raw))[0]; r?.status.isActiveFinal; // false, a correction is not active-final r?.status.supersedes; // true, this value replaces a prior one r?.flag?.meaning; // "above-normal"; an unknown flag → "undefined", never "normal" r?.range?.kind; // "closed" (low "10", high "40"); an unparseable range → "unparsed", no invented bound ``` Both interpreted views also say **what they were graded against**, present whether or not the letter was recognized, so an unknown code is distinguishable from one this library has not caught up to: ```ts import { parseAstmRecords, results } from "@cosyte/astm"; const r = results(parseAstmRecords(raw))[0]; r?.flag?.vocabulary.attributed; // true: the flag letters have a named code system r?.flag?.vocabulary.system; // its identifier r?.flag?.vocabulary.version; // the version THIS LIBRARY compared against, not what the sender meant r?.status.vocabulary.attributed; // false: no citable published source binds the status letters r?.status.vocabulary.reason; // fixed prose saying so ``` > Units are vendor **free text**, never UCUM. A _numeric_ result value with no units raises an > `ASTM_RECORD_UNITS_ABSENT` warning: a missing unit is flagged, never defaulted, guessed, or > converted. The reference-range delimiter is `[OSS-derived]`; anything that > does not match `low-high` / `low` is surfaced verbatim. Flag recognition is exact-match > after trimming, so `hu` is not `HU`; see the limitations page for the vocabularies and the > eighteen letters recognized. ## Tell a query apart from a result upload A `Q` (request-information) record means the message is a **host-query request**, not a result set, so it must never be read as one. `parseAstmRecords` classifies every message up front; gate on `classification.isHostQueryRequest` before treating records as results. ```ts runnable import { parseAstmRecords, query } from "@cosyte/astm"; // An H/P/Q/L host-query request asking for all tests on a specimen. const raw = "H|\\^&\rP|1\rQ|1|^SPEC-7|^SPEC-7|ALL\rL|1\r"; const msg = parseAstmRecords(raw); msg.classification.kind; // => "host-query" ``` The `Q` **dominates**: even a message that (anomalously) carries both a `Q` and an `R` is classified `host-query` and flagged, a query is never silently read as a result upload. That rule is a count of record type letters, so it holds only while every letter was legible. If any record's type is unsupported and no `Q` was read, the kind is **withheld**: `classification.kind` is `indeterminate` rather than `results` or `orders`, and `classification.hasUnrecognized` is `true`. The unreadable letter may have been the `Q`, and answering `results` over it is exactly the misreading this rule exists to prevent, so the parser declines to answer instead of guessing which letter was sent. The `hasQuery` / `hasResults` / `hasOrders` counts stay truthful throughout, so a caller that wants the raw tally still has it, and `isHostQueryRequest` being `false` is not a warrant that a message is a result set: read `kind` for that. A `Q` that **was** read still dominates, since an unreadable letter can only add a kind, never remove the query already on the wire. The `Q` record's range IDs, the `ALL` keyword, and the request-information status codes are surfaced **verbatim** and flagged `[OSS-derived]` (their exact structure is paywalled), never guessed. `M` (manufacturer) and `S` (scientific) records carry vendor-defined QC / calibration / maintenance data. They are surfaced **verbatim** on `record.rawLine` and **never** interpreted into clinical fields: a QC value must not be read as a patient result. ## Decode a framed byte stream The record examples above assume **de-framed** record bytes. When you receive a raw ASTM byte stream straight off a serial line or socket, it arrives wrapped in **E1381/CLSI-LIS01 frames** (` FN text CS `) with a modulo-256 checksum and a frame number. `decodeAstmFrames` verifies each checksum, tracks the frame-number sequence, and reassembles multi-frame records; a bad-checksum frame is surfaced **flagged untrusted and never merged**, and a sequence gap is **never silently bridged**. ```ts runnable import { decodeAstmFrames } from "@cosyte/astm"; // One final (ETX) frame carrying the record text "L|1\r", with its correct checksum "3A". const bytes = new Uint8Array([0x02, 0x31, 0x4c, 0x7c, 0x31, 0x0d, 0x03, 0x33, 0x41, 0x0d, 0x0a]); const { frames } = decodeAstmFrames(bytes); frames[0]?.checksum.valid; // => true ``` `parseFramedAstm` composes the two layers at the edge: decode the frames, then parse the trusted, reassembled records into a message in one call. Only frames the framing layer vouched for reach the record parser, so a corrupted frame can never become a confident wrong value: ```ts import { parseFramedAstm, results } from "@cosyte/astm"; const { message, frames, frameWarnings } = parseFramedAstm(framedBytes); frameWarnings; // bad checksum / sequence gap / unterminated / oversize, each with a frame number + offset results(message)[0]?.value; // parsed from the reassembled, checksum-verified record bytes ``` > A checksum mismatch is a **warning** in the default lenient mode (the frame is kept for audit, > flagged `trusted: false`, and excluded from `records`) and a thrown `AstmFrameStrictError` under > `{ strict: true }`. The checksum is emitted uppercase but **accepted lowercase**: a real-vendor > quirk. Frame warnings carry only a **frame number + byte offset**, never the record bytes. ## Serialize and build (emit) Emit is the conservative inverse of parse. `serializeAstmRecords` turns a parsed message back into a spec-clean, `CR`-terminated stream: always the **canonical** `H|\^&` delimiters, every embedded delimiter re-escaped, so it round-trips: ```ts runnable import { parseAstmRecords, serializeAstmRecords } from "@cosyte/astm"; const raw = "H|\\^&\rP|1|PRAC|LAB\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"; serializeAstmRecords(parseAstmRecords(raw)); // => "H|\\^&\rP|1|PRAC|LAB\rR|1|^^^687|28.6|U/L||N||F\rL|1\r" ``` Canonical means canonical for the **whole** stream. A message that arrived under a vendor delimiter set comes back with every record in the canonical set, including the free-form `M` and `S` rows: the header can never declare one set while the rows below it use another, because re-reading that stream would collapse those rows' fields into one. `M`/`S` bytes are reproduced exactly as they arrived whenever they are already in the delimiters being emitted. A canonical message that was already spec-clean comes back byte-for-byte. Bytes can still differ without anything being lost: emit writes the spec-clean form of what was read, so a value the parser kept as a literal is escaped on the way out, and `O&BRIEN` comes back as `O&E&BRIEN`. Pass a second argument to emit against a different set: `serializeAstmRecords(msg, msg.delimiters)` puts a message back out in the delimiters it arrived under, and the header declares that set. Only three characters of a header's declaration carry a role (repeat, component, escape); a declaration that carries more keeps the surplus on emit rather than losing it, so `H|\^&#` comes back as `H|\^&#`. That holds on the default canonical path too: normalizing replaces the four roles, and the surplus holds none of them. Two things drop a surplus, and the second one is **not** confined to transcoding. A message emitted into a _different_ delimiter set than the one it arrived under loses it, because the surplus belonged to the declaration being replaced. And a surplus that could not be read back as a surplus is dropped on **every** path, the default canonical one included: that means a surplus carrying the field separator, or carrying any control character. Of the 33 C0 and `DEL` characters, 31 can reach a surplus and every one of them costs it; the other two are `CR` and `LF`, which end the record as it is read, so no surplus ever holds them. **The whole surplus goes, not just the offending character**, so `H|\^&#` plus a control character plus `$` comes back as plain `H|\^&`. Keeping part of it would mean emitting a declaration nobody wrote. Nothing reports it: emit returns a plain string. The set you pass is checked before any bytes are written. Each separator must be exactly one character, none may be a `CR`/`LF`, and no two may be the same character: otherwise the stream could not be read back as the records that produced it, and emit returns a plain string with no channel to warn you. A set that fails is an `AstmSerializeError` with code `ASTM_EMIT_INVALID_DELIMITERS`. This is stricter than the reader, which tolerates a few declarations it cannot reverse, so a message that parsed can still be refused when you ask for it back in its own set: the alternative was output that read back with a different field tree and said nothing. Those three conditions read the **set** and nothing else, so they cannot see a collision that depends on which record is being written. A separator equal to a record's own type letter passes all three and then escapes that letter away, so each record's emitted line is checked separately to start with the letter the record models; one that does not is an `AstmSerializeError` with code `ASTM_EMIT_TYPE_LETTER_COLLISION`. Without it the corruption was **silent** in its worst branch: where the escape character is itself a record type letter, the escaped letter starts with a real one, so a `P` record came back as an `R` whose `value` was the patient's laboratory ID, reported `F` for final, on a stream whose only warning was the `ASTM_NONSTANDARD_DELIMITERS` a clean non-canonical stream carries too. This one is a **transcoding** check, not a judgement on the set you asked for, so it fires with no delimiter argument at all. A stream whose header declares a vendor set and which carries one garbled line beginning `|` parses to an unsupported record whose type letter is `|`; emitting it on the default canonical path would escape that `|` away, so `serializeAstmRecords(msg)` refuses it, and so does `serializeFramedAstm(msg)`. "Emit against the canonical delimiters" is therefore not a way around it: the fix is to drop or repair the record whose type letter cannot be written. What the two refusals together promise is that every record re-reads as its own **type**. They do not promise that every field lands where it did: an escape sequence whose body is an unrecognized character that is itself a delimiter in force is read as one opaque atom, so that delimiter never becomes a boundary and the fields after it shift. That is reported on the parse side as `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, which no profile may tolerate, alongside the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, and its mirror (a delimiter the leftmost alignment let split where a competing alignment would have held it) as `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, with `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` on the subset of that where the gained boundary is a **field** boundary and the reading taken resumes on an escape character heading no sequence it can interpret, so a result's units and status move with it, and `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` on the subset where it is a **repeat** boundary, where no field moves and the field it divides is instead read out of its first repeat alone, and `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS` on the subset where it is a **component** boundary, where nothing leaves the record and every component after it moves along the component list, so a coding scheme or a given name is read out of a position the other alignment does not put it in. Emitting normalizes all of them away rather than preserving them, so none reaches a second generation: catch them on the first read. The low-level `encodeComponent` and `serializeField` helpers take no record and carry neither guarantee. If you emit against a set of your own rather than the canonical one, check the round-trip on your own traffic. `buildAstmMessage` constructs a spec-clean stream from typed input, and **never fabricates**. It emits only the values you supply; an omitted field stays empty, never a defaulted clinical value. A result whose status you did not set reads back as `unspecified`, never `final`: ```ts runnable import { buildAstmMessage, parseAstmRecords, results } from "@cosyte/astm"; const raw = buildAstmMessage({ records: [{ type: "R", universalTestId: ["", "", "", "687"], value: "28.6", units: "U/L" }], }); results(parseAstmRecords(raw))[0]?.status.meaning; // => "unspecified" ``` Every value is escape-encoded on emit, so an embedded delimiter can never break framing: a titre `1^40` is emitted as `1&S&40` and reads back as one component. A value carrying a `CR`/`LF` (which no escape can encode) is refused with a typed `AstmSerializeError` rather than a corrupted wire. ## Frame it for the wire `composeAstmFrames` is the inverse of `decodeAstmFrames`: it wraps reassembled record bytes into ` FN text CS ` frames, **computing** each modulo-256 checksum and frame number and splitting any record over 240 bytes: never faking either. `serializeFramedAstm` composes both emit layers at the edge: ```ts runnable import { parseAstmRecords, serializeFramedAstm, parseFramedAstm, results } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"); const bytes = serializeFramedAstm(msg); // spec-clean framed stream results(parseFramedAstm(bytes).message)[0]?.value; // => "28.6" ``` A frame carries **bytes**, so a record passed as a `string` is read as one byte per character. A character above `U+00FF` has no byte to stand for, and nothing this library reads from an ASTM stream says which character encoding to turn it into, so it is refused with `ASTM_FRAME_UNENCODABLE_CHARACTER` rather than quietly written as a different character. To frame content outside Latin-1, encode it with the code page your instrument uses and pass the `Uint8Array`: `composeAstmFrames` takes bytes directly and writes them through untouched. A record carrying a raw `STX`, `ETB` or `ETX` byte is refused too, with `ASTM_FRAME_RESERVED_BYTE`, and that one has **no** bytes-instead escape hatch: those three are what the decoder reads as the shape of a frame, and framing has no escape sequence to hide one behind. Writing it through truncated the frame at that byte, and it was not reliably loud: where the two following bytes happened to be the short frame's checksum, the truncated frame verified and a whole record was absorbed into the previous one with no warning from either layer. Take the byte out of the value before framing. The **record** layer still carries it: `serializeAstmRecords` returns a string, and a raw-transport consumer that never frames anything round-trips such a value byte for byte. To compose one transfer across several calls, pass `startFrameNumber` to continue the sequence where the previous call left off: joining the results is byte-identical to composing the whole list in one call, rollover included. It has to be a whole number from `0` to `7`, because a frame's number is a single ASCII digit, and `composeAstmFrames` checks it before it reads a record (`ASTM_FRAME_INVALID_START_FRAME_NUMBER`). Nothing returns the number to continue from, and the frame count is not it once a record splits, so read it off the part you just composed: `((decodeAstmFrames(part).frames.at(-1)?.frameNumber ?? 0) + 1) % 8`. Note what a continuation is not: read on its own, a stream that starts anywhere but `1` opens on a sequence gap, so the decoder does not emit that first record. What `parseFramedAstm` does after that varies with the message shape: it may throw, and it may return a message one record short. The record layer never reports the loss, because it only ever sees the records that survived, so **read `frameWarnings`**. Leave the option alone unless you are genuinely continuing a sequence. ## Map a local code to LOINC (LIVD, bring-your-own) An analyzer transmits a proprietary **local** test code in the Universal Test ID; a standard **LOINC** is mapped downstream. Supply your own IICC LIVD catalog and `applyLivd` annotates the message: **additively**. It never touches the raw code or value, and it **never guesses a LOINC**: an unmapped or ambiguous code is surfaced as such (a wrong LOINC would mis-identify the test). No terminology data is bundled: you bring the catalog. ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8", loincLongName: "AST" }]); const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"); applyLivd(msg, catalog).annotations[0]?.mapping.status; // => "mapped" ``` A code the catalog does not hold stays verbatim and is reported `unmapped` with a value-free `ASTM_LIVD_UNMAPPED_CODE` warning, never a fabricated LOINC: ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8" }]); const msg = parseAstmRecords("H|\\^&\rR|1|^^^999|5|U/L||N||F\rL|1\r"); applyLivd(msg, catalog).annotations[0]?.mapping.status; // => "unmapped" ``` ### Your catalog answers, and the wire never does The Universal Test ID's first component is a LOINC slot, and the guide this catalog format comes from puts transmitting LOINC directly from IVD instruments explicitly out of scope: the analyte arrives as a vendor-defined code. So the lookup runs whenever a vendor local code is present, keyed on that code alone, and a populated first component never answers instead of it. **This package performs no LOINC validation of any kind**, so it never decides whether such a value "looks like" a LOINC: `Glucose` and `2345-7` there are treated identically. The value is carried verbatim as `unvalidatedWireValue`, never reported as a LOINC, and never used as a lookup key. ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8" }]); const msg = parseAstmRecords("H|\\^&\rR|1|Glucose^^^687|28.6|U/L||N||F\rL|1\r"); const [a] = applyLivd(msg, catalog).annotations; a?.reportedCode; // => "687" a?.unvalidatedWireValue; // => "Glucose" a?.wireValueDisagreesWithCatalog; // => true ``` `wireValueDisagreesWithCatalog` says the two differ and **nothing else**: both values stay surfaced, neither is marked correct, neither is rewritten, and nothing claims the difference was settled. It is `true` only where your catalog vouched for exactly one LOINC, the first component is populated, and the two are not byte-identical; `false` everywhere else, and never absent. With no vendor local code there is nothing to look up, so nothing is looked up. The record reports that, rather than reporting the wire value as a code: ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8" }]); const msg = parseAstmRecords("H|\\^&\rR|1|Glucose|28.6|U/L||N||F\rL|1\r"); const [a] = applyLivd(msg, catalog).annotations; a?.mapping.status; // => "no-vendor-code" a?.reportedCode; // => undefined a?.unvalidatedWireValue; // => "Glucose" ``` ### One vendor code, several LOINCs: the units decide One vendor analyte code mapping to several LOINCs is the **ordinary** case. The guide this catalog format comes from gives the commonest chemistry analytes as its worked examples (a serum glucose as a mass concentration versus a substance concentration; a urine analyte as a spot concentration versus a 24 hour excretion rate) and its remedy is a mapping per unit. So a catalog row carries a **representative unit of measure**, and the `R` record already states its units on the wire: ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const glucose = defineLivdCatalog([ { vendorCode: "GLU", loinc: "2345-7", representativeUnit: "mg/dL" }, { vendorCode: "GLU", loinc: "14749-6", representativeUnit: "mmol/L" }, ]); const msg = parseAstmRecords("H|\\^&\rR|1|^^^GLU|5.5|mmol/L||N||F\rL|1\r"); const m = applyLivd(msg, glucose).annotations[0]?.mapping; m?.status; // => "mapped" m?.status === "mapped" ? m.loinc : undefined; // => "14749-6" ``` **That comparison is verbatim and case sensitive, and it is not UCUM.** Nothing is normalized, case folded, scaled or converted on either side, so `MG/DL` does not match `mg/dL` and `g/L` never matches `mg/dL` however convertible they are. UCUM requires a program claiming full conformance to compare unit expressions by their **semantics**, so this is a deliberately limited choice: the only comparison that cannot invent an equivalence. Every unit-selected answer says so on its own output, rather than leaving you to read a matched unit as a conformance claim this package does not make: ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const glucose = defineLivdCatalog([ { vendorCode: "GLU", loinc: "2345-7", representativeUnit: "mg/dL" }, { vendorCode: "GLU", loinc: "14749-6", representativeUnit: "mmol/L" }, ]); const msg = parseAstmRecords("H|\\^&\rR|1|^^^GLU|5.5|mg/dL||N||F\rL|1\r"); const m = applyLivd(msg, glucose).annotations[0]?.mapping; m?.status === "mapped" ? m.unitComparison?.comparison : undefined; // => "verbatim-case-sensitive" m?.status === "mapped" ? m.unitComparison?.ucumSemantic : undefined; // => false ``` Refusing still beats guessing. The units matching no candidate, matching more than one, or not being readable at all (absent, empty, whitespace only, or lost to a truncated or malformed record) are all `ambiguous`, with every candidate surfaced and no LOINC chosen. An unreadable units field reads as "no units reported": you get a typed annotation, never an exception and never a dropped record. ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const glucose = defineLivdCatalog([ { vendorCode: "GLU", loinc: "2345-7", representativeUnit: "mg/dL" }, { vendorCode: "GLU", loinc: "14749-6", representativeUnit: "mmol/L" }, ]); const msg = parseAstmRecords("H|\\^&\rR|1|^^^GLU|5.5|MG/DL||N||F\rL|1\r"); const m = applyLivd(msg, glucose).annotations[0]?.mapping; m?.status; // => "ambiguous" m?.status === "ambiguous" ? m.reason : undefined; // => "no-candidate-matched-units" m?.status === "ambiguous" ? [...m.candidates] : []; // => ["2345-7", "14749-6"] ``` A row's `vendorSpecimenDescription` and `vendorResultDescription` are carried verbatim and **never matched on**: both are free text the guide says is not to be parsed by software that automates the mapping, so they are surfaced for a human to read. Only the representative unit ever selects. All three attributes are optional, and a catalog carrying none of them answers exactly as it did before they existed. ## Next - [Core Concepts](https://docs.cosyte.com/astm/concepts-archetype.md): the parser archetype and the tolerance model. - **API Reference**: every export, generated from source. --- URL: https://docs.cosyte.com/astm/concepts-archetype Markdown: https://docs.cosyte.com/astm/concepts-archetype.md # Core Concepts `@cosyte/astm` follows the shared **Cosyte parser archetype**: the same mental model every `@cosyte/*` parser implements, so what you learn here transfers across the suite. `@cosyte/hl7` is the reference implementation; this package mirrors its shape. ## Postel's Law: lenient parse, strict emit The parser is **liberal in what it accepts** and the serializer is **conservative in what it emits**. Vendor quirks and tolerated deviations don't fail a parse, they become **warnings**, while anything the serializer produces is spec-clean. A `{ strict: true }` option escalates every tolerated deviation to a thrown error for callers who want the parse to fail loudly instead. ## The tolerance tiers Every deviation the parser encounters falls into one of three tiers: - **Tier 1: spec-clean.** No deviation; no warning. - **Tier 2: recoverable.** A vendor quirk the parser tolerates. It returns a **warning** with a stable code and positional context, and keeps going. Never thrown (unless `strict`). - **Tier 3: fatal.** Unrecoverable structural corruption. **Always thrown**, even in lenient mode. ## Stable warning + fatal codes Warnings and fatal errors carry **stable codes**: `WARNING_CODES` (Tier 2) and `FATAL_CODES` (Tier 3). Consumers branch on these, so a code's name is part of the public contract: renaming or removing one is a **breaking change**. Codes are `key === value` entries, so the full set survives an `Object.values(...)` snapshot into a stability tripwire. There are three registries, one per byte-level surface: `WARNING_CODES` (record layer), `FRAME_WARNING_CODES` (the frame codec), and `LTP_WARNING_CODES` (the transport reducer), plus `FATAL_CODES` for the Tier-3 throws. See the **API Reference** for the exact set this release ships. ## Immutability Parsed values are immutable by default; mutation happens only through explicit methods. This keeps a parsed document safe to share across a pipeline without defensive copying. --- URL: https://docs.cosyte.com/astm/architecture Markdown: https://docs.cosyte.com/astm/architecture.md # The two-layer architecture ASTM is not one standard but **two independent layers under one interface**, and `@cosyte/astm` mirrors that split exactly. Understanding the boundary is the key to using the library well: you decode the layers independently and compose them only at the one point they meet. ## Two standards, one domain | Layer | Standard | What it governs | Entry points | | ----------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | **Records** | ASTM E1394-97 → **CLSI LIS02-A2** | _Message content_: the `H`/`P`/`O`/`R`/`C`/`Q`/`L`/`S`/`M` record grammar with self-declaring delimiters | `parseAstmRecords`, `serializeAstmRecords`, `buildAstmMessage` | | **Frames** | ASTM E1381-02 → **CLSI LIS01-A2** | _Low-level transfer_: `STX`-framed records, modulo-256 checksum, frame numbers, the `ENQ`/`ACK`/`NAK`/`EOT` handshake | `decodeAstmFrames`, `composeAstmFrames`, `ltpReduce` | | **Common** | _None_ | Shared vocabulary: the delimiter model, the escape codec, the date value, code-system provenance, the warning registries | `CANONICAL_DELIMITERS`, value types | The two standards **share nothing but the domain and the payload boundary**. A frame carries record bytes; a record knows nothing about frames. That is why the package is one repo, two composable layers, and a thin common core. ## Decode the layers independently Middleware often hands you **already-de-framed** record bytes (the framing was stripped upstream, or the vendor drops framing over raw TCP entirely). In that case you never touch the frame layer: ```ts import { parseAstmRecords, results } from "@cosyte/astm"; // De-framed record bytes straight into the record parser. const msg = parseAstmRecords(deFramedBytes); results(msg)[0]?.value; ``` When you receive a raw byte stream off a serial line or socket, the frame layer decodes it first, and `parseFramedAstm` composes the two at the edge, only frames the framing layer _vouched for_ (checksum verified, in sequence) ever reach the record parser: ```ts import { parseFramedAstm, results } from "@cosyte/astm"; const { message, frames, frameWarnings } = parseFramedAstm(framedBytes); results(message)[0]?.value; // parsed only from trusted, reassembled record bytes ``` ## The transport reality the frame layer handles Over a serial line, records always arrive in full E1381 frames. Over TCP it **varies within a single vendor**: some analyzers keep the full `ENQ`/`ACK` + `STX`/checksum framing, others drop all low-level framing and stream records directly ("TCP itself ensures correctness"). `detectFraming` auto-detects framed vs raw from the leading byte and **defaults to framed on an ambiguous lead** (with a profile override): never a silent guess into data loss. `ltpReduce` models the establishment → transfer → termination state machine as a **pure reducer** over transport events, so it is deterministic and fully testable without a socket. The library never owns the wire or the clock: it models the state transitions, and you drive them with your own I/O. ## Why this shape - **Safety lives in the payload.** The record layer leads because that is where a wrong value, flag, status, or patient ID causes clinical harm, so it gets the earliest, most rigorous treatment. - **Independent testing.** Each layer is fuzzed and property-tested on its own; the record tokenizer and the frame codec are separate byte-level surfaces with separate warning registries (`WARNING_CODES`, `FRAME_WARNING_CODES`, `LTP_WARNING_CODES`). - **Composability.** A consumer takes exactly the layer they need. The two only meet in `parseFramedAstm` / `serializeFramedAstm`, and that seam is deliberately thin. ## Where to go next - [Quickstart](https://docs.cosyte.com/astm/quickstart.md): parse a result, decode a framed stream, serialize and build. - [Core Concepts](https://docs.cosyte.com/astm/concepts-archetype.md): the shared parser archetype and the tolerance tiers. - [What it does, and does not do](https://docs.cosyte.com/astm/limitations.md): the honest boundary before you rely on it. --- URL: https://docs.cosyte.com/astm/patient-data Markdown: https://docs.cosyte.com/astm/patient-data.md # What `@cosyte/astm` does with patient data ASTM instrument traffic carries protected health information. The `P` record concentrates it (name, mother's maiden name, birthdate, sex, practice and lab identifiers), `C` free text can carry it, and a result value with its flags is clinical data about a person. Before you put lab traffic through this library, this page states exactly what it does with those bytes, so the decision is made on what the code does rather than on a promise. `@cosyte/astm` is **HIPAA-capable, not HIPAA-compliant**. Compliance is a property of a system, not of a library, and the last section is the part of that system this package cannot do for you. Every identifier on this page is invented. ## What it logs **Nothing.** The library has no logger and no verbosity option, because it produces no output: it writes nothing to standard output or standard error, and calls no logging API. What it gives you instead is data you decide to log. Warnings and errors carry **positional context only, never a value**. A position is the record's ordinal index, its type letter, and the 1-based field and component indices; the message beside it is a fixed constant. That is what makes a warning safe to log verbatim: it cannot leak a name, an identifier, or a result. ```ts runnable import { parseAstmRecords } from "@cosyte/astm"; const raw = "H|\\^&\rP|1|PRAC|LAB||DOE^JANE||20200101|F\rR|1|^^^687|28.6||||F\rL|1\r"; const warning = parseAstmRecords(raw).warnings[0]; warning?.code; // => "ASTM_RECORD_UNITS_ABSENT" warning?.position; // => { recordIndex: 2, recordType: "R", fieldIndex: 5 } ``` The warning says a result in record 2 is missing its units. It does not say whose result, or what the value was, and nothing in the object does. There is exactly one deliberate exception, and it is not about your data: the error that refuses an invalid `startFrameNumber` names the value it received, because that value is your own option argument rather than anything read off the wire. ## What it retains **Nothing between calls.** Parsing is a pure function of its input. It returns a message and keeps no reference to it: there is no cache, no pool, no buffer that survives the call, and no module-level store of anything read off a stream. The only module-level state the package has is the default profile you may set yourself, and a profile holds vendor tolerance rules, never message content. Drop your reference to a parsed message and the patient data goes with it. What the message itself holds is the other half of that sentence, and it is the half worth knowing: **a parsed message is a verbatim copy of what you gave it.** Every field keeps its exact wire text on `field.raw`, and the header and the free-text records keep their whole line on `record.rawLine`. The object graph is deeply frozen, so nothing can be redacted out of it in place. Treat a parsed message as the payload it came from: do not put one in an error report, a crash dump, a snapshot test or a support ticket. ## What it writes to disk **Nothing.** The package imports no Node built-in at all: no file system, no sockets, no HTTP. It cannot open a file, a port or a connection, and it reads no environment variable. Zero runtime dependencies means there is no transitive package that could do any of it on the library's behalf either. It also does not own the serial line or the socket your analyzer is on. The library decodes and encodes byte streams and gives you a pure transport reducer; the input and output adapter is yours, which means every byte that reaches a disk, a wire or a screen got there through code you wrote. No telemetry is collected and nothing is phoned home. ## What you still own Everything at the edges, which is most of the compliance surface: - **Transport security.** Serial or TCP, encryption in transit, and network segmentation between the analyzer and your service. - **Storage.** Where a parsed message or a raw payload is persisted, how it is encrypted at rest, how long it is kept and how it is destroyed. - **Logging discipline.** Never log a raw payload. Log the warning, not the record: the warning was designed to be safe to log, and the record was not. - **Access control and audit.** Who may read a message, and the record of who did. - **De-identification.** This package does not de-identify anything. It surfaces what the wire says, including the `P` record, verbatim. - **Test data.** Keep fixtures synthetic. Real traffic in a test corpus is the most common way a patient identifier reaches a repository, and a repository is forever. Read [What it does, and does not do](https://docs.cosyte.com/astm/limitations.md) for the rest of the boundary, including the cases where a value the parser hands back is contested and the raw line is the thing to read. --- URL: https://docs.cosyte.com/astm/guides-overview Markdown: https://docs.cosyte.com/astm/guides-overview.md # Guides Task-oriented recipes ("how do I X?") for `@cosyte/astm`. Each is a short, copy-pasteable answer to one real integration question. For a guided first read, start with the [Quickstart](https://docs.cosyte.com/astm/quickstart.md); for the boundaries of what the library promises, read [What it does, and does not do](https://docs.cosyte.com/astm/limitations.md). ## Tell a corrected or cancelled result apart from a final one A result's `status` is **always present** and fail-safe: a correction (`C`) or cancel (`X`) never reads as active-final, and an absent status is `unspecified`, never assumed `final`. Gate on it before you treat a value as current: ```ts runnable import { parseAstmRecords, results } from "@cosyte/astm"; const raw = "H|\\^&\rO|1|ACC\rR|1|^^^687|30.1|U/L|10-40|H||C\rL|1\r"; const r = results(parseAstmRecords(raw))[0]; r?.status.isActiveFinal; // => false ``` `status.supersedes` is `true` for a correction (this value replaces a prior one): branch on it to decide what to store, and never let a superseded value read as current. ## Fail fast at an integration boundary The parser is lenient by default. When you want a tolerated deviation to be a hard error instead (at the edge of your system, before bad data flows in), re-parse with `{ strict: true }`: ```ts import { parseAstmRecords, AstmStrictError } from "@cosyte/astm"; try { parseAstmRecords(raw, { strict: true }); } catch (err) { if (err instanceof AstmStrictError) { // err.warnings holds every deviation that would have been tolerated in lenient mode } } ``` ## Branch on a specific vendor quirk Every tolerated deviation is a warning with a **stable code**. Match on `WARNING_CODES` to decide whether to tolerate, log, or reject a specific quirk, the code is part of the public contract, so this branch will not silently drift: ```ts import { parseAstmRecords, WARNING_CODES } from "@cosyte/astm"; const { warnings } = parseAstmRecords(raw); for (const w of warnings) { if (w.code === WARNING_CODES.ASTM_RECORD_UNITS_ABSENT) { // a numeric result arrived without units: flag it for review, never default the unit } } ``` ## Decode a stream when you do not know if it is framed Serial is always framed; over TCP some vendors keep framing and some drop it. `detectFraming` reads the leading byte and defaults to **framed** on an ambiguous lead (never a silent guess), and a profile can force the transport when you know the vendor: ```ts import { detectFraming } from "@cosyte/astm"; const { framing, defaulted, warnings } = detectFraming(bytes); // framing: "framed" | "raw". An ambiguous lead → framing "framed", defaulted true, // and one ASTM_LTP_AMBIGUOUS_TRANSPORT warning. Pass { override: "raw" } to force it. ``` ## Map a local test code to LOINC Analyzers transmit a proprietary **local** code; LOINC is mapped downstream. Supply your own IICC LIVD catalog and `applyLivd` annotates the message **additively**, it never touches the raw code or value and never guesses a LOINC: ```ts runnable import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8", loincLongName: "AST" }], { publisher: "Example Diagnostics", publicationVersion: "LIVD-2026-01-A", loincVersion: "2.78", loincCopyright: "This content LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at http://loinc.org/terms-of-use", }); const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"); const annotation = applyLivd(msg, catalog).annotations[0]; annotation?.mapping.status; // => "mapped" annotation?.catalogLoincVersion; // => "2.78" catalog.publication?.publisher; // => "Example Diagnostics" catalog.warnings?.length; // => 0 ``` The catalog is consulted whenever a vendor local code is present, and is keyed on that code alone. A value in the Universal Test ID's first component is carried verbatim as `unvalidatedWireValue`, is never used as a lookup key, and is never reported as a LOINC: this package performs **no LOINC validation of any kind**, so it never decides what such a value "looks like". ### Say which publication the mapping came from The four optional elements beside the rows describe the **publication**, not any one row: who published it, which publication version it is, which version of LOINC the mapping was made against, and the LOINC copyright statement. The LOINC version rides onto **every** annotation the catalog produces as `catalogLoincVersion`, whatever the lookup answered, so a mapping records what it was made against and can be reproduced later. **The LOINC license requires a statement of attribution and notice that LOINC content is copyrighted**, and `loincCopyright` is where your statement rides, so it travels beside the mappings it applies to rather than living in a document somewhere else. Declaring no LOINC version is allowed and **nothing refuses**: the catalog is built and every lookup answers exactly as it would have. `catalog.warnings` then carries one value-free `ASTM_LIVD_CATALOG_NO_LOINC_VERSION` warning, whose message is a constant and whose `catalog` field names your publication from what you declared, or says positively that you declared no identity. **And the limits, beside the capability.** This package validates **none** of this metadata: not the LOINC version, not the publisher, not the publication version, not the copyright text. It performs no LOINC validation of any kind, so each value is stored and surfaced exactly as you supplied it, and `catalogLoincVersion` never means a LOINC was checked against that version. **Carrying an attribution statement is not discharging the obligation to make one**: the terminology data and its license obligations stay yours, and this package makes no statement on your behalf. ## Round-trip a payload Parse, inspect, and re-emit spec-clean output. The serializer is the conservative inverse of the parser, so a parsed message round-trips through canonical delimiters with every embedded delimiter re-escaped: ```ts runnable import { parseAstmRecords, serializeAstmRecords } from "@cosyte/astm"; const raw = "H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"; serializeAstmRecords(parseAstmRecords(raw)) === raw; // => true ``` ## More - [Vendor profiles](https://docs.cosyte.com/astm/guides-vendor-profiles.md): tolerating a documented quirk, and the safety gate that bounds what a profile may tolerate. - [Quickstart](https://docs.cosyte.com/astm/quickstart.md): the one-line parse and every emit path. - [The two-layer architecture](https://docs.cosyte.com/astm/architecture.md): records vs frames, and when you need each. - [What it does with patient data](https://docs.cosyte.com/astm/patient-data.md): logging, retention and what stays yours. - **API Reference**: every shipped export, generated from source. --- URL: https://docs.cosyte.com/astm/guides-vendor-profiles Markdown: https://docs.cosyte.com/astm/guides-vendor-profiles.md # Vendor profiles An analyzer that deviates from the standard in a documented, harmless way makes a lenient parse warn on every message it sends. Turning the warning off everywhere is the wrong fix: the next instrument needs it. A **profile** is how you say "I expect this one deviation, from this instrument, and here is the document that says so", while every other tolerance the parser applies stays exactly as loud as it was. **This package ships no named per-vendor profile, and that is deliberate.** The two built-ins are `astmProfiles.default`, a conservative baseline, and `astmProfiles.referenceCorpus`, grounded firsthand in the redistributable public reference corpus. A named per-vendor built-in ships only when a public, vendor-attributed quirk document grounds it, and inspection of the public corpus found the record layer spec-clean, so none is asserted. The engine fully supports them; you write the one your instrument's interface document describes, which is the only person who has that document. Naming an analyzer as an example of a behaviour is not the same thing as shipping a profile for it, and this page does the first and not the second. ## Apply a vendor profile Build a profile with `defineAstmProfile`, then hand it to the parser. Nothing else changes: the same call, the same message shape, the same accessors. ```ts runnable import { defineAstmProfile, parseAstmRecords } from "@cosyte/astm"; const site = defineAstmProfile({ name: "site-analyzer", description: "Bench analyzer on the chemistry line", tolerate: [ { code: "ASTM_NONSTANDARD_DELIMITERS", rationale: "declares its own delimiter set in every header, per its interface document", }, ], provenance: { source: "Bench analyzer host interface document", reference: "on file, internal" }, }); const raw = "H#~*!\rR#1#^^^687#28.6#U/L##N##F\rL#1\r"; const withProfile = parseAstmRecords(raw, { profile: site }); withProfile.warnings[0]?.code; // => "PROFILE_QUIRK_APPLIED" withProfile.warnings[0]?.toleratedCode; // => "ASTM_NONSTANDARD_DELIMITERS" ``` Without the profile the same stream reports `ASTM_NONSTANDARD_DELIMITERS` at the header. With it, the warning is **re-badged, not removed**: it comes back as `PROFILE_QUIRK_APPLIED`, flagged `expected`, carrying the profile's name and the code it stands in for. So a reviewer reading your logs can still see the deviation happened, and can see which document said it was fine. A few things worth knowing while you write one: - **`rationale` is required on every toleration** and `provenance` is where the grounding goes. A profile with no reason recorded is a warning switched off with the reason lost. - **`match` narrows a toleration structurally**, to a record type and a 1-based field index, so you can expect a deviation in result units without blanket-tolerating it across the message. Matching is on structural identifiers only; there is no matching on any field value. - **`extends` composes profiles.** Lineage, tolerations, transport and provenance merge, and the merged result is re-validated, so nothing gets in through a hand-built parent. - **`transport` forces raw or framed** for a vendor whose TCP link drops framing, instead of leaving it to leading-byte detection. - **`describe()`** prints the whole thing back as text, which is what to paste into a review. Register it as the process default with `setDefaultAstmProfile` when every stream on a service comes from the same instrument; pass it per call when they do not. ## The profile safety gate A profile can only ever tolerate a deviation that **cannot cost you a value**, and the rule that decides which those are is **default-deny**. Every warning code in the three registries the gate covers, the record codes, the frame codes and the transport codes, is safety-critical **unless** it is on the library's tolerable list, and a code added later is safety-critical by default until it is argued onto that list. Name a safety-critical code in `tolerate` and `defineAstmProfile` throws `AstmProfileDefinitionError` rather than building the profile. Ask `isSafetyCriticalCode(code)` when you want the answer for a specific code; do not copy a list of codes into your own source, because the set moves with the library and every snapshot of it written down has gone stale. The gate is enforced twice, and the second time is load-bearing rather than redundant. `AstmProfile` is a plain interface, so a hand-authored object literal type-checks and never passes through the factory at all. `applyAstmProfile` therefore re-checks at apply time and declines to downgrade a safety-critical warning whatever the profile says, so the original warning survives and `{ strict: true }` still escalates it. **A profile never alters an extracted value.** The transform runs at the warning layer and nowhere else: it re-badges a warning the profile expects, no warning is ever dropped, no record, field or value is touched, and a spec-clean message parses byte-identically with or without a profile. That is why a profile is safe to apply to production traffic, and it is also the bound on what one can do for you. A profile cannot repair a stream. Where the parser reports that a reading is contested, the answer is to read the raw line, not to tolerate the code. ## More - [Branch on a specific vendor quirk](https://docs.cosyte.com/astm/guides-overview.md): matching a warning code without a profile. - [What it does, and does not do](https://docs.cosyte.com/astm/limitations.md): the honest boundary, including the deviations no profile may tolerate. --- URL: https://docs.cosyte.com/astm/api Markdown: https://docs.cosyte.com/astm/api.md # @cosyte/astm ## Classes ### AstmAmbiguousStreamError Thrown by a flat extractor when the stream it was handed does not determine a single answer: either because it carries several messages, or because its one message carries several patients. The throw **is** the fix. A caller who gets this error is strictly better off than one who silently received another patient's results, and the remedy is mechanical: walk [messages](#messages) and read each message's own records. **PHI:** carries a stable code, a position, and two counts. Never a field value, never a patient identifier. #### Example ```ts import { parseAstmRecords, messages, results, AstmAmbiguousStreamError } from "@cosyte/astm"; const msg = parseAstmRecords(raw); try { results(msg); } catch (err) { if (err instanceof AstmAmbiguousStreamError) { for (const m of messages(msg)) m.results.length; } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AstmAmbiguousStreamError**(`code`, `message`, `position`, `counts`): [`AstmAmbiguousStreamError`](#astmambiguousstreamerror) **`Internal`** ###### Parameters ###### code [`AmbiguousCode`](#ambiguouscode) ###### message `string` ###### position [`AstmPosition`](#astmposition) ###### counts ###### messageCount `number` ###### patientCount `number` ###### Returns [`AstmAmbiguousStreamError`](#astmambiguousstreamerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`AmbiguousCode`](#ambiguouscode) The stable discriminant: see [AMBIGUOUS\_CODES](#ambiguous_codes). ##### messageCount > `readonly` **messageCount**: `number` How many `H` … `L` messages the stream carries. ##### patientCount > `readonly` **patientCount**: `number` How many `P` records the message in question carries. ##### position > `readonly` **position**: [`AstmPosition`](#astmposition) Where the ambiguity became visible: the second header, or the second `P`. Value-free. *** ### AstmFrameEncodeError Thrown by [composeAstmFrames](#composeastmframes) when the input cannot be framed into a spec-clean stream. Carries a stable code + positional context, never the record bytes and never the offending character (PHI discipline). **The unencodable-character case, and why it is a refusal.** A record passed as a `string` is a **byte string**: character `i` becomes byte `i`, which is the exact inverse of how this package turns record bytes back into a string (one `String.fromCharCode` per byte, so every byte survives 1:1). A character above `U+00FF` has no byte to become. Turning one into bytes takes a character encoding, and nothing this package reads from an ASTM stream says which one: it reads no character-set declaration from any record, so picking one would be a guess at bytes the caller never supplied. Emit also has no warning channel, so a warning here could only be ignored while the wrong bytes still shipped. **Framing content outside Latin-1.** Encode it yourself, with the code page your instrument actually uses, and hand [composeAstmFrames](#composeastmframes) the `Uint8Array`: it accepts bytes directly and writes them through untouched. The refusal removes no capability, it routes you to the parameter that already carried it. **The reserved-byte case, and why it has no escape hatch.** `STX`, `ETB` and `ETX` are what [decodeAstmFrames](#decodeastmframes) reads as frame structure: `STX` opens a frame, and the first `ETB`/`ETX` after it *is* the end of that frame's text. A record carrying one of those bytes therefore cannot be framed and read back, whichever form it arrives in, and this layer has no escape sequence to hide one behind. Passing bytes instead of a string does **not** route around it, because the byte is the problem rather than the encoding. Take the byte out of the value before framing: which byte belongs in a clinical value is the sender's call, not this library's, so it refuses rather than substituting or deleting one. **The start-frame-number case, and why an option gets an error of its own.** A frame's number is a single ASCII digit, `FN_ZERO + n` for `n` in `0`-`7`, and nothing used to check that `options.startFrameNumber` was one. What went out instead was whatever byte that sum truncated to: measured on this package's own round trip, `-1` wrote `/` into the frame-number position and `NaN`, `Infinity` and `-Infinity` each wrote a `NUL` byte into every frame of the stream, after which the decoder read no frame number at all and emitted **none** of the records. Out-of-domain values that happened to land back on a digit were worse in a quieter way: `1.5` and `257` both produced the byte-for-byte stream a `startFrameNumber` of `1` produces, so the option silently accepted a value it documented as invalid. This one refusal is about the caller's own option rather than about record content, so its message names the value received. Nothing from the stream is quoted. #### Example ```ts import { composeAstmFrames, AstmFrameEncodeError } from "@cosyte/astm"; try { composeAstmFrames([]); } catch (err) { if (err instanceof AstmFrameEncodeError) err.code; // "ASTM_FRAME_EMPTY_RECORD" } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AstmFrameEncodeError**(`message`, `recordIndex?`, `code?`, `characterIndex?`): [`AstmFrameEncodeError`](#astmframeencodeerror) **`Internal`** ###### Parameters ###### message `string` ###### recordIndex? `number` ###### code? [`AstmFrameEncodeErrorCode`](#astmframeencodeerrorcode-1) = `"ASTM_FRAME_EMPTY_RECORD"` ###### characterIndex? `number` ###### Returns [`AstmFrameEncodeError`](#astmframeencodeerror) ###### Overrides `Error.constructor` #### Properties ##### characterIndex? > `readonly` `optional` **characterIndex?**: `number` Position of the offending character within that record, when applicable: its index in the string, never the character itself and never its code point. Enough to find it in the caller's own data. For a record supplied as a `Uint8Array` this is the offending byte's index, which is the same position: a record string is a byte string, so the two indices coincide. ##### code > `readonly` **code**: [`AstmFrameEncodeErrorCode`](#astmframeencodeerrorcode-1) Stable discriminant. ##### recordIndex? > `readonly` `optional` **recordIndex?**: `number` Index of the offending record within the input, when applicable. *** ### AstmFrameStrictError Thrown by [decodeAstmFrames](#decodeastmframes) in `strict` mode when the lenient codec would otherwise have accumulated one or more frame warnings (a bad checksum, a sequence gap, an unterminated frame, or an oversize frame). Carries every warning (code + position, never the frame's record bytes) so a caller can see each deviation. #### Example ```ts import { decodeAstmFrames, AstmFrameStrictError } from "@cosyte/astm"; try { decodeAstmFrames(someFramedBytes, { strict: true }); } catch (err) { if (err instanceof AstmFrameStrictError) err.warnings.length; // >= 1 } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AstmFrameStrictError**(`warnings`): [`AstmFrameStrictError`](#astmframestricterror) **`Internal`** ###### Parameters ###### warnings readonly [`AstmFrameWarning`](#astmframewarning)[] ###### Returns [`AstmFrameStrictError`](#astmframestricterror) ###### Overrides `Error.constructor` #### Properties ##### warnings > `readonly` **warnings**: readonly [`AstmFrameWarning`](#astmframewarning)[] *** ### AstmParseError Thrown by [parseAstmRecords](#parseastmrecords) when the input violates one of the Tier-3 unrecoverable structural rules (empty input, no leading `H`, or an `H` that cannot declare its delimiters). Carries positional context so consumers can log an actionable error. **PHI:** the error carries a stable code + position only, never a field value. #### Example ```ts import { parseAstmRecords, AstmParseError } from "@cosyte/astm"; try { parseAstmRecords("P|1"); } catch (err) { if (err instanceof AstmParseError && err.code === "ASTM_RECORD_NO_HEADER") { // err.position is available; err carries no field value } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AstmParseError**(`code`, `message`, `position`): [`AstmParseError`](#astmparseerror) **`Internal`** Construct a new `AstmParseError`. Both fields are required so every thrower populates positional context and no PHI-bearing snippet is ever attached. ###### Parameters ###### code [`FatalCode`](#fatalcode) ###### message `string` ###### position [`AstmPosition`](#astmposition) ###### Returns [`AstmParseError`](#astmparseerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`FatalCode`](#fatalcode) ##### position > `readonly` **position**: [`AstmPosition`](#astmposition) *** ### AstmProfileDefinitionError Thrown by `defineAstmProfile` when a profile definition is invalid: a missing or empty `name`, an unknown option key, an invalid `transport` value, or a `tolerate` entry whose code is unknown, whose `rationale` is empty, or, the load-bearing safety rule, whose code is **safety-critical** (a result value, abnormal flag, result status, reference range, units, patient/comment context, message-kind, code system, an unrecognized record type, or any frame/LTP integrity warning). Carries the offending profile's `name` when it is known, so a multi-profile build names the culprit. #### Example ```ts import { defineAstmProfile, AstmProfileDefinitionError } from "@cosyte/astm"; try { defineAstmProfile({ name: "unsafe", tolerate: [{ code: "ASTM_FRAME_BAD_CHECKSUM", rationale: "no" }], }); } catch (err) { if (err instanceof AstmProfileDefinitionError) err.profileName; // "unsafe" } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AstmProfileDefinitionError**(`message`, `profileName?`): [`AstmProfileDefinitionError`](#astmprofiledefinitionerror) **`Internal`** Construct a new `AstmProfileDefinitionError`. ###### Parameters ###### message `string` A human-readable explanation of what was invalid. ###### profileName? `string` The offending profile's name, when known. ###### Returns [`AstmProfileDefinitionError`](#astmprofiledefinitionerror) ###### Overrides `Error.constructor` #### Properties ##### profileName? > `readonly` `optional` **profileName?**: `string` The offending profile's `name`, when it could be read. *** ### AstmSerializeError Thrown by the record/frame emit side when a value cannot be serialized into a spec-clean stream: specifically when a component contains a record terminator (`CR`/`LF`), which the ASTM escape codec cannot encode and which would break framing if emitted raw. Carries a stable code + positional context, never the offending value (PHI discipline). #### Example ```ts import { serializeAstmRecord, AstmSerializeError, parseAstmRecords } from "@cosyte/astm"; const rec = parseAstmRecords("H|\\^&\rL|1\r").records[1]!; try { serializeAstmRecord(rec); } catch (err) { if (err instanceof AstmSerializeError) err.code; // "ASTM_EMIT_UNENCODABLE_VALUE" } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AstmSerializeError**(`message`, `recordIndex?`, `code?`): [`AstmSerializeError`](#astmserializeerror) **`Internal`** ###### Parameters ###### message `string` ###### recordIndex? `number` ###### code? [`AstmSerializeErrorCode`](#astmserializeerrorcode-1) = `"ASTM_EMIT_UNENCODABLE_VALUE"` ###### Returns [`AstmSerializeError`](#astmserializeerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`AstmSerializeErrorCode`](#astmserializeerrorcode-1) Stable discriminant. `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value; `ASTM_EMIT_INVALID_DELIMITERS` for a delimiter set that cannot be emitted reversibly; `ASTM_EMIT_TYPE_LETTER_COLLISION` for a record whose own type letter the set being emitted with would escape away. ##### recordIndex? > `readonly` `optional` **recordIndex?**: `number` 0-based ordinal of the record within the message, when known. *** ### AstmStrictError Thrown by [parseAstmRecords](#parseastmrecords) in `strict` mode when the lenient parser would otherwise have accumulated one or more Tier-2 warnings. Carries the warnings (code + position, never a value) so a caller can see every deviation. #### Example ```ts import { parseAstmRecords, AstmStrictError } from "@cosyte/astm"; try { parseAstmRecords("H|\\^&\rZ|1\r", { strict: true }); } catch (err) { if (err instanceof AstmStrictError) err.warnings.length; // >= 1 } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new AstmStrictError**(`warnings`): [`AstmStrictError`](#astmstricterror) **`Internal`** ###### Parameters ###### warnings readonly [`AstmRecordWarning`](#astmrecordwarning)[] ###### Returns [`AstmStrictError`](#astmstricterror) ###### Overrides `Error.constructor` #### Properties ##### warnings > `readonly` **warnings**: readonly [`AstmRecordWarning`](#astmrecordwarning)[] ## Interfaces ### AbnormalFlag A recognized (or explicitly *un*recognized) abnormal flag. The raw field text is always preserved; `recognized` is `false` and `meaning` is `"undefined"` for any letter outside the graded vocabulary, the flag is surfaced, never dropped, and **never coerced to `normal`**. `vocabulary` is present either way, so a consumer holding an unrecognized flag can tell a code no published vocabulary defines from one this library has not caught up to. #### Example ```ts import { interpretAbnormalFlag } from "@cosyte/astm"; const f = interpretAbnormalFlag("HH"); f.meaning; // "critically-above-normal" f.recognized; // true f.vocabulary.version; // the code system version compared against ``` #### Properties ##### code? > `readonly` `optional` **code?**: [`AbnormalFlagCode`](#abnormalflagcode-1) The recognized code, present only when the raw text is a recognized flag. ##### meaning > `readonly` **meaning**: [`AbnormalFlagMeaning`](#abnormalflagmeaning-1) The modeled meaning; `"undefined"` (never `"normal"`) for an unrecognized flag. ##### raw > `readonly` **raw**: `string` The verbatim field text, exactly as received. ##### recognized > `readonly` **recognized**: `boolean` Whether the raw text matched a flag in the graded vocabulary. ##### vocabulary > `readonly` **vocabulary**: [`NamedVocabulary`](#namedvocabulary) The published code system this flag was graded against, named by identifier and version. Always present, recognized or not. The version says what this library compared against, **not** what the sender meant. *** ### AstmDate A parsed ASTM date/time. Immutable plain data; the populated fields extend exactly as far as [AstmDate.precision](#precision). Absent components are left `undefined` rather than defaulted, so a consumer can tell "midnight" from "no time given". No timezone is modeled: the value is instrument-local. #### Example ```ts import { parseAstmDate } from "@cosyte/astm"; const d = parseAstmDate("20240315"); d?.precision; // "day" d?.hour; // undefined (not 0) ``` #### Properties ##### day? > `readonly` `optional` **day?**: `number` ##### hour? > `readonly` `optional` **hour?**: `number` ##### minute? > `readonly` `optional` **minute?**: `number` ##### month? > `readonly` `optional` **month?**: `number` ##### precision > `readonly` **precision**: [`AstmDatePrecision`](#astmdateprecision-1) How far the components are populated. ##### raw > `readonly` **raw**: `string` The raw digit string as it appeared on the wire. ##### second? > `readonly` `optional` **second?**: `number` ##### truncated? > `readonly` `optional` **truncated?**: `true` `true` when the digit run does **not** align to a whole-component boundary: an odd number of digits that cuts a two-digit component (month/day/hour/minute/second) in half (lengths 5, 7, 9, 11, 13). The full run is preserved in [AstmDate.raw](#raw-1) and the structured value is truncated to the last **complete** component: the dangling digit is **never zero-filled into a fabricated time**. Absent (never `false`) for a clean value. A caller surfaces this as a value-free `ASTM_RECORD_PARTIAL_TIMESTAMP` warning. ##### year > `readonly` **year**: `number` *** ### AstmField One ASTM field, as split from a record. The tree holds **decoded** component strings (escape sequences already resolved), while [AstmField.raw](#raw-2) preserves the exact wire text of the field for round-trip and audit. `components` is the first repeat's components (the common single-repeat case); `repeats` holds every repeat when a field uses the repeat delimiter. #### Properties ##### components > `readonly` **components**: readonly `string`[] Components of the first repeat, each escape-decoded. Empty field → `[""]`. ##### raw > `readonly` **raw**: `string` The exact field text as it appeared on the wire (escapes NOT decoded). ##### repeats > `readonly` **repeats**: readonly readonly `string`[][] Every repeat, each an array of decoded components. `repeats[0] === components`. *** ### AstmFrame One decoded ASTM frame. `text` is the frame's record-byte payload (the bytes between the frame number and the terminator, escapes untouched, this is the framing layer, not the record layer). `trusted` is the single flag a consumer gates on: it is `true` only for a fully-terminated frame whose checksum validated, and such frames are the only ones reassembled into `records`. #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of the `STX` that opened this frame, within the decoded stream. ##### checksum > `readonly` **checksum**: [`FrameChecksum`](#framechecksum) The checksum verdict. ##### frameNumber? > `readonly` `optional` **frameNumber?**: `number` The frame's sequence number as read from the `FN` byte. Normally `0` to `7`; a value outside that range means the `FN` byte was not a `0` to `7` digit (a corruption that also trips a sequence-gap warning). `undefined` when the frame was too truncated to carry a frame number at all. ##### oversize > `readonly` **oversize**: `boolean` `true` when the frame's record text exceeded the 240-byte limit. ##### terminator? > `readonly` `optional` **terminator?**: [`FrameTerminator`](#frameterminator) `ETB` (intermediate) or `ETX` (final); `undefined` for an unterminated frame. ##### text > `readonly` **text**: `Uint8Array` The frame's record-byte payload (may be empty). A **copy**, safe for the caller to retain. ##### trusted > `readonly` **trusted**: `boolean` `true` only when the frame was fully terminated **and** its checksum validated. Only trusted frames are reassembled into [DecodeAstmFramesResult.records](#records-2); an untrusted frame is surfaced here but never merged. ##### unterminated > `readonly` **unterminated**: `boolean` `true` when no valid terminator + checksum was found for this frame (a truncated/partial frame). *** ### AstmFramePosition Where in a framed byte stream a warning or error originated. Both members are positional; neither is a value. #### Example ```ts import type { AstmFramePosition } from "@cosyte/astm"; const at: AstmFramePosition = { frameNumber: 2, byteOffset: 251 }; ``` #### Properties ##### byteOffset > `readonly` **byteOffset**: `number` Byte offset of the `STX` that opened the frame, within the decoded stream. ##### frameNumber? > `readonly` `optional` **frameNumber?**: `number` The frame's sequence number (`0` to `7`) when it could be read, or `undefined` when the frame was so truncated the number was not present. *** ### AstmFrameWarning A single frame-codec warning: a stable code, a value-free human-readable message, and positional context (frame number + byte offset). #### Example ```ts import type { AstmFrameWarning } from "@cosyte/astm"; const w: AstmFrameWarning = { code: "ASTM_FRAME_BAD_CHECKSUM", message: "Frame checksum mismatch.", position: { frameNumber: 2, byteOffset: 251 }, }; ``` #### Properties ##### code > `readonly` **code**: [`FrameWarningCode`](#framewarningcode) ##### message > `readonly` **message**: `string` Human-readable detail for logs. Never contains the frame's record bytes. ##### position > `readonly` **position**: [`AstmFramePosition`](#astmframeposition) *** ### AstmLivdCatalogWarning A catalog-level terminology warning: a stable code, a value-free constant message, and the catalog's own declared identity. It carries no position, because no record is implicated, and it is never produced while a message is annotated. #### Example ```ts import type { AstmLivdCatalogWarning } from "@cosyte/astm"; const w: AstmLivdCatalogWarning = { code: "ASTM_LIVD_CATALOG_NO_LOINC_VERSION", message: "...", catalog: { declared: true, publisher: "Example Diagnostics", publicationVersion: "2026-01" }, }; ``` #### Properties ##### catalog > `readonly` **catalog**: [`LivdCatalogIdentity`](#livdcatalogidentity) What the catalog declared about itself, or the positive statement that it declared none. ##### code > `readonly` **code**: `"ASTM_LIVD_CATALOG_NO_LOINC_VERSION"` Always `ASTM_LIVD_CATALOG_NO_LOINC_VERSION`. ##### message > `readonly` **message**: `string` Human-readable detail for logs. A **constant**: never a test code, a value, a LOINC, or any text the consumer supplied. The catalog is named by the `catalog` field, not by this string. *** ### AstmLivdWarning A single terminology (LIVD) warning: a stable code, a value-free human-readable message, and positional context. Never carries a test code, a value, or a LOINC. #### Example ```ts import type { AstmLivdWarning } from "@cosyte/astm"; const w: AstmLivdWarning = { code: "ASTM_LIVD_UNMAPPED_CODE", message: "Reported test code had no LIVD mapping, surfaced unmapped, never a guessed LOINC.", position: { recordIndex: 3, recordType: "R" }, }; ``` #### Properties ##### code > `readonly` **code**: [`LivdWarningCode`](#livdwarningcode) ##### message > `readonly` **message**: `string` Human-readable detail for logs. Never contains a test code, a value, or a LOINC. ##### position > `readonly` **position**: [`AstmPosition`](#astmposition) *** ### AstmLtpWarning A single LTP protocol / transport warning: a stable code, a value-free human-readable message, and, for a frame-scoped deviation, the frame number only. Never carries a frame's record bytes. #### Example ```ts import type { AstmLtpWarning } from "@cosyte/astm"; const w: AstmLtpWarning = { code: "ASTM_LTP_FRAME_REJECTED", message: "Frame rejected, NAK sent, retransmit expected.", frameNumber: 2, }; ``` #### Properties ##### code > `readonly` **code**: [`LtpWarningCode`](#ltpwarningcode) ##### frameNumber? > `readonly` `optional` **frameNumber?**: `number` The frame's sequence number when the warning is frame-scoped; absent otherwise. ##### message > `readonly` **message**: `string` Human-readable detail for logs. Never contains the frame's record bytes. *** ### AstmMessage A parsed ASTM message: the header, the ordered records (the header is also `records[0]`), the resolved delimiters, and the accumulated warnings. #### Example ```ts import { parseAstmRecords } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rL|1\r"); msg.header.delimiters.field; // "|" msg.records.length; // 2 ``` #### Properties ##### classification > `readonly` **classification**: [`AstmMessageClassification`](#astmmessageclassification-1) The host-query classification of this message: whether it is a request (`Q` present), a result upload, an order download, or indeterminate. Gate on [AstmMessageClassification.isHostQueryRequest](#ishostqueryrequest) before reading records as results. ##### delimiters > `readonly` **delimiters**: [`Delimiters`](#delimiters-2) The delimiters the **first** header declared. A stream may carry several messages (`H` … `L`), and a later header may declare a different set, which is honored from that header onward. So this is not necessarily the set every record was read with: each header reports its own on [HeaderRecord.delimiters](#delimiters-3), and a change is flagged `ASTM_RECORD_DELIMITERS_REDECLARED`. ##### header > `readonly` **header**: [`HeaderRecord`](#headerrecord) The **first** header in the stream. A multi-header stream's later headers are in [AstmMessage.records](#records). ##### profile? > `readonly` `optional` **profile?**: `object` The vendor profile that was active for this parse, when one applied: its `name` and resolved `lineage` (attribution only; the profile object itself is not embedded). Absent when no profile applied. A profile that tolerated a deviation shows up as a `PROFILE_QUIRK_APPLIED` entry in [AstmMessage.warnings](#warnings-2), not here. ###### lineage > `readonly` **lineage**: readonly `string`[] ###### name > `readonly` **name**: `string` ##### records > `readonly` **records**: readonly [`AstmRecord`](#astmrecord)[] ##### warnings > `readonly` **warnings**: readonly [`AstmRecordWarning`](#astmrecordwarning)[] *** ### AstmMessageClassification The message-level classification from the host-query flow. `isHostQueryRequest` is the single boolean a consumer should gate on before treating records as results: it is `true` **iff** a `Q` record is present. #### Example ```ts import { parseAstmRecords } from "@cosyte/astm"; const req = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r"); req.classification.kind; // "host-query" req.classification.isHostQueryRequest; // true, never read its records as results ``` #### Properties ##### hasOrders > `readonly` **hasOrders**: `boolean` At least one `O` (order) record is present. ##### hasQuery > `readonly` **hasQuery**: `boolean` At least one `Q` (request-information) record is present. ##### hasResults > `readonly` **hasResults**: `boolean` At least one `R` (result) record is present. ##### hasUnrecognized > `readonly` **hasUnrecognized**: `boolean` At least one record's type letter was **not recognized** (an [UnsupportedRecord](#unsupportedrecord)), so the letter counts above are known to be incomplete. Any of them may have been a `Q`, which is why a message carrying one is classified `indeterminate` unless a `Q` was read outright. `ASTM_RECORD_UNKNOWN_TYPE` reports the same condition on [AstmMessage.warnings](#warnings-2). ##### isHostQueryRequest > `readonly` **isHostQueryRequest**: `boolean` `true` **iff** `kind === "host-query"` (a `Q` record was read), the safety surface: gate on this before treating records as results, so a query is never misread as a result upload. `false` is **not** a warrant that the message is a result set. Read `kind` for that: it is `indeterminate` whenever the reader could not account for every type letter. ##### kind > `readonly` **kind**: [`AstmMessageKind`](#astmmessagekind) The message kind. `indeterminate` also covers a message the reader declines to classify: when [AstmMessageClassification.hasUnrecognized](#hasunrecognized) is `true` and no `Q` was read, the kind is withheld rather than guessed, because the unreadable letter may have been that `Q`. The `has*` flags below stay truthful in that case, so a caller that wants the raw counts still has them. *** ### AstmParseOptions Options for [parseAstmRecords](#parseastmrecords). Lenient by default (Postel's Law). #### Properties ##### profile? > `readonly` `optional` **profile?**: [`AstmProfile`](#astmprofile) \| `null` An active vendor [AstmProfile](#astmprofile). Its quirk tolerances downgrade the deviations it *expects* to `PROFILE_QUIRK_APPLIED` warnings (values are never altered). An explicit profile wins over the process-scoped default ([setDefaultAstmProfile](#setdefaultastmprofile)); pass `null` to opt out of the default for a single call. Omit to use the default, or no profile when none is registered. ##### strict? > `readonly` `optional` **strict?**: `boolean` When `true`, escalate any Tier-2 deviation to a thrown [AstmStrictError](#astmstricterror) instead of accumulating a warning. Off by default. A deviation a profile *expected* (a `PROFILE_QUIRK_APPLIED`) does **not** escalate: it is known, benign, and still recorded. *** ### AstmPosition Where in a record stream a warning or fatal originated. Every field is positional; none is a value. #### Example ```ts import type { AstmPosition } from "@cosyte/astm"; const at: AstmPosition = { recordIndex: 3, recordType: "R", fieldIndex: 4 }; ``` #### Properties ##### componentIndex? > `readonly` `optional` **componentIndex?**: `number` 1-based component index within the field, when the deviation is component-scoped. ##### fieldIndex? > `readonly` `optional` **fieldIndex?**: `number` 1-based field index within the record (ASTM fields are 1-indexed). ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ##### recordType? > `readonly` `optional` **recordType?**: `string` The record's type letter (`H`/`P`/`O`/`R`/`L`/…), when known. *** ### AstmProfile A frozen, immutable vendor/conformance profile. Produced by [defineAstmProfile](#defineastmprofile); consumers pass it to `parseAstmRecords(raw, { profile })` (or register it as the process default), and feed its [AstmProfile.transport](#transport) to `detectFraming(bytes, { override })`. Hand-authoring the object literal is supported but discouraged: the factory validates the safety rules and attaches `describe()`. #### Example ```ts import { parseAstmRecords, astmProfiles } from "@cosyte/astm"; const msg = parseAstmRecords(raw, { profile: astmProfiles.referenceCorpus }); msg.profile?.name; // "referenceCorpus" ``` #### Properties ##### describe? > `readonly` `optional` **describe?**: () => `string` Multi-line human-readable summary; always present on factory-built profiles. ###### Returns `string` ##### description? > `readonly` `optional` **description?**: `string` Optional human-readable description. ##### lineage > `readonly` **lineage**: readonly `string`[] Resolved lineage: `[...parents, name]`, first-occurrence deduped. ##### name > `readonly` **name**: `string` The profile's unique name (registry key / attribution label). ##### provenance? > `readonly` `optional` **provenance?**: [`AstmProfileProvenance`](#astmprofileprovenance-1) The cited public grounding for this profile's quirks (absent for `default`). ##### tolerate > `readonly` **tolerate**: readonly [`AstmQuirkTolerance`](#astmquirktolerance)[] The expected, non-safety-critical deviations this profile tolerates. ##### transport? > `readonly` `optional` **transport?**: [`AstmFraming`](#astmframing) The raw-vs-framed-TCP override. When set, a consumer forces this framing via `detectFraming(bytes, { override: profile.transport })`, the way a profile that knows a vendor's transport reality (e.g. framing dropped over raw TCP) bypasses leading-byte auto-detection. Absent means "let detection decide." *** ### AstmProfileProvenance Provenance for an [AstmProfile](#astmprofile): the **real, cited public artifact** a profile's quirks are grounded in. A quirk is encoded only when a real (de-identified) document or a redistributable reference corpus grounds it; this record is where that grounding is stated, so a reviewer can trace every tolerated deviation back to evidence rather than invention. #### Example ```ts import type { AstmProfileProvenance } from "@cosyte/astm"; const prov: AstmProfileProvenance = { source: "kxepal/python-astm codec.py", reference: "https://github.com/kxepal/python-astm/blob/master/astm/codec.py", retrieved: "2026-07-21", }; ``` #### Properties ##### note? > `readonly` `optional` **note?**: `string` Optional clarifying note about what in the source grounds the quirks. ##### reference > `readonly` **reference**: `string` A citation the grounding can be traced to: a URL, DOI, or repo+path. ##### retrieved? > `readonly` `optional` **retrieved?**: `string` When the grounding was last verified (ISO date) or the pinned commit SHA. ##### source > `readonly` **source**: `string` Short human-readable name of the grounding source (corpus, transcript, or spec). *** ### AstmQuirkMatch Optional structural narrowing for an [AstmQuirkTolerance](#astmquirktolerance). When present, the tolerance applies only to warnings whose PHI-free [AstmPosition](#astmposition) matches every provided field, so a profile can expect a deviation on one record type (e.g. an unknown escape only inside `R` result values) without blanket-tolerating it everywhere. Matching is on **structural identifiers only** (record-type letter, 1-based field index); there is no matching on any field *value*, by construction. #### Example ```ts import type { AstmQuirkMatch } from "@cosyte/astm"; const onlyResultUnits: AstmQuirkMatch = { recordType: "R", fieldIndex: 5 }; ``` #### Properties ##### fieldIndex? > `readonly` `optional` **fieldIndex?**: `number` Match only warnings carrying this 1-based field index in their position. ##### recordType? > `readonly` `optional` **recordType?**: `string` Match only warnings carrying this record-type letter in their position. *** ### AstmQuirkTolerance One expected deviation declared by a profile. `code` names an **existing, non-safety-critical** warning code the profile expects; `rationale` documents why (grounded in the profile's [AstmProfileProvenance](#astmprofileprovenance-1)); optional `match` narrows it to a structural location. `defineAstmProfile` throws if `code` is safety-critical or is not a real warning code, or if `rationale` is empty. #### Example ```ts import type { AstmQuirkTolerance } from "@cosyte/astm"; const t: AstmQuirkTolerance = { code: "ASTM_UNKNOWN_ESCAPE_SEQUENCE", rationale: "Corpus stacks that treat '&' as literal data emit non-standard escape bodies.", }; ``` #### Properties ##### code > `readonly` **code**: [`AnyAstmWarningCode`](#anyastmwarningcode) The existing, non-safety-critical warning code this profile expects. ##### match? > `readonly` `optional` **match?**: [`AstmQuirkMatch`](#astmquirkmatch) Optional structural narrowing (record type / field index). ##### rationale > `readonly` **rationale**: `string` Why the profile expects this deviation: grounded in its provenance. *** ### AstmRecordWarning A single Tier-2 warning: a stable code, a value-free human-readable message, and positional context. Plain data, accumulated onto `AstmMessage.warnings`. #### Example ```ts import type { AstmRecordWarning } from "@cosyte/astm"; const w: AstmRecordWarning = { code: "ASTM_RECORD_UNKNOWN_TYPE", message: "Unknown record type.", position: { recordIndex: 2, recordType: "Z" }, }; ``` #### Properties ##### code > `readonly` **code**: [`WarningCode`](#warningcode) ##### expected? > `readonly` `optional` **expected?**: `boolean` `true` when an active vendor [AstmProfile](#astmprofile) *expected* this deviation and re-badged it as a [WARNING\_CODES.PROFILE\_QUIRK\_APPLIED](#profile_quirk_applied). An `expected` warning does **not** escalate to a thrown `AstmStrictError` in strict mode (the whole point of the profile is that this deviation is known and benign): it is still recorded, so nothing is hidden. Absent on an untolerated warning. ##### message > `readonly` **message**: `string` Human-readable detail for logs. Never contains a field value. ##### position > `readonly` **position**: [`AstmPosition`](#astmposition) ##### profile? > `readonly` `optional` **profile?**: `string` The name of the [AstmProfile](#astmprofile) that tolerated this warning, when `expected`. ##### toleratedCode? > `readonly` `optional` **toleratedCode?**: [`WarningCode`](#warningcode) When `code` is [WARNING\_CODES.PROFILE\_QUIRK\_APPLIED](#profile_quirk_applied), the original warning code the profile tolerated, so a consumer can still see *which* deviation was re-badged as expected. *** ### AstmStreamMessage One `H` … `L` message inside a parsed stream, with its own records and nothing else. This is the unit the flat extractors were always assumed to be reading. `AstmMessage` models the whole stream; this models a message within it. #### Properties ##### comments > `readonly` **comments**: readonly [`CommentRecord`](#commentrecord)[] Every `C` (comment) record in this message, in wire order. ##### delimiters > `readonly` **delimiters**: [`Delimiters`](#delimiters-2) The delimiter set in force for this message: the header's own resolved set, which is the set its records were actually read with. When a later header's declaration was unusable, this is the set that stayed in force, never a guessed one. ##### header > `readonly` **header**: [`HeaderRecord`](#headerrecord) This message's header record. ##### index > `readonly` **index**: `number` 0-based ordinal of this message within the stream. ##### orders > `readonly` **orders**: readonly [`OrderRecord`](#orderrecord)[] Every `O` (order) record in this message, in wire order. ##### patient > `readonly` **patient**: [`PatientRecord`](#patientrecord) \| `undefined` This message's patient, but **only when the message determines one**: the single `P` when it carries exactly one, and `undefined` when it carries none **or** several. `patients.length` distinguishes those two cases; there is no third meaning. A message carrying several patients is not answered with the first one, because "the first `P`" is precisely the guess that files a result against the wrong person. ##### patients > `readonly` **patients**: readonly [`PatientRecord`](#patientrecord)[] Every `P` record in this message, in wire order (usually zero or one). ##### queries > `readonly` **queries**: readonly [`QueryRecord`](#queryrecord)[] Every `Q` (request-information) record in this message, in wire order. For this message's host-query classification, call `classifyMessage(m.records)`. The `classification` on the parsed model is folded over the **whole stream**, so it is not per-message and is not mirrored here; deriving it from a message's own records is. ##### records > `readonly` **records**: readonly [`AstmRecord`](#astmrecord)[] Every record of this message in wire order, the header first. ##### results > `readonly` **results**: readonly [`ResultRecord`](#resultrecord)[] Every `R` (result) record in this message, in wire order. *** ### CommentInput Input for a `C` (comment) record. #### Properties ##### commentType? > `readonly` `optional` **commentType?**: `string` Field 5: comment type code, emitted verbatim. ##### seq? > `readonly` `optional` **seq?**: `string` ##### source? > `readonly` `optional` **source?**: `string` Field 3: comment source. ##### text? > `readonly` `optional` **text?**: `string` Field 4: comment text (a single component). Use [CommentInput.textComponents](#textcomponents) for a structured comment. ##### textComponents? > `readonly` `optional` **textComponents?**: readonly `string`[] Field 4: comment text as explicit components (takes precedence over `text` when set). ##### type > `readonly` **type**: `"C"` *** ### CommentRecord The `C` (comment) record: free-text context attached to a parent record. A comment is **attached by position** to the immediately-preceding `H`/`P`/`O`/`R` record ([CommentRecord.parentIndex](#parentindex)); consecutive comments share that parent. **Fail-safe:** a comment with no valid preceding parent is an **orphan**: it is attached to the message root ([CommentRecord.attachedToRoot](#attachedtoroot) `true`) and a value-free `ASTM_RECORD_ORPHAN_COMMENT` warning fires, **never silently dropped**, so a comment carrying (e.g.) "QC / non-compliant" context can never float away unnoticed. #### Extends - `RecordBase` #### Properties ##### attachedToRoot > `readonly` **attachedToRoot**: `boolean` `true` when no valid parent preceded: the comment is attached to the message root (and warned). ##### commentType? > `readonly` `optional` **commentType?**: `string` Field 5: comment type code, surfaced verbatim. **`[OSS-derived]`:** `I` (instrument) is the only value seen in the permissively-licensed real transcripts; other values (e.g. `G`/`T`/`P`) are defined only in the paywalled CLSI LIS02-A2 and are **not** interpreted here: the raw code is surfaced, never mapped to a guessed meaning. ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### parentIndex? > `readonly` `optional` **parentIndex?**: `number` The `recordIndex` of the `H`/`P`/`O`/`R` record this comment is attached to, or `undefined` when the comment is an orphan attached to the message root (see [CommentRecord.attachedToRoot](#attachedtoroot)). ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### seq? > `readonly` `optional` **seq?**: `string` Field 2: sequence number. ##### source? > `readonly` `optional` **source?**: `string` Field 3: comment source (who/what produced it), surfaced verbatim. ##### text? > `readonly` `optional` **text?**: `string` Field 4: the comment text, surfaced as the **full** field text (all components), never truncated to the first component. The component structure is in [CommentRecord.textComponents](#textcomponents-1). ##### textComponents? > `readonly` `optional` **textComponents?**: readonly `string`[] Field 4: the comment text split into its decoded components (comment text is component-capable; multiple components are a normal structured comment, **not** an ambiguity). Present only when the field carried more than one component. ##### type > `readonly` **type**: `"C"` The record's raw type letter. ###### Overrides `RecordBase.type` *** ### ComposeFramesOptions Options for [composeAstmFrames](#composeastmframes). #### Properties ##### startFrameNumber? > `readonly` `optional` **startFrameNumber?**: `number` The frame number to start the sequence at: a whole number from `0` to `7`. Defaults to `1`, the number ASTM gives the first frame of a transfer and the one [decodeAstmFrames](#decodeastmframes) expects to read first. **Any other value writes a continuation rather than the start of a transfer, and that is what the option is for**: composing one transfer across more than one call. Concatenating `composeAstmFrames(head)` with `composeAstmFrames(tail, { startFrameNumber: n })`, where `n` is the number after the last frame `head` used, is byte-identical to composing the whole list in a single call and decodes with an empty warnings array, across the `7 → 0` rollover included. **Decoded on its own, a stream that starts anywhere but `1` opens on a sequence gap.** The decoder never bridges a gap silently, so it warns (`ASTM_FRAME_SEQUENCE_GAP`) and does not emit that first record. **What [parseFramedAstm](#parseframedastm) does after that varies with the message shape and no rule is offered for it here.** It may throw, under more than one code, and it may return a message that is simply one record short. **The one thing that does hold is that the record layer never reports the loss**: `parseFramedAstm` hands the record parser only the frames the codec vouched for, so `message.warnings` carries what the surviving records warrant and nothing about the record that did not survive. **Read `frameWarnings`.** That is the cost of the option rather than a defect in the caller's records, and it is why `1` is the default. **Finding the number to continue from.** Nothing here returns it, and the frame count is not it once a record splits: decode the part you just composed and read the last frame's number, then add one modulo 8, as in `((decodeAstmFrames(part).frames.at(-1)?.frameNumber ?? 0) + 1) % 8`. A frame carries no number only when the stream ends immediately after its `STX`, which is not something this encoder writes, so that fallback never fires on a part composed here. Getting the number wrong costs the record at the join, warned rather than silently. A value outside `0`-`7`, or one that is not a whole number, is refused with `ASTM_FRAME_INVALID_START_FRAME_NUMBER`. *** ### DateParts The calendar components a parsed value actually stated: the shared return shape of [toObject](#toobject) across every `@cosyte` parser. Every value is a `number` and `month` is **spec-native 1 to 12**, never the JS `Date` 0 to 11. A component the value did not state is **absent**: the key is not present at all, rather than present and `undefined`, so `Object.keys()` of the result is exactly the set of stated components and the value's precision is recoverable from it. There is no `precision`, `raw`, `valid` or `truncated` key: this is the calendar reading, not the parse record. Deleting `offsetMinutes` leaves an object `Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject` accept with no key rename and no value adjustment, which is why the keys are singular and the month is 1-based. Neither library is a dependency of this package: the shape is the interop, not an import. `millisecond` and `offsetMinutes` belong to the shared shape and are **never populated by this package**: an ASTM timestamp carries no timezone, and this parser retains no fractional-second component (the digits stay in [AstmDate.raw](#raw-1)). They are declared so that code written over two `@cosyte` parsers reads one shape rather than two. #### Properties ##### day? > `readonly` `optional` **day?**: `number` Day of month, 1 to 31. ##### hour? > `readonly` `optional` **hour?**: `number` Hour of day, 0 to 23. ##### millisecond? > `readonly` `optional` **millisecond?**: `number` Millisecond. Never populated here: this parser retains no fractional second. ##### minute? > `readonly` `optional` **minute?**: `number` Minute of hour, 0 to 59. ##### month? > `readonly` `optional` **month?**: `number` Calendar month, **1 to 12**, as the standard states it. ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` Signed minutes east of UTC. Never populated here: ASTM states no offset. ##### second? > `readonly` `optional` **second?**: `number` Second of minute, 0 to 59. ##### year? > `readonly` `optional` **year?**: `number` Four-digit calendar year, exactly as stated (`50` is the year 50, not 1950). *** ### DecodeAstmFramesResult The result of [decodeAstmFrames](#decodeastmframes): the reassembled record bytes, every decoded frame, and the accumulated frame warnings. #### Properties ##### frames > `readonly` **frames**: readonly [`AstmFrame`](#astmframe)[] Every decoded frame, trusted or not, in wire order. ##### records > `readonly` **records**: readonly `Uint8Array`\<`ArrayBufferLike`\>[] The reassembled record byte-strings: one entry per complete record (a run of frames closed by an `ETX`), in wire order. **Only** clean reassemblies appear: a record whose frames included a bad checksum, a sequence gap, or an unterminated frame is **omitted** (its frames are still in [DecodeAstmFramesResult.frames](#frames), flagged). Each entry is ready to hand to `parseAstmRecords`. ##### warnings > `readonly` **warnings**: readonly [`AstmFrameWarning`](#astmframewarning)[] The frame warnings accumulated during a lenient decode (empty in a clean decode). *** ### DefineAstmProfileOptions Options accepted by [defineAstmProfile](#defineastmprofile). Mirrors the [AstmProfile](#astmprofile) shape minus the derived `lineage`/`describe`, plus the `extends` input key. Every field except `name` is optional. #### Example ```ts import { defineAstmProfile, type DefineAstmProfileOptions } from "@cosyte/astm"; const opts: DefineAstmProfileOptions = { name: "my-analyzer", transport: "raw", tolerate: [{ code: "ASTM_NONSTANDARD_DELIMITERS", rationale: "declares its own set" }], }; const p = defineAstmProfile(opts); ``` #### Properties ##### description? > `readonly` `optional` **description?**: `string` ##### extends? > `readonly` `optional` **extends?**: [`AstmProfile`](#astmprofile) \| readonly [`AstmProfile`](#astmprofile)[] ##### name > `readonly` **name**: `string` ##### provenance? > `readonly` `optional` **provenance?**: [`AstmProfileProvenance`](#astmprofileprovenance-1) ##### tolerate? > `readonly` `optional` **tolerate?**: readonly [`AstmQuirkTolerance`](#astmquirktolerance)[] ##### transport? > `readonly` `optional` **transport?**: [`AstmFraming`](#astmframing) *** ### Delimiters The four ASTM delimiters resolved from an `H` record. Immutable; carried on the parsed message as delimiter provenance. #### Example ```ts import { readDelimiters } from "@cosyte/astm"; const d = readDelimiters("H|\\^&"); d.field; // "|" d.repeat; // "\\" d.component; // "^" d.escape; // "&" ``` #### Properties ##### component > `readonly` **component**: `string` Component separator: ASTM `^` by default. ##### escape > `readonly` **escape**: `string` Escape character: ASTM `&` by default (introduces `&F&`/`&S&`/`&R&`/`&E&`). ##### field > `readonly` **field**: `string` Field separator: the char immediately after `H` in the header. ##### repeat > `readonly` **repeat**: `string` Repeat (repetition) separator: ASTM `` by default. *** ### DetectFramingOptions Options for [detectFraming](#detectframing). #### Properties ##### override? > `readonly` `optional` **override?**: [`AstmFraming`](#astmframing) A profile-supplied override. When set, detection is bypassed entirely and this value is returned with no warning: the way a vendor profile forces raw for a cobas b121 or framed for a cobas 4800 regardless of the leading byte. *** ### DetectFramingResult The result of [detectFraming](#detectframing): the decided framing plus any warning (exactly one `ASTM_LTP_AMBIGUOUS_TRANSPORT` when the lead was unrecognizable and the mode was defaulted; empty otherwise). #### Properties ##### defaulted > `readonly` **defaulted**: `boolean` `true` when the lead byte was unrecognizable and `framing` was defaulted (not inferred). ##### framing > `readonly` **framing**: [`AstmFraming`](#astmframing) The decided transport framing. ##### warnings > `readonly` **warnings**: readonly [`AstmLtpWarning`](#astmltpwarning)[] The detection warnings (a single ambiguity warning, or none). *** ### FrameChecksum A frame's checksum verdict: the modulo-256 value recomputed from the frame's bytes, the value declared on the wire (or `undefined` when it was unreadable), and whether they matched. #### Properties ##### computed > `readonly` **computed**: `number` The modulo-256 checksum recomputed over the frame (frame number through terminator). ##### declared? > `readonly` `optional` **declared?**: `number` The checksum declared on the wire (the two hex chars after the terminator), read case-insensitively; `undefined` when those bytes were missing or not hex. ##### valid > `readonly` **valid**: `boolean` `true` only when a declared checksum was present **and** equal to `computed`. *** ### FramedAstmResult The result of [parseFramedAstm](#parseframedastm): the parsed message, plus the frame-layer detail (every decoded frame and the frame warnings) so a consumer keeps full visibility into the transport below the records. #### Properties ##### frames > `readonly` **frames**: readonly [`AstmFrame`](#astmframe)[] Every decoded frame, trusted or not, in wire order. ##### frameWarnings > `readonly` **frameWarnings**: readonly [`AstmFrameWarning`](#astmframewarning)[] The frame-layer warnings (bad checksum, sequence gap, unterminated, oversize). ##### message > `readonly` **message**: [`AstmMessage`](#astmmessage) The message parsed from the trusted, reassembled record bytes. *** ### FrameOptions Options for [decodeAstmFrames](#decodeastmframes). Lenient by default (Postel's Law). #### Properties ##### strict? > `readonly` `optional` **strict?**: `boolean` When `true`, escalate any tolerated frame deviation (bad checksum, sequence gap, unterminated, oversize) to a thrown [AstmFrameStrictError](#astmframestricterror) instead of accumulating a warning. Off by default. *** ### HeaderInput Optional header fields (after the delimiter declaration). #### Properties ##### fields? > `readonly` `optional` **fields?**: readonly `string`[] Extra `H`-record fields (field 3 onward), emitted verbatim in order. Field 2 is always `\^&`. *** ### HeaderRecord The `H` (header) record. Carries the delimiters it declared as provenance. #### Extends - `RecordBase` #### Properties ##### delimiters > `readonly` **delimiters**: [`Delimiters`](#delimiters-2) The four delimiters **this** header put into force: the set its own fields, and every record after it, were read with. A stream may carry several messages (`H` … `L`) and each header declares its own set, so on a multi-header stream these can differ between headers; [AstmMessage.delimiters](#delimiters) is the first header's. When a later header cannot declare a usable set, the set already in force is kept and reported here (with an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning) rather than a guessed one. ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### rawLine > `readonly` **rawLine**: `string` The header's exact wire text (terminator excluded), kept as **provenance**: the delimiters are read from this raw text, never from [RecordBase.fields](#fields-2). It is **not** what the serializer emits from. To change a header field, edit [RecordBase.fields](#fields-2): `fields[0]` is the type letter, `fields[1]` is the delimiter declaration, and the header's ASTM data fields follow from `fields[2]`. Editing this string instead has no effect on emit. ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### type > `readonly` **type**: `"H"` The record's raw type letter. ###### Overrides `RecordBase.type` *** ### LivdAnnotation One record's LIVD annotation: which record, the code the catalog was consulted with (verbatim), how it was recognized, the lookup outcome, and the two facts about a populated component 1 that ride alongside **any** outcome. Additive: it points at the record by index and never replaces it. #### Example ```ts import type { LivdAnnotation } from "@cosyte/astm"; const a: LivdAnnotation = { recordIndex: 3, recordType: "R", reportedCode: "687", unvalidatedWireValue: "Glucose", wireValueDisagreesWithCatalog: true, provenance: "local-code", mapping: { status: "mapped", loinc: "1920-8", source: "livd", derived: true }, }; ``` #### Properties ##### catalogLoincVersion? > `readonly` `optional` **catalogLoincVersion?**: `string` The **LOINC version the catalog declared**, verbatim, carried on **every** annotation that catalog produced, whatever the outcome of the lookup. It is what makes a mapping reproducible: the reader can see which version of LOINC the consumer's catalog says it was built against. **It asserts nothing about this LOINC.** It is provenance about the CATALOG, not a claim that any LOINC was checked, found, or validated against that version: this package performs no LOINC validation of any kind, and nothing here reads the string beyond carrying it. Absent when the catalog declared no LOINC version (including a catalog a consumer implemented by hand, which declares none), never a default, a placeholder or an empty string. ##### mapping > `readonly` **mapping**: [`LivdMapping`](#livdmapping) The lookup outcome: never a guessed LOINC. ##### provenance > `readonly` **provenance**: [`UniversalTestIdProvenance`](#universaltestidprovenance-1) How the identifier was recognized in the Universal Test ID (provenance only, never a lookup). ##### recordIndex > `readonly` **recordIndex**: `number` The `recordIndex` of the annotated `R`/`O` record. ##### recordType > `readonly` **recordType**: `"O"` \| `"R"` The annotated record's type. ##### reportedCode? > `readonly` `optional` **reportedCode?**: `string` The vendor/local code the catalog was consulted with, verbatim. Absent when no vendor/local code was present, because then nothing was looked up. It is never a component 1 value. ##### unvalidatedWireValue? > `readonly` `optional` **unvalidatedWireValue?**: `string` Component 1 when populated, verbatim: a wire value this library **does not vouch for**, carried on every outcome and never validated, never promoted to a LOINC, and never used as a lookup key. Absent when component 1 is empty. ##### wireValueDisagreesWithCatalog > `readonly` **wireValueDisagreesWithCatalog**: `boolean` `true` if and only if the catalog vouched for exactly one LOINC for the vendor/local code, component 1 is populated, and that value is not byte-identical to that LOINC. `false` in **every** other case, including where the catalog vouched for no single LOINC (a miss, an ambiguity, no vendor code, no code at all): asserting a disagreement there would claim the catalog spoke about a code it never spoke about. It reports the difference and **nothing else**. Neither value is marked correct, neither is suppressed, and no field says the difference was settled. *** ### LivdCandidate One candidate mapping behind an `ambiguous` answer: the LOINC plus the LIVD attributes that tell candidates apart, so a human can choose what this package refuses to guess. One per catalog row for the vendor code, in catalog order, so a LOINC appearing in two rows appears twice. Every candidate is surfaced, including one that is not unit qualified: it can never be selected on a unit, but hiding it would hide a mapping the consumer's own catalog holds. #### Example ```ts import type { LivdCandidate } from "@cosyte/astm"; const c: LivdCandidate = { loinc: "2345-7", vendorSpecimenDescription: "Serum or Plasma", representativeUnit: "mg/dL", unitQualified: true, }; ``` #### Properties ##### loinc > `readonly` **loinc**: `string` The candidate LOINC, verbatim from the catalog row. Never validated. ##### loincLongName? > `readonly` `optional` **loincLongName?**: `string` The row's LOINC Long Common Name, when supplied. ##### representativeUnit? > `readonly` `optional` **representativeUnit?**: `string` The row's representative unit, verbatim, when supplied. ##### unitQualified > `readonly` **unitQualified**: `boolean` `false` when the representative unit is absent, empty or whitespace only. Such a candidate is **never** selected by a unit comparison against any reported units, and is surfaced here anyway. ##### vendorResultDescription? > `readonly` `optional` **vendorResultDescription?**: `string` The row's Vendor Result Description, verbatim, when supplied. Never matched on. ##### vendorSpecimenDescription? > `readonly` `optional` **vendorSpecimenDescription?**: `string` The row's Vendor Specimen Description, verbatim, when supplied. Never matched on. *** ### LivdCatalog An immutable, consumer-supplied LIVD catalog. Look a vendor code up with [LivdCatalog.lookup](#lookup); the catalog **never** picks between conflicting LOINCs and **never** mutates. Build one with [defineLivdCatalog](#definelivdcatalog). #### Properties ##### publication? > `readonly` `optional` **publication?**: [`LivdPublication`](#livdpublication) The publication-level metadata the consumer declared, each value **verbatim**. Absent where the catalog declared none, and each field inside it absent where that value was not declared: a blank is never stored as a blank, and nothing is ever defaulted. Optional on the interface, so a catalog a consumer implements by hand needs no metadata and stays source compatible. ##### size > `readonly` **size**: `number` The number of distinct vendor codes indexed (not the number of input rows). ##### warnings? > `readonly` `optional` **warnings?**: readonly [`AstmLivdCatalogWarning`](#astmlivdcatalogwarning)[] The value-free warnings raised where this catalog was **defined**, in the order they were raised. [defineLivdCatalog](#definelivdcatalog) always sets it, empty where it had nothing to say. These are facts about the CATALOG, so they never join the per-record warning stream [applyLivd](#applylivd) returns and they implicate no record. Optional on the interface, so a hand-implemented catalog that offers none stays source compatible. #### Methods ##### lookup() > **lookup**(`vendorCode`, `reportedUnits?`): [`LivdLookup`](#livdlookup) Look a vendor code up, verbatim (exact, case-sensitive). Returns `mapped` on a single-LOINC hit, `unmapped` on a miss, and `ambiguous` when the code carries more than one distinct LOINC that the reported units did not settle: never a guess. `reportedUnits` are the units the `R` record carried, verbatim. They are compared with each candidate's [LivdEntry.representativeUnit](#representativeunit-1) by exact, case-sensitive string equality and by nothing else. Omitting them, or passing an empty or whitespace-only string, means **no units were reported**: no unit-qualified candidate is chosen and a code carrying several candidates stays `ambiguous`. A code carrying exactly one candidate LOINC is answered either way. Implementing this interface by hand stays source compatible: a `lookup` declared with the vendor code alone simply ignores the units, and answers as it always did. ###### Parameters ###### vendorCode `string` The reported vendor/local test code. ###### reportedUnits? `string` The units the record reported, verbatim, when it reported any. ###### Returns [`LivdLookup`](#livdlookup) The lookup outcome. *** ### LivdEntry One LIVD mapping row a consumer supplies: a vendor test code and the LOINC it maps to, plus optional human-readable / provenance fields. Modeled on the IICC LIVD digital format's data elements. #### Example ```ts import type { LivdEntry } from "@cosyte/astm"; const e: LivdEntry = { vendorCode: "687", loinc: "1920-8", loincLongName: "AST" }; ``` #### Properties ##### loinc > `readonly` **loinc**: `string` The **LOINC Code** this vendor code maps to (e.g. `"1920-8"`). Taken from the consumer's catalog as-is and **never validated, altered, or invented**: the parser does not ship a LOINC table and cannot check it; it only carries what the catalog says. ##### loincLongName? > `readonly` `optional` **loincLongName?**: `string` The **LOINC Long Common Name**, when the catalog supplies it: an optional human-readable label. ##### manufacturer? > `readonly` `optional` **manufacturer?**: `string` The device **Manufacturer**, when the catalog scopes the mapping to a device (optional provenance). ##### model? > `readonly` `optional` **model?**: `string` The device **Model**, when the catalog scopes the mapping to a device (optional provenance). ##### representativeUnit? > `readonly` `optional` **representativeUnit?**: `string` The **representative unit of measure** for this mapping, preferably a UCUM unit (e.g. `"mg/dL"`, `"mmol/L"`, `"mmol/(24.h)"`). This is the **one** attribute a lookup ever selects on: when a vendor analyte code carries several candidate LOINCs, the candidate whose representative unit is **exactly equal** to the units the `R` record reported is chosen, compared verbatim and case sensitively with no normalization, conversion or scale factor on either side. Absent, empty or whitespace only means **not unit qualified**: such a candidate is never selected by a unit comparison, though it is still surfaced among an ambiguous answer's candidates. Optional, so an existing catalog keeps working unchanged. ##### vendorAnalyteName? > `readonly` `optional` **vendorAnalyteName?**: `string` The **Vendor Analyte Name**: the vendor's human-readable analyte label, when supplied. ##### vendorCode > `readonly` **vendorCode**: `string` The **Vendor Analyte Code**: the vendor transmission code the instrument sends (the local code in an ASTM Universal Test ID, component 4). The mapping key; compared **verbatim** (exact, case-sensitive) against the reported code, never normalized or fuzzy-matched. ##### vendorResultDescription? > `readonly` `optional` **vendorResultDescription?**: `string` The **Vendor Result Description**: the vendor's own human-readable text for the result this mapping produces (binary, ordinal, nominal, or a numeric result with its unit). Carried **verbatim** and surfaced for a human to read; **never matched on**, for the same reason as [LivdEntry.vendorSpecimenDescription](#vendorspecimendescription-1). Optional. ##### vendorSpecimenDescription? > `readonly` `optional` **vendorSpecimenDescription?**: `string` The **Vendor Specimen Description**: the vendor's own human-readable text for the specimen this mapping is for, such as `"Serum or Plasma"`. Carried **verbatim** (never trimmed, case folded or normalized) and surfaced for a human to read. **Never matched on**: the mapping guide states this text is not intended to be parsed by software that automates the mapping, so choosing on it would be a string guess. Optional, so an existing catalog keeps working unchanged. *** ### LivdPublication The publication-level metadata a consumer declares about the LIVD publication their catalog was built from: what the mapping guide defines about the publication rather than about any one row. Every field is **optional** and every declared value is preserved **verbatim**: nothing is trimmed, case folded, reordered, reformatted, defaulted or validated, and no value is ever rejected, corrected or warned about on account of its content or its shape. A value that is absent, empty or whitespace only is one case, **no value declared**, which is the only inspection any of these strings gets. **None of it is checked, and none of it is a claim by this package.** This library is not the LOINC licensee and performs no LOINC validation of any kind, so it cannot tell a real LOINC version from a typed one and does not try. #### Example ```ts import type { LivdPublication } from "@cosyte/astm"; const p: LivdPublication = { publisher: "Example Diagnostics", loincVersion: "2.78" }; ``` #### Properties ##### loincCopyright? > `readonly` `optional` **loincCopyright?**: `string` The **LOINCCopyright**: the attribution statement the LOINC license requires beside content that carries LOINC codes. Stored verbatim so it travels with the mapping it applies to. **Carrying it is not discharging it.** The obligation is the consumer's, this package supplies no statement on anyone's behalf, and it checks neither the presence nor the wording of one. ##### loincVersion? > `readonly` `optional` **loincVersion?**: `string` The **LOINC Version ID**: the version of LOINC the mapping was made against. Carried verbatim onto every annotation this catalog produces ([LivdAnnotation.catalogLoincVersion](#catalogloincversion)), so a mapping can be reproduced later. It is **provenance the consumer declared**, never a statement that any LOINC was checked against it: nothing here validates a LOINC or a LOINC version. Declaring none is allowed and surfaces a value-free warning on [LivdCatalog.warnings](#warnings-5); the catalog is still built and every answer is unchanged. ##### publicationVersion? > `readonly` `optional` **publicationVersion?**: `string` The **Publication Version ID**: human-readable information the vendor provides that tells one LIVD publication version from another. Verbatim, never parsed and never ordered against another. ##### publisher? > `readonly` `optional` **publisher?**: `string` The **Publisher**: the entity publishing the mapping information. Verbatim, never checked. *** ### LivdResult The result of [applyLivd](#applylivd): the per-record annotations (one per `R`/`O` record) and the value-free warnings for every unmapped or ambiguous code. Both arrays are deeply frozen; the source message is untouched. #### Properties ##### annotations > `readonly` **annotations**: readonly [`LivdAnnotation`](#livdannotation)[] One annotation per `R`/`O` record, in wire order. ##### warnings > `readonly` **warnings**: readonly [`AstmLivdWarning`](#astmlivdwarning)[] A value-free warning per `unmapped`/`ambiguous` code: never per `mapped`, and never per `no-vendor-code`/`no-code`, where no lookup happened at all. Per-record only. A warning about the CATALOG, such as one declaring no LOINC version, is raised where the catalog is defined and stays on [LivdCatalog.warnings](#warnings-5): it implicates no record, so it never joins this stream and never changes its codes, its order or its length. *** ### LivdUnitComparison How a candidate was selected on its unit, stated on the answer so a consumer cannot read a matched unit as a UCUM conformance claim this package does not make. Present on a [LivdLookup](#livdlookup) **only** when a unit comparison actually chose between candidate LOINCs. Its whole content is a disclosure: the comparison was a verbatim, case-sensitive string equality, and **not** a UCUM semantic comparison. Nothing was normalized, case folded, scaled or converted on either side. #### Example ```ts import type { LivdUnitComparison } from "@cosyte/astm"; const c: LivdUnitComparison = { comparison: "verbatim-case-sensitive", ucumSemantic: false, reportedUnits: "mg/dL", representativeUnit: "mg/dL", note: "...", }; ``` #### Properties ##### comparison > `readonly` **comparison**: `"verbatim-case-sensitive"` Always `"verbatim-case-sensitive"`: an exact string equality, nothing normalized. ##### note > `readonly` **note**: `string` A human-readable restatement of the two facts above, for a log or a review screen. ##### reportedUnits > `readonly` **reportedUnits**: `string` The units the record reported, verbatim. ##### representativeUnit > `readonly` **representativeUnit**: `string` The chosen candidate's representative unit, verbatim. Byte-identical to `reportedUnits`. ##### ucumSemantic > `readonly` **ucumSemantic**: `false` Always `false`. This package does not compare unit expressions by their semantics. *** ### LtpState The reducer's immutable session state. Every [ltpReduce](#ltpreduce) call returns a new, frozen `LtpState`; the previous one is never mutated. #### Properties ##### expectedFrame > `readonly` **expectedFrame**: `number` The next frame number the receiver expects (`1 → … → 7 → 0 → …`), meaningful in `transfer`. A trusted frame carrying this number is accepted and appended; any other number is a duplicate retransmit (idempotent re-`ACK`) or an out-of-sequence frame (rejected with `NAK`), never silently bridged. ##### lastAcceptedFrame? > `readonly` `optional` **lastAcceptedFrame?**: `number` The frame number of the last frame accepted into the current record, used to recognise a duplicate retransmit; `undefined` before the first frame of a transfer is accepted. ##### openRecord > `readonly` **openRecord**: `Uint8Array` The bytes accumulated so far for the in-progress record: the concatenation of the `ETB` frames accepted since the last `ETX`. Empty when no record is open. These bytes are **never** delivered on their own: only an `ETX` completes a record, at which point they (plus the final frame's text) become one entry in [LtpState.records](#records-3). An `EOT` or an `ENQ` restart discards them unread. ##### phase > `readonly` **phase**: [`LtpPhase`](#ltpphase) The current protocol phase. ##### recordOpen > `readonly` **recordOpen**: `boolean` `true` when a record is mid-reassembly (an `ETB` frame was accepted, awaiting its `ETX`). ##### records > `readonly` **records**: readonly `Uint8Array`\<`ArrayBufferLike`\>[] The reassembled bytes of every **complete** record delivered so far in the session (each closed by an `ETX` frame with a clean run). Concatenate these and hand them to `parseAstmRecords` to get the message. Only trusted, in-sequence frames contribute; a rejected or partial record never appears. *** ### LtpTransition The result of one [ltpReduce](#ltpreduce) step: the next [LtpState](#ltpstate), the [LtpAction](#ltpaction)s to perform (in order), and any [AstmLtpWarning](#astmltpwarning)s raised. #### Properties ##### actions > `readonly` **actions**: readonly [`LtpAction`](#ltpaction)[] The actions the consumer should take, in order. ##### state > `readonly` **state**: [`LtpState`](#ltpstate) The next session state (frozen). ##### warnings > `readonly` **warnings**: readonly [`AstmLtpWarning`](#astmltpwarning)[] Warnings raised by this step (empty on a clean, expected event). *** ### ManufacturerRecord The `M` (manufacturer) record: vendor-defined free-form data (QC / calibration / maintenance), surfaced **VERBATIM** and **never interpreted into typed clinical fields**. Interpreting a vendor `M` record as clinical data would be a fabrication, so this record carries **no typed accessors at all**: the exact wire text is in [ManufacturerRecord.rawLine](#rawline-1) (byte-preserving) and the tokenized tree in [RecordBase.fields](#fields-2). Nothing is parsed into a value, a code, or a unit. #### Extends - `RecordBase` #### Properties ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### rawLine > `readonly` **rawLine**: `string` The record's exact wire text (terminator excluded), preserved byte-for-byte. Emit reproduces these bytes exactly whenever a reader using the delimiters being emitted against would recover the fields this record models: always the case when the record is already in those delimiters, and also when it carries no delimiter either set would split on. Otherwise the record is re-encoded from [RecordBase.fields](#fields-2), so the row can never go out in delimiters the header does not declare. To change a value, edit `fields`; editing this string has no effect on emit. ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### type > `readonly` **type**: `"M"` The record's raw type letter. ###### Overrides `RecordBase.type` *** ### MessageInput The message to build: an optional header, the body records, and an optional terminator code. #### Properties ##### header? > `readonly` `optional` **header?**: [`HeaderInput`](#headerinput) Header fields; the canonical `H|\^&` declaration is always emitted. ##### records > `readonly` **records**: readonly [`AstmRecordInput`](#astmrecordinput)[] The body records, in order. `H` and `L` are supplied by the builder. ##### terminationCode? > `readonly` `optional` **terminationCode?**: `string` Field 3 of the auto-appended `L` (terminator) record: the termination code (e.g. `N` normal). Omitted by default (not defaulted to a value the caller did not choose); the `L` seq is always emitted. *** ### NamedVocabulary A published code system this library graded a value against, named by identifier and version. #### Example ```ts import { ABNORMAL_FLAG_VOCABULARY } from "@cosyte/astm"; ABNORMAL_FLAG_VOCABULARY.attributed; // true ABNORMAL_FLAG_VOCABULARY.version; // the version compared against ``` #### Properties ##### attributed > `readonly` **attributed**: `true` Always `true`: a published source is cited. ##### system > `readonly` **system**: `string` The code system's canonical identifier, verbatim from the published source. ##### version > `readonly` **version**: `string` The code system version this library compared against, verbatim from the published source. **Not** a claim about the version the sender used. *** ### OrderInput Input for an `O` (order) record. #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` Field 12: action code, emitted verbatim. ##### instrumentSpecimenId? > `readonly` `optional` **instrumentSpecimenId?**: `string` Field 4: instrument specimen ID. ##### priority? > `readonly` `optional` **priority?**: `string` Field 6: priority, emitted verbatim. ##### reportType? > `readonly` `optional` **reportType?**: `string` Field 26: report type, emitted verbatim. ##### seq? > `readonly` `optional` **seq?**: `string` ##### specimenId? > `readonly` `optional` **specimenId?**: `string` Field 3: specimen / accession ID. ##### type > `readonly` **type**: `"O"` ##### universalTestId? > `readonly` `optional` **universalTestId?**: readonly `string`[] Field 5: Universal Test ID components (verbatim, in order; e.g. `["", "", "", "687"]`). *** ### OrderRecord The `O` (order) record: binds a result to a specimen. #### Extends - `RecordBase` #### Properties ##### actionCode? > `readonly` `optional` **actionCode?**: `string` Field 12: action code, surfaced raw (e.g. `C` cancel, `A` add, `N` new). The exact field index (`~12`) and the code set are `[OSS-derived]` (paywalled): surfaced verbatim, never interpreted. ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### instrumentSpecimenId? > `readonly` `optional` **instrumentSpecimenId?**: `string` Field 4: instrument specimen ID. ##### priority? > `readonly` `optional` **priority?**: `string` Field 6: priority, surfaced raw (e.g. `S` STAT, `R` routine, `A` ASAP). Vendor letters vary; the code set is `[OSS-derived]` (the exact enumeration is in the paywalled CLSI LIS02-A2), so the value is surfaced verbatim and **never mapped to a guessed meaning**. ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### reportType? > `readonly` `optional` **reportType?**: `string` Field 26: report type, surfaced raw (e.g. `F` final, `P` preliminary, `X` cancel). The exact field index (`~26`) and the code set are `[OSS-derived]` (paywalled): surfaced verbatim. ##### seq? > `readonly` `optional` **seq?**: `string` Field 2: sequence number. ##### specimenId? > `readonly` `optional` **specimenId?**: `string` Field 3: specimen / accession ID. ##### type > `readonly` **type**: `"O"` The record's raw type letter. ###### Overrides `RecordBase.type` ##### universalTestId? > `readonly` `optional` **universalTestId?**: [`UniversalTestId`](#universaltestid-6) Field 5: Universal Test ID (same caret structure as a result's). *** ### PatientInput Input for a `P` (patient) record. The three IDs stay distinct: none defaults from another. #### Properties ##### birthDate? > `readonly` `optional` **birthDate?**: `string` Field 8: birthdate (`YYYYMMDDHHMMSS`), emitted verbatim, never reformatted. ##### laboratoryAssignedId? > `readonly` `optional` **laboratoryAssignedId?**: `string` Field 4: laboratory-assigned patient ID. ##### mothersMaidenName? > `readonly` `optional` **mothersMaidenName?**: `string` Field 7: mother's maiden name. ##### name? > `readonly` `optional` **name?**: [`PatientNameInput`](#patientnameinput) Field 6: patient name (`Last^First^Middle`). ##### patientIdThree? > `readonly` `optional` **patientIdThree?**: `string` Field 5: a third patient identifier. ##### practiceAssignedId? > `readonly` `optional` **practiceAssignedId?**: `string` Field 3: practice-assigned patient ID. ##### seq? > `readonly` `optional` **seq?**: `string` Structural sequence number; auto-computed when omitted. ##### sex? > `readonly` `optional` **sex?**: `string` Field 9: sex, emitted verbatim (never defaulted). ##### type > `readonly` **type**: `"P"` *** ### PatientName A patient name (`Last^First^Middle`), each component surfaced verbatim. #### Properties ##### first? > `readonly` `optional` **first?**: `string` ##### last? > `readonly` `optional` **last?**: `string` ##### middle? > `readonly` `optional` **middle?**: `string` ##### raw > `readonly` **raw**: `string` *** ### PatientNameInput A patient name split into its components; only the supplied parts are emitted. #### Properties ##### first? > `readonly` `optional` **first?**: `string` ##### last? > `readonly` `optional` **last?**: `string` ##### middle? > `readonly` `optional` **middle?**: `string` *** ### PatientRecord The `P` (patient) record. **Safety:** the practice-assigned ID (field 3) and the laboratory-assigned ID (field 4) are modeled as **distinct** fields and never collapsed, conflating them is the primary result-misfiling path. #### Extends - `RecordBase` #### Properties ##### birthDate? > `readonly` `optional` **birthDate?**: [`AstmDate`](#astmdate) Field 8: birthdate (`YYYYMMDDHHMMSS`, precision-preserving; a truncated run sets `truncated`). ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### laboratoryAssignedId? > `readonly` `optional` **laboratoryAssignedId?**: `string` Field 4: laboratory-assigned patient ID. Distinct from [PatientRecord.practiceAssignedId](#practiceassignedid-1). ##### mothersMaidenName? > `readonly` `optional` **mothersMaidenName?**: `string` Field 7: mother's maiden name, surfaced verbatim (a surname component; PHI). ##### name? > `readonly` `optional` **name?**: [`PatientName`](#patientname) Field 6: patient name (`Last^First^Middle`). ##### patientIdThree? > `readonly` `optional` **patientIdThree?**: `string` Field 5: a third patient identifier (e.g. a national/alternate ID), surfaced verbatim. Kept separate from the practice- and laboratory-assigned IDs: the three never collapse into one. ##### practiceAssignedId? > `readonly` `optional` **practiceAssignedId?**: `string` Field 3: practice-assigned patient ID. Distinct from [PatientRecord.laboratoryAssignedId](#laboratoryassignedid-1). ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### seq? > `readonly` `optional` **seq?**: `string` Field 2: sequence number. ##### sex? > `readonly` `optional` **sex?**: `string` Field 9: sex, surfaced raw (`M`/`F`/`U`/vendor value). ##### type > `readonly` **type**: `"P"` The record's raw type letter. ###### Overrides `RecordBase.type` *** ### QueryInput Input for a `Q` (request-information) record. #### Properties ##### endingRangeId? > `readonly` `optional` **endingRangeId?**: `string` Field 4: ending range ID, emitted verbatim. ##### queriesAllTests? > `readonly` `optional` **queriesAllTests?**: `boolean` Field 5: emit the literal `ALL` universal-query keyword instead of a Universal Test ID. ##### requestInformationStatus? > `readonly` `optional` **requestInformationStatus?**: `string` Field 13: request-information status, emitted verbatim. ##### seq? > `readonly` `optional` **seq?**: `string` ##### startingRangeId? > `readonly` `optional` **startingRangeId?**: `string` Field 3: starting range ID, emitted verbatim. ##### type > `readonly` **type**: `"Q"` ##### universalTestId? > `readonly` `optional` **universalTestId?**: readonly `string`[] Field 5: Universal Test ID components; ignored when [QueryInput.queriesAllTests](#queriesalltests) is set. *** ### QueryRecord The `Q` (Request Information) record: the host-query request. A `Q` record asks the LIS for information (e.g. outstanding orders for a specimen); its **presence classifies the whole message as a request, never a result set** (see [AstmMessage.classification](#classification)). Its safety-relevant fields, the starting/ending range ID and the request-information status, are surfaced **verbatim**; the field *positions* are the public ASTM E1394 layout, but their internal structure and code meanings are **`[OSS-derived / paywalled]`** and are therefore **never interpreted or guessed**. #### Extends - `RecordBase` #### Properties ##### endingRangeId? > `readonly` `optional` **endingRangeId?**: `string` Field 4: ending range ID number, surfaced verbatim (same `[OSS-derived]` caveat as field 3). ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### queriesAllTests > `readonly` **queriesAllTests**: `boolean` `true` when field 5 is the literal `ALL` universal-query keyword (case-insensitive). **`[OSS-derived / paywalled]`:** the token is surfaced because it appears in the OSS references, but its exact host-query *behavior* (which tests a bare `ALL` selects, and whether the vendor answers with a full `H/P/O/L` or a `P/O`-only response) is paywalled and vendor-specific, **not decided here**. ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### requestInformationStatus? > `readonly` `optional` **requestInformationStatus?**: `string` Field 13: request-information status code(s), surfaced **verbatim**. The status code *set* is **`[OSS-derived / paywalled]`**: with no publicly-groundable enumeration, this parser recognizes **none** of them and interprets nothing, every present status is surfaced raw and flagged with a value-free `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS` warning. Never mapped to a guessed meaning. ##### seq? > `readonly` `optional` **seq?**: `string` Field 2: sequence number. ##### startingRangeId? > `readonly` `optional` **startingRangeId?**: `string` Field 3: starting range ID number, surfaced as the **full** verbatim field text (never truncated to a component). Its caret component structure (e.g. patient ID ^ specimen ID) is **`[OSS-derived / paywalled]`**: the raw split is available via [RecordBase.fields](#fields-2), but the *meaning* of each component is **never** assigned here. ##### type > `readonly` **type**: `"Q"` The record's raw type letter. ###### Overrides `RecordBase.type` ##### universalTestId? > `readonly` `optional` **universalTestId?**: [`UniversalTestId`](#universaltestid-6) Field 5: Universal Test ID (the same caret structure as an `O`/`R` record's), recognized by provenance only. When the field is the literal universal-query keyword, [QueryRecord.queriesAllTests](#queriesalltests-1) is set instead: see its caveat. *** ### ReferenceRange A parsed reference range. Bounds are surfaced as the **verbatim numeric text** (never coerced to a float, never rounded, never converted) so nothing is lost or fabricated. **`[OSS-derived]`: the exact lower/upper delimiter (`-`) and the open-ended `x` forms are taken from the permissively-licensed OSS reference parsers and cross-verified vendor transcripts; they are not confirmed against the purchased CLSI LIS02-A2. Anything that does not match these forms is surfaced verbatim as `unparsed`, never guessed into a bound.** #### Example ```ts import { parseReferenceRange } from "@cosyte/astm"; const r = parseReferenceRange("3.5-5.0"); r.kind; // "closed" r.low; // "3.5" r.high; // "5.0" ``` #### Properties ##### high? > `readonly` `optional` **high?**: `string` The upper bound, verbatim numeric text (for `closed` and `open-low`). ##### kind > `readonly` **kind**: [`ReferenceRangeKind`](#referencerangekind-1) The recognized shape, or `"unparsed"` when the text matched no known form. ##### low? > `readonly` `optional` **low?**: `string` The lower bound, verbatim numeric text (for `closed` and `open-high`). ##### raw > `readonly` **raw**: `string` The verbatim field text, exactly as received. *** ### ResultInput Input for an `R` (result) record. No clinical field is defaulted; unsupplied ⇒ empty. #### Properties ##### abnormalFlags? > `readonly` `optional` **abnormalFlags?**: `string` Field 7: abnormal flags, emitted verbatim (never defaulted to `N`). ##### completedAt? > `readonly` `optional` **completedAt?**: `string` Field 13: test-completed timestamp, verbatim. ##### instrument? > `readonly` `optional` **instrument?**: `string` Field 14: instrument identifier. ##### operator? > `readonly` `optional` **operator?**: `string` Field 11: operator. ##### referenceRange? > `readonly` `optional` **referenceRange?**: `string` Field 6: reference range, emitted verbatim. ##### resultStatus? > `readonly` `optional` **resultStatus?**: `string` Field 9: result status, emitted verbatim (never defaulted to `F`). ##### seq? > `readonly` `optional` **seq?**: `string` ##### startedAt? > `readonly` `optional` **startedAt?**: `string` Field 12: test-started timestamp (`YYYYMMDDHHMMSS`), verbatim. ##### type > `readonly` **type**: `"R"` ##### units? > `readonly` `optional` **units?**: `string` Field 5: units (vendor free text; never defaulted, guessed, or converted). ##### universalTestId? > `readonly` `optional` **universalTestId?**: readonly `string`[] Field 3: Universal Test ID components (verbatim). ##### value? > `readonly` `optional` **value?**: `string` Field 4: the measured value, emitted verbatim (never defaulted). *** ### ResultRecord The `R` (result) record: the value itself. The raw safety-critical fields (value, `units`, `referenceRange`, `abnormalFlags`, `resultStatus`) are always surfaced exactly as received. The **modeled, fail-safe** semantics sit alongside them: `flag` (Table 0078, `undefined` never coerced to normal), `status` (a `C`/`X` never reads as active-final; an absent status is `unspecified`, never `final`), and `range` (open/closed bounds surfaced verbatim, never fabricated). The raw strings and the modeled views coexist: nothing is collapsed or reconciled. #### Extends - `RecordBase` #### Properties ##### abnormalFlags? > `readonly` `optional` **abnormalFlags?**: `string` Field 7: abnormal flags, surfaced raw (HL7 Table 0078 values). ##### completedAt? > `readonly` `optional` **completedAt?**: [`AstmDate`](#astmdate) Field 13: test completed timestamp. ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### flag? > `readonly` `optional` **flag?**: [`AbnormalFlag`](#abnormalflag) Field 7: the abnormal flag interpreted against HL7 Table 0078, present only when the field carried a value. An unrecognized flag is `{ recognized: false, meaning: "undefined" }`, surfaced, never dropped, and **never coerced to `normal`**. ##### instrument? > `readonly` `optional` **instrument?**: `string` Field 14: instrument identifier. ##### operator? > `readonly` `optional` **operator?**: `string` Field 11: operator. ##### range? > `readonly` `optional` **range?**: [`ReferenceRange`](#referencerange) Field 6: the reference range parsed into low/high (or open-ended) bounds, present only when the field carried a value. An unparseable range is `kind: "unparsed"` with the raw text preserved and **no bound fabricated**. Bounds are verbatim numeric text, never coerced to floats. ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### referenceRange? > `readonly` `optional` **referenceRange?**: `string` Field 6: reference range, surfaced raw. ##### resultStatus? > `readonly` `optional` **resultStatus?**: `string` Field 9: result status, surfaced raw (`F`/`C`/`X`/…). ##### seq? > `readonly` `optional` **seq?**: `string` Field 2: sequence number. ##### startedAt? > `readonly` `optional` **startedAt?**: [`AstmDate`](#astmdate) Field 12: test started timestamp. ##### status > `readonly` **status**: [`ResultStatus`](#resultstatus-2) Field 9: the modeled result status. **Always present** (an absent field yields a typed `unspecified`, never assumed `final`), so `status.isActiveFinal` is always a reliable boolean: it is `true` only for a plain `F`, and `false` for a correction (`C`), a cancellation (`X`), a partial/preliminary/pending, an absent, or an unrecognized status. ##### type > `readonly` **type**: `"R"` The record's raw type letter. ###### Overrides `RecordBase.type` ##### units? > `readonly` `optional` **units?**: `string` Field 5: units (vendor free text; a missing unit is not defaulted). ##### universalTestId? > `readonly` `optional` **universalTestId?**: [`UniversalTestId`](#universaltestid-6) Field 3: Universal Test ID (local code in component 4 is the primary identifier). ##### value? > `readonly` `optional` **value?**: `string` Field 4: the measured value. In the ordinary single-component case this is the decoded scalar. When the value field carried an *unescaped* component delimiter (an ambiguity), this is the **full raw field text**, never a truncated component, and [ResultRecord.valueComponents](#valuecomponents) plus an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning are also present. ##### valueComponents? > `readonly` `optional` **valueComponents?**: readonly `string`[] Field 4 split into components, present only when the value field carried an *unescaped* component delimiter: i.e. it read as more than one component. Surfaced alongside the full raw value (and a warning) so an ambiguous split is visible, never resolved silently. *** ### ResultStatus A modeled result status. The three booleans are the safety surface a consumer reads instead of string-matching the code: - **`isActiveFinal`** is `true` **only** for a plain `F` (final). It is `false` for a correction (`C`), a cancellation (`X`), a preliminary/partial/pending result, an absent status, and an unrecognized one, so a superseded or cancelled result can **never** read as current/final. - **`supersedes`** is `true` for `C`: this value replaces a previously transmitted one. - **`cancelled`** is `true` for `X`: the result cannot be done / was cancelled. `vocabulary` is always the explicit "no citable published source": this letter set is modeled from what analyzers send, not from a vocabulary this library can name. #### Example ```ts import { interpretResultStatus } from "@cosyte/astm"; interpretResultStatus("C").isActiveFinal; // false (a correction is not active-final) interpretResultStatus("X").cancelled; // true interpretResultStatus(undefined).meaning; // "unspecified" (never "final") interpretResultStatus("F").vocabulary.attributed; // false: nothing is cited ``` #### Properties ##### cancelled > `readonly` **cancelled**: `boolean` `true` for `X`: the result cannot be done / was cancelled. ##### code? > `readonly` `optional` **code?**: [`ResultStatusCode`](#resultstatuscode-1) The recognized status code, present only when the raw text is a known status. ##### isActiveFinal > `readonly` **isActiveFinal**: `boolean` `true` **only** for a plain `F` (final): never for `C`, `X`, absent, or unrecognized. ##### meaning > `readonly` **meaning**: [`ResultStatusMeaning`](#resultstatusmeaning-1) The modeled meaning; `"unspecified"` when absent, `"undefined"` when unrecognized. ##### raw? > `readonly` `optional` **raw?**: `string` The verbatim field text, present only when field 9 carried a value. ##### recognized > `readonly` **recognized**: `boolean` Whether the raw text matched a recognized status letter. ##### supersedes > `readonly` **supersedes**: `boolean` `true` for `C`: this value supersedes a previously transmitted result. ##### vocabulary > `readonly` **vocabulary**: [`UnattributedVocabulary`](#unattributedvocabulary) The explicit statement that this letter set is bound by **no citable published source**. Always present and always unattributed, so an absent attribution and an unattributable one never look alike. *** ### ScientificRecord The `S` (scientific) record: vendor-defined free-form data, surfaced **VERBATIM** and **never interpreted into typed clinical fields** (same posture as [ManufacturerRecord](#manufacturerrecord)). #### Extends - `RecordBase` #### Properties ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### rawLine > `readonly` **rawLine**: `string` The record's exact wire text (terminator excluded), preserved byte-for-byte. Emit reproduces these bytes exactly whenever a reader using the delimiters being emitted against would recover the fields this record models: always the case when the record is already in those delimiters, and also when it carries no delimiter either set would split on. Otherwise the record is re-encoded from [RecordBase.fields](#fields-2), so the row can never go out in delimiters the header does not declare. To change a value, edit `fields`; editing this string has no effect on emit. ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### type > `readonly` **type**: `"S"` The record's raw type letter. ###### Overrides `RecordBase.type` *** ### TerminatorRecord The `L` (terminator) record: closes a message. #### Extends - `RecordBase` #### Properties ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### type > `readonly` **type**: `"L"` The record's raw type letter. ###### Overrides `RecordBase.type` *** ### ToDateOptions The options [toDate](#todate) accepts. Carries `assumeOffsetMinutes` and nothing else. #### Properties ##### assumeOffsetMinutes? > `readonly` `optional` **assumeOffsetMinutes?**: `number` The zone to read an offset-less value in, as signed minutes east of UTC (`-300` is UTC-05:00). Supplying `0` means "treat this value as UTC", which is a decision only the caller can make: an ASTM value states no offset, so without this option [toDate](#todate) returns `undefined` rather than guessing. *** ### UnattributedVocabulary The explicit statement that a code set is bound by **no citable published source**. It carries no identifier and no version, because inventing either would be the guess this library exists to refuse. #### Example ```ts import { RESULT_STATUS_VOCABULARY } from "@cosyte/astm"; RESULT_STATUS_VOCABULARY.attributed; // false RESULT_STATUS_VOCABULARY.reason; // fixed prose: why nothing is cited ``` #### Properties ##### attributed > `readonly` **attributed**: `false` Always `false`: no published source can be cited for this code set. ##### reason > `readonly` **reason**: `string` Fixed prose saying why nothing is cited. Never a code-system identifier. *** ### UniversalTestId A recognized ASTM Universal Test ID. All components are surfaced verbatim (already escape-decoded by the tokenizer); nothing is looked up. #### Example ```ts import { recognizeUniversalTestId } from "@cosyte/astm"; const u = recognizeUniversalTestId(["", "", "", "687"]); u.localCode; // "687" u.provenance; // "local-code" ``` #### Properties ##### codingScheme? > `readonly` `optional` **codingScheme?**: `string` Component 3: the coding-scheme selector, when present. ##### components > `readonly` **components**: readonly `string`[] The field's components, verbatim and in order. ##### localCode? > `readonly` `optional` **localCode?**: `string` Component 4, the vendor/local code: the only identifier a result is keyed on. ##### provenance > `readonly` **provenance**: [`UniversalTestIdProvenance`](#universaltestidprovenance-1) Where the primary identifier came from. ##### testName? > `readonly` `optional` **testName?**: `string` Component 2: the test / battery name, when present. ##### unvalidatedWireValue? > `readonly` `optional` **unvalidatedWireValue?**: `string` Component 1 when populated, verbatim: a wire value this library **does not vouch for**. It is not validated, is not reported as a LOINC or a LOINC candidate, and is never the code a result is keyed on. *** ### UnsupportedRecord Any record whose type letter is not modeled (a genuinely unknown letter). `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` are all modeled; anything else is surfaced with its raw fields intact and flagged with an `ASTM_RECORD_UNKNOWN_TYPE` warning, never dropped. **One of these may be a header.** Message grouping decides where a message starts by reading the type letter, so a header the reader could not recognize arrives here instead, opens no message, and the messages either side of it are grouped as one. Check `rawType` before trusting a split. **And one of these may be a `Q`.** Message classification counts type letters too, so a message carrying an unsupported record is classified `indeterminate` unless a `Q` was read outright: see [AstmMessageClassification.hasUnrecognized](#hasunrecognized). **A header that arrives here also failed to re-scope the delimiters.** The records after it are read with the set already in force, and where that set is not theirs they do not split at all, which is reported per record as `ASTM_RECORD_FIELDS_UNSEPARATED`. #### Extends - `RecordBase` #### Properties ##### fields > `readonly` **fields**: readonly [`AstmField`](#astmfield)[] The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. ###### Inherited from `RecordBase.fields` ##### rawType > `readonly` **rawType**: `string` The raw type letter as it appeared on the wire. ##### recordIndex > `readonly` **recordIndex**: `number` 0-based ordinal of the record within the message. ###### Inherited from `RecordBase.recordIndex` ##### type > `readonly` **type**: `"unsupported"` The record's raw type letter. ###### Overrides `RecordBase.type` *** ### VerbatimInput Input for an `M` (manufacturer) or `S` (scientific) record: vendor-defined free-form data. Emitted verbatim from the caller's fields; never interpreted. #### Properties ##### fields > `readonly` **fields**: readonly `string`[] Data fields (after the type letter), emitted verbatim in order. ##### type > `readonly` **type**: `"M"` \| `"S"` ## Type Aliases ### AbnormalFlagCode > **AbnormalFlagCode** = `"L"` \| `"H"` \| `"LL"` \| `"HH"` \| `"<"` \| `">"` \| `"N"` \| `"A"` \| `"AA"` \| `"U"` \| `"D"` \| `"B"` \| `"W"` \| `"S"` \| `"R"` \| `"I"` \| `"HU"` \| `"LU"` The recognized abnormal-flag letters, graded against [ABNORMAL\_FLAG\_VOCABULARY](#abnormal_flag_vocabulary) (the flag *value* set, a published fact set, not CLSI prose). That code system carries the concepts kept aligned with the HL7 v2 Table 0078 interpretation codes. Read in the context of an `R` record's field 7: here `S`/`R`/`I` are the microbiology susceptibility codes and `R` is **not** the repeat delimiter. Recognition is **exact match** after the surrounding whitespace is trimmed: letter case is never folded, so `hu` is unrecognized and stays that way. *** ### AbnormalFlagMeaning > **AbnormalFlagMeaning** = `"below-normal"` \| `"above-normal"` \| `"critically-below-normal"` \| `"critically-above-normal"` \| `"below-scale"` \| `"above-scale"` \| `"normal"` \| `"abnormal"` \| `"very-abnormal"` \| `"significant-change-up"` \| `"significant-change-down"` \| `"better"` \| `"worse"` \| `"susceptible"` \| `"resistant"` \| `"intermediate"` \| `"significantly-high"` \| `"significantly-low"` \| `"undefined"` The modeled meaning of a recognized [AbnormalFlagCode](#abnormalflagcode-1), plus the fail-safe sentinel `undefined` (a flag was present but is not in the graded vocabulary), which is **never** collapsed to `normal`. *** ### AmbiguousAlignmentSink > **AmbiguousAlignmentSink** = (`segmentIndex`) => `void` A callback the split calls when the escape character that **closed** an unrecognized escape sequence could instead have **opened** one whose body is the delimiter being split on, so the two alignments of the same bytes disagree about whether that delimiter ends a field, repeat or component. The leftmost reading is kept and nothing is re-split; the callback lets the parser surface a value-free `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` warning. Optional so the split can be used purely. Two exclusions, both deliberate. The first is what keeps this off conformant streams, and it is **wider than the case it is justified on**, so the residue is named here rather than left to be found: - **The earlier sequence's body must not be a recognized mnemonic.** The test is which alignment this codec's own vocabulary supports, not which one is tidier. Where the earlier body is unrecognized the reading taken rests on a triple the codec cannot interpret, while the competitor's body is the delimiter character, which it usually cannot interpret either: nothing prefers one, and that is what this reports. Where the earlier body is a recognized mnemonic the reading taken interprets a construct (`&F&` is the sender escaping a field separator, which is what the mechanism is for) and the competitor **usually** interprets none, so the vocabulary usually prefers one, and reporting it would report the escape mechanism working. Usually, not always: see the second residue below. **What that argument does not cover, measured.** It does not follow that the reading taken is *conformant*: under `28.6&F&|&U/L` it is `&F&`, a real separator, and then a bare escape character, which this package reports as a deviation of its own. And where the declared set names a mnemonic letter as a splitting delimiter, both alignments interpret exactly one construct and neither is preferred, yet the exclusion still silences it. Both residues are recorded with their measurements rather than closed by widening this test: the criterion that would cover them is a different one (counting what each alignment interprets), and swapping criteria moves which streams a published package refuses. - **The following character must be the delimiter this split is taken on.** The split runs once per role, so a character that is a delimiter in a *later* role is reported by that role's pass, on the segment it survives into, and never twice. A delimiter with no escape character two positions past it is excluded by the rule that defines the condition rather than by a judgement: the sequence the competing alignment would need never closes, so there is no competitor. #### Parameters ##### segmentIndex `number` The 0-based index of the segment being accumulated when the ambiguity was seen, so a caller splitting a record into fields can report which field it sat in. A caller splitting one field into repeats or components already knows the field index and ignores this. #### Returns `void` *** ### AmbiguousCode > **AmbiguousCode** = *typeof* [`AMBIGUOUS_CODES`](#ambiguous_codes)\[keyof *typeof* [`AMBIGUOUS_CODES`](#ambiguous_codes)\] A value from [AMBIGUOUS\_CODES](#ambiguous_codes): the discriminant on [AstmAmbiguousStreamError](#astmambiguousstreamerror). *** ### AnyAstmWarningCode > **AnyAstmWarningCode** = [`WarningCode`](#warningcode) \| [`FrameWarningCode`](#framewarningcode) \| [`LtpWarningCode`](#ltpwarningcode) Any warning code from any of the three registries: the record layer (`ASTM_RECORD_*` / `ASTM_NONSTANDARD_DELIMITERS` / `ASTM_UNKNOWN_ESCAPE_SEQUENCE`), the frame codec (`ASTM_FRAME_*`), or the LTP protocol layer (`ASTM_LTP_*`). A [AstmQuirkTolerance](#astmquirktolerance) may *name* any of them, but the safety gate refuses all but the small, benign, record-layer subset, so the union exists mainly to let the gate reject a frame/LTP code with a precise, typed message. *** ### AstmDatePrecision > **AstmDatePrecision** = `"year"` \| `"month"` \| `"day"` \| `"hour"` \| `"minute"` \| `"second"` The precision to which an [AstmDate](#astmdate) is populated. *** ### AstmFrameEncodeErrorCode > **AstmFrameEncodeErrorCode** = `"ASTM_FRAME_EMPTY_RECORD"` \| `"ASTM_FRAME_UNENCODABLE_CHARACTER"` \| `"ASTM_FRAME_RESERVED_BYTE"` \| `"ASTM_FRAME_INVALID_START_FRAME_NUMBER"` The reasons [composeAstmFrames](#composeastmframes) refuses rather than emitting bytes that are not the record it was handed. - `ASTM_FRAME_EMPTY_RECORD`: an empty record list, or a record with no bytes. A frame must carry a record; an empty one is a structural error, never an empty frame. - `ASTM_FRAME_UNENCODABLE_CHARACTER`: a record given as a `string` holds a character above `U+00FF`, which has no single byte to stand for. - `ASTM_FRAME_RESERVED_BYTE`: a record holds a byte this layer reads as frame structure (`STX`, `ETB` or `ETX`). Framing has no escape mechanism, so such a byte cannot be carried inside a frame at all. - `ASTM_FRAME_INVALID_START_FRAME_NUMBER`: `options.startFrameNumber` is not a whole number from `0` to `7`. A frame's number is one ASCII digit, so a value outside that range has no digit to be written as. #### Example ```ts import type { AstmFrameEncodeErrorCode } from "@cosyte/astm"; const code: AstmFrameEncodeErrorCode = "ASTM_FRAME_UNENCODABLE_CHARACTER"; ``` *** ### AstmFraming > **AstmFraming** = `"framed"` \| `"raw"` The transport framing of an ASTM byte stream: `"framed"` (E1381 `STX`/checksum frames, the serial and framed-TCP realities) or `"raw"` (framing dropped, de-framed record bytes streamed directly, the raw-TCP reality). *** ### AstmMessageKind > **AstmMessageKind** = `"host-query"` \| `"results"` \| `"orders"` \| `"indeterminate"` How a message is classified by the host-query flow. - `host-query` (the message carries at least one `Q` record): it is a **request for information**, and **must never be read as a result set** (the load-bearing safety distinction of this layer). `Q` **dominates**: a `Q` present classifies the message as a request even if a result record is also present (an anomaly, separately warned), so a `Q`-bearing message is never silently treated as a result upload. - `results` (no `Q`, at least one `R` result record): a result upload / response. - `orders` (no `Q`, no `R`, at least one `O` order record): an order download, or a query response before results are attached. - `indeterminate`: none of the above (e.g. header + terminator only). Not guessed into one of the other kinds. *** ### AstmRecord > **AstmRecord** = [`HeaderRecord`](#headerrecord) \| [`PatientRecord`](#patientrecord) \| [`OrderRecord`](#orderrecord) \| [`ResultRecord`](#resultrecord) \| [`CommentRecord`](#commentrecord) \| [`QueryRecord`](#queryrecord) \| [`ManufacturerRecord`](#manufacturerrecord) \| [`ScientificRecord`](#scientificrecord) \| [`TerminatorRecord`](#terminatorrecord) \| [`UnsupportedRecord`](#unsupportedrecord) The discriminated union of every parsed record. *** ### AstmRecordInput > **AstmRecordInput** = [`PatientInput`](#patientinput) \| [`OrderInput`](#orderinput) \| [`ResultInput`](#resultinput) \| [`CommentInput`](#commentinput) \| [`QueryInput`](#queryinput) \| [`VerbatimInput`](#verbatiminput) Any record the message builder can emit (the terminator `L` is appended automatically). *** ### AstmSerializeErrorCode > **AstmSerializeErrorCode** = `"ASTM_EMIT_UNENCODABLE_VALUE"` \| `"ASTM_EMIT_INVALID_DELIMITERS"` \| `"ASTM_EMIT_TYPE_LETTER_COLLISION"` The reasons emit refuses rather than writing a stream that cannot be read back. - `ASTM_EMIT_UNENCODABLE_VALUE`: a component holds a record terminator (`CR`/`LF`), which the escape codec has no mnemonic for. - `ASTM_EMIT_INVALID_DELIMITERS`: the delimiter set to emit against failed one of the three conditions readback requires (see [serializeAstmRecords](#serializeastmrecords)). Those conditions are checked against the *set alone*, so this code means a set was rejected, not that every unreversible set is. - `ASTM_EMIT_TYPE_LETTER_COLLISION`: this record's type letter would not be the first character of its own emitted line, so the record would read back as a **different** record. Raised per record rather than per set, because whether a set collides depends on which record is being written: a record read under one delimiter set and written under another can hit it with **no** set passed at all, so it is not avoided by emitting on the default canonical path. #### Example ```ts import type { AstmSerializeErrorCode } from "@cosyte/astm"; const code: AstmSerializeErrorCode = "ASTM_EMIT_INVALID_DELIMITERS"; ``` *** ### DelimiterDeclarationFault > **DelimiterDeclarationFault** = `"not-a-header"` \| `"record-too-short"` \| `"definition-truncated"` \| `"field-separator-reused"` Why a header record could not declare a usable delimiter set. Four distinct conditions, kept distinct because a consumer is told which one it was and two of them describe records that are **not** short. - `not-a-header`: the record does not begin with an `H` type letter. - `record-too-short`: fewer than five characters, so `H` plus a field separator plus a three-character definition cannot fit. - `definition-truncated`: the delimiter-definition field runs to the next field separator (or to the end of the record) and holds fewer than three characters. **This is the one a full-length header reaches**, and it is what a declaration naming its own field separator among the other three produces, because that separator ends the definition where it appears. - `field-separator-reused`: the field separator is also the repeat, component or escape character. *** ### DelimiterReadResult > **DelimiterReadResult** = \{ `delimiters`: [`Delimiters`](#delimiters-2); `ok`: `true`; \} \| \{ `fault`: [`DelimiterDeclarationFault`](#delimiterdeclarationfault); `ok`: `false`; \} The result of reading delimiters from a header record: either resolved or a named failure. *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] A value from [FATAL\_CODES](#fatal_codes): the type carried by a thrown [AstmParseError](#astmparseerror). *** ### FrameTerminator > **FrameTerminator** = `"ETB"` \| `"ETX"` Which terminator closed a frame: `ETB` (record continues) or `ETX` (record complete). *** ### FrameWarningCode > **FrameWarningCode** = *typeof* [`FRAME_WARNING_CODES`](#frame_warning_codes)\[keyof *typeof* [`FRAME_WARNING_CODES`](#frame_warning_codes)\] Discriminant type for [AstmFrameWarning.code](#code-5). Narrowing by this code lets consumers write exhaustive `switch` blocks against [FRAME\_WARNING\_CODES](#frame_warning_codes). *** ### LivdAmbiguityReason > **LivdAmbiguityReason** = `"no-reported-units"` \| `"no-candidate-matched-units"` \| `"multiple-candidates-matched-units"` Why a code carrying several candidate LOINCs stayed `ambiguous` after the unit comparison ran, or why the comparison could not run at all. - `no-reported-units`: the record reported no usable units (absent, empty or whitespace only), so nothing could be compared. A unit-qualified candidate is never chosen in this case. - `no-candidate-matched-units`: units were reported and **no** unit-qualified candidate's representative unit was exactly equal to them. - `multiple-candidates-matched-units`: units were reported and **more than one distinct** candidate LOINC matched them exactly. Picking one would be a guess. Every one of them surfaces every candidate and chooses no LOINC. *** ### LivdCatalogIdentity > **LivdCatalogIdentity** = \{ `declared`: `true`; `publicationVersion?`: `string`; `publisher?`: `string`; \} \| \{ `declared`: `false`; `reason`: `string`; \} What a LIVD catalog declared about its own identity: the two publication elements that name a publication, or the explicit statement that it declared neither. The same two shapes the rest of this package uses for attribution: a named source, or a positive "nothing citable", never an absent field and never an invented name. Discriminate on `declared`. #### Union Members ##### Type Literal \{ `declared`: `true`; `publicationVersion?`: `string`; `publisher?`: `string`; \} ###### declared > `readonly` **declared**: `true` Always `true`: the catalog declared at least one of the two elements below. ###### publicationVersion? > `readonly` `optional` **publicationVersion?**: `string` The **Publication Version ID** the catalog declared, verbatim, when it declared one. ###### publisher? > `readonly` `optional` **publisher?**: `string` The **Publisher** the catalog declared, verbatim, when it declared one. *** ##### Type Literal \{ `declared`: `false`; `reason`: `string`; \} ###### declared > `readonly` **declared**: `false` Always `false`: the catalog declared neither a publisher nor a publication version. ###### reason > `readonly` **reason**: `string` Fixed prose saying that no identity was declared. Never an invented or indexed name. #### Example ```ts import type { LivdCatalogIdentity } from "@cosyte/astm"; const named: LivdCatalogIdentity = { declared: true, publisher: "Example Diagnostics" }; ``` *** ### LivdLookup > **LivdLookup** = \{ `loinc`: `string`; `loincLongName?`: `string`; `representativeUnit?`: `string`; `status`: `"mapped"`; `unitComparison?`: [`LivdUnitComparison`](#livdunitcomparison); `vendorResultDescription?`: `string`; `vendorSpecimenDescription?`: `string`; \} \| \{ `status`: `"unmapped"`; \} \| \{ `candidateDetails?`: readonly [`LivdCandidate`](#livdcandidate)[]; `candidates`: readonly `string`[]; `reason?`: [`LivdAmbiguityReason`](#livdambiguityreason); `status`: `"ambiguous"`; \} The outcome of looking a vendor code up in a [LivdCatalog](#livdcatalog). A distinct value per safe disposition: a hit is `mapped`; a miss is `unmapped`; a code that matched more than one **distinct** LOINC and that the reported units did not settle is `ambiguous` with the candidates surfaced but **none chosen**. There is deliberately no "guessed" case. **The added fields are all optional, and a catalog carrying none of the three LIVD attributes answers byte-identically to how it answered before they existed.** They appear only where the consumer's own catalog rows supply something to put in them. #### Union Members ##### Type Literal \{ `loinc`: `string`; `loincLongName?`: `string`; `representativeUnit?`: `string`; `status`: `"mapped"`; `unitComparison?`: [`LivdUnitComparison`](#livdunitcomparison); `vendorResultDescription?`: `string`; `vendorSpecimenDescription?`: `string`; \} Exactly one LOINC: one entry, several entries that all agree on the same LOINC, or several candidates of which exactly one had a representative unit equal to the units the record reported (and then, and only then, `unitComparison` says how that comparison was made). ###### loinc > `readonly` **loinc**: `string` ###### loincLongName? > `readonly` `optional` **loincLongName?**: `string` ###### representativeUnit? > `readonly` `optional` **representativeUnit?**: `string` The chosen row's representative unit, verbatim, when supplied. **This is provenance about the CATALOG ROW, not a restatement of what the record reported.** Where a vendor code carried a single candidate LOINC across several rows that spell the unit differently, the answer takes the FIRST row's attributes (exactly as it has always taken the first row's `loincLongName`), so this can name a unit the record did not report. Only [LivdUnitComparison](#livdunitcomparison) asserts the two were equal, and it is present only where a unit actually chose between candidates. ###### status > `readonly` **status**: `"mapped"` ###### unitComparison? > `readonly` `optional` **unitComparison?**: [`LivdUnitComparison`](#livdunitcomparison) Present **if and only if** a unit comparison chose this LOINC from more than one candidate. Absent where the code carried a single candidate LOINC, which is answered whether or not the units agree: nothing was selected on a unit there, so claiming it was would be false. ###### vendorResultDescription? > `readonly` `optional` **vendorResultDescription?**: `string` The chosen row's Vendor Result Description, verbatim, when supplied. Never matched on. ###### vendorSpecimenDescription? > `readonly` `optional` **vendorSpecimenDescription?**: `string` The chosen row's Vendor Specimen Description, verbatim, when supplied. Never matched on. *** ##### Type Literal \{ `status`: `"unmapped"`; \} No entry for this code: a miss. The code stays verbatim; no LOINC is invented. *** ##### Type Literal \{ `candidateDetails?`: readonly [`LivdCandidate`](#livdcandidate)[]; `candidates`: readonly `string`[]; `reason?`: [`LivdAmbiguityReason`](#livdambiguityreason); `status`: `"ambiguous"`; \} More than one distinct LOINC, unsettled: surfaced for inspection, never resolved to one. ###### candidateDetails? > `readonly` `optional` **candidateDetails?**: readonly [`LivdCandidate`](#livdcandidate)[] Every candidate row with its LIVD attributes, one per catalog row (so a LOINC held by two rows appears twice), including rows that are not unit qualified. Present only where at least one row for this vendor code carries one of the three LIVD attributes. ###### candidates > `readonly` **candidates**: readonly `string`[] Every distinct candidate LOINC, deduplicated, in first-seen catalog order. ###### reason? > `readonly` `optional` **reason?**: [`LivdAmbiguityReason`](#livdambiguityreason) Why the unit comparison did not settle it. Present under the same condition as `candidateDetails`: a catalog carrying none of the LIVD attributes answers exactly as it did before this field existed. ###### status > `readonly` **status**: `"ambiguous"` *** ### LivdMapping > **LivdMapping** = \{ `derived`: `true`; `loinc`: `string`; `loincLongName?`: `string`; `representativeUnit?`: `string`; `source`: `"livd"`; `status`: `"mapped"`; `unitComparison?`: [`LivdUnitComparison`](#livdunitcomparison); `vendorResultDescription?`: `string`; `vendorSpecimenDescription?`: `string`; \} \| \{ `status`: `"unmapped"`; \} \| \{ `candidateDetails?`: readonly [`LivdCandidate`](#livdcandidate)[]; `candidates`: readonly `string`[]; `reason?`: [`LivdAmbiguityReason`](#livdambiguityreason); `status`: `"ambiguous"`; \} \| \{ `status`: `"no-vendor-code"`; \} \| \{ `status`: `"no-code"`; \} The outcome of looking one record's Universal Test ID up in a LIVD catalog: a closed, mutually exclusive discriminant. There is no case in which a LOINC is guessed, and no case in which a value the wire carried is reported as a LOINC. - `mapped`, the vendor/local code resolved to a single LOINC **via the catalog** (labeled `derived: true`, `source: "livd"`). - `unmapped`: a vendor/local code was looked up and the catalog held no entry for it (a hit whose LOINC is a zero-length string is reported here too: an empty LOINC is not an answer). - `ambiguous`: a vendor/local code matching more than one distinct LOINC; the candidates are surfaced but **none is chosen**. - `no-vendor-code`: component 1 is populated and there is no vendor/local code, so nothing was looked up. The wire value is surfaced unvalidated, and is never used as a lookup key: the catalog is keyed on the vendor transmission code. - `no-code`: the record carried no usable test code at all (name-only/empty), so there was nothing to map. #### Union Members ##### Type Literal \{ `derived`: `true`; `loinc`: `string`; `loincLongName?`: `string`; `representativeUnit?`: `string`; `source`: `"livd"`; `status`: `"mapped"`; `unitComparison?`: [`LivdUnitComparison`](#livdunitcomparison); `vendorResultDescription?`: `string`; `vendorSpecimenDescription?`: `string`; \} ###### derived > `readonly` **derived**: `true` ###### loinc > `readonly` **loinc**: `string` ###### loincLongName? > `readonly` `optional` **loincLongName?**: `string` ###### representativeUnit? > `readonly` `optional` **representativeUnit?**: `string` The chosen catalog row's representative unit, verbatim, when supplied. ###### source > `readonly` **source**: `"livd"` ###### status > `readonly` **status**: `"mapped"` ###### unitComparison? > `readonly` `optional` **unitComparison?**: [`LivdUnitComparison`](#livdunitcomparison) Present **if and only if** the record's units chose this LOINC from more than one candidate, and it states what that comparison was: verbatim and case sensitive, not UCUM semantic. ###### vendorResultDescription? > `readonly` `optional` **vendorResultDescription?**: `string` The chosen catalog row's Vendor Result Description, verbatim, when supplied. ###### vendorSpecimenDescription? > `readonly` `optional` **vendorSpecimenDescription?**: `string` The chosen catalog row's Vendor Specimen Description, verbatim, when supplied. *** ##### Type Literal \{ `status`: `"unmapped"`; \} *** ##### Type Literal \{ `candidateDetails?`: readonly [`LivdCandidate`](#livdcandidate)[]; `candidates`: readonly `string`[]; `reason?`: [`LivdAmbiguityReason`](#livdambiguityreason); `status`: `"ambiguous"`; \} ###### candidateDetails? > `readonly` `optional` **candidateDetails?**: readonly [`LivdCandidate`](#livdcandidate)[] Every candidate row with its LIVD attributes, when the catalog supplied any. ###### candidates > `readonly` **candidates**: readonly `string`[] ###### reason? > `readonly` `optional` **reason?**: [`LivdAmbiguityReason`](#livdambiguityreason) Why the units did not settle it, when the catalog supplied the attributes to say. ###### status > `readonly` **status**: `"ambiguous"` *** ##### Type Literal \{ `status`: `"no-vendor-code"`; \} *** ##### Type Literal \{ `status`: `"no-code"`; \} *** ### LivdWarningCode > **LivdWarningCode** = *typeof* [`LIVD_WARNING_CODES`](#livd_warning_codes)\[keyof *typeof* [`LIVD_WARNING_CODES`](#livd_warning_codes)\] Discriminant type for every code in [LIVD\_WARNING\_CODES](#livd_warning_codes), the per-record codes and the catalog-level one alike. Narrowing by this code lets consumers write exhaustive `switch` blocks against the registry. It tracks the registry exactly, so `Object.values(LIVD_WARNING_CODES)` stays a snapshot of this type. *** ### LtpAction > **LtpAction** = \{ `type`: `"sendAck"`; \} \| \{ `type`: `"sendNak"`; \} \| \{ `type`: `"sendEot"`; \} \| \{ `record`: `Uint8Array`; `type`: `"deliverRecord"`; \} An action the reducer tells the consumer to take. The consumer performs the I/O (write the byte to the socket, hand the record to the parser); the reducer only decides. `sendAck`/`sendNak`/`sendEot` are single control bytes; `deliverRecord` carries the freshly-completed record's reassembled bytes. *** ### LtpEvent > **LtpEvent** = \{ `type`: `"enq"`; \} \| \{ `type`: `"ack"`; \} \| \{ `type`: `"nak"`; \} \| \{ `type`: `"eot"`; \} \| \{ `frame`: [`AstmFrame`](#astmframe); `type`: `"frame"`; \} An inbound protocol event the consumer feeds the reducer: one of the four LTP control signals (`ENQ`/`ACK`/`NAK`/`EOT`) read off the wire, or a `frame` the consumer already decoded with `decodeAstmFrames` (the reducer reuses the codec's `trusted`/`checksum` verdict, it never re-derives it). *** ### LtpPhase > **LtpPhase** = `"neutral"` \| `"transfer"` The protocol phase. The line is either **neutral** (idle, awaiting an `ENQ` to establish, or reset there after an `EOT`) or in **transfer** (establishment accepted; frames and their per-frame `ACK`/`NAK` flow until the sender's `EOT`). The classic three-phase LIS01 model (establishment → transfer → termination) collapses to these two, since establishment is the `neutral → transfer` edge and termination is the `transfer → neutral` edge. *** ### LtpWarningCode > **LtpWarningCode** = *typeof* [`LTP_WARNING_CODES`](#ltp_warning_codes)\[keyof *typeof* [`LTP_WARNING_CODES`](#ltp_warning_codes)\] Discriminant type for [AstmLtpWarning.code](#code-8). Narrowing by this code lets consumers write exhaustive `switch` blocks against [LTP\_WARNING\_CODES](#ltp_warning_codes). *** ### ReferenceRangeKind > **ReferenceRangeKind** = `"closed"` \| `"open-low"` \| `"open-high"` \| `"unparsed"` The shape of a parsed reference range. - `closed`: both a low and a high bound (`3.5-5.0`). - `open-low`: an upper bound only (`<5`), so everything at or below `high`. - `open-high`: a lower bound only (`>10`), so everything at or above `low`. - `unparsed`: the text did not match a recognized form; both bounds are absent and the raw text is surfaced. **A bound is never fabricated.** *** ### ResultStatusCode > **ResultStatusCode** = `"F"` \| `"C"` \| `"P"` \| `"R"` \| `"S"` \| `"I"` \| `"X"` The recognized result-status letters (`R`-record field 9). The clinically load-bearing members are **`C` (correction, supersedes a previously transmitted value)** and **`X` (cannot be done / cancelled)**. This set is bound by **no citable published source**: see [RESULT\_STATUS\_VOCABULARY](#result_status_vocabulary), which every interpreted status carries. As with the flags, matching is exact after trimming; letter case is never folded. *** ### ResultStatusMeaning > **ResultStatusMeaning** = `"final"` \| `"correction"` \| `"preliminary"` \| `"previously-transmitted"` \| `"partial"` \| `"pending"` \| `"cancelled"` \| `"unspecified"` \| `"undefined"` The modeled meaning of a result status, plus two fail-safe sentinels: `unspecified` (the field was absent, **never assumed `final`**) and `undefined` (a letter was present but is not a recognized status). *** ### ShiftedComponentsSink > **ShiftedComponentsSink** = (`segmentIndex`) => `void` A callback the split calls on **exactly the condition [ShiftedFieldsSink](#shiftedfieldssink) reports**, wired instead to the split taken on the **component** separator. The third and last of the three sinks that share that one predicate, for the reason the other two share it: the predicate is about the bytes and the split does not know which role it is being taken on, so the caller that does know names what the gained boundary costs. Do not "simplify" the three into one: the codes would then have to make one claim covering all of them, and the three claims are different. **What a gained component boundary costs, and it is neither of the other two.** No field number changes, so nothing shifts between field-indexed slots, and nothing leaves the record, so nothing truncates. Components are modeled **inside** a field, so what happens is that every component after the gained boundary sits further right than the competing alignment puts it, and the slots that indexes into are named things: a Universal Test ID's LOINC-candidate slot, test name, **coding scheme** and **local code**; a patient name's last, first and middle. Under the canonical set `&F&^&GLU^L^687` reads four components, so `L` is the coding scheme and `687` the vendor's local code, while the competing alignment reads three and `687` is the **coding scheme**. `DOE&F&^&JANE^A` reads a given name of `&JANE` and a middle name of `A`, while the competing alignment makes `A` the **given** name with no middle name at all. **Every gained boundary at or before the LAST MODELED COMPONENT INDEX moves a modeled slot, not only the first.** That is the structural difference from [TruncatedFieldSink](#truncatedfieldsink): a field is modeled out of its first repeat alone, so there only the first gained boundary reaches a modeled slot, while here the shift propagates from wherever the boundary sits to the end of the component list. **TWO bounds run the other way, and the report fires inside both, which is over-reporting and never under-reporting.** Neither is an oversight and neither is closed by narrowing the guard, because narrowing changes which streams a published package refuses and wants its own measurement. Both axes are swept on their own rather than left to the shared corpus, which holds them fixed. - **Past the last modeled component index, nothing NAMED moves.** A model reads a fixed number of components: a patient name three (last, first, middle), a Universal Test ID four. A contested boundary further right than that still shifts the components after it and leaves every named slot byte-identical under both alignments, and this fires there. - **Inside a LATER repeat nothing modeled moves at all.** `components` is `repeats[0]`, so a component boundary gained inside the second or a later repeat changes only `repeats[n]` for that `n`. It fires in both because the boundary is still one the bytes do not force, and a consumer reading `components` or `repeats` is still reading an alignment guess. **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, and the components read are the components that were always read. What is new is that the moved slots are reported by a code no profile may tolerate, where before they were covered only by tolerable ones. **Withholding them is a separate question, deliberately not answered here**: declining to model a slot changes an extracted value for every consumer of a package already on the registry. **The tail is weighed one construct deep, exactly as in the other two sinks, and one tail is excluded for the same reason.** Where the escape character the reading taken resumes on heads a sequence this codec **recognizes**, it interprets a construct and the components are the ones the sender wrote: under a set naming the component separator `F`, `GLU&F&F&F&L` is that separator escaped, written, and escaped again, with nothing reported at all. Refusing that is the over-refusal that sank an earlier candidate criterion for this family, and that tail is the only one on which a stream's escaping can be clean, on the declarations the companion paragraph on [ShiftedFieldsSink](#shiftedfieldssink) scopes that to. Where the tail heads a sequence whose body is *unrecognized*, the reading taken consumes a triple it cannot read, the slots move exactly as far, and this reports it. **That silence is a TRADE, and it is not a claim that nothing was lost there.** The gained component boundary is exactly as real on the excluded tail and can be entirely silent: under the canonical set `&F&^&F&GLU^L^687` reads one component more than the competing alignment with `warnings: []`, so a coding scheme and a local code still sit where an alignment guess put them. Reporting it would refuse a stream whose escaping is working. **⚠️ HOW FAR THE COMPONENTS MOVE IS THE DISPLACEMENT [ShiftedFieldsSink](#shiftedfieldssink) STATES, READ ON THE COMPONENT SEPARATOR.** It is not fixed, and it takes three values and not one: zero on the tie class (`DOE&F&^&^&JANE^A` reads three components under both, with `A` the middle name under both, and `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` has already refused that record), and more than one once a field carries more than one contested construct. That paragraph is not repeated here on purpose: it is one claim, stated where the link points. This third bound differs from the two above in what it is about: those two fire where nothing named moves because of where the boundary **sits**, while the tie class is about the bytes **past** it. What holds on **every** firing tuple is that the two readings disagree and that both consume every byte, so neither is forced. #### Parameters ##### segmentIndex `number` The 0-based index of the component being accumulated when the contested boundary was taken. It is **not** a field index, and it is **not** a repeat index: the caller splitting one repeat into components already knows both, and reports the field. For how far the components after it move, see the displacement paragraph on [ShiftedFieldsSink](#shiftedfieldssink). #### Returns `void` *** ### ShiftedFieldsSink > **ShiftedFieldsSink** = (`segmentIndex`) => `void` A callback the split calls when a competing escape alignment decided a boundary **and the reading taken cannot read what lies immediately past it**: the escape character the leftmost reading resumes on heads no sequence this codec can *interpret*, while the competing alignment is exactly the reading that gives that character a job, as the close of its own triple. Two tails satisfy that, and they are reported alike because they cost alike: - **It heads no sequence at all**, so it is kept as a bare literal and reported separately as `ASTM_UNPAIRED_ESCAPE_CHARACTER`. - **It heads a sequence whose body is not a recognized mnemonic**, so the triple is preserved verbatim, never guessed at, and reported separately as `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. **Consuming a triple is not interpreting one**, and the boundary is bought with bytes this reading cannot read either way. Wired **only to the split taken on the field separator**, because that is the whole of its claim: a gained *field* boundary shifts every later field, so a record's modeled slots after it are decided by the alignment rather than by the sender's own positions. On a result record that is the units slot and the **result status** slot: the sender's trailing letter lands in field 9 under the reading taken and in no field at all under the competing one, so a status of `final` can be a consequence of the alignment rather than something the sender put there. A gained *repeat* or *component* boundary divides one field and reaches nothing outside it, so it moves no **field-indexed** slot and is deliberately outside this. **That bound is a choice, not a consequence, and the difference matters**: components are modeled *inside* a field (a Universal Test ID's coding scheme and local code, a patient name's parts), so a gained repeat or component boundary does reach a modeled slot. The **repeat** half of that is reported by [TruncatedFieldSink](#truncatedfieldsink), on the same tail test and under its own code, because what it costs is a different thing. The **component** half is reported by [ShiftedComponentsSink](#shiftedcomponentssink), on that same tail test and under a code of its own again, because what it costs is a third thing: there the components move along the component list rather than leaving the record. The callback lets the parser surface a value-free `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` warning. Optional so the split can be used purely. **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, and the status read is the status that was always read. What is new is that the shift is reported by a code no profile may tolerate, where before it was covered only by tolerable ones. **This is independent of [AmbiguousAlignmentSink](#ambiguousalignmentsink), and fires alongside it rather than instead of it.** That one asks whether the codec's vocabulary prefers the reading taken *at* the contested position and is silent where the earlier body is a recognized mnemonic; this one asks what the reading taken makes of the bytes *after* the boundary and does not consult the earlier body at all. The two questions are different, so neither test is widened to answer the other. **The tail is weighed one construct deep, and that bound is stated rather than left to be found.** Exactly one tail is excluded: **the escape character heads a sequence this codec recognizes.** Then the reading taken interprets a construct and leaves nothing unread, while the competing alignment would leave it bare. Under a set naming the field separator `F`, `28.6&F&F&F&U/L` is the sender escaping that separator, writing it, and escaping it again: entirely well formed, and refusing it is the over-refusal that sank the preceding candidate criterion for this family. **That exclusion is not a matter of degree, which is why it is the only one.** It is the sole tail on which a stream's escaping can be clean at all, wherever the escape role is a character distinct from the three splitting roles: on the other two the package already reports a deviation. **Where the escape role is NOT distinct that reasoning does not hold**; see the companion paragraph on [ShiftedFieldsSink](#shiftedfieldssink). A tail comparison that instead weighs which alignment the bytes *prefer* would exclude the unrecognized body too (the competing alignment leaves two escape characters bare there against the reading taken's one unreadable body) and it is the wrong question: these codes report a **cost**, not a preference, and the cost is identical under both reported tails. **That silence is a TRADE, and it is not a claim that nothing was lost there.** The gained field boundary is exactly as real on the excluded tail, and it can be entirely silent: under the canonical set `R|1|^^^687|28.6&F&|&F&U/L||||F` reads **nine** fields against the competing alignment's eight with `warnings: []`, so a result status of `final` is read out of a slot the other reading does not have. Reporting it would refuse a stream whose escaping is working, which is the trade this bound makes. **Read the raw line when an escape character sits next to a delimiter, whether or not anything fired.** **⚠️ HOW FAR THE DISPLACEMENT RUNS. Every other surface names the KIND of cost and leaves the magnitude to this paragraph**, because a restatement is a separate claim that goes stale on its own, and on this family they have. **The displacement is not fixed, and "one place" describes only the single-construct case the three codes were first measured on.** It takes three values, and two of them were left out of every surface that quoted a figure: - **Zero, on the tie class.** Where the sequence past the boundary carries the **field separator itself** as its body, the reading taken holds that character inside an opaque atom while the competing alignment splits on it, so the two readings return the **same number** of fields in **different places**: no field index moves and what differs is the **contents**. `R|1|^^^687|28.6&F&|&|&U/L||||F` reads nine fields under both, status `F` in field 9 under both, units `&|&U/L` against `&U/L`. That class costs no stream its disposition: the tail body is a splitting delimiter in force, so `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` has already refused the record. - **One, on a record carrying exactly one contested construct.** This is the case every corpus in this family was built on, and the only one "one place" was ever true of. - **One more for each additional contested construct.** The competing reading resumes a character further on at each contested position, so it falls further out of step with the reading taken and the gap widens once per construct. `A&Z&|&BX&Z&|&F&C` reads **three** fields against **one**. **The number of these warnings is not the displacement either, in either direction.** A gained boundary whose tail is a recognized mnemonic is excluded from the report and still displaces, so one warning can sit on a displacement of two; and where one of several constructs is a tie, two warnings can sit on a displacement of one. A consumer cannot recover the offset by counting. **Read the raw line.** What holds on **every** firing tuple is that the two readings disagree, that both consume every byte so neither is forced, and that the reading taken never reads **fewer** segments than the competing one. Measured in `test/records/alignment-offset-rephasing.test.ts`. **⚠️ WHICH REPORTS ACCOMPANY THIS ONE, AND THE DECLARATION ON WHICH NONE DOES.** The exclusion above rests on a defence against over-refusal: firing requires an escape character heading no sequence this codec can interpret, and the package reports each of those in its own right, so a stream whose escaping raises nothing is never refused because of these codes. **That defence is true wherever the escape role is a character distinct from the three splitting roles, and it is FALSE where it is not.** On a set naming the escape character in a splitting role too, one byte both opens a sequence and ends a segment, the split claims it first, and neither `ASTM_UNPAIRED_ESCAPE_CHARACTER` nor `ASTM_UNKNOWN_ESCAPE_SEQUENCE` ever sees a sequence to raise: this fires with **neither** companion. What refuses the stream there is the declaration itself, as `ASTM_RECORD_DELIMITER_ROLE_COLLISION`, which no profile may tolerate. So the defence survives only in the form **no stream whose escaping AND whose declaration are both clean is refused by one of these codes.** **That replacement is scoped to the STREAM and does not hold per message**: the collision is reported once per set change rather than once per record, so a second header re-declaring the same colliding set raises nothing while these codes fire again in its message. A consumer scoping warnings to a message can therefore see one of them standing entirely alone. The **field** role cannot collide with the escape role at all: the declaration is the three characters after the field separator and stops at the next one, so such a header terminates itself one character short and is refused. Measured in `test/records/alignment-companion-universal.test.ts`. #### Parameters ##### segmentIndex `number` The 0-based index of the field being accumulated when the contested boundary was taken. For how far the fields after it are displaced, see the displacement paragraph above: it is not a fixed one place. #### Returns `void` *** ### SwallowedDelimiterSink > **SwallowedDelimiterSink** = () => `void` A callback the codec calls when an **unrecognized** escape body is itself one of the three splitting delimiters in force (field, repeat, component), so the atom rule kept that character out of the split and a boundary the sender's bytes carried never became one. The value is preserved verbatim either way; the callback lets the parser surface a value-free `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` warning. Optional so the codec can be used purely. The **escape** character is deliberately not in that test: it is not a splitting role, so `&&&` under the canonical set loses no boundary. A body that is a *recognized* mnemonic is not in it either, whatever character it is: `&F&` under a set naming `F` as the repeat delimiter is the escaped field delimiter the sender wrote, and reading it as a swallowed repeat boundary would report the escape mechanism working as a defect. #### Returns `void` *** ### TruncatedFieldSink > **TruncatedFieldSink** = (`segmentIndex`) => `void` A callback the split calls on **exactly the condition [ShiftedFieldsSink](#shiftedfieldssink) reports**, wired instead to the split taken on the **repeat** separator. The two sinks test the same predicate, because the predicate is about the bytes and the split does not know which role it is being taken on; they are separate so the caller that does know can name what the gained boundary costs, which is not the same thing in the two roles. Do not "simplify" them into one: the codes would then have to make one claim covering both, and the claim is different. **What a gained repeat boundary costs, and it is not a shift.** No field-indexed slot moves: the units slot and the result-status slot are read out of the same field numbers under either alignment. What the report says is that **the field is read as more repeats than the competing alignment gives it**, and that holds wherever it fires **except on the named class below**, where the two readings read the same number of repeats in different places. What it costs depends on **which** boundary was gained, and the two cases are stated separately rather than folded into one claim: - **The gained boundary is the FIRST in the field, and then a modeled slot is lost.** A field's modeled value and its components are taken from its **first repeat alone**, so everything past the boundary is still on the wire, still in `repeats`, and gone from every modeled slot. Under a set naming the repeat separator `F`, the value `28.6&S&F&U/L` reads as the two repeats `28.6^` and `&U/L`, and every value extractor answers `28.6^`, while the competing alignment reads one repeat carrying all of it. The same boundary inside a Universal Test ID field leaves the components of the first repeat only, so a UTID whose bytes carry a test code can come back as a single component holding a decoded delimiter, with the coding scheme and local code read from nothing at all. A patient name loses its given and middle names the same way. - **The gained boundary is a LATER one, and then no modeled slot moves.** The first repeat is identical under both alignments, so the value, the components and every slot taken from them read the same either way; what differs is the repeat structure after the first. **This still fires there, deliberately and measurably** (`5.0F28.6&S&F&U/L` under that same set), because the boundary is still one the bytes do not force and a consumer reading `repeats` is still reading an alignment guess. Relative to the modeled slots that is **over-reporting, never under-reporting**, which is the direction this package errs in. Narrowing the sink to the first boundary would change which streams a published package refuses and wants its own measurement, so the bound is written down instead of guessed at. **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, `repeats` still carries every one of them, and the value read is the value that was always read. What is new is that the gained boundary is reported by a code no profile may tolerate, where before it was covered only by tolerable ones. **The tail is weighed one construct deep, exactly as in [ShiftedFieldsSink](#shiftedfieldssink), and one tail is excluded for the same reason.** Where the escape character the reading taken resumes on heads a sequence this codec **recognizes**, it interprets a construct and the two repeats are the ones the sender wrote: under a set naming the repeat separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again, with nothing reported at all. Refusing that is the over-refusal that sank an earlier candidate criterion for this family, and that tail is the only one on which a stream's escaping can be clean, on the declarations the companion paragraph on [ShiftedFieldsSink](#shiftedfieldssink) scopes that to. Where the tail heads a sequence whose body is *unrecognized*, the reading taken consumes a triple it cannot read, the truncation is the same truncation, and this reports it. **That silence is a TRADE, and it is not a claim that nothing was lost there.** The gained repeat boundary is exactly as real on the excluded tail and can be entirely silent: under the canonical set `28.6&S&\&S&U/L` reads a value of `28.6^` with `warnings: []`, so `^U/L` leaves every modeled slot at a boundary the bytes do not force. Reporting it would refuse a stream whose escaping is working. **⚠️ THE TRUNCATION IS NOT UNIVERSAL OVER THE FIRING POPULATION, AND THE EXCEPTION IS NAMED RATHER THAN LEFT TO BE FOUND.** Where the sequence past the boundary carries the **repeat separator itself** as its body, the reading taken holds that character inside an opaque atom while the competing alignment splits on it, so the two readings return the **same number** of repeats in **different places**: the field is not read as more repeats than the competing alignment gives it, and what differs is the **contents**. Under the canonical set `28.6&F&\&\&U/L` reads two repeats under both. That class is bounded and costs no stream its disposition: the tail body is then a splitting delimiter in force, so `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` has already refused the record. What holds on **every** firing tuple is that the two readings disagree and that both consume every byte, so neither is forced. #### Parameters ##### segmentIndex `number` The 0-based index of the repeat being accumulated when the contested boundary was taken, so `0` is the boundary that ends the first repeat and is the one that reaches a modeled slot. It is **not** a field index: the caller splitting one field into repeats already knows which field it is in, and reports that. For how many repeats the two readings differ by, and why the count of these warnings does not give it, see the displacement paragraph on [ShiftedFieldsSink](#shiftedfieldssink). #### Returns `void` *** ### UniversalTestIdProvenance > **UniversalTestIdProvenance** = `"local-code"` \| `"unvalidated-wire-value-only"` \| `"name-only"` \| `"empty"` Where a Universal Test ID's usable identifier came from. *** ### UnknownEscapeSink > **UnknownEscapeSink** = () => `void` A callback the codec calls when it encounters an escape sequence whose body is not one of the four recognized mnemonics. The sequence is preserved verbatim; the callback lets the parser surface a value-free `ASTM_UNKNOWN_ESCAPE_SEQUENCE` warning. Optional so the codec can be used purely. #### Returns `void` *** ### UnpairedEscapeSink > **UnpairedEscapeSink** = () => `void` A callback the codec calls when it encounters an escape character that does not head a three-character escape sequence. The character is preserved verbatim as a literal; the callback lets the parser surface a value-free `ASTM_UNPAIRED_ESCAPE_CHARACTER` warning. Optional so the codec can be used purely. #### Returns `void` *** ### VocabularyAttribution > **VocabularyAttribution** = [`NamedVocabulary`](#namedvocabulary) \| [`UnattributedVocabulary`](#unattributedvocabulary) What an interpreted wire letter was graded against: either a named published code system or an explicit "no citable source". Discriminate on [NamedVocabulary.attributed](#attributed). *** ### WarningCode > **WarningCode** = *typeof* [`WARNING_CODES`](#warning_codes)\[keyof *typeof* [`WARNING_CODES`](#warning_codes)\] Discriminant type for [AstmRecordWarning.code](#code-10). Narrowing by this code lets consumers write exhaustive `switch` blocks and guarantees a typo-free comparison against [WARNING\_CODES](#warning_codes). ## Variables ### ABNORMAL\_FLAG\_CODES > `const` **ABNORMAL\_FLAG\_CODES**: readonly [`AbnormalFlagCode`](#abnormalflagcode-1)[] Every abnormal-flag letter this library recognizes, in declaration order. A letter outside this list is surfaced verbatim as unrecognized, never guessed into a meaning. #### Example ```ts import { ABNORMAL_FLAG_CODES } from "@cosyte/astm"; ABNORMAL_FLAG_CODES.includes("HU"); // true ``` *** ### ABNORMAL\_FLAG\_VOCABULARY > `const` **ABNORMAL\_FLAG\_VOCABULARY**: [`NamedVocabulary`](#namedvocabulary) The vocabulary the `R` record's abnormal-flag letters (field 7) are graded against: the HL7 v3 ObservationInterpretation code system, which carries the concepts kept aligned with the HL7 v2 Table 0078 interpretation codes this library has always recognized. The identifier and version are transcribed character for character from that code system's published source of truth. **The version records what this library compared against, not what the sender meant.** #### Example ```ts import { ABNORMAL_FLAG_VOCABULARY, interpretAbnormalFlag } from "@cosyte/astm"; interpretAbnormalFlag("HU").vocabulary === ABNORMAL_FLAG_VOCABULARY; // true ``` *** ### ALL\_ASTM\_WARNING\_CODES > `const` **ALL\_ASTM\_WARNING\_CODES**: `ReadonlySet`\<[`AnyAstmWarningCode`](#anyastmwarningcode)\> The set of every real warning code, for O(1) membership checks (used by the validator to distinguish "unknown code" from "known but forbidden"). Frozen. #### Example ```ts import { ALL_ASTM_WARNING_CODES } from "@cosyte/astm"; ALL_ASTM_WARNING_CODES.has("ASTM_LTP_FRAME_REJECTED"); // true ``` *** ### AMBIGUOUS\_CODES > `const` **AMBIGUOUS\_CODES**: `object` Stable codes for the two ways a flat, stream-scoped accessor can fail to name a single answer. Renaming a code is a **breaking change**. #### Type Declaration ##### ASTM\_AMBIGUOUS\_MULTI\_MESSAGE > `readonly` **ASTM\_AMBIGUOUS\_MULTI\_MESSAGE**: `"ASTM_AMBIGUOUS_MULTI_MESSAGE"` = `"ASTM_AMBIGUOUS_MULTI_MESSAGE"` The stream carries more than one `H` … `L` message, so a stream-wide answer spans patients. ##### ASTM\_AMBIGUOUS\_MULTI\_PATIENT > `readonly` **ASTM\_AMBIGUOUS\_MULTI\_PATIENT**: `"ASTM_AMBIGUOUS_MULTI_PATIENT"` = `"ASTM_AMBIGUOUS_MULTI_PATIENT"` The one message in the stream carries more than one `P`, so "the patient" is not determined. #### Example ```ts import { parseAstmRecords, patient, AMBIGUOUS_CODES, AstmAmbiguousStreamError } from "@cosyte/astm"; try { patient(parseAstmRecords(raw)); } catch (err) { if (err instanceof AstmAmbiguousStreamError) { err.code === AMBIGUOUS_CODES.ASTM_AMBIGUOUS_MULTI_MESSAGE; } } ``` *** ### ASTM\_ACK > `const` **ASTM\_ACK**: `6` = `0x06` Acknowledge (`0x06`): the receiver accepted the last establishment or frame. *** ### ASTM\_ENQ > `const` **ASTM\_ENQ**: `5` = `0x05` Enquiry (`0x05`): the sender's request to establish a transfer. *** ### ASTM\_EOT > `const` **ASTM\_EOT**: `4` = `0x04` End of transmission (`0x04`): the sender terminated the transfer; the line returns to neutral. *** ### ASTM\_NAK > `const` **ASTM\_NAK**: `21` = `0x15` Negative acknowledge (`0x15`): the receiver rejected the last frame; retransmit, do not accept. *** ### astmProfiles > `const` **astmProfiles**: `object` Namespace object exposing the built-in profiles: the conservative `default` baseline plus the evidence-backed `referenceCorpus` (grounded firsthand in the redistributable OSS reference corpus), each authored via the public `defineAstmProfile()` API and carrying its cited provenance. Named per-vendor profiles (cobas / Sysmex / …) are **deferred** pending a firsthand vendor-attributed quirk document: the engine fully supports them; we do not ship ungrounded ones. #### Type Declaration ##### default > `readonly` **default**: [`AstmProfile`](#astmprofile) ##### referenceCorpus > `readonly` **referenceCorpus**: [`AstmProfile`](#astmprofile) #### Example ```ts import { parseAstmRecords, astmProfiles } from "@cosyte/astm"; const msg = parseAstmRecords(raw, { profile: astmProfiles.referenceCorpus }); msg.profile?.name; // "referenceCorpus" ``` *** ### CANONICAL\_DELIMITERS > `const` **CANONICAL\_DELIMITERS**: [`Delimiters`](#delimiters-2) The canonical ASTM delimiter set (`H|\^&`). Used only as documentation and as a comparison baseline for the non-standard-delimiter warning: never as a parse-time default (delimiters are always read from the header). #### Example ```ts import { CANONICAL_DELIMITERS } from "@cosyte/astm"; CANONICAL_DELIMITERS.repeat; // "\\" ``` *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable string codes for every Tier-3 fatal the record parser may throw. Consumers narrow on `err.code` to react to specific structural failures. Renaming a code is a **breaking change**. #### Type Declaration ##### ASTM\_RECORD\_NO\_HEADER > `readonly` **ASTM\_RECORD\_NO\_HEADER**: `"ASTM_RECORD_NO_HEADER"` = `"ASTM_RECORD_NO_HEADER"` The first record is not an `H` (header) record: an ASTM message must lead with `H`. ##### ASTM\_RECORD\_UNDECLARED\_DELIMITERS > `readonly` **ASTM\_RECORD\_UNDECLARED\_DELIMITERS**: `"ASTM_RECORD_UNDECLARED_DELIMITERS"` = `"ASTM_RECORD_UNDECLARED_DELIMITERS"` The first `H` record could not declare all four delimiters (field/repeat/component/escape). One code, four reasons, and the message says which: the record is not a header, it is shorter than a header plus a three-character definition, its definition field holds fewer than three characters before the next field separator, or its field separator is also one of the other three. Only the second of those is "too short", so read the message rather than assuming it. **Two of the four cannot be reached on this fatal, and are not dead code.** A first record that is not an `H` raises `ASTM_RECORD_NO_HEADER` before the declaration is ever read, and a field separator reused among the other three ends the delimiter definition where it appears, so the truncation reason answers first. The reader names all four because it is also called directly and on later headers, where the same conditions are a warning rather than this fatal. ##### EMPTY\_INPUT > `readonly` **EMPTY\_INPUT**: `"EMPTY_INPUT"` = `"EMPTY_INPUT"` Input was empty or whitespace-only: there is nothing to parse. Shared across layers. #### Example ```ts import { parseAstmRecords, FATAL_CODES, AstmParseError } from "@cosyte/astm"; try { parseAstmRecords(""); } catch (err) { if (err instanceof AstmParseError && err.code === FATAL_CODES.EMPTY_INPUT) { // handle empty input } } ``` *** ### FRAME\_WARNING\_CODES > `const` **FRAME\_WARNING\_CODES**: `object` Stable string codes for every frame-codec warning. `key === value` so `Object.values(...)` yields a stable snapshot set. #### Type Declaration ##### ASTM\_FRAME\_BAD\_CHECKSUM > `readonly` **ASTM\_FRAME\_BAD\_CHECKSUM**: `"ASTM_FRAME_BAD_CHECKSUM"` = `"ASTM_FRAME_BAD_CHECKSUM"` A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is never silently trusted: the "checksums are routinely not validated" claim was refuted; we validate. ##### ASTM\_FRAME\_OVERSIZE > `readonly` **ASTM\_FRAME\_OVERSIZE**: `"ASTM_FRAME_OVERSIZE"` = `"ASTM_FRAME_OVERSIZE"` A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced (and, if its checksum validates, reassembled): the deviation is flagged, not silently dropped (warn in lenient, thrown in `strict`). ##### ASTM\_FRAME\_SEQUENCE\_GAP > `readonly` **ASTM\_FRAME\_SEQUENCE\_GAP**: `"ASTM_FRAME_SEQUENCE_GAP"` = `"ASTM_FRAME_SEQUENCE_GAP"` A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`): a frame was possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous; the in-progress record is tainted and not emitted as a clean reassembly. ##### ASTM\_FRAME\_UNTERMINATED > `readonly` **ASTM\_FRAME\_UNTERMINATED**: `"ASTM_FRAME_UNTERMINATED"` = `"ASTM_FRAME_UNTERMINATED"` A frame opened with `STX` but no valid terminator + checksum was found before the stream ended (or before the next `STX`), or a record's frames ended on an intermediate `ETB` with no final `ETX`. The partial bytes are surfaced flagged untrusted and **no partial record is invented** (warn in lenient, thrown in `strict`). #### Example ```ts import { decodeAstmFrames, FRAME_WARNING_CODES } from "@cosyte/astm"; const bytes = new Uint8Array([]); // ...a framed stream... void bytes; FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM; // "ASTM_FRAME_BAD_CHECKSUM" ``` *** ### LIVD\_CATALOG\_IDENTITY\_UNDECLARED > `const` **LIVD\_CATALOG\_IDENTITY\_UNDECLARED**: [`LivdCatalogIdentity`](#livdcatalogidentity) The identity reported for a catalog that declared neither a publisher nor a publication version: a positive statement that it declared none. It exists so that case is said rather than left to an absent field, and so nothing has to invent a name, an ordinal or a position to stand in for one. Frozen. #### Example ```ts import { LIVD_CATALOG_IDENTITY_UNDECLARED } from "@cosyte/astm"; LIVD_CATALOG_IDENTITY_UNDECLARED.declared; // false ``` *** ### LIVD\_WARNING\_CODES > `const` **LIVD\_WARNING\_CODES**: `object` Stable string codes for every terminology (LIVD) warning. `key === value` so `Object.values(...)` yields a stable snapshot set. #### Type Declaration ##### ASTM\_LIVD\_AMBIGUOUS\_MAPPING > `readonly` **ASTM\_LIVD\_AMBIGUOUS\_MAPPING**: `"ASTM_LIVD_AMBIGUOUS_MAPPING"` = `"ASTM_LIVD_AMBIGUOUS_MAPPING"` A vendor/local test code matched **more than one distinct LOINC** in the catalog (e.g. the same transmission code used by two devices for different analytes). The mapping is `ambiguous` and the candidate LOINCs are surfaced for inspection, but **none is chosen**: refusing to pick is the fail-safe, since guessing wrong mis-identifies the test. ##### ASTM\_LIVD\_CATALOG\_NO\_LOINC\_VERSION > `readonly` **ASTM\_LIVD\_CATALOG\_NO\_LOINC\_VERSION**: `"ASTM_LIVD_CATALOG_NO_LOINC_VERSION"` = `"ASTM_LIVD_CATALOG_NO_LOINC_VERSION"` A consumer-supplied LIVD catalog was defined **without a LOINC version**, so a mapping it produces cannot say which version of LOINC it was made against. Advisory and **never a refusal**: the catalog is still built, still indexed and still answers exactly as it would have. Raised **once, where the catalog is defined**, and never per record: it is a fact about the catalog rather than about any message, so it never joins the per-record warning stream. ##### ASTM\_LIVD\_UNMAPPED\_CODE > `readonly` **ASTM\_LIVD\_UNMAPPED\_CODE**: `"ASTM_LIVD_UNMAPPED_CODE"` = `"ASTM_LIVD_UNMAPPED_CODE"` A record carried a vendor/local test code, but the consumer-supplied LIVD catalog held **no** entry for it. The code stays surfaced verbatim and the mapping is `unmapped`: a LOINC is **never** guessed. Purely advisory: the raw code and value are untouched. #### Example ```ts import { LIVD_WARNING_CODES } from "@cosyte/astm"; LIVD_WARNING_CODES.ASTM_LIVD_UNMAPPED_CODE; // "ASTM_LIVD_UNMAPPED_CODE" ``` *** ### LTP\_WARNING\_CODES > `const` **LTP\_WARNING\_CODES**: `object` Stable string codes for every LTP protocol / transport warning. `key === value` so `Object.values(...)` yields a stable snapshot set. #### Type Declaration ##### ASTM\_LTP\_AMBIGUOUS\_TRANSPORT > `readonly` **ASTM\_LTP\_AMBIGUOUS\_TRANSPORT**: `"ASTM_LTP_AMBIGUOUS_TRANSPORT"` = `"ASTM_LTP_AMBIGUOUS_TRANSPORT"` The transport detector could not tell a framed stream from a raw (unframed) one from its leading byte: it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns, never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops framing over TCP; cobas 4800 / Iguana retain it, both realities exist, so an unrecognizable lead is defaulted, not assumed.) ##### ASTM\_LTP\_FRAME\_REJECTED > `readonly` **ASTM\_LTP\_FRAME\_REJECTED**: `"ASTM_LTP_FRAME_REJECTED"` = `"ASTM_LTP_FRAME_REJECTED"` The receiver rejected a received frame with a `NAK` instead of accepting it: because its checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text is **never** appended to the record and the transfer does **not** advance; the receiver awaits the sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame drives retransmit, not acceptance. ##### ASTM\_LTP\_UNEXPECTED\_EVENT > `readonly` **ASTM\_LTP\_UNEXPECTED\_EVENT**: `"ASTM_LTP_UNEXPECTED_EVENT"` = `"ASTM_LTP_UNEXPECTED_EVENT"` A control event arrived in a protocol state that did not expect it: e.g. an inbound `ACK`/`NAK` at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of data); it never advances the transfer as if valid. #### Example ```ts import { LTP_WARNING_CODES } from "@cosyte/astm"; LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED; // "ASTM_LTP_FRAME_REJECTED" ``` *** ### RESULT\_STATUS\_VOCABULARY > `const` **RESULT\_STATUS\_VOCABULARY**: [`UnattributedVocabulary`](#unattributedvocabulary) The attribution for the `R` record's result-status letters (field 9): **none**. The letters this library models are the ones real analyzers send, but the normative text that would bind them to a published code set is purchase-gated and has not been read here. A neighbouring HL7 table exists whose letters partly agree, and adopting it would be an assertion nothing in the public record supports. So this library says the set is unattributed rather than citing a source it cannot stand behind. #### Example ```ts import { interpretResultStatus } from "@cosyte/astm"; interpretResultStatus("F").vocabulary.attributed; // false ``` *** ### SAFETY\_CRITICAL\_CODES > `const` **SAFETY\_CRITICAL\_CODES**: `ReadonlySet`\<[`AnyAstmWarningCode`](#anyastmwarningcode)\> The forbidden set: computed as **every known code minus the tolerable allow-list**, so it is complete by construction. Frozen. A code appears here iff it is a real warning code that is not in [TOLERABLE\_CODES](#tolerable_codes). #### Example ```ts import { SAFETY_CRITICAL_CODES } from "@cosyte/astm"; SAFETY_CRITICAL_CODES.has("ASTM_RECORD_UNDEFINED_RESULT_STATUS"); // true SAFETY_CRITICAL_CODES.has("ASTM_RECORD_UNKNOWN_TYPE"); // true SAFETY_CRITICAL_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // true ``` *** ### TOLERABLE\_CODES > `const` **TOLERABLE\_CODES**: `ReadonlySet`\<[`AnyAstmWarningCode`](#anyastmwarningcode)\> The **only** warning codes a profile may list in its `tolerate` set: benign structural or syntactic vendor noise that cannot alter, drop, or fabricate an extracted value, **and** whose reported condition nothing else in this package reads: not to decide where one message ends and the next begins, and not to decide what kind of message it is. Frozen so it cannot be mutated at runtime to smuggle a code in. Adding to this set is a deliberate, reviewable act, and an addition has to satisfy both halves of that test, not just the first. `ASTM_RECORD_UNKNOWN_TYPE` is **not** on this list, and used to be. An unrecognized record type may be a header the reader did not recognize as one, in which case two messages have been read as one, or a `Q` it did not recognize, in which case the message kind is no longer knowable. Tolerating it can quiet the only report either happened. `ASTM_RECORD_FIELDS_UNSEPARATED` is not on this list either, and never was. It reports a record the delimiters in force could not split, so every modeled field of that record is missing: it fails the first half of the test outright. `ASTM_RECORD_DELIMITER_ROLE_COLLISION`, `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` and `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` are not on this list and must not be added to it. Each reports a boundary the reading cannot defend from the bytes (the first two a boundary that is not in the reading, the third one that may not be in the bytes), and each exists precisely because the only warnings its condition previously raised were among the four below. `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` is not on this list and must not be added to it either, and it is the strongest case of the four: it reports not merely a boundary but a modeled slot changing hands, up to and including a result status reading `final` that the competing alignment of the same bytes puts in no field at all. `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` is not on this list and must not be added to it either. It reports the same contested alignment deciding a **repeat** boundary, where no slot changes hands and a modeled reading is cut off instead: the field is read out of its first repeat alone, so where that boundary is the first one a value truncates and a Universal Test ID's coding scheme and local code are read from bytes that are no longer in any modeled slot. Reporting a value that is not the whole of what the sender wrote fails the first half of the two-clause test outright. `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS` is not on this list and must not be added to it either. It reports the same contested alignment deciding a **component** boundary, where nothing leaves the record and no field number changes, and every component after it moves along the component list instead: a Universal Test ID's coding scheme and local code, and a patient's given and middle names, are read out of positions the competing alignment does not put them in. Reporting a code system, a vendor's local code or a given name that the bytes do not unambiguously carry fails the first half of the two-clause test outright. #### Example ```ts import { TOLERABLE_CODES } from "@cosyte/astm"; TOLERABLE_CODES.has("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // true TOLERABLE_CODES.has("ASTM_RECORD_UNKNOWN_TYPE"); // false TOLERABLE_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // false ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, kept in lockstep with `package.json#version`. Changesets owns the bump and rewrites `package.json` only, so the release `version` script runs `scripts/sync-version.mjs` to rewrite this declaration in the same commit, and `test/sanity.test.ts` compares the two so a skipped sync goes red instead of shipping a version string that lies. #### Example ```ts import { VERSION } from "@cosyte/astm"; console.log(VERSION); ``` *** ### WARNING\_CODES > `const` **WARNING\_CODES**: `object` Stable string codes for every Tier-2 warning the record parser may emit. `key === value` so `Object.values(...)` yields a stable snapshot set. #### Type Declaration ##### ASTM\_NONSTANDARD\_DELIMITERS > `readonly` **ASTM\_NONSTANDARD\_DELIMITERS**: `"ASTM_NONSTANDARD_DELIMITERS"` = `"ASTM_NONSTANDARD_DELIMITERS"` The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. ##### ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_COMPONENTS > `readonly` **ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_COMPONENTS**: `"ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS"` = `"ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS"` Two escape alignments of the same bytes disagreed about a **component** boundary, the reading taken kept it, and the escape character that reading resumes on **heads no sequence this codec can interpret**. Same contested position and same tail test as [WARNING\_CODES.ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_FIELDS](#astm_record_alignment_shifted_fields) and [WARNING\_CODES.ASTM\_RECORD\_ALIGNMENT\_TRUNCATED\_FIELD](#astm_record_alignment_truncated_field), on the third and last splitting role, and what it costs is a third thing again, which is why it is a third code. **The components are neither shifted out of the record nor dropped from it: they MOVE along the component list.** A field's components are modeled *inside* it, so a gained component boundary shifts every component after it exactly as a gained field boundary shifts every later field, by the displacement [ShiftedFieldsSink](#shiftedfieldssink) states, **except on the one class named at the end of this entry**, where the two readings instead read the same number of components in different places. Nothing leaves the record and nothing changes field number; what changes is which modeled slot each component lands in, and those slots are named things: - **A Universal Test ID's coding scheme and local code.** Under the canonical set, `R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads four components, so `L` is the coding scheme and `687` the vendor's local code. The competing alignment reads three, so `L` is the test name and **`687` is the coding scheme**. A local code and a code-system selector are not the same thing, and a consumer routing on one of them routes on the alignment. - **A patient's given and middle names.** `P|1||MRN-0001||DOE&F&^&JANE^A||19700101|F` reads a given name of `&JANE` and a middle name of `A` under the reading taken, and under the competing one `A` is the **given** name with no middle name at all. Before this code the only warning on those streams was the **tolerable** [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character), so the widest gate-legal profile plus `{ strict: true }` accepted a test identity and a patient name whose parts were decided by the alignment rather than by the sender. **Every gained boundary at or before the LAST MODELED COMPONENT INDEX moves a slot, not only the first, and that is the difference from the repeat role.** There the field is modeled out of its first repeat alone, so only the first gained boundary reaches a modeled slot; here the shift propagates from wherever the boundary sits to the end of the component list. **TWO bounds run the other way, and this fires inside both**, which is **over-reporting, never under-reporting**: the direction this package errs in. Both axes are swept on their own rather than left to the shared corpus, which holds them fixed, and neither is closed by narrowing the guard, because narrowing changes which streams a published package refuses and wants its own measurement. - **Past the last modeled component index, nothing NAMED moves.** A model reads a fixed number of components: a patient name three (last, first, middle), a Universal Test ID four. A contested boundary further right than that still shifts the components after it while every named slot reads byte-identically under both alignments. - **Inside a LATER repeat nothing modeled moves at all**, because `components` is the first repeat, so what changes is `repeats[n]` for that `n` alone. It fires in both because the boundary is still one the bytes do not force, and a consumer reading `components` or `repeats` is still reading an alignment guess. **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, and the components read are the components that were always read. Picking the other alignment would be a different guess with no more evidence behind it, on a published package. **Withholding the moved slots is a separate question and is deliberately not answered here**, for the reason the shift report already gives: declining to model a slot changes an extracted value for every consumer of a package already on the registry. **The tail is weighed one construct deep**, exactly as for the other two, **and exactly one tail is excluded.** Where the escape character the reading resumes on heads a sequence this codec *recognizes*, it interprets a construct and the components are the ones the sender wrote: under a set naming the component separator `F`, `GLU&F&F&F&L` is that separator escaped, written, and escaped again, entirely well formed, and refusing it would be an over-refusal. That is the only tail on which a stream's escaping can be clean, on the declarations [ShiftedFieldsSink](#shiftedfieldssink) scopes that to, which is why it is the only exclusion. Where the tail heads a sequence whose body is *unrecognized*, the reading taken consumes a triple it cannot read, preserved verbatim and never guessed at, the slots move exactly as far, and **this reports it**. **That remaining silence is a TRADE, not a claim that nothing was lost.** On the excluded tail the gained boundary is exactly as real and it is `warnings: []`: under the canonical set `&F&^&F&GLU^L^687` reads one component more than the competing alignment, so `L` is the coding scheme and `687` the local code under one reading and `687` the **coding scheme** under the other, with an empty warning list. Reporting it would refuse a stream whose escaping is working. **⚠️ THE ONE CLASS WHERE NO COMPONENT INDEX MOVES, NAMED RATHER THAN LEFT TO BE FOUND.** It is a **third** bound running the other way, and unlike the two above it is about the bytes past the boundary rather than about where the boundary sits. Where the sequence past it carries the **component separator itself** as its body, the reading taken holds that character inside an opaque atom while the competing alignment splits on it, so the two readings return the **same number** of components in **different places** and what differs is their contents: under the canonical set `P|1||MRN-0001||DOE&F&^&^&JANE^A||19700101|F` reads three components under both, with `A` the middle name under both. This still fires there, and the class costs no stream its disposition: that tail body is a splitting delimiter in force, so [WARNING\_CODES.ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE](#astm_record_delimiter_swallowed_by_escape) has already refused the record. What holds on **every** firing tuple is that the two readings disagree and that both consume every byte, so neither is forced. **Catch it on the first read.** Emit rewrites the preserved sequences into recognized mnemonics, and those bytes carry the reading that was taken unambiguously, so a second-generation read is silent and is correct about its own bytes. A clean re-read is not evidence. ##### ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_FIELDS > `readonly` **ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_FIELDS**: `"ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS"` = `"ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS"` Two escape alignments of the same bytes disagreed about a **field** boundary, the reading taken kept it, and the escape character that reading resumes on **heads no sequence this codec can interpret**, so the boundary was bought with bytes this reading cannot read while the competing alignment is exactly the reading that can. Every field after that point sits further right than the competing alignment puts it, by a displacement that is **not fixed**: one place on a record carrying a single contested construct, one more for each additional one, and none at all **on the one class named at the end of this entry**, where the two readings instead read the same number of fields in different places. Counting these warnings does not give it; see [ShiftedFieldsSink](#shiftedfieldssink). **The shift is the harm, and on a result record it reaches the status slot.** Measured on the canonical set, `R|1|^^^687|28.6&F&|&U/L||||F` reads **9** fields under the reading taken and **8** under the competing one, so the sender's trailing `F` lands in field 9 (the result status) under the first and in no field at all under the second. The parse hands back units `&U/L` and a status of **`final`**, and both are consequences of the alignment rather than values the sender placed in those slots. A downstream system reading `final` would act on a result the bytes do not say was finalised. Before this code the only warning on that stream was the **tolerable** [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character), so the widest gate-legal profile plus `{ strict: true }` accepted it. **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, and the units and status read are the ones that were always read. Picking the other alignment would be a different guess with no more evidence behind it, and it would change values on a published package. What is new is that the shift is reported by a code no profile may tolerate. **It fires alongside [WARNING\_CODES.ASTM\_RECORD\_AMBIGUOUS\_ESCAPE\_ALIGNMENT](#astm_record_ambiguous_escape_alignment), not instead of it, and neither test is a widening of the other.** That code asks whether this codec's vocabulary prefers the reading taken *at* the contested position, and is silent where the earlier body is a recognized mnemonic. This one asks what the reading taken makes of the bytes *after* the boundary, and does not consult the earlier body at all. **Two deliberate bounds, both stated rather than left to be found.** - **Only the field role, and that bound is a CHOICE rather than a consequence.** A gained repeat or component boundary divides one field and reaches nothing outside it, so it moves no **field-indexed** slot: the units and the status stay where they were. **It does not follow that it moves no modeled slot at all, and writing that down would be false.** Components are modeled *inside* a field: a Universal Test ID's four components are the LOINC-candidate slot, the test name, the **coding scheme** and the **local code**, and a patient name's three are last, first and middle. A gained component boundary shifts those, so a local code can be read as a coding scheme and a given name as a middle name. Measured, and it used to be reported by nothing where the earlier body was a recognized mnemonic (only the tolerable [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character) fired, so a gate-legal profile accepted it). It is now reported by [WARNING\_CODES.ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_COMPONENTS](#astm_record_alignment_shifted_components), a separate code on the same tail test wired to the component split, because wiring this sink to another split is a different criterion needing its own population measurement. It is still not something **this** code covers. The repeat role costs the **value**, which this code does not report either, and which [WARNING\_CODES.ASTM\_RECORD\_ALIGNMENT\_TRUNCATED\_FIELD](#astm_record_alignment_truncated_field) reports. - **The tail is weighed one construct deep, and exactly one tail is excluded.** Where the escape character the reading taken resumes on heads a sequence this codec *recognizes*, the reading taken interprets a construct and leaves nothing unread: under a set naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again, entirely well formed, and refusing it would be an over-refusal. That is the only tail on which a stream's escaping can be clean, on the declarations [ShiftedFieldsSink](#shiftedfieldssink) scopes that to, which is why it is the only exclusion. Where the tail heads a sequence whose body is *unrecognized*, the reading taken consumes a triple it cannot read, preserved verbatim and never guessed at, the field shift is the same shift, and **this reports it**. Consuming a triple is not interpreting one. - **That remaining silence is a TRADE, not a claim that nothing was lost.** The gained field boundary is exactly as real on the excluded tail, and there it is not merely under-reported: it is `warnings: []`. Under the canonical set `R|1|^^^687|28.6&F&|&F&U/L||||F` reads **nine** fields against the competing alignment's eight and hands back a status of **`final`** with an empty warning list. Reporting it would refuse a stream whose escaping is working, which is the trade this bound makes. **Read the raw line when an escape character sits next to a delimiter, whether or not anything fired.** **⚠️ THE ONE CLASS WHERE NO FIELD INDEX MOVES, NAMED RATHER THAN LEFT TO BE FOUND.** Where the sequence past the boundary carries the **field separator itself** as its body, the reading taken holds that character inside an opaque atom while the competing alignment splits on it, so the two readings return the **same number** of fields in **different places**. Under the canonical set `R|1|^^^687|28.6&F&|&|&U/L||||F` reads nine fields under both, the status `F` sits in field 9 under both, and what differs is the units: `&|&U/L` against `&U/L`. This still fires there, which is over-reporting relative to the field indexes and never under-reporting, and the class costs no stream its disposition: that tail body is a splitting delimiter in force, so [WARNING\_CODES.ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE](#astm_record_delimiter_swallowed_by_escape) has already refused the record. What holds on **every** firing tuple is that the two readings disagree and that both consume every byte, so neither is forced. **Catch it on the first read.** Emit rewrites the preserved sequences into recognized mnemonics, and those bytes carry the reading that was taken unambiguously, so a second-generation read is silent and is correct about its own bytes. A clean re-read is not evidence. ##### ASTM\_RECORD\_ALIGNMENT\_TRUNCATED\_FIELD > `readonly` **ASTM\_RECORD\_ALIGNMENT\_TRUNCATED\_FIELD**: `"ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD"` = `"ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD"` Two escape alignments of the same bytes disagreed about a **repeat** boundary, the reading taken kept it, and the escape character that reading resumes on **heads no sequence this codec can interpret**. Same contested position and same tail test as [WARNING\_CODES.ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_FIELDS](#astm_record_alignment_shifted_fields), on a different delimiter role, and what it costs is a different thing, which is why it is a different code. **Nothing shifts, and that is not the same as nothing being lost.** No field-indexed slot moves: the units and the result status are read out of the same field numbers under either alignment. What this reports is that **the field is read as more repeats than the competing alignment gives it**, which holds wherever it fires **except on the one class named at the end of this entry**. What that costs depends on **which** boundary was gained, and the two cases are stated separately rather than folded into one claim. **Where it is the FIRST boundary in the field, a modeled slot is lost**, because a field's modeled value and components are taken from its **first repeat alone**. Everything past the boundary stays on the wire and stays in `repeats`, and leaves every modeled slot: - **A value truncates.** Under `H|F^&`, `R|1|^^^687|28.6&S&F&U/L||||F` reads the value field as the two repeats `28.6^` and `&U/L`, so a consumer reads `28.6^` and the rest of the value is gone. The competing alignment reads one repeat carrying all of it. - **A modeled component list is deleted rather than shifted.** The same boundary inside a Universal Test ID leaves the components of the first repeat only: under `H|F^&`, `R|1|&F&F&687|28.6|U/L||||F` reads a UTID of one component holding a decoded field separator, with no coding scheme and no local code, where the bytes carry `687`. A patient name loses its given and middle names the same way. That is the half of this condition the shift report could not cover, because components are modeled **inside** a field. Before this code the only warnings on those streams were **tolerable** ones ([WARNING\_CODES.ASTM\_NONSTANDARD\_DELIMITERS](#astm_nonstandard_delimiters) and, where the tail applies, [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character)), so the widest gate-legal profile plus `{ strict: true }` accepted a truncated value and an emptied Universal Test ID. **Where it is a LATER boundary, no modeled slot moves and this still fires.** The first repeat is identical under both alignments, so the value, the components and every slot taken from them read the same either way (`R|1|^^^687|5.0F28.6&S&F&U/L||||F` under `H|F^&` reads a value of `5.0` under both), and what differs is the repeat structure after the first. It fires there deliberately: the boundary is still one the bytes do not force and a consumer reading `repeats` is still reading an alignment guess. Relative to the modeled slots that is **over-reporting, never under-reporting**. Narrowing it to the first boundary would change which streams a published package refuses and wants its own measurement, so the bound is written down instead. **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, `repeats` still carries every one of them, and the value read is the value that was always read. Picking the other alignment would be a different guess with no more evidence behind it, on a published package. **Two further deliberate bounds, both stated rather than left to be found.** - **Only the repeat role.** The **component** role reaches a modeled slot too, and differently: there the components stay in the record and move along the component list, so a local code reads as a coding scheme and a given name as a middle name. That is measured and is now reported by [WARNING\_CODES.ASTM\_RECORD\_ALIGNMENT\_SHIFTED\_COMPONENTS](#astm_record_alignment_shifted_components), a third code on the same tail test wired to the component split. It is still not covered **here**, because wiring a sink to another split is another criterion, which is why it took its own population measurement. - **The tail is weighed one construct deep**, exactly as for the shift report, **and exactly one tail is excluded.** Where the escape character the reading resumes on heads a sequence this codec *recognizes*, it interprets a construct and the repeats are the ones the sender wrote: under a set naming the repeat separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again, entirely well formed, and refusing it would be an over-refusal. That is the only tail on which a stream's escaping can be clean, on the declarations [ShiftedFieldsSink](#shiftedfieldssink) scopes that to, which is why it is the only exclusion. Where the tail heads a sequence whose body is *unrecognized*, the reading taken consumes a triple it cannot read, preserved verbatim and never guessed at, the truncation is the same truncation, and **this reports it**. - **That remaining silence is a TRADE, not a claim that nothing was lost.** On the excluded tail the gained boundary is exactly as real and it is `warnings: []`: under the canonical set `R|1|^^^687|28.6&S&\&S&U/L|U/L||||F` reads a value of `28.6^`, and `^U/L` leaves every modeled slot, with an empty warning list. Reporting it would refuse a stream whose escaping is working. **⚠️ THE ONE CLASS WHERE THE FIELD IS NOT READ AS MORE REPEATS, NAMED RATHER THAN LEFT TO BE FOUND.** Where the sequence past the boundary carries the **repeat separator itself** as its body, the reading taken holds that character inside an opaque atom while the competing alignment splits on it, so the two readings return the **same number** of repeats in **different places** and what differs is their contents: under the canonical set `28.6&F&\&\&U/L` reads two repeats under both. This still fires there, which is over-reporting relative to the repeat count and never under-reporting, and the class costs no stream its disposition: that tail body is a splitting delimiter in force, so [WARNING\_CODES.ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE](#astm_record_delimiter_swallowed_by_escape) has already refused the record. What holds on **every** firing tuple is that the two readings disagree and that both consume every byte, so neither is forced. **Catch it on the first read.** Emit rewrites the preserved sequences into recognized mnemonics, and those bytes carry the reading that was taken unambiguously, so a second-generation read is silent and is correct about its own bytes. A clean re-read is not evidence. ##### ASTM\_RECORD\_AMBIGUOUS\_ESCAPE\_ALIGNMENT > `readonly` **ASTM\_RECORD\_AMBIGUOUS\_ESCAPE\_ALIGNMENT**: `"ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT"` = `"ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT"` Escape sequences are matched greedily and leftmost, so the escape character that **closed** an unrecognized sequence could not also **open** the next one. Where it could have, and where the body it would have held is the delimiter that was split on, the same bytes carry two alignments that disagree by one boundary: under the reading taken that delimiter ends a field, repeat or component, and under the other it sits inside an opaque atom and ends nothing. The leftmost reading is kept, every byte is preserved, and nothing is re-split. **This is the mirror of [WARNING\_CODES.ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE](#astm_record_delimiter_swallowed_by_escape), and the direction is the reason it needs its own code.** That one reports a boundary the reading **lost**; this one reports a boundary the reading may have **gained**, which is the more dangerous direction, because a gained boundary hands back a value the sender's bytes do not unambiguously carry. Measured on the canonical set: `R|1|^^^687|28.6&Z&|&U/L||||F` reads `value` = `28.6&Z&` and `units` = `&U/L` under the leftmost alignment, and reads as a single unsplit field carrying both under the other. **Two exclusions, both deliberate, and the first is wider than its own argument.** Where the earlier sequence's body is a **recognized** mnemonic nothing is reported, because the reading taken interprets a construct (`&F&` is the sender escaping a field separator, which is what the mechanism is for) while the competing alignment's body is a delimiter character it usually cannot interpret at all, so this codec's own vocabulary prefers the reading taken. That is not the same as the reading taken being conformant, and where the declared set names a mnemonic letter as a splitting delimiter both alignments interpret one construct and neither is preferred, yet this stays silent. Both residues are measured and recorded rather than closed by widening the test. And where the escape character after the delimiter does not itself close a sequence there is no competing alignment at all, so an ordinary escaped value followed by an ordinary boundary is silent. **It is a report, not a repair, and not a round-trip guard.** The reading is unchanged: picking the other alignment would be a different guess with no more evidence behind it. Emit then rewrites the preserved sequences into recognized mnemonics, and those bytes carry the reading that was taken unambiguously, so a second-generation read is silent and is correct about its own bytes. The first read of the wire bytes is the only place the ambiguity exists to be caught. ##### ASTM\_RECORD\_AMBIGUOUS\_MESSAGE\_KIND > `readonly` **ASTM\_RECORD\_AMBIGUOUS\_MESSAGE\_KIND**: `"ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND"` = `"ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND"` A message carried **both** a `Q` (request) and an `R` (result) record: a contradictory shape. The message is classified `host-query` (the `Q` **dominates**, so it is never read as a result set) and this warning flags the anomaly. Positional context only; no field value. ##### ASTM\_RECORD\_AMBIGUOUS\_VALUE\_SPLIT > `readonly` **ASTM\_RECORD\_AMBIGUOUS\_VALUE\_SPLIT**: `"ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT"` = `"ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT"` A result value field carried an *unescaped* component delimiter, so it split into more than one component. Both the full raw value and the split are surfaced and this warning fires: the ambiguity is never resolved silently into a truncated value. ##### ASTM\_RECORD\_DELIMITER\_ROLE\_COLLISION > `readonly` **ASTM\_RECORD\_DELIMITER\_ROLE\_COLLISION**: `"ASTM_RECORD_DELIMITER_ROLE_COLLISION"` = `"ASTM_RECORD_DELIMITER_ROLE_COLLISION"` A header declared **one character in two roles**, so the boundary between those two roles is not recoverable from the bytes. The declaration is still read and honored and no record is dropped: what is gone is a distinction the sender's own bytes no longer carry. The field separator is not part of this: a declaration naming it in another role is refused earlier (the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal on the first header, [WARNING\_CODES.ASTM\_RECORD\_UNREADABLE\_REDECLARATION](#astm_record_unreadable_redeclaration) on a later one). What this code covers is the three unordered pairs among the rest: **repeat/component, repeat/escape, component/escape**. Measured on `H|^^&` (repeat and component both `^`): the field `A^B^C^D` reads back as four repeats of one component each, so `components` holds only `A` and a two-repeats-of-two-components reading cannot be recovered. Measured on `H|\&&` (component and escape both `&`): `A&B` splits into two components while `A&F&B` reads as the single component `A|B`, so the same character means two different things depending on what follows it. **It is not tolerable**, and the reason is the pair it travels with: such a set is always non-canonical, so before this code existed the only warning on the stream was [WARNING\_CODES.ASTM\_NONSTANDARD\_DELIMITERS](#astm_nonstandard_delimiters), which a profile may tolerate. That made a structurally unreadable declaration indistinguishable, to a strict consumer, from an ordinary vendor set. Emit refuses the same sets outright (`ASTM_EMIT_INVALID_DELIMITERS`). One warning per header that **changes** the set in force into such a set, not one per colliding pair. A later header restating the colliding set already in force is a no-op and warns nothing, on the same rule as [WARNING\_CODES.ASTM\_NONSTANDARD\_DELIMITERS](#astm_nonstandard_delimiters): the set it names was already reported when it came into force. ##### ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE > `readonly` **ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE**: `"ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE"` = `"ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE"` An escape sequence whose body was **not** a recognized mnemonic held a character that is one of the three splitting delimiters in force, and the atom rule (an `&X&` triple is opaque) kept it out of the split, so a boundary the bytes carried never became one. The sequence is preserved verbatim in the value; nothing is dropped and nothing is re-split. Measured on the canonical set: `R|1|^^^687|28.6&|&U/L||||F` reads `value` = `28.6&|&U/L`, with **no units** and status `unspecified` rather than `final`. **This is the code that says a boundary was lost.** The same condition also raises [WARNING\_CODES.ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE](#astm_unknown_escape_sequence), which stays and stays tolerable: that code reports only that a body was not recognized, which is true of bodies that cost nothing. This one is the narrower, safety-critical half, and no profile may tolerate it. **What it does not do is repair anything.** The atom rule is unchanged (it is what keeps `&F&` one token under a set that names `F` as a delimiter), so the value is byte-identical to what it was before this code existed. It also cannot see the condition through a re-emit: emit rewrites the preserved sequence into recognized mnemonics, and the resulting stream says that value unambiguously, so a second-generation read is silent and correct about its own bytes. The place to catch this is the first read of the wire bytes, which is where it now refuses a strict parse. ##### ASTM\_RECORD\_DELIMITERS\_REDECLARED > `readonly` **ASTM\_RECORD\_DELIMITERS\_REDECLARED**: `"ASTM_RECORD_DELIMITERS_REDECLARED"` = `"ASTM_RECORD_DELIMITERS_REDECLARED"` A later `H` (header) record declared a **different** delimiter set from the one in force, and the parser **followed it**: that header and every record after it are read with the newly-declared delimiters, until the next `H`. Records already read keep the set that was in force when they were read: a redeclaration never reinterprets bytes that have already been consumed. A stream carrying several messages back to back is ordinary, and a header that simply **repeats** the delimiters already in force is a no-op that warns nothing. This code fires only when the set actually changes, because that is the point at which a reader still using the old set would begin merging fields together. ##### ASTM\_RECORD\_FIELDS\_UNSEPARATED > `readonly` **ASTM\_RECORD\_FIELDS\_UNSEPARATED**: `"ASTM_RECORD_FIELDS_UNSEPARATED"` = `"ASTM_RECORD_FIELDS_UNSEPARATED"` The delimiters in force found **no field separator at all** in a record that carries content beyond its type letter, so the whole record read back as a single field and none of its modeled fields could be recovered. The raw line is surfaced intact and nothing is dropped, but on a result record it means the value, the units and the status are all absent from the parsed model. This is one signature of a record being read with a delimiter set that does not belong to it, which is how a delimiter-scoping mistake turns into lost values. It is reported rather than repaired, because recovering the fields would mean guessing which set the sender meant. **Its absence is not evidence that a record was read in its own set.** This tests one of the four delimiter roles, the field separator, and only in its total form, where that separator occurs in the line (unescaped). Two whole classes of the same loss are outside it: a foreign set whose field separator happens to occur somewhere in the line still splits (on the wrong boundaries, silently, and this can happen to one record inside a run of these warnings); and a set differing in the repeat, component or escape role usually splits into fields normally, where a mis-split component can cost a test identity while the value survives, and where an `&X&` sequence whose body is an unrecognized character that is itself a delimiter in force is an opaque atom, so that delimiter does not split and every field after it shifts. The escape role's worst case has narrowed and not disappeared: an escape character heading no sequence is now read as a literal and reported under [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character) rather than merging the rest of the record, and a delimiter swallowed inside an `&X&` body now raises [WARNING\_CODES.ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE](#astm_record_delimiter_swallowed_by_escape) as well as the tolerable [WARNING\_CODES.ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE](#astm_unknown_escape_sequence), though it still splits the same way. The mirror of that case, where the leftmost alignment lets a delimiter split that a competing alignment would have held, gains a boundary rather than losing one, so the record splits into more fields than another reading gives and this code cannot see it either ([WARNING\_CODES.ASTM\_RECORD\_AMBIGUOUS\_ESCAPE\_ALIGNMENT](#astm_record_ambiguous_escape_alignment) reports that). Treat this code as a report that one record definitely lost its fields, never as a sweep that would have fired if any had. ##### ASTM\_RECORD\_ORPHAN\_COMMENT > `readonly` **ASTM\_RECORD\_ORPHAN\_COMMENT**: `"ASTM_RECORD_ORPHAN_COMMENT"` = `"ASTM_RECORD_ORPHAN_COMMENT"` A `C` (comment) record had no valid preceding `H`/`P`/`O`/`R` parent: an **orphan**. The comment is attached to the message root (`attachedToRoot: true`) and surfaced, **never dropped**. ##### ASTM\_RECORD\_PARTIAL\_TIMESTAMP > `readonly` **ASTM\_RECORD\_PARTIAL\_TIMESTAMP**: `"ASTM_RECORD_PARTIAL_TIMESTAMP"` = `"ASTM_RECORD_PARTIAL_TIMESTAMP"` A `YYYYMMDDHHMMSS` timestamp had an odd digit run that truncates a two-digit component in half (e.g. a partial day/hour). The raw run is preserved and the structured value stops at the last **complete** component: the dangling digit is **never zero-filled into a fabricated time**. ##### ASTM\_RECORD\_UNDEFINED\_ABNORMAL\_FLAG > `readonly` **ASTM\_RECORD\_UNDEFINED\_ABNORMAL\_FLAG**: `"ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG"` = `"ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG"` A result's abnormal-flag field (`R` field 7) carried a letter outside HL7 Table 0078. The flag is surfaced as `undefined`: never dropped, and **never coerced to `normal`** (a clinical error). ##### ASTM\_RECORD\_UNDEFINED\_RESULT\_STATUS > `readonly` **ASTM\_RECORD\_UNDEFINED\_RESULT\_STATUS**: `"ASTM_RECORD_UNDEFINED_RESULT_STATUS"` = `"ASTM_RECORD_UNDEFINED_RESULT_STATUS"` A result's status field (`R` field 9) carried a letter that is not a recognized status. It is surfaced as `undefined` and, like every non-`F` status, never reads as active-final. ##### ASTM\_RECORD\_UNINTERPRETED\_QUERY\_STATUS > `readonly` **ASTM\_RECORD\_UNINTERPRETED\_QUERY\_STATUS**: `"ASTM_RECORD_UNINTERPRETED_QUERY_STATUS"` = `"ASTM_RECORD_UNINTERPRETED_QUERY_STATUS"` A `Q` (request-information) record carried a request-information status code (field 13). The code *set* is `[OSS-derived / paywalled]` with no publicly-groundable enumeration, so the parser interprets **none** of them: the status is surfaced verbatim and this value-free warning flags that it was passed through **uninterpreted**, never mapped to a guessed meaning. ##### ASTM\_RECORD\_UNITS\_ABSENT > `readonly` **ASTM\_RECORD\_UNITS\_ABSENT**: `"ASTM_RECORD_UNITS_ABSENT"` = `"ASTM_RECORD_UNITS_ABSENT"` A result carried a numeric value but no units (`R` field 5 empty). Units are vendor free text (not UCUM); a missing unit is flagged here and **never defaulted, guessed, or converted**. ##### ASTM\_RECORD\_UNKNOWN\_TYPE > `readonly` **ASTM\_RECORD\_UNKNOWN\_TYPE**: `"ASTM_RECORD_UNKNOWN_TYPE"` = `"ASTM_RECORD_UNKNOWN_TYPE"` A record's type letter is not one of the modeled types: surfaced as an unsupported record, never dropped. Treat it as a possible lost message boundary, because a header the reader did not recognize as one does not open a new message. ##### ASTM\_RECORD\_UNPARSEABLE\_REFERENCE\_RANGE > `readonly` **ASTM\_RECORD\_UNPARSEABLE\_REFERENCE\_RANGE**: `"ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE"` = `"ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE"` A result's reference-range field (`R` field 6) did not match a recognized form (`low-high`, `low`). The text is surfaced verbatim as `unparsed`: a bound is **never fabricated**. ##### ASTM\_RECORD\_UNREADABLE\_REDECLARATION > `readonly` **ASTM\_RECORD\_UNREADABLE\_REDECLARATION**: `"ASTM_RECORD_UNREADABLE_REDECLARATION"` = `"ASTM_RECORD_UNREADABLE_REDECLARATION"` A later `H` (header) record could not declare a usable delimiter set: it was not a header, it was too short, its delimiter definition held fewer than three characters, or the field separator it named also appeared among the other three, leaving the four roles indistinguishable. The delimiters **already in force are kept** and every record is still surfaced; a set is never guessed and no record is dropped. The same condition on the *first* header is unrecoverable and remains the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal: there is no earlier set to fall back to. ##### ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE > `readonly` **ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE**: `"ASTM_UNKNOWN_ESCAPE_SEQUENCE"` = `"ASTM_UNKNOWN_ESCAPE_SEQUENCE"` An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. ##### ASTM\_UNPAIRED\_ESCAPE\_CHARACTER > `readonly` **ASTM\_UNPAIRED\_ESCAPE\_CHARACTER**: `"ASTM_UNPAIRED_ESCAPE_CHARACTER"` = `"ASTM_UNPAIRED_ESCAPE_CHARACTER"` An escape character appeared where no escape sequence starts (an escape sequence is the escape character, one body character, and the escape character again). It is read as the **literal character it is** and kept byte-for-byte in the decoded value. Nothing is dropped and no byte is invented: this flags that the sender did not write the character the spec-clean way, which is `&E&`. **What it replaced is the reason it exists.** The codec used to read such a character as the opening of a sequence that never closed and merge the whole remainder of the record into the field it sat in, reporting nothing at all. One `&` in a result value cost the units, the abnormal flag and the status together and left the status reading `unspecified` rather than `final`; one in a surname cost the patient's birth date and sex. Emit then re-escaped the merged text into a spec-clean-looking line that read back as the same wrong value, so the mis-read survived a round trip without ever surfacing. **This code is not a statement about the rest of the record.** It reports one character. A *different* escape character in the same record may still head a real three-character sequence, and if that sequence's body happens to be a delimiter (`&|&` under the canonical set) the atom rule means that delimiter does not split. That case is reported separately, under [WARNING\_CODES.ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE](#astm_record_delimiter_swallowed_by_escape), and it still costs a field boundary. ##### PROFILE\_QUIRK\_APPLIED > `readonly` **PROFILE\_QUIRK\_APPLIED**: `"PROFILE_QUIRK_APPLIED"` = `"PROFILE_QUIRK_APPLIED"` The downgraded form an active vendor [AstmProfile](#astmprofile) produces from a deviation it *expects* (see `src/profiles/`). The original warning is **never dropped**: its code moves to [AstmRecordWarning.toleratedCode](#toleratedcode), the warning is re-badged `PROFILE_QUIRK_APPLIED` with `expected: true` and the tolerating profile named, so a consumer can filter known, grounded noise while the fact of the deviation, and where it was, survives. A profile can only ever reach this path for a **non-safety-critical** code (enforced at profile-definition time); a safety-critical deviation (a result value, flag, status, patient identifier, code system, or a frame-integrity warning) can **never** be tolerated, so it can never be re-badged here. #### Example ```ts import { parseAstmRecords, WARNING_CODES } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rL|1\r"); msg.warnings.some((w) => w.code === WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE); ``` ## Functions ### alignmentShiftedComponents() > **alignmentShiftedComponents**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS` warning. Emitted when a competing escape alignment decided a **component** boundary and the escape character the reading taken resumes on heads no sequence this codec can interpret, so every component after that point sits further right than the competing alignment puts it. No field number changes and nothing leaves the record, and that is the difference from [alignmentShiftedFields](#alignmentshiftedfields) and [alignmentTruncatedField](#alignmenttruncatedfield): what moves is which modeled slot each component lands in, so a Universal Test ID's coding scheme and local code, or a patient's given and middle names, are read out of positions the competing alignment does not put them in. **Every gained boundary at or before the last modeled component index moves those slots, not only the first**; past that index, and inside a **later repeat**, nothing named moves and this still fires, which is over-reporting and never under-reporting. **How far the components are displaced, including the tie class where they do not move at all, is stated once on [ShiftedFieldsSink](#shiftedfieldssink)**; it is not fixed. A profile may **not** tolerate this code. **Which tolerable escape report accompanies it depends on the tail**: [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character) where the tail heads no sequence at all, and [WARNING\_CODES.ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE](#astm_unknown_escape_sequence) where it heads one whose body is unrecognized. Both report strictly weaker facts. **Which of them accompanies this code, and the declaration under which neither does, is stated once on the tail test itself**; see [ShiftedFieldsSink](#shiftedfieldssink). It fires alongside [WARNING\_CODES.ASTM\_RECORD\_AMBIGUOUS\_ESCAPE\_ALIGNMENT](#astm_record_ambiguous_escape_alignment) where that also applies. The reading is unchanged: this reports the moved components, it does not repair them. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { alignmentShiftedComponents } from "@cosyte/astm"; alignmentShiftedComponents({ recordIndex: 2, recordType: "R", fieldIndex: 3 }); ``` *** ### alignmentShiftedFields() > **alignmentShiftedFields**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` warning. Emitted when a competing escape alignment decided a **field** boundary and the escape character the reading taken resumes on heads no sequence this codec can interpret, so every field after that point sits further right than the competing alignment puts it. On a result record that reaches the units and the **result status**: a trailing status letter lands in field 9 under the reading taken and in no field at all under the competing one. **How far the fields are displaced, and why counting these warnings does not give it, is stated once on [ShiftedFieldsSink](#shiftedfieldssink)**; it is not fixed. A profile may **not** tolerate this code. **Which tolerable escape report accompanies it depends on the tail**: [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character) where the tail heads no sequence at all, and [WARNING\_CODES.ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE](#astm_unknown_escape_sequence) where it heads one whose body is unrecognized. Both report strictly weaker facts. **Which of them accompanies this code, and the declaration under which neither does, is stated once on the tail test itself**; see [ShiftedFieldsSink](#shiftedfieldssink). It fires alongside [WARNING\_CODES.ASTM\_RECORD\_AMBIGUOUS\_ESCAPE\_ALIGNMENT](#astm_record_ambiguous_escape_alignment) where that also applies. The reading is unchanged: this reports the shift, it does not repair it. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { alignmentShiftedFields } from "@cosyte/astm"; alignmentShiftedFields({ recordIndex: 2, recordType: "R", fieldIndex: 4 }); ``` *** ### alignmentTruncatedField() > **alignmentTruncatedField**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` warning. Emitted when a competing escape alignment decided a **repeat** boundary and the escape character the reading taken resumes on heads no sequence this codec can interpret, so the field is read as more repeats than the competing alignment gives it. No field-indexed slot moves, and that is the difference from [alignmentShiftedFields](#alignmentshiftedfields). Where the gained boundary is the **first** in the field it still reaches a modeled slot, because a field's modeled value and components are taken from its first repeat alone: everything past the boundary stays in `repeats` and leaves every modeled slot, so a result value truncates and a Universal Test ID or a patient name loses the components that sat after it. At a **later** boundary nothing modeled moves and this still fires, which is over-reporting relative to those slots and never under-reporting. **By how many repeats the two readings differ, including the tie class where they do not differ at all, is stated once on [ShiftedFieldsSink](#shiftedfieldssink).** A profile may **not** tolerate this code. **Which tolerable escape report accompanies it depends on the tail**: [WARNING\_CODES.ASTM\_UNPAIRED\_ESCAPE\_CHARACTER](#astm_unpaired_escape_character) where the tail heads no sequence at all, and [WARNING\_CODES.ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE](#astm_unknown_escape_sequence) where it heads one whose body is unrecognized. Both report strictly weaker facts. **Which of them accompanies this code, and the declaration under which neither does, is stated once on the tail test itself**; see [ShiftedFieldsSink](#shiftedfieldssink). It fires alongside [WARNING\_CODES.ASTM\_RECORD\_AMBIGUOUS\_ESCAPE\_ALIGNMENT](#astm_record_ambiguous_escape_alignment) where that also applies. The reading is unchanged: this reports the gained boundary, it does not repair it. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { alignmentTruncatedField } from "@cosyte/astm"; alignmentTruncatedField({ recordIndex: 2, recordType: "R", fieldIndex: 4 }); ``` *** ### ambiguousEscapeAlignment() > **ambiguousEscapeAlignment**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` warning. Emitted when the escape character that closed an unrecognized escape sequence could instead have opened one holding the delimiter that was split on, so the bytes carry two alignments that disagree about that boundary. The leftmost alignment is kept and every byte is preserved; what the warning reports is that the boundary is a choice. A profile may **not** tolerate this code. It fires alongside [WARNING\_CODES.ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE](#astm_unknown_escape_sequence), which remains tolerable and reports the strictly weaker fact that a body was not recognized. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { ambiguousEscapeAlignment } from "@cosyte/astm"; ambiguousEscapeAlignment({ recordIndex: 4, recordType: "R", fieldIndex: 4 }); ``` *** ### ambiguousMessageKind() > **ambiguousMessageKind**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning. Emitted when a message carries both a `Q` (request) and an `R` (result) record; the message is classified as a host-query request (the `Q` dominates) and the anomaly is flagged. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { ambiguousMessageKind } from "@cosyte/astm"; ambiguousMessageKind({ recordIndex: 0, recordType: "H" }); ``` *** ### ambiguousValueSplit() > **ambiguousValueSplit**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning. Emitted when a result value field split on an unescaped component delimiter: the full raw value and the split are both surfaced, never a silent truncation. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { ambiguousValueSplit } from "@cosyte/astm"; ambiguousValueSplit({ recordIndex: 3, recordType: "R", fieldIndex: 4 }); ``` *** ### applyAstmProfile() > **applyAstmProfile**(`profile`, `warning`): [`AstmRecordWarning`](#astmrecordwarning) Apply a profile to a single warning. Returns a downgraded `PROFILE_QUIRK_APPLIED` warning when the profile expects this deviation; otherwise returns the original warning unchanged (referential identity preserved, so an un-tolerated warning is never reallocated). A warning that is already `expected` (e.g. re-processed) is passed through untouched. **The safety gate is re-checked here, and that is not redundant.** [defineAstmProfile](#defineastmprofile) refuses a safety-critical code at definition time, but [AstmProfile](#astmprofile) is a plain interface: a hand-authored object literal type-checks, and `parseAstmRecords(raw, { profile })` and `setDefaultAstmProfile` both accept one without re-running the factory. A definition-time-only gate therefore guards a door that has a second entrance. A safety-critical code is **not downgraded here whatever the profile says**, so the original warning survives and `strict` still escalates it. Silently declining to downgrade, rather than throwing, is the fail-safe direction: the signal is preserved either way, and a parse is not turned into an exception by a profile the caller built by hand. #### Parameters ##### profile [`AstmProfile`](#astmprofile) The active profile. ##### warning [`AstmRecordWarning`](#astmrecordwarning) One accumulated warning. #### Returns [`AstmRecordWarning`](#astmrecordwarning) The re-badged warning when tolerated, else the original. #### Example ```ts import { applyAstmProfile, astmProfiles, unknownEscapeSequence } from "@cosyte/astm"; const w = unknownEscapeSequence({ recordIndex: 4, recordType: "R", fieldIndex: 5 }); const out = applyAstmProfile(astmProfiles.referenceCorpus, w); out.code; // "PROFILE_QUIRK_APPLIED" out.toleratedCode; // "ASTM_UNKNOWN_ESCAPE_SEQUENCE" ``` *** ### applyAstmProfileToWarnings() > **applyAstmProfileToWarnings**(`warnings`, `profile`): [`AstmRecordWarning`](#astmrecordwarning)[] Apply a profile across a whole warning list, returning a NEW array (the input is never mutated). Returned unchanged (same reference contents, a shallow copy) when `profile` is `undefined`, so the no-profile path pays only a copy. Every tolerated warning is re-badged; every other warning passes through by identity, nothing is dropped, reordered, or reallocated beyond the re-badge. #### Parameters ##### warnings readonly [`AstmRecordWarning`](#astmrecordwarning)[] The accumulated record warnings. ##### profile [`AstmProfile`](#astmprofile) \| `undefined` The active profile, or `undefined` for no transform. #### Returns [`AstmRecordWarning`](#astmrecordwarning)[] A new array with tolerated warnings downgraded. #### Example ```ts import { applyAstmProfileToWarnings, astmProfiles } from "@cosyte/astm"; const out = applyAstmProfileToWarnings(msgWarnings, astmProfiles.referenceCorpus); ``` *** ### applyLivd() > **applyLivd**(`msg`, `catalog`): [`LivdResult`](#livdresult) Apply a consumer-supplied LIVD catalog to a parsed message, producing an **additive, advisory** layer of LOINC annotations over its `R` (result) and `O` (order) records. The source message is never mutated; the raw reported codes and values stay exactly as parsed. Fail-safe: with no matching entry a code is `unmapped` (+ an `ASTM_LIVD_UNMAPPED_CODE` warning); with a conflicting mapping it is `ambiguous` (+ an `ASTM_LIVD_AMBIGUOUS_MAPPING` warning); a LOINC is **never** guessed. A populated component 1 never short-circuits the lookup and is never reported as a LOINC: it is carried verbatim as an unvalidated wire value, and where it differs from the catalog's answer the difference is reported and left unresolved. A catalog is code a consumer supplies, so a lookup that throws propagates to the caller unchanged: a consumer's own failure is never reported as a catalog miss, and no partially annotated result is returned. #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed ASTM message. ##### catalog [`LivdCatalog`](#livdcatalog) The consumer-supplied LIVD catalog (build with [defineLivdCatalog](#definelivdcatalog)). #### Returns [`LivdResult`](#livdresult) The annotations and value-free warnings (deeply frozen). #### Example ```ts import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|Glucose^^^687|28.6|U/L||N||F\rL|1\r"); const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8", loincLongName: "AST" }]); const [a] = applyLivd(msg, catalog).annotations; a?.mapping; // { status: "mapped", loinc: "1920-8", loincLongName: "AST", source: "livd", derived: true } a?.unvalidatedWireValue; // "Glucose": carried verbatim, vouched for by nothing a?.wireValueDisagreesWithCatalog; // true: reported, never resolved ``` *** ### astmDateToLocalISO() > **astmDateToLocalISO**(`d`): `string` Render an [AstmDate](#astmdate) as an ISO-8601-*like* string truncated to its precision, with **no** `Z` and **no** offset: because ASTM carries no timezone and appending one would fabricate information. A consumer that knows the instrument's zone can attach it; this function never assumes UTC. #### Parameters ##### d [`AstmDate`](#astmdate) The date to render. #### Returns `string` e.g. `"2024-03-15T09:30"` (minute precision) or `"2024-03"` (month). #### Example ```ts import { astmDateToLocalISO, parseAstmDate } from "@cosyte/astm"; astmDateToLocalISO(parseAstmDate("20240315")!); // "2024-03-15" ``` *** ### attachComments() > **attachComments**(`records`, `warnings`): [`AstmRecord`](#astmrecord)[] Attach every `C` (comment) record to its parent by position: the immediately-preceding `H`/`P`/`O`/`R` record; consecutive comments share that parent. **Fail-safe:** a comment with no valid preceding parent is an **orphan**, it is attached to the message root (`attachedToRoot: true`) with an `ASTM_RECORD_ORPHAN_COMMENT` warning, **never dropped**. Returns a new array; the non-comment records pass through unchanged. #### Parameters ##### records readonly [`AstmRecord`](#astmrecord)[] The built records, in wire order. ##### warnings [`AstmRecordWarning`](#astmrecordwarning)[] The accumulator an orphan comment warns onto. #### Returns [`AstmRecord`](#astmrecord)[] The records with each comment's `parentIndex` / `attachedToRoot` resolved. #### Example ```ts import { attachComments } from "@cosyte/astm"; // A comment with no preceding H/P/O/R is an orphan attached to the root. const warnings: import("@cosyte/astm").AstmRecordWarning[] = []; const out = attachComments( [{ type: "C", recordIndex: 0, fields: [], attachedToRoot: false }], warnings, ); (out[0] as { attachedToRoot: boolean }).attachedToRoot; // true ``` *** ### buildAstmMessage() > **buildAstmMessage**(`input`): `string` Build a spec-clean ASTM/CLSI-LIS02 record stream from typed input. The builder emits the canonical `H|\^&` header (plus any header fields), then every body record with each supplied field at its correct 1-based position and every value escape-encoded, then an `L` terminator. **Nothing clinical is defaulted** (an unsupplied field is empty, never a guessed value) and the structural pieces (delimiters, record types, sequence counters, terminator) are computed. The result parses back to an equal message: build → parse fidelity by construction. #### Parameters ##### input [`MessageInput`](#messageinput) The message to build. #### Returns `string` The serialized, `CR`-terminated record stream. #### Throws [AstmSerializeError](#astmserializeerror) when a value contains an unencodable `CR`/`LF`. #### Example ```ts import { buildAstmMessage, parseAstmRecords, results } from "@cosyte/astm"; const raw = buildAstmMessage({ records: [{ type: "R", universalTestId: ["", "", "", "687"], value: "28.6", units: "U/L" }], }); results(parseAstmRecords(raw))[0]?.value; // "28.6" ``` *** ### classifyMessage() > **classifyMessage**(`records`): [`AstmMessageClassification`](#astmmessageclassification-1) Classify a record stream by the host-query flow. Pure and total: it only reads the record type letters, never a field value. `Q` dominates: a message with any `Q` record is a `host-query` request even when a result (`R`) record is also present, so a query is never misread as a result upload. (The `Q`+`R` anomaly is separately warned at parse time.) An **unsupported** record letter with no `Q` alongside it yields `indeterminate`, never `results` or `orders`: an unreadable letter may have been the `Q`, and claiming a positive kind over it is how a query comes to read as a result set. [AstmMessageClassification.hasUnrecognized](#hasunrecognized) reports why the answer is withheld, and the `has*` counts stay truthful throughout. #### Parameters ##### records readonly [`AstmRecord`](#astmrecord)[] The parsed records, in wire order. #### Returns [`AstmMessageClassification`](#astmmessageclassification-1) The message classification. #### Example ```ts import { classifyMessage, parseAstmRecords } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r"); classifyMessage(msg.records).kind; // "host-query" ``` *** ### comments() > **comments**(`msg`): readonly [`CommentRecord`](#commentrecord)[] Every comment (`C`) record in the message, in wire order. Each carries the `parentIndex` of the `H`/`P`/`O`/`R` it attaches to (or `attachedToRoot` when it is an orphan): use [commentsFor](#commentsfor) to get the comments of one record. #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed **single-message** stream. #### Returns readonly [`CommentRecord`](#commentrecord)[] The comment records (possibly empty). #### Throws [AstmAmbiguousStreamError](#astmambiguousstreamerror) (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream carries more than one message: see [messages](#messages). #### Example ```ts import { parseAstmRecords, comments } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|5|U/L||||F\rC|1|I|checked|G\rL|1\r"); comments(msg)[0]?.text; // "checked" ``` *** ### commentsFor() > **commentsFor**(`msg`, `parent`): readonly [`CommentRecord`](#commentrecord)[] The comment (`C`) records attached to a given parent record, in wire order. Unlike the other extractors this works on **any** stream, single- or multi-message: the parent record it is handed already names the message, so there is nothing to disambiguate. Returns the comments whose `parentIndex` is that record's `recordIndex`, so a comment carrying (e.g.) QC context is read against the record it modifies: never floated to the wrong one. #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed message. ##### parent [`AstmRecord`](#astmrecord) The `H`/`P`/`O`/`R` record whose comments to collect. #### Returns readonly [`CommentRecord`](#commentrecord)[] The attached comment records (possibly empty). #### Example ```ts import { parseAstmRecords, results, commentsFor } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|5|U/L||||F\rC|1|I|checked|G\rL|1\r"); commentsFor(msg, results(msg)[0]!)[0]?.text; // "checked" ``` *** ### composeAstmFrames() > **composeAstmFrames**(`records`, `options?`): `Uint8Array` Frame reassembled record bytes into a spec-clean ASTM/CLSI-LIS01 byte stream: the inverse of [decodeAstmFrames](#decodeastmframes). Each record is split at 240 text bytes into `ETB`-closed intermediate frames and a final `ETX` frame; frame numbers run `1, 2, … 7, 0, 1 …` continuously across the whole stream; every frame's modulo-256 checksum is computed and emitted uppercase. **A record in either accepted form is written through unchanged, or refused.** A record given as a `Uint8Array` is already bytes. A record given as a `string` is a **byte string**, character `i` becoming byte `i`, the exact inverse of how a decoded record becomes a string again. A character above `U+00FF` is not a byte, so it is refused with `ASTM_FRAME_UNENCODABLE_CHARACTER` rather than replaced by a different character (it used to be truncated to its low byte, which is another ordinary character, and reached the wire silently). To frame content outside Latin-1, encode it with the character encoding your instrument uses and pass the resulting `Uint8Array`. Those two forms are the whole of what `records` accepts. A caller reaching this from JavaScript with some **other** typed array is outside the signature and gets no such refusal: each element is still written as its low byte, the same substitution described above. The reserved-byte check below does not reach that route either, because it compares elements against the three byte values and an element like `0x0103` is not one of them, though its low byte becomes an `ETX` on the wire. Pass a `Uint8Array` or a string. **A record carrying a frame-structure byte is refused too, in either form.** `STX`, `ETB` and `ETX` are what the decoder reads as the shape of a frame, and framing has no escape sequence to hide one behind, so a record holding one is `ASTM_FRAME_RESERVED_BYTE` rather than a frame truncated at that byte. It used to be written as given, and the result was not reliably loud: with the two following bytes matching, the truncated frame verifies and a whole record is absorbed into the previous one with an empty warnings array at both layers. Supplying the record as a `Uint8Array` does not route around this check. **`options.startFrameNumber` is checked before *this function* reads a record**, so on this entry point the refusal never depends on the caller's data. ([serializeFramedAstm](#serializeframedastm) serializes every record *before* it gets here, so a record that cannot be serialized is refused first on that route.) It has to be a whole number from `0` to `7`, because a frame's number is one ASCII digit; anything else is `ASTM_FRAME_INVALID_START_FRAME_NUMBER` rather than whatever byte the arithmetic truncated to. A value other than the default `1` writes a **continuation** of a sequence already in progress, which is what the option is for, and a continuation decoded on its own opens on a sequence gap: see [ComposeFramesOptions](#composeframesoptions). What is still **not** guaranteed: that an accepted record round-trips through the *record* layer. A delimiter colliding with a record's type letter, for one, frames and de-frames byte-exactly and still re-reads as a different record. #### Parameters ##### records readonly (`string` \| `Uint8Array`\<`ArrayBufferLike`\>)[] The reassembled record byte-strings (`Uint8Array` or latin1 `string`), one entry per complete record. ##### options? [`ComposeFramesOptions`](#composeframesoptions) = `{}` Encode options. #### Returns `Uint8Array` The framed byte stream. #### Throws [AstmFrameEncodeError](#astmframeencodeerror) when `options.startFrameNumber` is not a whole number from `0` to `7` (`ASTM_FRAME_INVALID_START_FRAME_NUMBER`), the list is empty, a record has no bytes (`ASTM_FRAME_EMPTY_RECORD`), a record string holds a character above `U+00FF` (`ASTM_FRAME_UNENCODABLE_CHARACTER`), or a record holds an `STX`, `ETB` or `ETX` byte (`ASTM_FRAME_RESERVED_BYTE`). #### Example ```ts import { composeAstmFrames, decodeAstmFrames } from "@cosyte/astm"; const records = [new TextEncoder().encode("H|\\^&\r"), new TextEncoder().encode("L|1\r")]; const bytes = composeAstmFrames(records); decodeAstmFrames(bytes).records.length; // 2 ``` *** ### computeChecksum() > **computeChecksum**(`bytes`, `start`, `endInclusive`): `number` Sum bytes `[start, endInclusive]` of `bytes` modulo 256: the ASTM frame checksum span (frame number through the terminator, inclusive). #### Parameters ##### bytes `Uint8Array` The full decoded byte stream. ##### start `number` First index to include (the frame-number byte, one past `STX`). ##### endInclusive `number` Last index to include (the `ETB`/`ETX` terminator byte). #### Returns `number` The checksum in `0` to `255`. #### Example ```ts import { computeChecksum } from "@cosyte/astm"; // bytes: STX '1' 'A' ETX → sum '1' + 'A' + ETX, mod 256. computeChecksum(new Uint8Array([0x02, 0x31, 0x41, 0x03]), 1, 3); // 0x75 ``` *** ### decodeAstmFrames() > **decodeAstmFrames**(`bytes`, `options?`): [`DecodeAstmFramesResult`](#decodeastmframesresult) Decode a framed ASTM byte stream into frames + reassembled record bytes. Bytes outside a frame (before the first `STX`, or between a frame's trailing `LF` and the next `STX`) are skipped: in ASTM they are low-level transfer control (`ENQ`/`ACK`/`NAK`/`EOT`), never record content, so skipping them is not data loss. #### Parameters ##### bytes `Uint8Array` The framed byte stream. ##### options? [`FrameOptions`](#frameoptions) = `{}` Decode options; lenient unless `strict` is set. #### Returns [`DecodeAstmFramesResult`](#decodeastmframesresult) The decoded frames, the reassembled trusted record bytes, and any warnings. #### Throws [AstmParseError](#astmparseerror) with `EMPTY_INPUT` when `bytes` is empty (both modes). #### Throws [AstmFrameStrictError](#astmframestricterror) when `strict` is set and any deviation occurred. #### Example ```ts import { decodeAstmFrames } from "@cosyte/astm"; // One final frame carrying the record text "R|1|": checksum "AF" over FN..ETX (mod 256). const bytes = new Uint8Array([0x02, 0x31, 0x52, 0x7c, 0x31, 0x7c, 0x03, 0x41, 0x46, 0x0d, 0x0a]); const { records, frames } = decodeAstmFrames(bytes); frames[0]?.frameNumber; // 1 records.length; // 1 ``` *** ### decodeEscapes() > **decodeEscapes**(`leaf`, `d`, `onUnknown?`, `onUnpaired?`, `onSwallowedDelimiter?`): `string` Decode the four recognized ASTM escape mnemonics in a single already-split leaf (a component string), substituting the *active* delimiters read from the header. Unrecognized `&X&` bodies are preserved verbatim and reported through `onUnknown` (never dropped, never guessed), and an escape character that heads no sequence at all is preserved as a literal and reported through `onUnpaired`. This runs **after** splitting, so a decoded delimiter becomes ordinary literal text and can never introduce a new split boundary. #### Parameters ##### leaf `string` One component string, escape-aware split already applied. ##### d [`Delimiters`](#delimiters-2) The delimiters resolved from the header. ##### onUnknown? [`UnknownEscapeSink`](#unknownescapesink) Called once per unrecognized escape body encountered. ##### onUnpaired? [`UnpairedEscapeSink`](#unpairedescapesink) Called once per escape character that heads no sequence. ##### onSwallowedDelimiter? [`SwallowedDelimiterSink`](#swalloweddelimitersink) Called once per unrecognized escape body that is itself a splitting delimiter in force, so that delimiter never split. #### Returns `string` The decoded string. #### Example ```ts import { decodeEscapes, CANONICAL_DELIMITERS } from "@cosyte/astm"; decodeEscapes("1&S&40", CANONICAL_DELIMITERS); // "1^40" decodeEscapes("O&Brien", CANONICAL_DELIMITERS); // "O&Brien" (literal, reported) ``` *** ### deepFreeze() > **deepFreeze**\<`T`\>(`value`): `T` Recursively freeze an object graph (objects and arrays), returning the same reference typed as deeply readonly. Cyclic graphs are not produced by the parser, so a simple recursive walk suffices. #### Type Parameters ##### T `T` #### Parameters ##### value `T` The value to freeze in place. #### Returns `T` The same value, now deeply frozen. #### Example ```ts import { deepFreeze } from "@cosyte/astm"; const frozen = deepFreeze({ a: [1, 2] }); Object.isFrozen(frozen.a); // true ``` *** ### defineAstmProfile() > **defineAstmProfile**(`opts`): [`AstmProfile`](#astmprofile) Build a frozen [AstmProfile](#astmprofile) from a validated options object. Throws [AstmProfileDefinitionError](#astmprofiledefinitionerror) on a bad name, an unknown option key, an invalid `transport`, or an invalid `tolerate` entry, including the **safety rule**: a profile may never tolerate a safety-critical warning code (default-deny across all three registries). `extends` composes profiles: lineage, `tolerate`, `transport`, `provenance`, and `description` merge (parents left-to-right, then self; scalars are child-wins). The merged `tolerate` set is re-validated so a safety-critical code cannot sneak in via a hand-crafted parent. #### Parameters ##### opts [`DefineAstmProfileOptions`](#defineastmprofileoptions) The profile definition; see [DefineAstmProfileOptions](#defineastmprofileoptions). #### Returns [`AstmProfile`](#astmprofile) A frozen, immutable profile with `describe()` attached. #### Throws [AstmProfileDefinitionError](#astmprofiledefinitionerror) on any invalid definition. #### Example ```ts import { defineAstmProfile } from "@cosyte/astm"; const site = defineAstmProfile({ name: "acme-analyzer", description: "Acme's inbound raw-TCP analyzer", transport: "raw", tolerate: [ { code: "ASTM_NONSTANDARD_DELIMITERS", rationale: "declares its own delimiter set" }, ], provenance: { source: "Acme host-interface manual", reference: "internal-2026" }, }); site.lineage; // ["acme-analyzer"] console.log(site.describe?.()); ``` *** ### defineLivdCatalog() > **defineLivdCatalog**(`entries`, `publication?`): [`LivdCatalog`](#livdcatalog) Build a [LivdCatalog](#livdcatalog) from LIVD entries a consumer supplies. Entries are indexed by their `vendorCode` (verbatim). When several entries share a vendor code: - all agreeing on the same `loinc` → a single `mapped` result (the first entry's optional `loincLongName` and LIVD attributes are kept), whether or not the reported units equal that entry's representative unit; - disagreeing (two distinct LOINCs) → the reported units decide, by exact case-sensitive equality against each row's [LivdEntry.representativeUnit](#representativeunit-1). Exactly one distinct LOINC matching is `mapped`, and the answer states that the comparison was verbatim. None matching, more than one matching, or no units reported is an `ambiguous` result carrying every distinct candidate and **no** choice between them. Publication-level metadata is supplied beside the rows, and every element of it is optional, so the single-argument call keeps working exactly as it did. Each declared value is preserved verbatim and readable back off [LivdCatalog.publication](#publication); none of it is validated. Declaring no LOINC version is allowed and puts a value-free [AstmLivdCatalogWarning](#astmlivdcatalogwarning) on [LivdCatalog.warnings](#warnings-5): a nudge, never a refusal, and no lookup answers differently because of it. The returned catalog is deeply frozen; nothing is mutated after construction. #### Parameters ##### entries readonly [`LivdEntry`](#livdentry)[] The consumer's LIVD mapping rows. ##### publication? [`LivdPublication`](#livdpublication) The consumer's publication-level metadata, when they declare any. #### Returns [`LivdCatalog`](#livdcatalog) An immutable catalog. #### Examples ```ts import { defineLivdCatalog } from "@cosyte/astm"; const catalog = defineLivdCatalog([ { vendorCode: "687", loinc: "1920-8", loincLongName: "AST" }, { vendorCode: "690", loinc: "1742-6", loincLongName: "ALT" }, ]); catalog.lookup("687"); // { status: "mapped", loinc: "1920-8", loincLongName: "AST" } catalog.lookup("999"); // { status: "unmapped" } ``` ```ts import { defineLivdCatalog } from "@cosyte/astm"; // One vendor analyte code, two LOINCs, told apart by their representative unit. const glucose = defineLivdCatalog([ { vendorCode: "GLU", loinc: "2345-7", representativeUnit: "mg/dL" }, { vendorCode: "GLU", loinc: "15074-8", representativeUnit: "mmol/L" }, ]); glucose.lookup("GLU", "mmol/L").status; // "mapped": 15074-8, on a verbatim unit match glucose.lookup("GLU", "MMOL/L").status; // "ambiguous": the comparison is case sensitive glucose.lookup("GLU").status; // "ambiguous": no units reported, so nothing to compare ``` *** ### delimiterRoleCollision() > **delimiterRoleCollision**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_DELIMITER_ROLE_COLLISION` warning. Emitted when a header declares one character in two of the repeat / component / escape roles. The declaration is honored and no record is dropped; the message names no character, because a delimiter is a byte off the wire. A profile may **not** tolerate this code: it reports a distinction the bytes no longer carry, and the only other warning such a set raises ([WARNING\_CODES.ASTM\_NONSTANDARD\_DELIMITERS](#astm_nonstandard_delimiters)) is tolerable. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { delimiterRoleCollision } from "@cosyte/astm"; delimiterRoleCollision({ recordIndex: 0, recordType: "H" }); ``` *** ### delimitersRedeclared() > **delimitersRedeclared**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_DELIMITERS_REDECLARED` warning. Emitted when a later `H` record declares a delimiter set different from the one in force; the new set is honored from that header onward. Positional context only: never the delimiters themselves. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { delimitersRedeclared } from "@cosyte/astm"; delimitersRedeclared({ recordIndex: 5, recordType: "H" }); ``` *** ### delimiterSwallowedByEscape() > **delimiterSwallowedByEscape**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` warning. Emitted when an unrecognized escape body is itself a splitting delimiter in force, so the atom rule kept it out of the split. The value is preserved verbatim and is identical with the warning and without it; what the warning reports is the boundary that did not happen. A profile may **not** tolerate this code. It fires alongside [WARNING\_CODES.ASTM\_UNKNOWN\_ESCAPE\_SEQUENCE](#astm_unknown_escape_sequence), which remains tolerable and reports the strictly weaker fact that a body was not recognized. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { delimiterSwallowedByEscape } from "@cosyte/astm"; delimiterSwallowedByEscape({ recordIndex: 4, recordType: "R", fieldIndex: 4 }); ``` *** ### describeVocabulary() > **describeVocabulary**(`vocabulary`): `string` Render a vocabulary attribution as fixed prose for a warning message. The result is a function of the attribution constants alone: it embeds no field text, no value, and no position, so a warning built with it stays value-free. #### Parameters ##### vocabulary [`VocabularyAttribution`](#vocabularyattribution) The attribution to describe. #### Returns `string` Fixed prose naming the code system and version, or the unattributed reason. #### Example ```ts import { describeVocabulary, ABNORMAL_FLAG_VOCABULARY } from "@cosyte/astm"; describeVocabulary(ABNORMAL_FLAG_VOCABULARY); // "graded against version " ``` *** ### detectFraming() > **detectFraming**(`bytes`, `options?`): [`DetectFramingResult`](#detectframingresult) Detect whether an ASTM byte stream is framed or raw from its leading byte. - Leading `STX` (`0x02`) or `ENQ` (`0x05`) ⇒ `"framed"`. - A leading bare record letter (`H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L`) ⇒ `"raw"`. - Anything else (including an empty stream) is **ambiguous** ⇒ defaults to `"framed"` and emits one `ASTM_LTP_AMBIGUOUS_TRANSPORT` warning. An `override` short-circuits all of the above, returning the forced mode with no warning. #### Parameters ##### bytes `Uint8Array` The stream to inspect (only the leading byte is read). ##### options? [`DetectFramingOptions`](#detectframingoptions) = `{}` Detection options; an `override` forces the result. #### Returns [`DetectFramingResult`](#detectframingresult) The decided framing, whether it was defaulted, and any warning. #### Example ```ts import { detectFraming } from "@cosyte/astm"; detectFraming(new Uint8Array([0x02])).framing; // "framed" (STX) detectFraming(new Uint8Array([0x48])).framing; // "raw" (leading "H") detectFraming(new Uint8Array([0x2a])).framing; // "framed" (ambiguous → defaulted, + warning) detectFraming(new Uint8Array([0x48]), { override: "framed" }).framing; // "framed" (forced) ``` *** ### encodeComponent() > **encodeComponent**(`leaf`, `d`, `recordIndex?`): `string` Escape-encode one component leaf for spec-clean emit: the inverse of `decodeEscapes`. Each character is read **once**, in one left-to-right pass, and written either as itself or as a whole `escape` + mnemonic + `escape` triple (`&` → `&E&`, the field / component / repeat delimiters → `&F&` / `&S&` / `&R&`). Nothing this encoder writes is examined again, which is what makes it the exact inverse of the decoder: the decoder reads the same output one token at a time, so every triple it meets is a triple this encoder wrote. A `CR`/`LF` in the leaf has no escape mnemonic and would break framing, so it is rejected with an [AstmSerializeError](#astmserializeerror) rather than emitted raw, as is a delimiter set the result could not be read back with. #### Parameters ##### leaf `string` One already-decoded component string. ##### d [`Delimiters`](#delimiters-2) The delimiters to emit against (canonical for spec-clean output). ##### recordIndex? `number` The enclosing record's index, for error context. #### Returns `string` The escaped component text. #### Throws [AstmSerializeError](#astmserializeerror) for a `CR`/`LF` in the leaf (`ASTM_EMIT_UNENCODABLE_VALUE`), or for a delimiter set failing one of the three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`). This helper takes a leaf and no record, so it never raises `ASTM_EMIT_TYPE_LETTER_COLLISION`: a caller assembling a record line out of it is outside that check and gets no guarantee that a type letter survives. #### Example ```ts import { encodeComponent, CANONICAL_DELIMITERS } from "@cosyte/astm"; encodeComponent("1^40", CANONICAL_DELIMITERS); // "1&S&40" ``` *** ### fieldScalar() > **fieldScalar**(`field`): `string` \| `undefined` The primary scalar of a field: its first repeat's first component, decoded. Returns `undefined` for a truly empty field so callers can distinguish "absent" from a value: never defaulting a missing value. #### Parameters ##### field [`AstmField`](#astmfield) \| `undefined` #### Returns `string` \| `undefined` #### Example ```ts import { fieldScalar, tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm"; const f = tokenizeRecord("R|1|^^^687|28.6", CANONICAL_DELIMITERS); fieldScalar(f[3]); // "28.6" ``` *** ### fieldsUnseparated() > **fieldsUnseparated**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_FIELDS_UNSEPARATED` warning. The raw line is surfaced intact; what is lost is every **modeled** field of the record, because the delimiters in force never split it. **This one is not cosmetic either.** A record with content but no field separator is a record being read with a delimiter set that does not belong to it, and on a result record that costs the value, the units and the status in one go. The fields are not reconstructed, because doing so would mean guessing which set the sender meant, so a profile is not permitted to tolerate this code. **It is a report, not a sweep.** See [WARNING\_CODES](#warning_codes) for the two classes of the same loss it does not see: its absence never certifies a record split correctly. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { fieldsUnseparated } from "@cosyte/astm"; fieldsUnseparated({ recordIndex: 4, recordType: "R" }); ``` *** ### frameBadChecksum() > **frameBadChecksum**(`position`): [`AstmFrameWarning`](#astmframewarning) Build an `ASTM_FRAME_BAD_CHECKSUM` warning. The frame is surfaced flagged untrusted and never merged into a record; the warning carries position only. #### Parameters ##### position [`AstmFramePosition`](#astmframeposition) #### Returns [`AstmFrameWarning`](#astmframewarning) #### Example ```ts import { frameBadChecksum } from "@cosyte/astm"; frameBadChecksum({ frameNumber: 2, byteOffset: 251 }); ``` *** ### frameOversize() > **frameOversize**(`position`): [`AstmFrameWarning`](#astmframewarning) Build an `ASTM_FRAME_OVERSIZE` warning. The 240-byte text limit was exceeded without a split; the frame is flagged, not silently dropped. #### Parameters ##### position [`AstmFramePosition`](#astmframeposition) #### Returns [`AstmFrameWarning`](#astmframewarning) #### Example ```ts import { frameOversize } from "@cosyte/astm"; frameOversize({ frameNumber: 1, byteOffset: 0 }); ``` *** ### frameSequenceGap() > **frameSequenceGap**(`position`): [`AstmFrameWarning`](#astmframewarning) Build an `ASTM_FRAME_SEQUENCE_GAP` warning. A frame was possibly dropped; the record is not concatenated across the gap as if contiguous. #### Parameters ##### position [`AstmFramePosition`](#astmframeposition) #### Returns [`AstmFrameWarning`](#astmframewarning) #### Example ```ts import { frameSequenceGap } from "@cosyte/astm"; frameSequenceGap({ frameNumber: 3, byteOffset: 502 }); ``` *** ### frameUnterminated() > **frameUnterminated**(`position`): [`AstmFrameWarning`](#astmframewarning) Build an `ASTM_FRAME_UNTERMINATED` warning. The partial bytes are surfaced flagged untrusted; no partial record is invented. #### Parameters ##### position [`AstmFramePosition`](#astmframeposition) #### Returns [`AstmFrameWarning`](#astmframewarning) #### Example ```ts import { frameUnterminated } from "@cosyte/astm"; frameUnterminated({ byteOffset: 730 }); ``` *** ### getAstmProfile() > **getAstmProfile**(`name`): [`AstmProfile`](#astmprofile) \| `undefined` Look up a built-in profile by name. Returns `undefined` when no built-in has that name (a user-defined profile is not in this registry, pass it directly). #### Parameters ##### name `string` The built-in profile name. #### Returns [`AstmProfile`](#astmprofile) \| `undefined` The profile, or `undefined`. #### Example ```ts import { getAstmProfile } from "@cosyte/astm"; getAstmProfile("referenceCorpus")?.provenance?.source; ``` *** ### getDefaultAstmProfile() > **getDefaultAstmProfile**(): [`AstmProfile`](#astmprofile) \| `undefined` Return the current process-scoped default profile, or `undefined` if none is registered. #### Returns [`AstmProfile`](#astmprofile) \| `undefined` The default profile, or `undefined`. #### Example ```ts import { getDefaultAstmProfile } from "@cosyte/astm"; const p = getDefaultAstmProfile(); if (p !== undefined) console.log("default profile:", p.name); ``` *** ### hasCollidingRoles() > **hasCollidingRoles**(`d`): `boolean` Whether a resolved set names one character in two roles, so the boundary between those two roles cannot be recovered from the bytes. [readDelimiters](#readdelimiters) already refuses a declaration whose **field** separator is one of the other three (it returns `undefined`, which is the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal on the first header and the `ASTM_RECORD_UNREADABLE_REDECLARATION` warning on a later one), so what is left to test here is the three unordered pairs among the remaining roles: **repeat/component, repeat/escape, and component/escape**. Three pairs, named rather than counted, because the count is only meaningful with the list. Such a declaration is read and honored (nothing is guessed, no record is dropped) and the loss it causes is real and was previously silent. Measured on the canonical-looking `H|^^&`, where the repeat and component roles are both `^`: the field `A^B^C^D` reads back as **four repeats of one component each**, so a two-repeats-of-two-components reading is unrecoverable and `components` holds only `A`. On `H|\&&`, where the component and escape roles are both `&`, the same character splits (`A&B` reads as two components) or opens an atom (`A&F&B` reads as the single component `A|B`) depending only on what follows it. Emit refuses such a set outright (`ASTM_EMIT_INVALID_DELIMITERS`). #### Parameters ##### d [`Delimiters`](#delimiters-2) A resolved delimiter set. #### Returns `boolean` `true` iff two of the repeat / component / escape roles share a character. #### Example ```ts import { hasCollidingRoles, readDelimiters } from "@cosyte/astm"; hasCollidingRoles(readDelimiters("H|\\^&")!); // false hasCollidingRoles(readDelimiters("H|^^&")!); // true (repeat === component) ``` *** ### interpretAbnormalFlag() > **interpretAbnormalFlag**(`raw`): [`AbnormalFlag`](#abnormalflag) Interpret an `R`-record abnormal-flag field (field 7) against [ABNORMAL\_FLAG\_VOCABULARY](#abnormal_flag_vocabulary). Leading/trailing whitespace is ignored for the lookup but the `raw` text is preserved verbatim. Matching is otherwise **exact**: letter case is never folded. An unrecognized flag yields `{ recognized: false, meaning: "undefined" }`, surfaced, never dropped, and **never `"normal"`**, and still carries the vocabulary it was graded against. #### Parameters ##### raw `string` The verbatim field-7 text. #### Returns [`AbnormalFlag`](#abnormalflag) The interpreted flag. #### Example ```ts import { interpretAbnormalFlag } from "@cosyte/astm"; interpretAbnormalFlag("U").meaning; // "significant-change-up" interpretAbnormalFlag("HU").meaning; // "significantly-high" interpretAbnormalFlag("ZZ").meaning; // "undefined" (never "normal") ``` *** ### interpretResultStatus() > **interpretResultStatus**(`raw`): [`ResultStatus`](#resultstatus-2) Interpret an `R`-record result-status field (field 9). An **absent** status (`undefined` or empty) is typed `unspecified`: **never assumed `final`**. A **present but unrecognized** letter is `undefined` (recognized `false`). In every non-`F` case `isActiveFinal` is `false`, so a correction, a cancellation, or an unknown status can never read as current. Every returned status carries [RESULT\_STATUS\_VOCABULARY](#result_status_vocabulary): the explicit statement that no published source binding this letter set can be cited. #### Parameters ##### raw `string` \| `undefined` The verbatim field-9 text, or `undefined`/empty when absent. #### Returns [`ResultStatus`](#resultstatus-2) The modeled status. #### Example ```ts import { interpretResultStatus } from "@cosyte/astm"; const s = interpretResultStatus("F"); s.isActiveFinal; // true s.supersedes; // false s.vocabulary.attributed; // false: nothing is cited for this set ``` *** ### isNonStandard() > **isNonStandard**(`d`): `boolean` Whether a resolved delimiter set differs from the canonical `H|\^&`. A `true` result is a tolerated vendor quirk, surfaced as a value-free warning so a consumer can notice a non-standard stream without the parser refusing it. #### Parameters ##### d [`Delimiters`](#delimiters-2) #### Returns `boolean` #### Example ```ts import { isNonStandard, readDelimiters } from "@cosyte/astm"; isNonStandard(readDelimiters("H|\\^&")!); // false ``` *** ### isSafetyCriticalCode() > **isSafetyCriticalCode**(`code`): `boolean` True when `code` is a known warning code that is **not** tolerable: i.e. a profile may never list it. A code that is not a real warning code at all returns `false` here (the validator reports "unknown code" separately, a distinct failure with a distinct message). #### Parameters ##### code `string` The warning code to test. #### Returns `boolean` `true` iff the code is a real code outside the tolerable allow-list. #### Example ```ts import { isSafetyCriticalCode } from "@cosyte/astm"; isSafetyCriticalCode("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // false (tolerable) isSafetyCriticalCode("ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT"); // true isSafetyCriticalCode("ASTM_RECORD_UNKNOWN_TYPE"); // true (it can mean a lost message boundary) ``` *** ### listAstmProfiles() > **listAstmProfiles**(): readonly `string`[] The names of every built-in profile, in registration order. #### Returns readonly `string`[] The built-in names. #### Example ```ts import { listAstmProfiles } from "@cosyte/astm"; listAstmProfiles(); // ["default", "referenceCorpus"] ``` *** ### livdAmbiguousMapping() > **livdAmbiguousMapping**(`position`): [`AstmLivdWarning`](#astmlivdwarning) Build an `ASTM_LIVD_AMBIGUOUS_MAPPING` warning. The code matched multiple distinct LOINCs; none is chosen (refusing to pick is the fail-safe). #### Parameters ##### position [`AstmPosition`](#astmposition) Where the ambiguous code was seen (record ordinal + type; never the code). #### Returns [`AstmLivdWarning`](#astmlivdwarning) The warning. #### Example ```ts import { livdAmbiguousMapping } from "@cosyte/astm"; livdAmbiguousMapping({ recordIndex: 4, recordType: "O" }); ``` *** ### livdCatalogMissingLoincVersion() > **livdCatalogMissingLoincVersion**(`catalog`): [`AstmLivdCatalogWarning`](#astmlivdcatalogwarning) Build an `ASTM_LIVD_CATALOG_NO_LOINC_VERSION` warning. Advisory: the catalog it describes is still built and still answers every lookup exactly as it would have. #### Parameters ##### catalog [`LivdCatalogIdentity`](#livdcatalogidentity) What the catalog declared about its identity; pass [LIVD\_CATALOG\_IDENTITY\_UNDECLARED](#livd_catalog_identity_undeclared) where it declared none. #### Returns [`AstmLivdCatalogWarning`](#astmlivdcatalogwarning) The warning. #### Example ```ts import { livdCatalogMissingLoincVersion, LIVD_CATALOG_IDENTITY_UNDECLARED } from "@cosyte/astm"; livdCatalogMissingLoincVersion(LIVD_CATALOG_IDENTITY_UNDECLARED); ``` *** ### livdUnmappedCode() > **livdUnmappedCode**(`position`): [`AstmLivdWarning`](#astmlivdwarning) Build an `ASTM_LIVD_UNMAPPED_CODE` warning. The reported code stays verbatim and the mapping is `unmapped`; no LOINC is fabricated. #### Parameters ##### position [`AstmPosition`](#astmposition) Where the unmapped code was seen (record ordinal + type; never the code). #### Returns [`AstmLivdWarning`](#astmlivdwarning) The warning. #### Example ```ts import { livdUnmappedCode } from "@cosyte/astm"; livdUnmappedCode({ recordIndex: 3, recordType: "R" }); ``` *** ### lookupLivdForRecord() > **lookupLivdForRecord**(`record`, `catalog`): [`LivdAnnotation`](#livdannotation) Annotate one `R` or `O` record against a LIVD catalog. The building block of [applyLivd](#applylivd); useful when a consumer holds a single record. Never mutates the record and never fabricates a LOINC. #### Parameters ##### record [`OrderRecord`](#orderrecord) \| [`ResultRecord`](#resultrecord) The result/order record to annotate. ##### catalog [`LivdCatalog`](#livdcatalog) The consumer-supplied LIVD catalog. #### Returns [`LivdAnnotation`](#livdannotation) The record's annotation. #### Example ```ts import { parseAstmRecords, results, defineLivdCatalog, lookupLivdForRecord } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"); const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8" }]); lookupLivdForRecord(results(msg)[0]!, catalog).mapping.status; // "mapped" ``` *** ### ltpAmbiguousTransport() > **ltpAmbiguousTransport**(): [`AstmLtpWarning`](#astmltpwarning) Build an `ASTM_LTP_AMBIGUOUS_TRANSPORT` warning. The detector defaulted to framed; a profile override can force raw. #### Returns [`AstmLtpWarning`](#astmltpwarning) #### Example ```ts import { ltpAmbiguousTransport } from "@cosyte/astm"; ltpAmbiguousTransport(); ``` *** ### ltpFrameRejected() > **ltpFrameRejected**(`frameNumber?`): [`AstmLtpWarning`](#astmltpwarning) Build an `ASTM_LTP_FRAME_REJECTED` warning. The receiver sent a `NAK`; the frame's text was never appended and the transfer did not advance. #### Parameters ##### frameNumber? `number` The rejected frame's sequence number, when it could be read. #### Returns [`AstmLtpWarning`](#astmltpwarning) #### Example ```ts import { ltpFrameRejected } from "@cosyte/astm"; ltpFrameRejected(2); ``` *** ### ltpInitialState() > **ltpInitialState**(): [`LtpState`](#ltpstate) The neutral starting state for a fresh session: line idle, no record in flight, nothing delivered yet. A consumer creates one of these when a connection opens and folds inbound events through [ltpReduce](#ltpreduce) from here. #### Returns [`LtpState`](#ltpstate) A frozen neutral [LtpState](#ltpstate). #### Example ```ts import { ltpInitialState, ltpReduce } from "@cosyte/astm"; let state = ltpInitialState(); const step = ltpReduce(state, { type: "enq" }); step.actions; // [{ type: "sendAck" }] state = step.state; ``` *** ### ltpReduce() > **ltpReduce**(`state`, `event`): [`LtpTransition`](#ltptransition) Advance the LTP session by one inbound event: a pure function, no I/O. Behaviour by phase: - **neutral**: `enq` accepts establishment (`sendAck`, enter transfer); `eot` is a benign line reset (no-op); `ack`/`nak` are unexpected at a receiver (surfaced, never read as acceptance); a `frame` before establishment is tolerated (Postel's Law), the session auto-establishes and processes it, with a warning. - **transfer**: a `frame` is accepted (`sendAck`, appended, sequence advanced) only when it is trusted *and* carries the expected number; a duplicate of the last accepted frame is idempotently re-`ACK`ed without re-appending; a bad or out-of-sequence frame is `NAK`ed and dropped. `eot` terminates the transfer and returns to neutral (a record left open on an `ETB` is **not** delivered). `enq` restarts establishment. `ack`/`nak` are unexpected. #### Parameters ##### state [`LtpState`](#ltpstate) The current session state. ##### event [`LtpEvent`](#ltpevent) The inbound event. #### Returns [`LtpTransition`](#ltptransition) The next state, the actions to take, and any warnings. #### Example ```ts import { ltpInitialState, ltpReduce } from "@cosyte/astm"; // A bad-checksum frame is NAKed, never ACKed, and never appended. const est = ltpReduce(ltpInitialState(), { type: "enq" }).state; const bad = { trusted: false } as never; ltpReduce(est, { type: "frame", frame: bad }).actions; // [{ type: "sendNak" }] ``` *** ### ltpUnexpectedEvent() > **ltpUnexpectedEvent**(`frameNumber?`): [`AstmLtpWarning`](#astmltpwarning) Build an `ASTM_LTP_UNEXPECTED_EVENT` warning. The event was surfaced and handled defensively; it never advanced the transfer as if valid. #### Parameters ##### frameNumber? `number` The frame number, when the unexpected event was a frame; omitted otherwise. #### Returns [`AstmLtpWarning`](#astmltpwarning) #### Example ```ts import { ltpUnexpectedEvent } from "@cosyte/astm"; ltpUnexpectedEvent(); ``` *** ### messages() > **messages**(`msg`): readonly [`AstmStreamMessage`](#astmstreammessage)[] Split a parsed stream into the `H` … `L` messages it carries. This is the safe path and it never throws: a single-message stream yields exactly one entry, and pairing a patient with a result inside one entry is correct by construction. Returns an **array**, not an iterator: the stream is already fully parsed and frozen, so there is no work to defer, and every other extractor in this package returns a `readonly` array. #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed stream. #### Returns readonly [`AstmStreamMessage`](#astmstreammessage)[] The messages, in wire order; never empty for a parsed stream. #### Example ```ts import { parseAstmRecords, messages } from "@cosyte/astm"; const stream = parseAstmRecords("H|\\^&\rP|1|PRAC-1\rR|1|^^^687|1.0|U/L||N||F\rL|1\r"); for (const m of messages(stream)) { m.patient?.practiceAssignedId; // "PRAC-1" m.results[0]?.value; // "1.0" } ``` *** ### nonStandardDelimiters() > **nonStandardDelimiters**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_NONSTANDARD_DELIMITERS` warning. The declared delimiters are used as-is; this only flags that they differ from the canonical set. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { nonStandardDelimiters } from "@cosyte/astm"; nonStandardDelimiters({ recordIndex: 0, recordType: "H" }); ``` *** ### orders() > **orders**(`msg`): readonly [`OrderRecord`](#orderrecord)[] Every order (`O`) record in the message, in wire order. #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed **single-message** stream. #### Returns readonly [`OrderRecord`](#orderrecord)[] The order records (possibly empty). #### Throws [AstmAmbiguousStreamError](#astmambiguousstreamerror) (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream carries more than one message: see [messages](#messages). #### Example ```ts import { parseAstmRecords, orders } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rO|1|ACC-42||^^^687|R\rL|1\r"); orders(msg)[0]?.specimenId; // "ACC-42" ``` *** ### orphanComment() > **orphanComment**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_ORPHAN_COMMENT` warning. Emitted when a `C` record had no valid preceding `H`/`P`/`O`/`R` parent; the comment is attached to the message root and surfaced, never dropped. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { orphanComment } from "@cosyte/astm"; orphanComment({ recordIndex: 5, recordType: "C" }); ``` *** ### parseAstmDate() > **parseAstmDate**(`raw`): [`AstmDate`](#astmdate) \| `undefined` Parse an ASTM `YYYYMMDDHHMMSS` value, preserving whatever precision is present. Returns `undefined` for a value that is not a usable timestamp (fewer than four leading digits for the year, or non-digit content): a caller keeps the raw field text either way, so nothing is lost. A partial value is parsed, never rejected. Extra trailing digits beyond seconds (fractional seconds some vendors append) are ignored for the structured value; the full input remains in [AstmDate.raw](#raw-1). #### Parameters ##### raw `string` The raw field text. #### Returns [`AstmDate`](#astmdate) \| `undefined` The parsed date, or `undefined` when it is not a timestamp. #### Example ```ts import { parseAstmDate } from "@cosyte/astm"; parseAstmDate("20240315093000")?.precision; // "second" parseAstmDate("202403")?.precision; // "month" ``` *** ### parseAstmRecords() > **parseAstmRecords**(`raw`, `options?`): [`AstmMessage`](#astmmessage) Parse an ASTM/CLSI-LIS02 record stream into an immutable [AstmMessage](#astmmessage). The stream is a sequence of records separated by `CR` (with `LF`/`CRLF` tolerated); the first record must be an `H` header, which declares the delimiters. Lenient by default: set `strict` to reject any tolerated deviation. **Several messages in one stream.** A message runs `H` … `L`, so a stream may carry more than one, and each `H` declares the delimiters for the records that follow it. A later header that declares a **different** set is honored from that header onward and flagged `ASTM_RECORD_DELIMITERS_REDECLARED`; records already read keep the set they were read with, so a redeclaration never reinterprets earlier bytes. A header that merely restates the set in force is a no-op and warns nothing. A later header that cannot declare a usable set keeps the delimiters already in force and warns `ASTM_RECORD_UNREADABLE_REDECLARATION`: a set is never guessed, and no record is dropped. Per-header sets are on each [HeaderRecord.delimiters](#delimiters-3); [AstmMessage.delimiters](#delimiters) is the first header's. #### Parameters ##### raw `string` \| `Uint8Array`\<`ArrayBufferLike`\> The de-framed record bytes, as a string or `Uint8Array` (decoded latin1 so byte values survive 1:1). ##### options? [`AstmParseOptions`](#astmparseoptions) = `{}` Parse options; lenient unless `strict` is set. #### Returns [`AstmMessage`](#astmmessage) The parsed, deeply-frozen message. #### Throws [AstmParseError](#astmparseerror) on a Tier-3 fatal (`EMPTY_INPUT`, `ASTM_RECORD_NO_HEADER`, `ASTM_RECORD_UNDECLARED_DELIMITERS`). #### Throws [AstmStrictError](#astmstricterror) when `strict` is set and a deviation occurs. #### Example ```ts import { parseAstmRecords, results } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rP|1\rO|1|ACC\rR|1|^^^687|28.6|U/L||N||F\rL|1|N\r"); results(msg)[0]?.value; // "28.6" ``` *** ### parseChecksumHex() > **parseChecksumHex**(`bytes`, `i0`, `i1`): `number` \| `undefined` Read the two-hex-char checksum at `bytes[i0]`/`bytes[i1]`, **case-insensitively** (a lowercase checksum is a tolerated real-vendor quirk). Returns `undefined` when either position is out of range or not a hex digit: the caller then treats the frame as having no readable declared checksum, never as a match. #### Parameters ##### bytes `Uint8Array` The full decoded byte stream. ##### i0 `number` Index of the high hex nibble (immediately after the terminator). ##### i1 `number` Index of the low hex nibble. #### Returns `number` \| `undefined` The declared checksum in `0` to `255`, or `undefined` if unreadable. #### Example ```ts import { parseChecksumHex } from "@cosyte/astm"; parseChecksumHex(new Uint8Array([0x37, 0x35]), 0, 1); // 0x75 ("75") parseChecksumHex(new Uint8Array([0x37, 0x35]), 5, 6); // undefined (out of range) ``` *** ### parseFramedAstm() > **parseFramedAstm**(`bytes`, `options?`): [`FramedAstmResult`](#framedastmresult) Decode a framed ASTM byte stream and parse its reassembled records in one call. Only **trusted, contiguous** frames are reassembled (a bad-checksum frame, a sequence gap, or an unterminated frame is surfaced in `frames`/`frameWarnings` but never fed to the record parser), so the parsed `message` reflects only bytes the framing layer vouched for. #### Parameters ##### bytes `Uint8Array` The framed byte stream. ##### options? [`FrameOptions`](#frameoptions) & [`AstmParseOptions`](#astmparseoptions) = `{}` Frame decode options **and** record parse options (both layers honor a shared `strict`). #### Returns [`FramedAstmResult`](#framedastmresult) The parsed message plus the frame-layer detail. #### Throws [AstmParseError](#astmparseerror) `EMPTY_INPUT` when the stream is empty, or when no trusted records could be reassembled (nothing to parse). #### Throws [AstmFrameStrictError](#astmframestricterror) / [AstmStrictError](#astmstricterror) in `strict` mode on a frame or record deviation, respectively. #### Example ```ts import { parseFramedAstm } from "@cosyte/astm"; // A single final frame carrying "H|\^&\r": checksum "E5" over FN..ETX (mod 256). const bytes = new Uint8Array([ 0x02, 0x31, 0x48, 0x7c, 0x5c, 0x5e, 0x26, 0x0d, 0x03, 0x45, 0x35, 0x0d, 0x0a, ]); const { message, frames } = parseFramedAstm(bytes); message.header.delimiters.field; // "|" frames.length; // 1 ``` *** ### parseReferenceRange() > **parseReferenceRange**(`raw`): [`ReferenceRange`](#referencerange) Parse an `R`-record reference-range field (field 6) into a low/high (or open-ended) pair. Recognized forms are `low-high` (closed), `low` (open-high). Anything else, including an ambiguous multi-dash string or a bare non-numeric token, is returned as `kind: "unparsed"` with the raw text preserved and **no bound invented**. Bounds are surfaced as verbatim text, not coerced to numbers. #### Parameters ##### raw `string` The verbatim field-6 text. #### Returns [`ReferenceRange`](#referencerange) The parsed reference range. #### Example ```ts import { parseReferenceRange } from "@cosyte/astm"; parseReferenceRange("<5").kind; // "open-low" parseReferenceRange(">10").low; // "10" parseReferenceRange("weird").kind; // "unparsed" (never a fabricated bound) ``` *** ### partialTimestamp() > **partialTimestamp**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_PARTIAL_TIMESTAMP` warning. Emitted when a `YYYYMMDDHHMMSS` value had an odd digit run that truncates a component; the raw run is preserved and no time is fabricated. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { partialTimestamp } from "@cosyte/astm"; partialTimestamp({ recordIndex: 2, recordType: "P", fieldIndex: 8 }); ``` *** ### patient() > **patient**(`msg`): [`PatientRecord`](#patientrecord) \| `undefined` The message's patient (`P`) record, or `undefined` when the message carries none. This is the identity a result files against, so it is answered only when the stream determines exactly one. It **refuses** the two shapes where "the patient" is a guess: a stream carrying several messages, and a single message carrying several `P` records. In both, the old behaviour, the first `P` in the stream, is the wrong-patient path itself. #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed single-message stream carrying at most one patient. #### Returns [`PatientRecord`](#patientrecord) \| `undefined` The patient record, or `undefined` when the message carries none. #### Throws [AstmAmbiguousStreamError](#astmambiguousstreamerror) (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream carries more than one message, or (`ASTM_AMBIGUOUS_MULTI_PATIENT`) when its one message carries more than one `P`. Use [messages](#messages) and read `patients` in both cases. #### Example ```ts import { parseAstmRecords, patient } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rP|1|PRAC|LAB\rL|1\r"); patient(msg)?.practiceAssignedId; // "PRAC" ``` *** ### primaryCode() > **primaryCode**(`u`): `string` \| `undefined` The primary code to key a result on: the local (vendor) code, and nothing else. Returns `undefined` when the field carries no vendor/local code, **including** when component 1 is populated: never a guess, and never a value this library does not vouch for. Read `unvalidatedWireValue` for that value. #### Parameters ##### u [`UniversalTestId`](#universaltestid-6) A recognized Universal Test ID. #### Returns `string` \| `undefined` The vendor/local code, or `undefined`. #### Example ```ts import { primaryCode, recognizeUniversalTestId } from "@cosyte/astm"; primaryCode(recognizeUniversalTestId(["2345-7", "Glucose", "LN", "687"])); // "687" primaryCode(recognizeUniversalTestId(["Glucose", "", "", ""])); // undefined ``` *** ### profileQuirkApplied() > **profileQuirkApplied**(`original`, `profileName`): [`AstmRecordWarning`](#astmrecordwarning) Build a `PROFILE_QUIRK_APPLIED` warning: the downgraded form an active vendor profile produces from a deviation it *expects*. The original warning is **not dropped**: its `code` moves to `toleratedCode`, the warning is re-badged `PROFILE_QUIRK_APPLIED`, `expected` is set, and the tolerating profile is named. The original `position` and `message` are preserved (both PHI-free by the same construction as every other factory), so a consumer can filter known, grounded noise while the fact of the deviation, and where it was, survive. A profile can only ever reach this path for a **non-safety-critical** code (enforced at profile-definition time by the safety gate). #### Parameters ##### original [`AstmRecordWarning`](#astmrecordwarning) The warning the profile tolerated. ##### profileName `string` The name of the tolerating profile. #### Returns [`AstmRecordWarning`](#astmrecordwarning) The re-badged, still-informative warning. #### Example ```ts import { profileQuirkApplied, unknownEscapeSequence } from "@cosyte/astm"; const original = unknownEscapeSequence({ recordIndex: 4, recordType: "R", fieldIndex: 5 }); const w = profileQuirkApplied(original, "referenceCorpus"); w.code; // "PROFILE_QUIRK_APPLIED" w.toleratedCode; // "ASTM_UNKNOWN_ESCAPE_SEQUENCE" ``` *** ### query() > **query**(`msg`): readonly [`QueryRecord`](#queryrecord)[] Every request-information (`Q`) record in the message, in wire order. A non-empty result means the message is a **host-query request**, not a result set: see [AstmMessage.classification](#classification) (`isHostQueryRequest`). #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed **single-message** stream. #### Returns readonly [`QueryRecord`](#queryrecord)[] The query records (possibly empty). #### Throws [AstmAmbiguousStreamError](#astmambiguousstreamerror) (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream carries more than one message: see [messages](#messages). #### Example ```ts import { parseAstmRecords, query } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r"); query(msg)[0]?.queriesAllTests; // true ``` *** ### readDelimiterDeclaration() > **readDelimiterDeclaration**(`headerRecord`): [`DelimiterReadResult`](#delimiterreadresult) The same read as [readDelimiters](#readdelimiters), keeping **why** it failed. The reason is not cosmetic. A caller that reports every failure as "too short" says something false about `H||^&`, a full-length header whose definition field is empty because its own field separator ends it, and a consumer reading that diagnostic looks for the wrong thing. The two conditions are separate rules that happen to coincide today (see the fault list on [DelimiterDeclarationFault](#delimiterdeclarationfault)), so they are named separately. **`field-separator-reused` is unreachable through this function today, and the check stays.** A field separator occurring in the definition positions sits at index 2, 3 or 4, so it ends the definition where it appears and the definition is under three characters: `definition-truncated` answers first, every time. The outcome is right either way (such a declaration does not resolve at all). The check is the statement of an invariant the length rule currently enforces on its behalf, and the two are not the same rule: a change to how the definition field is bounded would separate them again. Deleting it would leave the invariant unstated and the change silent. #### Parameters ##### headerRecord `string` The raw `H` record text (no trailing CR/LF). #### Returns [`DelimiterReadResult`](#delimiterreadresult) The resolved delimiters, or the named reason they could not be read. #### Example ```ts import { readDelimiterDeclaration } from "@cosyte/astm"; readDelimiterDeclaration("H|\\^&").ok; // true readDelimiterDeclaration("H||^&"); // { ok: false, fault: "definition-truncated" } ``` *** ### readDelimiters() > **readDelimiters**(`headerRecord`): [`Delimiters`](#delimiters-2) \| `undefined` Read the four delimiters from a header record's raw text (a single `H` record, its terminator already stripped). Returns `undefined` when the record cannot declare all four delimiters. The four ways that happens are named by [readDelimiterDeclaration](#readdelimiterdeclaration), which is this function with the reason kept; use it wherever the reason is shown to a consumer, because "too short" is false of some of them. What the caller does with that depends on **which** header it is. On the first header it is the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal, because there is no earlier set to fall back to. On any later header (a stream may carry several messages, each declaring its own set) the delimiters already in force are kept and an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning is raised instead; a set is never guessed and no record is dropped. **A set two of whose other three roles share a character is resolved, not refused**, because the stream is still readable and refusing it would drop records the sender did send. What it costs is the boundary between those two roles, which the bytes no longer carry: see [hasCollidingRoles](#hascollidingroles), which the parse path calls to report it (`ASTM_RECORD_DELIMITER_ROLE_COLLISION`). This function does not throw: delimiter resolution and the escalation decision are kept separate so the reader stays pure and testable. #### Parameters ##### headerRecord `string` The raw `H` record text (no trailing CR/LF). #### Returns [`Delimiters`](#delimiters-2) \| `undefined` The resolved delimiters, or a declared failure. #### Example ```ts import { readDelimiters } from "@cosyte/astm"; const d = readDelimiters("H|\\^&|||sender"); d.field; // "|" ``` *** ### recognizeUniversalTestId() > **recognizeUniversalTestId**(`components`): [`UniversalTestId`](#universaltestid-6) Recognize a Universal Test ID from a field's already-decoded components. The provenance is decided **positionally**, by which components are populated, and never by what a value looks like. #### Parameters ##### components readonly `string`[] The component strings (escape-decoded), in order. #### Returns [`UniversalTestId`](#universaltestid-6) The recognized, provenance-tagged Universal Test ID. #### Example ```ts import { recognizeUniversalTestId, primaryCode } from "@cosyte/astm"; primaryCode(recognizeUniversalTestId(["", "Glucose", "L", "687"])); // "687" ``` *** ### resolveProfileTransport() > **resolveProfileTransport**(`profile`): [`AstmFraming`](#astmframing) \| `undefined` The profile's transport override, if any: the value a consumer feeds to `detectFraming(bytes, { override })` to force framed/raw and bypass leading-byte auto-detection. `undefined` means "let detection decide." #### Parameters ##### profile [`AstmProfile`](#astmprofile) \| `undefined` The active profile, or `undefined`. #### Returns [`AstmFraming`](#astmframing) \| `undefined` `"framed"` / `"raw"` when the profile forces one, else `undefined`. #### Example ```ts import { resolveProfileTransport, detectFraming, astmProfiles } from "@cosyte/astm"; const override = resolveProfileTransport(myRawTcpProfile); const { framing } = detectFraming(bytes, override !== undefined ? { override } : {}); ``` *** ### results() > **results**(`msg`): readonly [`ResultRecord`](#resultrecord)[] Every result (`R`) record in the message, in wire order. #### Parameters ##### msg [`AstmMessage`](#astmmessage) A parsed **single-message** stream. #### Returns readonly [`ResultRecord`](#resultrecord)[] The result records (possibly empty). #### Throws [AstmAmbiguousStreamError](#astmambiguousstreamerror) (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream carries more than one message: see [messages](#messages). #### Example ```ts import { parseAstmRecords, results } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"); results(msg)[0]?.units; // "U/L" ``` *** ### serializeAstmRecord() > **serializeAstmRecord**(`record`, `d?`): `string` Serialize a single ASTM record to its spec-clean wire text (no trailing terminator). Emits with the given delimiters, defaulting to the canonical set. The header (`H`) is special-cased: its delimiter-definition field is emitted as the **literal** declaration of `d`, never escaped, escaping it would corrupt the very declaration a reader depends on, followed by any characters the modeled declaration carried beyond the three a reader takes its roles from. Those surplus characters are kept unless the surplus could not be read back as surplus, when **all** of it is dropped: that covers a surplus carrying the field delimiter or any control character, and it is silent, because emit has no warning channel. Manufacturer (`M`) and scientific (`S`) records are reproduced **byte-identically** from their preserved `rawLine` when they are already in `d`, and re-encoded from their fields when they are not, so their fields never collapse into one on the next read. #### Parameters ##### record [`AstmRecord`](#astmrecord) The record to serialize. ##### d? [`Delimiters`](#delimiters-2) = `CANONICAL_DELIMITERS` The delimiters to emit against; defaults to `H|\^&`. #### Returns `string` The record's wire text, terminator excluded. #### Throws [AstmSerializeError](#astmserializeerror) when a component contains an unencodable `CR`/`LF` (`ASTM_EMIT_UNENCODABLE_VALUE`), when `d` fails one of the three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), or when this record's type letter would not be the first character of its own emitted line (`ASTM_EMIT_TYPE_LETTER_COLLISION`), which would make it read back as a different record. That last one is a transcoding condition and needs no `d` argument to fire: a record read under a different delimiter set can carry a type letter the canonical set escapes away. #### Example ```ts import { serializeAstmRecord, parseAstmRecords } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"); serializeAstmRecord(msg.records[1]!); // "R|1|^^^687|28.6|U/L||N||F" ``` *** ### serializeAstmRecords() > **serializeAstmRecords**(`input`, `d?`): `string` Serialize a whole ASTM message (or a bare record list) to a spec-clean, `CR`-terminated record stream: the inverse of `parseAstmRecords`. Emit is **conservative**: the canonical `H|\^&` delimiters, every embedded delimiter re-escaped, each record closed with a `CR`. A message parsed with non-canonical delimiters is **normalized** to the canonical set on emit: every record, `M` and `S` included, so the emitted stream is in one delimiter set and re-parsing it recovers every field. Passing `d` explicitly emits against that set instead, and the header declares it. Normalization replaces the four delimiter **roles**; a header declaration carrying characters beyond the three that hold a role keeps them, on this path as on any other, rather than being truncated. The exception is a surplus that could not be read back as one, carrying the field delimiter or any control character: there the **whole** surplus is dropped, on every path including the default one, and silently, because emit returns a bare string with no channel to say so. **`d` is checked before anything is written.** Each of the four separators must be exactly one character, none may be a record terminator, and no two may share a character: otherwise the emitted bytes cannot be read back as the records that produced them, and emit has no warning channel with which to say so. A set that fails is an [AstmSerializeError](#astmserializeerror) with code `ASTM_EMIT_INVALID_DELIMITERS`. This is stricter than the parser, which reads some sets it cannot reverse, so `serializeAstmRecords(msg, msg.delimiters)` can refuse a message that parsed: in exactly the cases where it used to emit a stream that read back wrong. **And each record is checked against the set it is being written with.** Those three conditions read the set alone, so they cannot see that `field` = `R` escapes an `R` record's own type letter away. Every record's emitted line is therefore checked to start with the letter the record models, and one that does not is `ASTM_EMIT_TYPE_LETTER_COLLISION` rather than a stream that reads back as different records. What neither check promises is that every *field* lands where it did: an escape sequence whose body is an unrecognized character that is itself a delimiter in force is an opaque atom, so that delimiter never becomes a boundary, and the parse side reports that (`ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, plus the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`) rather than emit refusing it. The mirror case, where a sequence ends where another could have begun so a delimiter splits that the competing alignment would have held, is reported the same way (`ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`). Emitting normalizes both away rather than preserving them, so neither reaches a second generation. #### Parameters ##### input [`AstmMessage`](#astmmessage) \| readonly [`AstmRecord`](#astmrecord)[] A parsed [AstmMessage](#astmmessage) or a list of [AstmRecord](#astmrecord)s. ##### d? [`Delimiters`](#delimiters-2) = `CANONICAL_DELIMITERS` The delimiters to emit against; defaults to the canonical `H|\^&` set. #### Returns `string` The serialized record stream (`CR` after every record). #### Throws [AstmSerializeError](#astmserializeerror) when a component contains an unencodable `CR`/`LF` (`ASTM_EMIT_UNENCODABLE_VALUE`), when `d` fails one of the three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), or when a record's type letter would not be the first character of its own emitted line (`ASTM_EMIT_TYPE_LETTER_COLLISION`). The last of the three fires on the **default** canonical path as well: it compares each record against the set being emitted with, so a record read under a different set can carry a type letter the canonical set escapes away. Omitting `d` is not a way around it. #### Example ```ts import { parseAstmRecords, serializeAstmRecords } from "@cosyte/astm"; const raw = "H|\\^&\rP|1\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"; serializeAstmRecords(parseAstmRecords(raw)); // === raw ``` *** ### serializeField() > **serializeField**(`field`, `d?`): `string` Serialize a single [AstmField](#astmfield) to its spec-clean wire text, re-escaping each component. A low-level helper for callers assembling a field outside a whole record. #### Parameters ##### field [`AstmField`](#astmfield) The field to serialize. ##### d? [`Delimiters`](#delimiters-2) = `CANONICAL_DELIMITERS` The delimiters to emit against; defaults to `H|\^&`. #### Returns `string` The escaped field text. #### Throws [AstmSerializeError](#astmserializeerror) when a component contains an unencodable `CR`/`LF` (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`). Like [encodeComponent](#encodecomponent) this takes no record, so it never raises `ASTM_EMIT_TYPE_LETTER_COLLISION`: encoding a record's type-letter field through it will escape that letter away without objecting. #### Example ```ts import { serializeField, tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm"; const fields = tokenizeRecord("R|1|^^^687|1&S&40", CANONICAL_DELIMITERS); serializeField(fields[3]!); // "1&S&40" ``` *** ### serializeFramedAstm() > **serializeFramedAstm**(`input`, `options?`): `Uint8Array` Serialize an ASTM message (or a bare record list) and frame it into a spec-clean byte stream in one call: the inverse of [parseFramedAstm](#parseframedastm), composing the two emit layers at the edge. Each record is serialized to spec-clean, `CR`-terminated wire text (canonical delimiters, embedded delimiters re-escaped) and then framed **independently** (one record per `ETX`-closed frame run), so the framing exactly mirrors what [decodeAstmFrames](#decodeastmframes) reassembles: `parseFramedAstm(serializeFramedAstm(msg))` yields an equal message **with the default `startFrameNumber`**. That clause is load-bearing rather than pedantic: a non-default start writes a continuation of a sequence already in progress, and a continuation read on its own opens on a sequence gap, so the decoder does not emit its first record and this round trip does not hold for it. See [ComposeFramesOptions](#composeframesoptions). A value carrying a character above `U+00FF` is **refused** here rather than framed: the record layer is happy to hold one, but a frame carries bytes and nothing in the message says which character encoding to turn it into. A value carrying a raw `STX`, `ETB` or `ETX` is refused too, and being Latin-1 is no exemption: those three are what [decodeAstmFrames](#decodeastmframes) reads as the shape of a frame, and framing has no escape sequence to hide one behind. The record layer is happy to hold those as well, so this is a message that serializes to records perfectly well and cannot be framed. #### Parameters ##### input [`AstmMessage`](#astmmessage) \| readonly [`AstmRecord`](#astmrecord)[] A parsed [AstmMessage](#astmmessage) or a list of [AstmRecord](#astmrecord)s. ##### options? [`ComposeFramesOptions`](#composeframesoptions) = `{}` Frame-encode options. #### Returns `Uint8Array` The framed byte stream. #### Throws [AstmSerializeError](#astmserializeerror) when a value contains an unencodable `CR`/`LF` (`ASTM_EMIT_UNENCODABLE_VALUE`), or when a record's own type letter would not survive being written in the canonical set this function serializes with (`ASTM_EMIT_TYPE_LETTER_COLLISION`). The second reaches messages parsed under a **different** delimiter set, since this function passes no set of its own. #### Throws [AstmFrameEncodeError](#astmframeencodeerror) when `options.startFrameNumber` is not a whole number from `0` to `7` (`ASTM_FRAME_INVALID_START_FRAME_NUMBER`), there are no records to frame (`ASTM_FRAME_EMPTY_RECORD`), a value holds a character above `U+00FF` (`ASTM_FRAME_UNENCODABLE_CHARACTER`), or a record holds an `STX`, `ETB` or `ETX` byte (`ASTM_FRAME_RESERVED_BYTE`). #### Example ```ts import { parseAstmRecords, serializeFramedAstm, parseFramedAstm } from "@cosyte/astm"; const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r"); const bytes = serializeFramedAstm(msg); parseFramedAstm(bytes).message.records.length; // 3 ``` *** ### setDefaultAstmProfile() > **setDefaultAstmProfile**(`profile`): `void` Register a process-scoped default profile that `parseAstmRecords(raw)` applies when no explicit `profile` option is passed. Pass `null` (or `undefined`) to clear. An explicit `parseAstmRecords(raw, { profile })` always wins; `{ profile: null }` opts out of the default for a single call. **Test hygiene:** the only mutable module-scoped state here, tests that call this MUST clear it in teardown or default-profile bleed infects later tests. #### Parameters ##### profile [`AstmProfile`](#astmprofile) \| `null` The profile to register as default, or `null` to clear. #### Returns `void` #### Example ```ts import { setDefaultAstmProfile, astmProfiles, parseAstmRecords } from "@cosyte/astm"; setDefaultAstmProfile(astmProfiles.referenceCorpus); const msg = parseAstmRecords(raw); // uses referenceCorpus setDefaultAstmProfile(null); // clear ``` *** ### splitEscapeAware() > **splitEscapeAware**(`text`, `delimiter`, `escape`, `onAmbiguousAlignment?`, `onShiftedFields?`, `onTruncatedField?`, `onShiftedComponents?`): `string`[] Split `text` on `delimiter`, treating an escape sequence (escape, one body character, escape) as an opaque atom so a delimiter that appears *inside* an escape body never causes a split. Returns the raw (still-encoded) segments: decoding is the caller's next step, per the escape-aware-split-then-decode contract. For the four canonical mnemonics the opacity is belt-and-suspenders (their bodies are letters, not delimiters), but it makes the "an escaped delimiter is one token" guarantee hold for any declared delimiter set, including adversarial input. A single body character is all that guarantee needs. An escape character that heads no sequence is **not** an escape: it is ordinary text, and it opens no atom. Reading it as the opening of a sequence that never closes is what used to merge the whole remainder of a record into one field. Decoding the resulting leaf is what reports it, so this function stays a pure split. Note the two rules together: a delimiter after such a character does split, and a delimiter sitting inside a real three-character atom does not. The second of those is reported, from the decode step rather than from here, whenever the atom's body was not a recognized mnemonic. **Atoms are matched greedily, leftmost first, and where that choice decides a boundary it is reported from here.** The escape character closing one triple cannot also open the next, so `28.6&Z&|&U/L` is read as the atom `&Z&`, then a field separator that splits, and not as `&Z`, then the atom `&|&` whose field separator would not have split. Both alignments are in the bytes and they disagree by one boundary. The reading is **not** changed (that would only pick the other alignment, with no more evidence for it); it is reported through `onAmbiguousAlignment`, and only where the earlier body was unrecognized, since a recognized mnemonic is a construct this codec can interpret and the competitor's body usually is not. That exclusion is wider than that argument: see [AmbiguousAlignmentSink](#ambiguousalignmentsink). #### Parameters ##### text `string` The field or repeat string to split. ##### delimiter `string` The delimiter to split on. ##### escape `string` The active escape character. ##### onAmbiguousAlignment? [`AmbiguousAlignmentSink`](#ambiguousalignmentsink) Called once per unrecognized escape sequence whose closing escape character could instead have opened a sequence holding this delimiter, so the boundary taken here is not the only reading of the bytes. ##### onShiftedFields? [`ShiftedFieldsSink`](#shiftedfieldssink) Called once per contested boundary where the escape character this reading resumes on heads no sequence this codec can *interpret* (none at all, or one whose body is not a recognized mnemonic), so the boundary was bought with bytes the reading cannot read. Wired only by the split taken on the field separator: see [ShiftedFieldsSink](#shiftedfieldssink) for why, and for the one tail it deliberately does not report. ##### onTruncatedField? [`TruncatedFieldSink`](#truncatedfieldsink) Called on that same condition, wired only by the split taken on the repeat separator, where the cost is not a shift. **What it is, its two index bounds and the one class in which nothing moves index are stated in [TruncatedFieldSink](#truncatedfieldsink) and not restated here.** ##### onShiftedComponents? [`ShiftedComponentsSink`](#shiftedcomponentssink) Called on that same condition again, wired only by the split taken on the component separator, where nothing shifts between fields and nothing leaves the record. **What the boundary costs, its three index bounds and the one class in which nothing moves index are stated in [ShiftedComponentsSink](#shiftedcomponentssink) and not restated here.** #### Returns `string`[] The raw segments, in order. #### Example ```ts import { splitEscapeAware } from "@cosyte/astm"; splitEscapeAware("a^b^c", "^", "&"); // ["a", "b", "c"] splitEscapeAware("1&S&40", "^", "&"); // ["1&S&40"] (escape body is opaque) splitEscapeAware("O&Brien^John", "^", "&"); // ["O&Brien", "John"] (unpaired: literal) ``` *** ### toChecksumHex() > **toChecksumHex**(`checksum`): `string` Format a checksum byte as the two **uppercase** hex characters ASTM puts on the wire (the conservative-emit form). Decode accepts lowercase; emit is uppercase. #### Parameters ##### checksum `number` A value in `0` to `255`. #### Returns `string` Two uppercase hex characters, zero-padded (e.g. `"0A"`, `"75"`). #### Example ```ts import { toChecksumHex } from "@cosyte/astm"; toChecksumHex(0x0a); // "0A" ``` *** ### toDate() > **toDate**(`value`, `options?`): `Date` \| `undefined` Convert a parsed ASTM date to an absolute-instant JS `Date`, **only** when the caller has made the zone determinate. ASTM carries no timezone, so `toDate` returns `undefined` unless `options.assumeOffsetMinutes` says which zone the instrument was in. The host machine's zone is never read and UTC is never assumed. A value stating no year is never an instant either, and neither is one whose components are not a real calendar date: month 13 becomes January of the next year in a JS `Date`, and that silent roll-over is the reason the range is checked before the instant is built rather than after. An `assumeOffsetMinutes` that names no usable zone is refused the same way, with `undefined`: one that is not a finite number (`NaN`, either infinity), and one so large that applying it puts the result outside the range a JS `Date` represents. **The answer is never an `Invalid Date`.** Such a `Date` satisfies this signature and defeats it, because a caller cannot tell it from a real instant without testing `getTime()` for `NaN`, and `toISOString()` on it throws. Components below the stated precision fill to their lowest legal value (month and day to 1, time to 0) **for instant construction only**: the value itself is unchanged, and [toObject](#toobject) and [toISO](#toiso) report exactly what they reported before the call. #### Parameters ##### value [`AstmDate`](#astmdate) \| `null` \| `undefined` A parsed date, or the `undefined` [parseAstmDate](#parseastmdate) returns. ##### options? [`ToDateOptions`](#todateoptions) The zone assumption to apply, as signed minutes east of UTC. #### Returns `Date` \| `undefined` The instant, or `undefined` when the zone or the year is unstated. #### Example ```ts import { parseAstmDate, toDate } from "@cosyte/astm"; const d = parseAstmDate("20240315"); toDate(d); // undefined: no zone was stated toDate(d, { assumeOffsetMinutes: -300 }); // 2024-03-15T05:00:00.000Z toDate(parseAstmDate("19881301"), { assumeOffsetMinutes: 0 }); // undefined, not 1989 ``` *** ### toISO() > **toISO**(`value`): `string` \| `undefined` Render a parsed ASTM date as ISO-8601, truncated to the precision it stated and **never** padded out to a precision it did not. No `Z` and no offset is ever appended, because ASTM states none and appending one would fabricate the instrument's zone. The string is deliberately zone-less. It is identical to what [astmDateToLocalISO](#astmdatetolocaliso) returns for every value [parseAstmDate](#parseastmdate) produces **whose components are a real calendar date**; the shared name is the portable route and the repo-native name is unchanged. On a run that is not, the two differ on purpose and only in one direction: `astmDateToLocalISO` still renders the digits it was given (`"1988-13-01"`), because that is its pinned behaviour, and `toISO` answers `undefined` rather than emit a string no ISO-8601 reader accepts. Returns `undefined` for `undefined`, for `null`, for a value stating no component, for one whose components are not a real calendar date, and for one stating no year: an ASTM timestamp opens with a mandatory four-digit year, so a yearless value is not one this parser reads. Never throws. #### Parameters ##### value [`AstmDate`](#astmdate) \| `null` \| `undefined` A parsed date, or the `undefined` [parseAstmDate](#parseastmdate) returns. #### Returns `string` \| `undefined` e.g. `"2024-03-15T09:30"` (minute precision) or `"2024-03"` (month). #### Example ```ts import { parseAstmDate, toISO } from "@cosyte/astm"; toISO(parseAstmDate("202403150930")); // "2024-03-15T09:30" toISO(parseAstmDate("not a date")); // undefined toISO(parseAstmDate("20240230")); // undefined: February has no 30th ``` *** ### tokenizeHeader() > **tokenizeHeader**(`record`, `d`, `onUnknownEscape?`, `onUnpairedEscape?`, `onSwallowedDelimiter?`, `onAmbiguousAlignment?`, `onAlignmentShiftedFields?`, `onAlignmentTruncatedField?`, `onAlignmentShiftedComponents?`): [`AstmField`](#astmfield)[] Tokenize an `H` (header) record into its fields. A header cannot go through [tokenizeRecord](#tokenizerecord): its second field is the **delimiter declaration**, which carries all three non-field delimiters *literally* rather than as escape sequences. Run through the generic tokenizer the declaration would be split on its own repeat and component characters and its escape character would be decoded and reported as unpaired, so what the header declares would come back as fragments plus a spurious warning. This tokenizer instead takes the declaration verbatim as one opaque field, never decoded, and applies the ordinary escape-aware tokenizer to the data portion that follows it. `fields[0]` is the type-letter field and `fields[1]` is the delimiter declaration (verbatim, never escape-decoded); the header's ASTM data fields follow from `fields[2]`. #### Parameters ##### record `string` The raw `H` record text (terminator already stripped). ##### d [`Delimiters`](#delimiters-2) The delimiters declared by this header. ##### onUnknownEscape? (`fieldIndex`) => `void` Called with the 0-based whole-record field index for each unrecognized escape sequence in the data portion. ##### onUnpairedEscape? (`fieldIndex`) => `void` Called with the 0-based whole-record field index for each unpaired escape character in the data portion. The declaration itself is opaque, so the escape character it names never reports here. ##### onSwallowedDelimiter? (`fieldIndex`) => `void` Called with the 0-based whole-record field index for each unrecognized escape sequence in the data portion whose body is a splitting delimiter in force. The declaration is opaque, so the delimiters it names literally never report here. ##### onAmbiguousAlignment? (`fieldIndex`) => `void` Called with the 0-based whole-record field index for each competing escape alignment in the data portion. The declaration is opaque, so the characters it names literally never report here either. ##### onAlignmentShiftedFields? (`fieldIndex`) => `void` Called with the 0-based whole-record field index for each contested field boundary in the data portion whose reading resumes on an escape character heading no sequence this codec can interpret. The declaration is opaque, so it never reports here either. ##### onAlignmentTruncatedField? (`fieldIndex`) => `void` Called with the 0-based whole-record field index for each contested **repeat** boundary in the data portion on that same tail test. The declaration is opaque, so it never reports here either. ##### onAlignmentShiftedComponents? (`fieldIndex`) => `void` Called with the 0-based whole-record field index for each contested **component** boundary in the data portion on that same tail test. The declaration is opaque, so it never reports here either. #### Returns [`AstmField`](#astmfield)[] The header's fields. #### Example ```ts import { tokenizeHeader, CANONICAL_DELIMITERS } from "@cosyte/astm"; const fields = tokenizeHeader("H|\\^&|||sender", CANONICAL_DELIMITERS); fields[1].raw; // "\\^&" fields[4].raw; // "sender" ``` *** ### tokenizeRecord() > **tokenizeRecord**(`record`, `d`, `onUnknownEscape?`, `onUnpairedEscape?`, `onSwallowedDelimiter?`, `onAmbiguousAlignment?`, `onAlignmentShiftedFields?`, `onAlignmentTruncatedField?`, `onAlignmentShiftedComponents?`): [`AstmField`](#astmfield)[] Tokenize a single record string (its terminator already stripped) into its fields. `fields[0]` is the type-letter field; ASTM data fields follow at 1-based indices. #### Parameters ##### record `string` The raw record text. ##### d [`Delimiters`](#delimiters-2) The delimiters resolved from the header. ##### onUnknownEscape? (`fieldIndex`) => `void` Called (with the 0-based field index) for each unrecognized escape sequence encountered, so the caller can warn. ##### onUnpairedEscape? (`fieldIndex`) => `void` Called (with the 0-based field index) for each escape character that heads no escape sequence and was therefore read as a literal. ##### onSwallowedDelimiter? (`fieldIndex`) => `void` Called (with the 0-based field index) for each unrecognized escape sequence whose body is a splitting delimiter in force, so that delimiter never became a boundary. ##### onAmbiguousAlignment? (`fieldIndex`) => `void` Called (with the 0-based field index) for each unrecognized escape sequence whose closing escape character could instead have opened one holding the delimiter that split, so the boundary is one of two readings the bytes carry. ##### onAlignmentShiftedFields? (`fieldIndex`) => `void` Called (with the 0-based field index) for each contested **field** boundary the reading took while resuming on an escape character that heads no sequence this codec can interpret. Wired only to the field split: a repeat or component boundary divides one field and so moves no field-indexed slot. That is a choice and not a consequence, because components are modeled inside a field. **What the boundary costs, and the one class in which nothing moves index, are stated in [ShiftedFieldsSink](#shiftedfieldssink) and not restated here.** ##### onAlignmentTruncatedField? (`fieldIndex`) => `void` Called (with the 0-based field index) for that same condition on the **repeat** split, where no field-indexed slot moves. **What the boundary costs, its two index bounds and the one class in which nothing moves index are stated in [TruncatedFieldSink](#truncatedfieldsink) and not restated here.** ##### onAlignmentShiftedComponents? (`fieldIndex`) => `void` Called (with the 0-based field index) for that same condition on the **component** split, where no field-indexed slot moves and nothing leaves the record. **What the boundary costs, its three index bounds and the one class in which nothing moves index are stated in [ShiftedComponentsSink](#shiftedcomponentssink) and not restated here.** All three splitting roles are wired now. #### Returns [`AstmField`](#astmfield)[] The record's fields. #### Example ```ts import { tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm"; const fields = tokenizeRecord("R|1|^^^687|28.6|U/L", CANONICAL_DELIMITERS); fields[3].components[0]; // "28.6" ``` *** ### toObject() > **toObject**(`value`): [`DateParts`](#dateparts) \| `undefined` Read a parsed ASTM date as the shared [DateParts](#dateparts) object: only the components the value stated, frozen, with a spec-native 1-to-12 month. Returns `undefined` for `undefined`, for `null`, for a value that stated no component at all, and for one whose stated components are not a real calendar date (a month outside 1 to 12, a day past the end of that month, an hour past 23, a minute or second past 59). Never throws, whatever it is handed. The truncation flag, the precision and the raw digit run are deliberately not carried over: a dangling half-component was never a complete component, so it appears in no field here. The out-of-range refusal is total, not per component: `"19881301"` answers `undefined`, not `{ year: 1988 }`, because a year-precision date of birth is not what that instrument sent either. #### Parameters ##### value [`AstmDate`](#astmdate) \| `null` \| `undefined` A parsed date, or the `undefined` [parseAstmDate](#parseastmdate) returns. #### Returns [`DateParts`](#dateparts) \| `undefined` The stated components, frozen, or `undefined`. #### Example ```ts import { parseAstmDate, toObject } from "@cosyte/astm"; toObject(parseAstmDate("20240315")); // { year: 2024, month: 3, day: 15 } Object.keys(toObject(parseAstmDate("2024")) ?? {}); // ["year"] toObject(parseAstmDate("19881301")); // undefined: there is no month 13 ``` *** ### undefinedAbnormalFlag() > **undefinedAbnormalFlag**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG` warning. The flag is surfaced as `undefined` (never coerced to `normal`); the warning carries only the position and fixed prose naming the **same** vocabulary the interpreted flag reports, read from the one shared constant so the two cannot drift apart. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { undefinedAbnormalFlag } from "@cosyte/astm"; undefinedAbnormalFlag({ recordIndex: 4, recordType: "R", fieldIndex: 7 }); ``` *** ### undefinedResultStatus() > **undefinedResultStatus**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_UNDEFINED_RESULT_STATUS` warning. The status is surfaced as `undefined` and, like every non-final status, never reads as active-final. The message carries the **same** attribution the interpreted status reports (there is no citable source for this letter set), from the one shared constant. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { undefinedResultStatus } from "@cosyte/astm"; undefinedResultStatus({ recordIndex: 4, recordType: "R", fieldIndex: 9 }); ``` *** ### uninterpretedQueryStatus() > **uninterpretedQueryStatus**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS` warning. Emitted when a `Q` record carries a request-information status code; the code set is paywalled, so the status is surfaced verbatim and never mapped to a guessed meaning. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { uninterpretedQueryStatus } from "@cosyte/astm"; uninterpretedQueryStatus({ recordIndex: 2, recordType: "Q", fieldIndex: 13 }); ``` *** ### unitsAbsent() > **unitsAbsent**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_UNITS_ABSENT` warning. Emitted when a result carries a numeric value but no units; units are never defaulted, guessed, or converted. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { unitsAbsent } from "@cosyte/astm"; unitsAbsent({ recordIndex: 4, recordType: "R", fieldIndex: 5 }); ``` *** ### unknownEscapeSequence() > **unknownEscapeSequence**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_UNKNOWN_ESCAPE_SEQUENCE` warning. The sequence is preserved verbatim in the decoded value; the warning body carries neither the sequence nor its surrounding text. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { unknownEscapeSequence } from "@cosyte/astm"; unknownEscapeSequence({ recordIndex: 4, recordType: "R", fieldIndex: 4 }); ``` *** ### unknownRecordType() > **unknownRecordType**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_UNKNOWN_TYPE` warning. The record is still surfaced (as an unsupported record), never dropped. **This one is not cosmetic.** Message grouping decides where a message starts by reading each record's type letter, so an unrecognized letter may be a header the reader failed to see, and two messages then read as one. A profile is therefore not permitted to tolerate this code. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { unknownRecordType } from "@cosyte/astm"; unknownRecordType({ recordIndex: 3, recordType: "Z" }); ``` *** ### unpairedEscapeCharacter() > **unpairedEscapeCharacter**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_UNPAIRED_ESCAPE_CHARACTER` warning. The character is preserved verbatim as a literal in the decoded value and opens no atom, so it does not merge the rest of the record; the warning body carries neither the character's surroundings nor any field value. **It is a statement about that one character, not about the record.** A different escape character in the same record may head a real three-character sequence, and if that sequence's body is an unrecognized character that is itself a delimiter in force, that delimiter does not split, which is reported separately by [WARNING\_CODES.ASTM\_RECORD\_DELIMITER\_SWALLOWED\_BY\_ESCAPE](#astm_record_delimiter_swallowed_by_escape). A profile **may** tolerate this code: the value it reports is byte-identical with the warning and without it, because reading the character as a literal is the parse, not a consequence of the warning. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { unpairedEscapeCharacter } from "@cosyte/astm"; unpairedEscapeCharacter({ recordIndex: 4, recordType: "R", fieldIndex: 4 }); ``` *** ### unparseableReferenceRange() > **unparseableReferenceRange**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE` warning. The range text is surfaced verbatim as `unparsed`; no bound is fabricated. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { unparseableReferenceRange } from "@cosyte/astm"; unparseableReferenceRange({ recordIndex: 4, recordType: "R", fieldIndex: 6 }); ``` *** ### unreadableRedeclaration() > **unreadableRedeclaration**(`position`): [`AstmRecordWarning`](#astmrecordwarning) Build an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning. Emitted when a later `H` record cannot declare a usable delimiter set; the set already in force is kept and every record is still surfaced. #### Parameters ##### position [`AstmPosition`](#astmposition) #### Returns [`AstmRecordWarning`](#astmrecordwarning) #### Example ```ts import { unreadableRedeclaration } from "@cosyte/astm"; unreadableRedeclaration({ recordIndex: 2, recordType: "H" }); ``` --- URL: https://docs.cosyte.com/astm/troubleshooting Markdown: https://docs.cosyte.com/astm/troubleshooting.md # Troubleshooting Common symptoms when integrating `@cosyte/astm`, and how to read what the parser is telling you. ## The parse "succeeded" but the result looks wrong `@cosyte/astm` is lenient: it recovers from vendor quirks rather than throwing. That means a surprising result usually comes with an explanation in `warnings`. Inspect them first: ```ts const { warnings } = parseAstmRecords(raw); for (const w of warnings) { console.warn(w.code, w.message, w.position); } ``` Each warning carries a **stable code** (`WARNING_CODES`) and positional context. If a deviation should be a hard failure for your integration, re-parse with `{ strict: true }` to have it thrown instead. ## A parse threw Only **Tier-3 fatal** conditions (`FATAL_CODES`) throw in lenient mode: these mark input the parser cannot recover into a structured result. In `{ strict: true }` mode, any tolerated deviation throws too. Catch and inspect the error's code to tell the two apart. ## An accessor threw `AstmAmbiguousStreamError` The parse succeeded; an accessor refused to answer. `patient()`, `results()`, `orders()`, `comments()` and `query()` read the **whole stream**, so they cannot answer for a stream that does not determine one answer, and refusing is the point: the alternative is one patient's results attributed to another. Two codes: - **`ASTM_AMBIGUOUS_MULTI_MESSAGE`**: the stream carries more than one `H` … `L` message. Read each message's own records instead: ```ts for (const m of messages(msg)) { m.patient; m.results; } ``` - **`ASTM_AMBIGUOUS_MULTI_PATIENT`** (from `patient()` only): one message carries several `P` records, so "the patient" is not determined. Read `messages(msg)[n].patients` for all of them. The error carries `code`, a value-free `position` pointing at where the ambiguity became visible, and `messageCount` / `patientCount`. It never carries an identifier or a value, so it is safe to log. `commentsFor()` never throws this: the parent record you hand it already names the message. ## Warning messages and logs Warning `message` fields are safe to log: they **never contain PHI**. Never log the raw payload itself; it may carry protected health information. ## A value came back without units, or a flag as "undefined" That is the fail-safe design, not a bug. A numeric result with no units raises `ASTM_RECORD_UNITS_ABSENT` and the unit is left empty: never defaulted or guessed. An abnormal flag the parser does not recognize is surfaced as `"undefined"`, never coerced to `"normal"`. An unparseable reference range is surfaced verbatim with no invented bound. In every case the library refuses to hand you a confident wrong value: inspect the warning and decide. An `"undefined"` flag or status also tells you **what it was graded against**, so you can act on the difference. `flag.vocabulary` names the code system and the version this library compared the letter with (a version that says what was compared, **not** what the sender meant), so a letter that is absent from that version is a gap to report rather than a mystery. `status.vocabulary.attributed` is always `false`: no citable published source binds the status letters, and `status.vocabulary.reason` says why. The `ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG` and `ASTM_RECORD_UNDEFINED_RESULT_STATUS` warning messages carry the same attribution, and remain value-free. Recognition is **exact match** after the surrounding whitespace is trimmed. A lower-case `hu` or `f` is reported unrecognized with its raw text intact rather than folded into a match: widening recognition is how a parser starts guessing. Normalize the case yourself if your instrument really sends it that way. ## `ASTM_UNPAIRED_ESCAPE_CHARACTER`: a value carries a bare ampersand An escape sequence is the escape character, **one** body character, and the escape character again (`&F&` `&S&` `&R&` `&E&`). An escape character that heads no such sequence is not an escape: it is kept as the literal character it is and opens no atom. So `R|1|^^^687|28.6&|U/L||N||F` gives you a value of `28.6&` with its units and its `final` status intact, and `O&Brien` in a surname leaves the birth date and sex where they are. The warning says the sender did not write the character the spec-clean way, which is `&E&`. The parser does not guess which it meant: it keeps the byte that arrived and reports it. If your feed does this routinely, the code is **tolerable**, so a vendor profile can expect it and let you keep parsing `{ strict: true }`. Emitting is unaffected: this package always writes a literal escape character as `&E&`, so a stream it produced never raises the code. **This warning is about one character, not about the record.** A three-character sequence is opaque by design (that is what keeps `&F&` one token under a set naming `F` as a delimiter), so a sequence whose body is an unrecognized character that is itself a delimiter in force swallows that delimiter: `R|1|^^^687|28.6&|&U/L||||F` reads a value of `28.6&|&U/L` with no units and status `unspecified`. That case raises `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` as well as `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. Only the second of those is tolerable, so a strict parse refuses the record whatever profile is in force. A **recognized** mnemonic is outside it, deliberately: `&F&` under a set naming `F` as the repeat delimiter is the sender escaping the field separator on purpose, and it raises neither code. The reading itself is unchanged, and it does not survive a re-emit: this package rewrites the preserved sequence into recognized mnemonics, and the resulting stream says that value unambiguously, so a second-generation read is silent. Catch it on the first read of the wire bytes. ## Two escape sequences could have been aligned differently `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` says the same bytes carry two readings that disagree about one field, repeat or component boundary. Sequences are matched greedily and leftmost, so the escape character closing one cannot also open the next. Where it could have, and where the body it would have held is the delimiter that split, one alignment ends a field there and the other holds that delimiter inside an opaque atom and ends nothing. `R|1|^^^687|28.6&Z&|&U/L||||F` reads a value of `28.6&Z&` and units of `&U/L` under the alignment taken, and reads as a single unsplit field under the other. The leftmost reading is kept and every byte is preserved. Taking the other alignment would be a different guess with no more evidence behind it, so what this reports is that the boundary you were handed is a choice, not that it is wrong. It is not tolerable, so a strict parse refuses the record whatever profile is in force. Two cases are outside it, deliberately. A **recognized** mnemonic before the delimiter is silent, because the reading taken interprets a construct (`&F&` is an escaped separator, which is what the mechanism is for) while the competitor's body is a delimiter character the parser usually cannot interpret, so it prefers the reading taken. **That is not a promise that the reading taken is conformant**, and two cases fall in the gap. The first, on the field separator, is now covered by a second code, described in the next section. The second is covered only where that code reaches: a declared set naming a mnemonic letter as a splitting delimiter leaves both alignments interpreting one construct each with **neither preferred**, and where the escape character past the boundary heads nothing this reader can interpret (it is bare, or it heads a sequence whose body is unrecognized) one of the next three sections' codes fires, one per splitting role: the field separator, then the repeat separator, then the component separator. What is left is the one tail they exclude: where the escape character past the boundary heads a sequence this reader RECOGNIZES, **nothing reports it at all**, which is the escape mechanism working. **That silence is a trade, not a claim that nothing was lost there**, so treat an escape character next to a delimiter as worth reading the raw line for, **whether or not anything fired**. The other excluded case is a delimiter with no escape character two positions past it, which is no competing alignment at all. What does fire is a subset of what already raises `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. Like its mirror above it does not survive a re-emit: catch it on the first read of the wire bytes. **What to do:** ask the sender to escape a literal escape character as `&E&`, which removes the competing alignment at the source. ## A competing alignment shifted every field after it, and a result status with them `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` answers a different question about the same position as the code above: not whether the parser's own vocabulary prefers the alignment taken, but what that alignment makes of the bytes **after** the boundary. The two readings resume one character apart, so they disagree past it, and on most tails about the whole rest of the record. Where the escape character the reading taken resumes on heads no sequence this reader can _interpret_, that reading bought a field boundary with bytes it cannot read, while the competing alignment is exactly the reading that can use them, as the close of its own sequence. Two tails satisfy that and cost the same: the character heads no sequence at all, or it heads one whose body is not a recognized mnemonic and is therefore kept verbatim rather than read. Consuming a sequence is not interpreting one. **On the field separator that matters clinically, because a gained field boundary shifts every later field.** `R|1|^^^687|28.6&F&|&U/L||||F` reads **nine** fields under the alignment taken and **eight** under the other, so the sender's trailing `F` is read out of field 9, the result status, under the first and out of no field at all under the second. The parse hands back units of `&U/L` and a status of `final`, and both are consequences of the alignment rather than values the sender placed in those slots. **Do not act on a `final` from a record carrying this code.** Before this code existed the only warning on that stream was the tolerable `ASTM_UNPAIRED_ESCAPE_CHARACTER`, so a strict parse under the widest legal profile accepted it. The reading is unchanged: this reports the shift, it does not repair it, because taking the other alignment would be a different guess with no more evidence behind it. It is not tolerable, so a strict parse refuses the record whatever profile is in force, and it fires alongside the code above rather than instead of it. Two bounds, both deliberate, and **neither is a promise that nothing was lost outside it**. It is wired to the **field** separator only, because a gained repeat or component boundary divides one field and so moves no field-indexed slot: the units and the status stay put. That is a choice, not a consequence. Components are modeled _inside_ a field, so a gained **component** boundary does move a modeled slot, and that is reported by its own code two sections below rather than by this one. On the repeat role the cost is the **value** and the field's components, reported by its own code in the next section. And it stays silent in exactly one case, where the escape character past the boundary heads a sequence this reader RECOGNIZES, because the reading taken is then the one making sense of those bytes: under a set naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again, which is entirely well formed. That is the only tail on which a stream's escaping can be clean, and so the only exclusion, **wherever the escape role is a character distinct from the three splitting roles**; where it is not, see "A header declared one character in two delimiter roles" below. **That silence is a trade, not a claim that nothing was lost there**: on that tail the gained field boundary is exactly as real and nothing at all is reported, so `R|1|^^^687|28.6&F&|&F&U/L||||F` reads nine fields against the other alignment's eight and hands back a status of `final` with `warnings: []`. **Treat an escape character sitting next to a delimiter as worth reading the raw line for, whether or not anything fired.** **The shift has one measured exception, and it is named here rather than left to be found.** Where the sequence past the boundary carries the **field separator itself** as its body, the reading taken holds that character inside an opaque atom while the competing alignment splits on it, so the two readings read the **same number** of fields in **different places**: `R|1|^^^687|28.6&F&|&|&U/L||||F` reads nine fields under both, the status `F` sits in field 9 under both, and what differs is the units. This code still fires there, which is over-reporting relative to the field indexes and never under-reporting, and that class costs no stream its disposition, because `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` has already refused the record. What holds wherever this fires is that the two readings disagree and that both consume every byte. Like the codes above it does not survive a re-emit: catch it on the first read of the wire bytes. **What to do:** read the raw line, and ask the sender to escape a literal escape character as `&E&`. ## A competing alignment split one field into repeats, so its value and identity read short `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` asks the same question as the code above, on the **repeat** separator instead. Nothing shifts there: the units slot and the result-status slot are read out of the same field numbers under either alignment, which is exactly why the shift report cannot see it. What it reports is that **the field is read as more repeats than the competing alignment gives it**. Where that gained boundary is the **first** one in the field it reaches a modeled slot, because a field's modeled value and its components are taken from its first repeat alone: everything past the boundary stays on the wire, stays in `repeats`, and leaves every modeled slot. Both costs are reachable on the **canonical** set, so no unusual declaration is needed: - **The value truncates.** `R|1|^^^687|28.6&S&\&U/L|U/L||||F` reads a value of `28.6^`, and `&U/L` leaves the result entirely. The competing alignment reads one repeat carrying all of it. - **A modeled identity empties.** `R|1|&F&\&687|28.6|U/L||||F` reads a Universal Test ID of one component holding a decoded field separator, so the local code `687` is in no modeled slot at all and the record is left with no code to key on, carrying only an unvalidated wire value the sender never wrote. `DOE&S&\&JANE^A` reads a last name and **no given or middle name**. Before this code existed the only warnings on those streams were tolerable ones, so a strict parse under the widest legal profile accepted a truncated value and an emptied test identity. The reading is unchanged: this reports the truncation, it does not repair it, and it fires alongside the codes above rather than instead of them. **At a later boundary this fires and nothing modeled moves**, because the first repeat is then the same under both readings and only the repeat structure after it differs. That is deliberate and measured: the boundary is still one the bytes do not force, and a consumer reading `repeats` is still reading an alignment guess. Relative to the modeled slots it is over-reporting, never under-reporting, so **check `repeats` on the field the warning names rather than assuming the value is wrong**. Two further bounds, both deliberate. It is wired to the **repeat** separator only: a gained **component** boundary reaches a modeled slot too, and differently, moving it along the component list rather than dropping it, which is the next section's code. And the tail bound is the previous section's, for the same reason, and so is its one exclusion: where the escape character past the boundary heads a sequence this reader RECOGNIZES, this stays silent, because `28.6&R&\&R&U/L` is the repeat separator escaped, written, and escaped again, and refusing it would refuse a well-formed stream. **That silence is a trade, not a claim that nothing was lost**: `28.6&S&\&S&U/L` still reads a value of `28.6^` with `warnings: []`. **The truncation has the same one exception as the shift**: where the sequence past the boundary carries the **repeat separator itself**, the two readings read the same number of repeats in different places, so the field is not read as more repeats at all, and that class was already refused by `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`. Like the codes above it does not survive a re-emit: catch it on the first read of the wire bytes. **What to do:** read the raw line, check `repeats` on the field the warning names before trusting its value, and ask the sender to escape a literal escape character as `&E&`. ## A competing alignment split one field into components, so a code system and a name moved `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS` asks the same question as the two codes above, on the **component** separator, which is the third and last role a split is taken on. Nothing shifts between fields and nothing leaves the record, which is why neither of the other two can see it: components are modeled _inside_ a field, so every component after the gained boundary sits **further right** than the competing alignment puts it, by a displacement that is not fixed. The slots that indexes into are named things. Both are reachable on the **canonical** set, so no unusual declaration is needed: - **A code system and a vendor's local code swap places.** `R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads a Universal Test ID of four components, so `L` is the coding scheme and `687` the local code. The competing alignment reads three, and `687` is the **coding scheme**. A code-system selector and a vendor's local code are not the same thing, so a consumer routing on either is routing on the alignment. - **A given name and a middle name shift along.** `P|1||MRN-0001||DOE&F&^&JANE^A||19700101|F` reads a given name of `&JANE` and a middle name of `A`. Under the competing alignment `A` is the **given** name and there is no middle name at all. Before this code existed the only warning on either stream was the tolerable `ASTM_UNPAIRED_ESCAPE_CHARACTER`, so a strict parse under the widest legal profile accepted both. The reading is unchanged: this reports the moved slots, it does not repair them, and it fires alongside the codes above rather than instead of them. **Every gained boundary at or before the last modeled component index moves those slots, not only the first**, because the shift propagates from it to the end of the component list. That is the one place this differs structurally from the repeat-separator code above, where only the first gained boundary reaches a modeled slot. **Two bounds run the other way, and it fires inside both.** Each is deliberate over-reporting, never under-reporting, so in either case **check the field's raw bytes before concluding that anything named moved**: - **Past the last modeled component index nothing named moves.** A model reads a fixed number of components: a patient name three (last, first, middle), a Universal Test ID four. A contested boundary further right than that still shifts the components after it, and every named slot reads the same under both alignments. `DOE^JANE^A^SFX&F&^&X` fires, and the name is `DOE` / `JANE` / `A` either way. - **Inside a later repeat nothing modeled moves at all**, because a field's components are read from its first repeat alone; what differs there is `repeats`. The tail bound is the one the other two carry, for the same reason, and so is its one exclusion: where the escape character past the boundary heads a sequence this reader RECOGNIZES, this stays silent, because `&F&^&F&GLU` is a field separator escaped, a component separator written, and a field separator escaped again, which is entirely well formed. **That silence is a trade, not a claim that nothing was lost**: `&F&^&F&GLU^L^687` still reads one component more than the competing alignment, with `warnings: []`. **A third bound runs the other way, and unlike the two above it is about the bytes past the boundary rather than where the boundary sits**: where the sequence past it carries the **component separator itself**, the two readings read the same number of components in different places, so `DOE&F&^&^&JANE^A` reads three components under both with `A` the middle name under both, and that class was already refused by `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`. Like the codes above it does not survive a re-emit: catch it on the first read of the wire bytes. **What to do:** read the raw line, check the field's components against the raw bytes before trusting a coding scheme or a name part, and ask the sender to escape a literal escape character as `&E&`. ## How far a contested alignment displaces things, and why you cannot count it The three codes above each say things sit **further right** than the competing alignment puts them. This section says how far, so that the three above name the KIND of cost and leave the magnitude here: it is one fact, and restating it per code is how it went stale before. **The displacement is not fixed, and it takes three values rather than one.** "One place" describes only the single-construct case the three codes were first measured on: - **Zero, on the tie class.** Where the sequence past the boundary carries the split delimiter itself as its body, both readings return the same number of segments in different places. Each code above names its own example, and `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` has already refused those records. - **One, on a record carrying a single contested construct.** This is what every published figure for these codes was measured on, and the only case "one place" was ever true of. - **One more for each additional contested construct.** The competing reading resumes one character further on at each contested position, so it falls further out of step and the gap widens once per construct. `A&Z&|&BX&Z&|&F&C` reads **three** fields against the competing alignment's **one**. **The number of warnings is not the displacement either, in either direction.** A gained boundary whose tail is a recognized mnemonic is excluded from the report and still displaces, so one warning can sit on a displacement of two; and where one of several constructs is a tie, two warnings can sit on a displacement of one. The practical consequence is worth stating plainly, because the two cases look identical in a log. On `R|1|^^^687|28.6&F&|&Z&U/L||||F` the sender's trailing `F` lands **in** the status slot and reads as `final`. On `R|1|^^^687|28.6&Z&|&BX&Z&|&F&U/L||||F` the same displacement runs twice, the `F` overshoots the status slot, and the status reads `unspecified`. One code, two opposite outcomes. **What to do:** do not step back by one and do not count warnings. Read the raw line and count the delimiters yourself. ## A header declared one character in two delimiter roles `ASTM_RECORD_DELIMITER_ROLE_COLLISION` says the `H` record named the same character in two of the repeat, component and escape roles, so the boundary between those two roles is not in the bytes. The declaration is still honored and no record is dropped: under `H|^^&` a field a canonical sender would have written as two repeats of two components reads back as four repeats of one component each. The code is **not** tolerable, deliberately. Every such set is by definition non-canonical, so before this code existed the only warning it raised was the tolerable `ASTM_NONSTANDARD_DELIMITERS`, and a profile expecting an ordinary vendor set left a strict parse accepting it. Emit refuses the same sets with `ASTM_EMIT_INVALID_DELIMITERS`, so `serializeAstmRecords(msg, msg.delimiters)` throws on such a message. If you own the sending side, fix the declaration; if you do not, treat the affected records' repeat and component structure as unrecoverable rather than as read. **Where the escape role is one of the colliding roles, this code is the only thing standing beside a contested-alignment warning.** Everywhere else, one of the two tolerable escape reports (`ASTM_UNPAIRED_ESCAPE_CHARACTER` or `ASTM_UNKNOWN_ESCAPE_SEQUENCE`) always fires next to the three codes above, because firing requires an escape character the reader cannot interpret. On a colliding set one byte both opens a sequence and ends a segment, the split claims it first, and neither escape report ever sees a sequence to raise: the alignment warning arrives with **neither** companion. The stream is still refused, by this code, which no profile may tolerate. **This one is reported once per set change, not once per record.** A second header re-declaring the same colliding set raises nothing, while the alignment warnings in its message fire again. If you scope warnings to a message rather than to the stream, you can see one of those three codes standing entirely alone, with nothing in that message explaining it. Look back at the stream's first header. A set naming the escape character as the **field** separator cannot be declared at all: the declaration is the three characters after the field separator and stops at the next one, so such a header terminates itself one character short and is refused outright. ## A framed stream lost a frame, or a checksum is wrong The frame layer validates every modulo-256 checksum and tracks the frame-number sequence. A bad-checksum frame is flagged `trusted: false` and **never merged** into a record (a warning in lenient mode, a thrown `AstmFrameStrictError` in strict); a sequence gap is warned and **never silently bridged**. Read `frameWarnings` from `parseFramedAstm`: each carries a frame number and byte offset, never the record bytes. ## `ASTM_FRAME_RESERVED_BYTE`: emit refused a record carrying `STX`, `ETB` or `ETX` `composeAstmFrames` (and `serializeFramedAstm` through it) throws an `AstmFrameEncodeError` with this code when a record holds one of the three bytes the framing reads as structure. There is no option to force it and no bytes-instead escape hatch, because the byte is unframable however it arrives: a frame's text ends at the first `ETB`/`ETX` after its `STX`, and framing has no escape sequence to hide one behind. Written through, it truncated the frame at that byte, which was **not** reliably loud: where the next two bytes happened to be the short frame's checksum, the truncated frame verified and a whole record was absorbed into the previous one with an empty `warnings` array at both layers. `recordIndex` and `characterIndex` locate it in your own data (the error never carries the bytes). Remove or replace the byte in the value before framing: which byte belongs in a clinical value is your call, not the library's. If you do not frame at all, nothing changes for you: `serializeAstmRecords` returns a string and round-trips such a value byte for byte. ## `ASTM_FRAME_INVALID_START_FRAME_NUMBER`: emit refused a `startFrameNumber` `composeAstmFrames` (and `serializeFramedAstm` through it) throws an `AstmFrameEncodeError` with this code when `options.startFrameNumber` is not a whole number from `0` to `7`. A frame's number is a single ASCII digit, so there is nothing else to write it as. The message names the value received: it is your own option, not stream content. `composeAstmFrames` checks it before it reads a record, so on that entry point the refusal never depends on your data. `serializeFramedAstm` serializes every record first, so a record that cannot be serialized at all is refused before this check runs on that route. It used to be written through unchecked, which is why the check exists. `-1` put a `/` in the frame-number position; `NaN` and either infinity put a `NUL` there in **every** frame, after which the decoder recognised no frame number at all and emitted none of the records. Values that truncated back onto a digit were quieter and no better: `1.5` and `257` each produced the exact stream a `startFrameNumber` of `1` produces, so the option silently accepted what it documented as invalid. The whole `0`-`7` range is still accepted, because a non-default start has a real use: composing one transfer across several calls. Continue the sequence at the number after the last frame the previous call used, and joining the results is byte-identical to composing the whole list in one call. What a continuation is **not** is the start of a transfer: read on its own, a stream that starts anywhere but `1` opens on a sequence gap and the decoder does not emit that first record. What `parseFramedAstm` does after that **varies with the message shape**. It may throw, under more than one code, and it may return a message that is simply one record short. What does hold is that the **record layer never reports the loss**: `parseFramedAstm` hands the record parser only the frames the codec vouched for, so `message.warnings` carries what the surviving records warrant and nothing about the record that did not survive. **Read `frameWarnings`.** If you are not continuing a sequence, do not set the option. To find the number to continue from, decode the part you just composed and read its last frame's number: `((decodeAstmFrames(part).frames.at(-1)?.frameNumber ?? 0) + 1) % 8`. The number of frames is not the same thing once a record has split across several of them. A frame carries no number only when the stream ends immediately after its `STX`, which is not something `composeAstmFrames` writes, so that fallback never fires on a part it composed. ## Known limitations `@cosyte/astm` is feature-complete across both layers, but its promise is deliberately narrow. See [What it does, and does not do](https://docs.cosyte.com/astm/limitations.md) for the full, honest boundary: no live I/O, units are verbatim free text (not UCUM), no bundled terminology dictionary (LIVD is bring-your-own), and `M`/`S` records are surfaced verbatim, never interpreted. The **API Reference** always reflects exactly what this release ships: treat it as the source of truth over any prose above. --- URL: https://docs.cosyte.com/astm/limitations Markdown: https://docs.cosyte.com/astm/limitations.md # What `@cosyte/astm` does, and does not do A lab result drives treatment. A parser that silently hands back a wrong value, unit, flag, status, or patient ID can cause harm. So this page is deliberately blunt about the promise and its edges. Read it before you rely on the library: the **API Reference** is always the exact truth of what a given release ships; this page is the honest shape of the whole. ## The promise (narrow, on purpose) `@cosyte/astm` is **liberal on decode, conservative on emit, and never returns a confident wrong value.** - **Liberal decode.** Real vendor quirks become typed, value-free **warnings** with stable codes and positional context (record + field/component index, or frame number + byte offset): not thrown errors and not silent data loss. A `{ strict: true }` mode escalates every tolerated deviation to a thrown error at an integration boundary. - **Conservative emit.** The serializer always produces spec-clean output: canonical `H|\^&` delimiters, every embedded delimiter re-escaped, every checksum and frame number computed, never faked. A header declaration carrying characters beyond the three that hold a delimiter role keeps them rather than being truncated, since deleting bytes is the larger claim. Emit returns a plain string and has no warning channel, so what it cannot write reversibly is a typed error at the call: a value carrying a `CR`/`LF` no escape can encode, a delimiter set that fails one of the three conditions readback requires, and a record whose own type letter that set would escape away. Those refusals cover a record's **type**; they do not promise every field lands where it did: see the non-goals below. - **Fail-safe on ambiguity.** A missing unit, an unrecognized abnormal flag, a corrected/cancelled result, a bad checksum, an unparseable range: each surfaces as a typed warning or error. The library refuses to guess a value into existence. ## What it does **not** do These are **non-goals**, not missing features: naming them so nothing over-trusts the parser. - **No live connection management.** The library decodes and encodes byte streams and provides a **pure** LTP protocol reducer (`ltpReduce`), but it does **not** own a serial port or a socket. The I/O adapter and the interactive **timeout / retransmit timing** are the consumer's. The standard's exact numeric timeouts and retry counts live in the paywalled body and are modeled as _transitions, not timers_. - **No unit semantics.** Units are surfaced as vendor **free text**: **not UCUM**, not normalized, not convertible. A numeric value with no units raises `ASTM_RECORD_UNITS_ABSENT`; a missing unit is never defaulted or guessed. - **No terminology dictionary, and no LOINC validation of any kind.** LOINC and SNOMED are **not bundled** (see licensing below). Vendor→LOINC mapping requires a **consumer-supplied** IICC LIVD catalog (`applyLivd`), keyed on the vendor local code alone, and an unmapped code stays verbatim, never a fabricated LOINC. A value in the Universal Test ID's LOINC slot is surfaced verbatim as an **unvalidated wire value**: this package never checks it, never reports it as a LOINC, and never keys a result on it, so a first component that really is a LOINC is still only a value the library does not vouch for. It ships no LOINC table and could not check one if it wanted to. - **No UCUM conformance: the unit comparison is verbatim and case sensitive.** Where a vendor code carries several candidate LOINCs, the candidate whose catalog `representativeUnit` is exactly equal to the units the record reported is selected, and that equality is a plain string comparison. Nothing is normalized, case folded, scaled or converted on either side, so `MG/DL` does not match `mg/dL`, `mg/L` never matches `ug/dL`, and a unit differing only in whitespace is a different unit. UCUM defines a case-insensitive variant of every terminal symbol and requires a program declaring full conformance to compare unit expressions by their **semantics**; this package declares no such conformance, and every unit-selected answer carries a `unitComparison` saying so rather than letting a matched unit read as a claim that was never made. A unit that matches no candidate, matches more than one, or is missing or unreadable leaves the answer `ambiguous` with every candidate surfaced and no LOINC chosen. - **A `mapped` answer's `representativeUnit` is the catalog row's, not the record's.** It is provenance about the row the answer came from, and where a code carries a single candidate LOINC across several rows spelling the unit differently the answer takes the first row's attributes, so that field can name a unit the record did not report. Only `unitComparison` asserts the two were equal, and it is present only where a unit actually chose between candidates. - **No matching on the vendor specimen or result description.** Both are carried verbatim beside the representative unit and surfaced for a human to read, and neither is ever compared against anything: the guide states directly that this information is not intended to be parsed by software that automates the mapping, so choosing on it would be a string guess. - **No interpretation of `M` / `S` records.** Vendor-defined manufacturer / scientific records (QC, calibration, maintenance) are surfaced **verbatim** on `record.rawLine` and never parsed into clinical fields: a QC value must not read as a patient result. - **No within-message patient scoping.** A message carrying **several** `P` records is not resolved into per-patient groups: `messages(msg)[n].patient` is `undefined` there and `patients` carries all of them, and `patient()` refuses rather than answering with the first. Deciding which `P` a given `R` files against would mean asserting a record hierarchy this project cannot ground (the clauses that define it are not in the freely available text), and guessing it is the wrong-patient failure by another route. Messages carrying several patients are real, chiefly on the download direction, so treat this as a boundary to handle rather than a case that will not arise. - **No choice of character encoding.** A frame carries **bytes**, and nothing this library reads from an ASTM stream says which character encoding those bytes are in: that is out-of-band knowledge your instrument's interface document holds. So a record handed to `composeAstmFrames` as a `string` is read as one byte per character, and a character above `U+00FF` is **refused** (`ASTM_FRAME_UNENCODABLE_CHARACTER`) rather than encoded on a guess. To put content outside Latin-1 on the wire, encode it yourself with the code page your instrument uses and pass the resulting `Uint8Array`, which `composeAstmFrames` writes through untouched. The record layer is unaffected: `serializeAstmRecords` returns a `string` and what you encode it with is yours to decide. - **No way to put a frame-structure byte inside a frame.** `STX`, `ETB` and `ETX` are what the decoder reads as the shape of a frame, and framing has no escape sequence for them, so a record carrying one is refused (`ASTM_FRAME_RESERVED_BYTE`) rather than written as a frame that truncates at that byte. Passing the record as a `Uint8Array` does not route around it: the byte is unframable however it arrives. Which byte belongs in a clinical value is the sender's call, so the library refuses rather than substituting or deleting one. The **record** layer deliberately still carries such a byte in every modeled value, and round-trips it exactly: only framing reserves it. The one position that does not keep it is the surplus of a header's delimiter declaration, which is not a modeled value and from which every control character is dropped on emit. Measured, 31 of the 33 C0/`DEL` characters can reach that surplus and all 31 cost it; `CR` and `LF` end the record while it is being read, so no surplus ever holds them. The **whole** surplus is dropped rather than the offending character alone, because a subsequence of an opaque run is a different run, and none of it is reported, because emit returns a plain string. - **No clinical judgement.** The library reports the abnormal flag and result status faithfully; it does **not** decide whether a value is "critical" or act on a correction/cancel. - **No claim that a letter set is current.** The record grammar is frozen; the vocabularies whose letters travel in it are not. So every interpreted flag and status says what it was graded against, and one of the two answers is "nothing". See "Which vocabulary a letter was graded against" below. - **No proof that an arbitrary delimiter set round-trips.** Emit refuses a set that fails one of the three conditions readback requires, and refuses any record whose own type letter the set being written with would escape away (`ASTM_EMIT_TYPE_LETTER_COLLISION`), so an emitted stream re-reads as the same record **types**. The second refusal is a transcoding condition rather than a judgement on the set you passed, so it also fires with **no** delimiter argument: a record read under a vendor's own set can carry a type letter the canonical set escapes away, and `serializeFramedAstm` refuses it too. That is not a guarantee that every field lands where it did: an escape sequence whose body is an unrecognized character that is itself a delimiter in force is read as one opaque atom, so that delimiter never becomes a boundary and the fields after it shift, reported on the parse side as `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` (not tolerable) alongside the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. Its mirror is outside the guarantee too: sequences are matched leftmost, so one can end where another could have begun and the delimiter between them splits where the competing alignment would have held it, gaining a boundary rather than losing one and reported as `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` (also not tolerable). Where that gained boundary is a **field** boundary and the reading taken resumes on an escape character heading no sequence this reader can _interpret_ (none at all, or one whose body is not a recognized mnemonic and is therefore kept verbatim rather than read), every later field shifts and a result's units and **status** are read out of slots the competing alignment does not put them in, up to a status of `final` the sender never wrote there; that is `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` (not tolerable either). It stays silent in exactly one case, where that trailing escape character heads a sequence this reader RECOGNIZES, which is the escape mechanism working and the only tail on which a stream's escaping can be clean, **wherever the escape role is a character distinct from the three splitting roles**. Where a header names the escape character in a splitting role too, these codes can fire with neither escape report beside them, and `ASTM_RECORD_DELIMITER_ROLE_COLLISION` is what refuses the stream. **That silence is a trade and not a claim that nothing was lost there**: the gained boundary is exactly as real, and on that tail it is `warnings: []`, so `R|1|^^^687|28.6&F&|&F&U/L||||F` reads nine fields against the competing alignment's eight with a status of `final` and nothing reported at all, `28.6&S&\&S&U/L` reads a value of `28.6^`, and `&F&^&F&GLU^L^687` reads one component more. Read the raw line when an escape character sits next to a delimiter, whether or not anything fired. **The shift itself has one measured exception, on all three roles**: where the sequence past the boundary carries the splitting delimiter itself as its body, the reading taken holds that character inside an opaque atom while the competing alignment splits on it, so the two readings read the **same number** of segments in **different places** and nothing moves index. The three codes still fire there, which is over-reporting and never under-reporting, and that class costs no stream its disposition, because `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` has already refused it. What holds wherever any of the three fires is that the two readings disagree and that both consume every byte. Where that gained boundary is a **repeat** boundary nothing shifts, and the field can still be read short: its modeled value and components come from its first repeat alone, so where the gained boundary is the **first** one `28.6&S&\&U/L` reads a value of `28.6^` and a Universal Test ID of `&F&\&687` reads one component holding a decoded field separator, with the local code in no modeled slot. That is `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` (not tolerable either), on the same tail bound; at a later boundary it fires and nothing modeled moves, which is over-reporting and never under-reporting. A gained **component** boundary reaches a modeled slot differently again, moving it along the component list rather than dropping it, so `&F&^&GLU^L^687` reads `687` as a vendor local code under the alignment taken and as the **coding scheme** under the other, and `DOE&F&^&JANE^A` reads `A` as a middle name under one and as the **given** name under the other. That is `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS` (not tolerable either), on the same tail bound; there every gained boundary at or before the last modeled component index moves those slots and not only the first, while past that index (a name models three components, a test identity four) and inside a later repeat it fires and nothing named moves. **None of these reports survives emitting**: this package rewrites the preserved characters into recognized mnemonics, so a re-emitted stream carries the reading that was taken with nothing ambiguous left in it. The low-level `encodeComponent` and `serializeField` helpers take no record and so carry neither guarantee. If you emit against a set of your own choosing rather than the canonical one, verify the round-trip on your own traffic. - **No vendor-proprietary quirks absent from public specs.** The profile engine fully supports named vendor profiles, but a named per-vendor built-in ships **only** when a public, vendor-attributed quirk document grounds it. Inspection of the public reference corpus found the record layer spec-clean, so no named vendor profiles are asserted. This is a deliberate abstention, not an omission. - **No POCT1-A, no HL7 v2, no "extended" vendor dialects as first-class.** Those are separate standards. A vendor that emits HL7 v2 instead of ASTM uses `@cosyte/hl7`. ## Which vocabulary a letter was graded against The `R` record carries two single-letter vocabularies, the **abnormal flag** (field 7) and the **result status** (field 9). The record grammar around them is archived and frozen; the vocabularies themselves are maintained elsewhere, on other bodies' schedules. A parser that reports a letter as `"undefined"` without saying what it compared against therefore hides a staleness: you cannot tell a code **no published vocabulary defines** from a code **this library has not caught up to**. So every interpreted flag and status carries a `vocabulary` attribution, present whether or not the letter was recognized, and the two answers are deliberately different. - **Abnormal flags are attributed.** They are graded against the HL7 v3 ObservationInterpretation code system, identifier `http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation`, version `4.0.0`, which carries the concepts kept aligned with the HL7 v2 Table 0078 interpretation codes. Read `flag.vocabulary.system` and `flag.vocabulary.version` rather than copying the strings. **That version says what this library compared against; it is not a claim about which version the sender meant**, and it is not a conformance statement about your instrument. - **Result statuses are attributed to nothing, and say so.** `status.vocabulary.attributed` is always `false` and `status.vocabulary.reason` carries the fixed prose explaining why: the normative text that would bind that letter set is purchase-gated and has not been read here. A neighbouring HL7 table exists whose letters partly agree, and adopting it would be an assertion nothing in the public record supports, so this library declines to cite a source it cannot stand behind. **An unattributed set is not an absent attribution**: `attributed: false` is a positive statement, distinguishable from a field that is simply missing, and it never carries an identifier. The eighteen abnormal-flag letters recognized are `L`, `H`, `LL`, `HH`, `<`, `>`, `N`, `A`, `AA`, `U`, `D`, `B`, `W`, `S`, `R`, `I`, `HU` and `LU`; `ABNORMAL_FLAG_CODES` is the list itself. Other concepts in that code system are deliberately **not** adopted, including the deprecated `H>` and `L<` whose replacements `HU` and `LU` are. Recognition is **exact match** after the surrounding whitespace is trimmed: `hu` is not `HU`, and the case variant is reported unrecognized rather than widened in. Anything outside the list is surfaced verbatim, reported unrecognized, and **never coerced to `normal`**, exactly as an unrecognized status is never coerced to `final`. None of this reaches the wire: the attribution is interpretation output, and a parsed-then-serialized record emits the same bytes it always did. ## The standard, and its "archived" status The normative standards are **CLSI LIS01-A2** (the low-level transfer protocol, formerly ASTM E1381-02) and **CLSI LIS02-A2** (message content, formerly ASTM E1394-97). Both CLSI editions are administratively **ARCHIVED**, "no longer reviewed through the consensus process but technically valid and retained", and there is **no successor**. Archived is **not** the same as obsolete: these are the de-facto in-force specs that shipping 2026 analyzers (Roche cobas, Sysmex XN, Siemens ADVIA, Mindray, Snibe) still implement. `@cosyte/astm` targets the second editions. ## Licensing posture - **The library is MIT.** Zero runtime dependencies; Node stdlib only. - **We parse the wire format and ship our own code.** The CLSI standards are copyrighted and purchase-gated. We never copy CLSI's descriptive prose into code, JSDoc, or docs. Code **values** we encode (the abnormal-flag letters graded against HL7 v3 ObservationInterpretation, the result-status letters graded against nothing citable) are **facts**, not CLSI's copyrighted text. - **LOINC and SNOMED are not bundled.** LOINC is © Regenstrief (attribution, no alteration, `X`-prefix for local codes); SNOMED redistribution is IHTSDO-governed. Bundling either is a licensing decision we do not make for you: bring your own catalog. - **Differential-tested against a permissively-licensed reference.** Conformance is checked firsthand against **python-astm** (BSD-3-Clause) on the checksum, the field/component split, and a cross-implementation frame decode, capturing its outputs once and vendoring none of its code. Where we are deliberately stricter (escape decoding, checksum validation, `Q` support), the difference is asserted on purpose. ## HIPAA posture `@cosyte/astm` is **HIPAA-capable, not HIPAA-compliant**: compliance is a property of a system, not a library. The `P` record concentrates PHI (name, mother's maiden name, birthdate, sex, IDs) and `C` free text can carry it. Fixtures are **synthetic-only**; warnings and errors carry **positional context only, never a value**; and a format-specific PHI scanner gates every change. Never log a raw payload. --- # @cosyte/transform > HL7 v2 to FHIR transformation layer built on the Cosyte parser models. These are the 7 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/transform/intro Markdown: https://docs.cosyte.com/transform/intro.md # @cosyte/transform Turn a parsed HL7 v2 message into **valid FHIR R4**, without reading the 900-page v2 spec and without hand-writing a ConceptMap. `@cosyte/transform` is the healthcare **transformation** layer of the Cosyte suite. Unlike the parsers, it is a **consumer**: it takes already-parsed [`@cosyte/hl7`](https://github.com/cosyte/hl7) composites and produces validated [`@cosyte/fhir`](https://github.com/cosyte/fhir) model nodes, grounded on the official **HL7 Version 2 to FHIR** Implementation Guide (`hl7.fhir.uv.v2mappings`). "Validated" there means a small internal required-element check before a resource ships, which is a floor and not a conformance claim. How far the output actually conforms is measured separately, against pinned FHIR R4 4.0.1 definitions and a pinned profile package, over the guide's own published test messages, and the outcome is published in the repository as `documentation/conformance/report.md`. Read that before relying on the word "validated": today it says none of the seven published messages produces a Bundle that is clean against R4 plus those profiles, and it lists every finding. > **Status:** `0.1`, published to npm. While the package is below 1.0, a breaking change ships in a > minor version and is called out in the changelog. See [Installation](https://docs.cosyte.com/transform/installation.md). This release > ships the > six safety-critical datatype converters and the value-free diagnostic channel, and > message-level assembly via `toFhir(msg)` for HL7 v2 **ADT → Patient + Encounter** > (+ **AL1 → AllergyIntolerance**, **DG1 → Condition**, **PR1 → Procedure** and > **IN1 → Coverage**), **ORU^R01 → DiagnosticReport + Observation**, the order-entry > graph **ORM_O01 / OML_O21 → ServiceRequest** and **RXO → MedicationRequest** (with an order's > **TQ1 → the schedule it carries**, `dosageInstruction.timing` or `occurrenceTiming`), and the thin > IG singles **VXU_V04 → Immunization**, **SIU_S12 → Appointment**, and **MDM_T02 → > DocumentReference**, plus **terminology value translation** of coded fields: route/site, > appointment type, order priority, substitution, and the AL1 allergy tables, translated through > their IG `mappedVia` ConceptMaps. A **narrow reverse path** > also ships, FHIR → v2: `toV2Patient(patient, trigger)` and `toV2Observation(observation, trigger)` > emit a complete v2 message carrying a `PID` or an `OBX`, lossy by design and never a round-trip. > Deeper terminology, profiles, and any wider FHIR → v2 conversion are not implemented. ## The fail-safe rule Every conversion is grounded on the IG: an unmapped code, an ambiguous datatype, a v2 timestamp with no timezone, or an unresolvable assigning authority becomes a **typed, value-free diagnostic**, never a silent default, never a fabricated value, never a guessed UTC offset. ## Install ```bash npm install @cosyte/transform @cosyte/hl7 @cosyte/fhir ``` `@cosyte/hl7` and `@cosyte/fhir` are **peer dependencies**: the transform maps between the models they own, so you install them alongside it. All three are on npm, and the command above installs them together. [Installation](https://docs.cosyte.com/transform/installation.md) has the detail. ## Next - [Quickstart](https://docs.cosyte.com/transform/quickstart.md): convert your first datatypes. - [Core concepts](https://docs.cosyte.com/transform/concepts-archetype.md): the fail-safe rule and the diagnostic channel. --- URL: https://docs.cosyte.com/transform/installation Markdown: https://docs.cosyte.com/transform/installation.md # Installation `@cosyte/transform` ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration. Its own **third-party runtime dependencies are zero**: it depends only on two Cosyte siblings. > **Status:** published on npm, and so are both of its peer dependencies, so the command below > installs it. For the current published version, ask the registry > (`npm view @cosyte/transform version`). See [Peer dependencies](#peer-dependencies) below. ## Prerequisites - **Node.js >= 22** (the whole `@cosyte/*` suite targets ES2023 / Node 22+). - A package manager: `pnpm`, `npm`, or `yarn`. ## Install ```bash npm install @cosyte/transform @cosyte/hl7 @cosyte/fhir ``` ## Peer dependencies `@cosyte/hl7` and `@cosyte/fhir` are declared as **peer dependencies**: `transform` maps between the models they own, so a consumer supplies them. Both are on the registry, and the command above installs them beside this package. This repo's own tests install both peers from the registry, exactly as a consumer does. ## Smoke test Once it is installed, confirm the package resolves and reports the release you actually have: ```ts import { VERSION } from "@cosyte/transform"; console.log(VERSION); ``` `VERSION` is the package's own `version`, kept in lockstep with it by the release tooling, so from the next release onward what it prints is the release you are running. > **Every release up to and including `0.0.4` prints `0.0.0`.** They all went out with the constant > stuck at its scaffold value, and a published version is never re-published, so those copies stay > wrong on the registry. So `0.0.0` does not identify _which_ of them you have: it only tells you the > constant is lying, not your install. Read the manifest instead, which was always correct: > `node -p "require('@cosyte/transform/package.json').version"`. Head to the [Quickstart](https://docs.cosyte.com/transform/quickstart.md). --- URL: https://docs.cosyte.com/transform/quickstart Markdown: https://docs.cosyte.com/transform/quickstart.md # Quickstart The **datatype converters** each take a parsed `@cosyte/hl7` composite and return a `{ value, issues }` pair: the FHIR datatype node it could faithfully produce, plus the value-free diagnostics it raised. The **message-level** entry `toFhir(msg)` assembles a whole HL7 v2 message into a FHIR `Bundle` over those same converters: an **ADT** admit becomes a **Patient + Encounter** graph, and an **ORU^R01** lab result becomes a **DiagnosticReport + Observation** graph. ## Convert a name ```ts runnable import { toFhirHumanName } from "@cosyte/transform"; const { value, issues } = toFhirHumanName({ familyName: "Public", givenName: "Jane", nameTypeCode: "L", // HL7 Table 0200 "Legal name" → FHIR name-use "official" }); // A clean, fully-mapped name raises no diagnostics. issues; // => [] ``` `value` is a FHIR `HumanName` node you can drop straight into a resource; `issues` is empty here because every part mapped cleanly. ## The fail-safe rule in action When a mapping is ambiguous, the converter **refuses to guess** and tells you why. A patient identifier whose assigning authority can't be resolved is emitted with its value and **no system**, never a synthesized one that could merge two patients: ```ts runnable import { toFhirIdentifier, createNamingSystem, ISSUE_CODES } from "@cosyte/transform"; const { issues } = toFhirIdentifier( { idNumber: "12345", assigningAuthority: { namespaceId: "HOSPMRN" } }, { namingSystem: createNamingSystem() }, // no registry entry for a bare "HOSPMRN" ); issues[0].code; // => "TRANSFORM_IDENTIFIER_SYSTEM_UNRESOLVED" ``` Register the authority explicitly (`createNamingSystem({ authorities: { HOSPMRN: "urn:oid:…" } })`) and the same call resolves `Identifier.system` with no diagnostic. ## Assemble a message Parse an `ADT^A01` with `@cosyte/hl7`, then hand it to `toFhir`: you get back a FHIR R4 **message `Bundle`** (a `MessageHeader`, then the `Patient` and `Encounter` it describes) plus the value-free issues. Every segment→resource map is grounded on the published HL7 v2-to-FHIR IG. ```ts import { parseHL7 } from "@cosyte/hl7"; import { toFhir, createNamingSystem } from "@cosyte/transform"; const msg = parseHL7(raw); // an ADT^A01 const { bundle, issues } = toFhir(msg, { namingSystem: createNamingSystem({ authorities: { HOSP: "urn:oid:1.2.840.114350" } }), }); // bundle.type === "message"; entry[0] is a MessageHeader, then Patient + Encounter. // Encounter.subject and every reference resolve to a urn:uuid: fullUrl inside the bundle. ``` The same fail-safe rule holds at the message level: an unmapped patient class, a naked timestamp, or an unresolvable identifier authority becomes a typed issue, never a fabricated FHIR value. A message whose trigger the IG has **no message map** for is still assembled from the reusable segment maps and flagged `TRANSFORM_SEGMENT_ASSEMBLED`, never invented. The Table-0001/0004 maps the assembly applies are exported for inspection: ```ts runnable import { ADMINISTRATIVE_GENDER_MAP, ENCOUNTER_CLASS_V3_MAP } from "@cosyte/transform"; ADMINISTRATIVE_GENDER_MAP["F"]; // => "female" ENCOUNTER_CLASS_V3_MAP["I"].code; // => "IMP" ``` ## Lab results (ORU^R01) An `ORU^R01` assembles into a **`DiagnosticReport`** per OBR with its **`Observation`** results, plus a **`Specimen`** per `SPM` that the report references, and each result's `NTE` comments as `Observation.note`. `OBX-2` discriminates the value type: `NM` → `valueQuantity`, `CWE` → `valueCodeableConcept`, `SN` → a structured range/ratio/comparator quantity, `DR` → `valuePeriod`, `NR` → `valueRange`, `TM` → `valueTime`, `NA` → `valueSampledData`, a `Base64` `ED` → the guide's named `valueAttachment` extension, `ST`/`TX` → `valueString`, so a result is **never** forced into a `Quantity` it isn't. The result-status maps are the clinical-safety heart of the graph and are exported for inspection: a **corrected** or **cancelled** result is modelled exactly and **never emitted as `final`**, and a status the IG map has no target for leaves `status` absent (the resource is then withheld) rather than being guessed. ```ts runnable import { OBSERVATION_STATUS_MAP, DIAGNOSTIC_REPORT_STATUS_MAP } from "@cosyte/transform"; OBSERVATION_STATUS_MAP["C"]; // => "corrected" OBSERVATION_STATUS_MAP["X"]; // => "cancelled" DIAGNOSTIC_REPORT_STATUS_MAP["F"]; // => "final" ``` ## Next - [Core concepts](https://docs.cosyte.com/transform/concepts-archetype.md): the fail-safe rule, the diagnostic channel, the six converters. - **API reference**: every export, generated from source. --- URL: https://docs.cosyte.com/transform/concepts-archetype Markdown: https://docs.cosyte.com/transform/concepts-archetype.md # Core concepts `@cosyte/transform` borrows the Cosyte parser suite's **disciplines**: fail-safe on ambiguity, stable typed diagnostics, immutable output, without being a byte parser. There is no wire format here; both endpoints are typed models. What replaces Postel's Law is the **fail-safe rule**. ## The fail-safe rule A transformed message drives treatment, filing, and identity matching, so the converters refuse to guess. On any of these ambiguities: - a v2 timestamp with a time-of-day but **no timezone** (FHIR forbids time without a zone), - an assigning authority that can't be **resolved to a system URI**, - a code with an **unrecognized or absent coding system**, - a unit that isn't valid **UCUM**, - a source component with **no FHIR target**, the converter produces the value it *can* faithfully emit (often reduced in precision) and raises a typed diagnostic. It never silently defaults, never pads a truncated date, never guesses a timezone, never synthesizes an identifier system, and never coerces an unmapped code to a plausible neighbor. ## The diagnostic channel is value-free Every diagnostic is a `TransformIssue`: a stable `code`, a severity, the **v2 location** (a segment / field / component index, never a value), and the **FHIR path** it concerns. Messages are static and per-code, so **no patient data can reach a log line**: the same value-free `OperationOutcome` discipline `@cosyte/fhir` proves. Render a list of issues as a real FHIR `OperationOutcome` with `toOperationOutcome(issues)`. Codes are `key === value` entries in `ISSUE_CODES` (non-fatal) and `FATAL_CODES`. Consumers branch on them, so a code's name is part of the public contract: renaming or removing one is a **breaking change**, and new codes are additions only. ## What the bundle did not represent An issues list that says nothing about a segment used to mean one of two very different things: the segment reached the bundle, or nobody looked. Two codes now tell them apart, and both are raised once per segment occurrence that contributed nothing to a resource the returned bundle contains: | Code | What it tells you | What to do with it | |---|---|---| | `TRANSFORM_SEGMENT_NOT_EMITTED` | The IG publishes a segment map for this name, and nothing in the bundle carries what the segment held. | A gap in this library. Ask for it. | | `TRANSFORM_SEGMENT_NO_IG_MAP` | The IG publishes no segment map for this name, **or** the name could not be classified at all. | A gap in the standard, or a damaged line. Not fixable here. | Both carry severity `information` and the FHIR issue type `informational`: they are an observation about the bundle you were handed, never a reason to fail a transform. Neither changes a resource, a value, or any other issue, and both are appended after every issue the assembly itself raised. An occurrence counts as having reached a resource only if the assembly took a value or the identity of it **into a resource that is in the bundle**. Being read and refused is not reaching: an `RXE` this library counts rather than assembles is reported, and so is a resource the emit gate withheld. A segment whose values were all dropped by a datatype or terminology step is **not** reported here, because the resource built from it did reach the bundle. ### Reading the location The location is the only message-derived thing either code carries, and it comes in exactly two shapes: - `NAME[k]` when the segment name passed the HL7 v2 segment-identifier shape (three characters, a leading letter). `k` is **1-based among the occurrences of that name**: a message with three `DG1` gives you `DG1[1]`, `DG1[2]`, `DG1[3]`, and the index is present even for a first occurrence. - `[#n]` when it did not. `n` is the 1-based position of the segment among all the segments of the message, counting the empty positions a blank line produces so the number matches the line the sender wrote. **No part of the name appears**, because a name this library cannot recognize is a name it will not reproduce: an unescaped line break inside a narrative field can forge a "segment" whose name is clinical text. **Two index conventions now coexist on one issues list, and this is the one place that says so.** The locations these two codes emit are 1-based, as above. The per-occurrence locations the library already emitted elsewhere (`NK1[0]`, `OBX[0]`, on the emit-gate and dropped-element codes) are **0-based**, and they have not changed. If you correlate two issues about the same occurrence, the same segment is `NK1[0]` on the older code and `NK1[1]` on these. **A damaged segment identifier reads as a standard gap, and the code therefore means two things.** A line whose identifier arrived as `AL11` carries a name that could not be classified, so it takes `TRANSFORM_SEGMENT_NO_IG_MAP` even though what the sender wrote was an IG-mapped `AL1`. That is forced, not chosen: the name is never re-derived from the line, so a name that cannot be classified stays unclassified, and a third code would only rename the same ignorance. Read the code as "the IG publishes no map for this, **or** this name could not be classified", never as a claim about the IG. The mapped-name set behind the split is transcribed by hand from the HL7 Version 2 to FHIR Implementation Guide's Segment Maps index at version **1.0.0** (published **2025-10-07**), retrieved **2026-08-22**, and is exported as `IG_MAPPED_SEGMENT_NAMES` beside `IG_SEGMENT_MAPS_VERSION`, `IG_SEGMENT_MAPS_PUBLISHED`, `IG_SEGMENT_MAPS_RETRIEVED` and `IG_SEGMENT_MAPS_SOURCE`, so a later release of the guide can be told apart from a defect here. Two limits worth knowing. A flagged segment is still **not** transformed: you learn what is missing, not what it said. And issue volume is unbounded on purpose, because a summarized answer is one you cannot act on segment by segment: a message with five thousand segments and nothing emitted returns five thousand of these. ## Grounded on the IG, never invented Every mapping is grounded firsthand on the published **HL7 Version 2 to FHIR** Implementation Guide's datatype and table ConceptMaps (LOINC/SNOMED URIs from the FHIR core terminology systems). Where the IG has no target for a code (an unmapped Table 0200 name-type, an unmapped Table 0190 address-type), the value is surfaced, **never guessed**. ## The six datatype converters | Converter | v2 → FHIR | Key fail-safe | |---|---|---| | `toFhirDateTime` | DTM/TS → `dateTime` | naked timestamp → date precision, never a guessed zone | | `toFhirIdentifier` | CX → `Identifier` | HD.1-only → value with no system, never synthesized | | `toFhirCodeableConcept` | CWE/CE → `CodeableConcept` | unmapped code → preserved, never coerced | | `toFhirHumanName` | XPN → `HumanName` | unmapped name-use → absent, never guessed | | `toFhirAddress` | XAD → `Address` | unmapped address-type → absent, never guessed | | `toFhirQuantity` | NM + units → `Quantity` | non-UCUM unit → verbatim, magnitude never converted | Each returns `{ value, issues }`; each output is designed to pass `@cosyte/fhir`'s `validateResource` when embedded in a resource: the transform's emit gate. ## Immutability Produced nodes are `@cosyte/fhir` immutable model nodes, and the input v2 composites are never mutated. A converted value is safe to share across a pipeline without defensive copying. --- URL: https://docs.cosyte.com/transform/guides-overview Markdown: https://docs.cosyte.com/transform/guides-overview.md # Guides Task-oriented recipes for `@cosyte/transform`. Each is a short, copy-pasteable answer to one real transformation question. A guide is written only once the behavior it documents is shipped and its runnable example passes the doc/code-agreement check. ## Resolve an assigning authority A patient identifier's assigning authority (HD) is resolved through a **NamingSystem registry**. The registry auto-derives a system URI only for the two unambiguous cases: a valid OID with `universalIdType: "ISO"` (`urn:oid:…`) and a valid UUID (`urn:uuid:…`), and otherwise consults the entries you seed. A bare namespace mnemonic (HD.1) is **never** turned into a URI on its own. ```ts runnable import { createNamingSystem } from "@cosyte/transform"; const registry = createNamingSystem({ authorities: { HOSPMRN: "urn:oid:1.2.840.114350.1.13.1" }, }); registry.resolveAssigningAuthority({ namespaceId: "HOSPMRN" }); // => "urn:oid:1.2.840.114350.1.13.1" ``` ## Supply a sender's timezone for naked timestamps By default a v2 timestamp with a time-of-day but no offset is reduced to date precision (FHIR forbids time without a zone). If you *know* the sending system's offset, assert it, and the value is emitted with that offset and flagged as sender-asserted, never inferred: ```ts import { toFhirDateTime } from "@cosyte/transform"; import { parseDtm } from "@cosyte/hl7"; toFhirDateTime(parseDtm("20260721143000"), { assumeTimezoneOffsetMinutes: -300 }); // => { value: "2026-07-21T14:30:00-05:00", issues: [ TRANSFORM_TIMESTAMP_NO_TIMEZONE ] } ``` ## Emit a v2 message from a FHIR resource The reverse path is narrow on purpose: a `Patient` becomes an `ADT`-shaped message carrying a `PID`, an `Observation` becomes an `ORU`-shaped message carrying an `OBX`. **You supply the trigger.** No FHIR resource carries an HL7 v2 message trigger, so there is nothing to infer one from, and an absent one returns no message plus a `TRANSFORM_MISSING_TRIGGER` diagnostic. ```ts runnable import { toV2Patient, ISSUE_CODES } from "@cosyte/transform"; import { parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","gender":"female"}'); const emitted = toV2Patient(resource, "A28"); emitted.value?.toString().includes("ADT^A28"); // => true const refused = toV2Patient(resource, ""); refused.value; // => undefined refused.issues[0]?.code === ISSUE_CODES.TRANSFORM_MISSING_TRIGGER; // => true ``` The direction is **lossy by design and not a round-trip**. The mapping guide runs v2 to FHIR, and several of its rows are many-to-one, so their inverse is ambiguous: `gender` `other` could have come from three different v2 codes, so it is refused rather than resolved to one of them. ```ts runnable import { toV2Patient, ISSUE_CODES } from "@cosyte/transform"; import { parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","gender":"other"}'); const { issues } = toV2Patient(resource, "A28"); issues[0]?.code === ISSUE_CODES.TRANSFORM_CODE_NOT_INVERTIBLE; // => true issues[0]?.v2Location; // => "PID.8" ``` The same rule covers what v2 requires and your resource does not carry. A `PID` needs PID-3 (Patient Identifier List) and PID-5 (Patient Name); an `OBX` needs OBX-11 (Observation Result Status). None of them has a safe default, so the field is left absent rather than padded with an invented value, and its absence is reported: check the issues before you send the message. ```ts runnable import { toV2Patient, ISSUE_CODES } from "@cosyte/transform"; import { parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient","identifier":[{"value":"MRN1"}]}'); const { value, issues } = toV2Patient(resource, "A28"); // The message is emitted, with the required name field absent rather than fabricated. value?.toString().includes("PID|||MRN1"); // => true issues[0]?.code === ISSUE_CODES.TRANSFORM_V2_REQUIRED_FIELD_ABSENT; // => true issues[0]?.v2Location; // => "PID.5" ``` If nothing in the resource grounds a single field of the target segment, there is no message at all, and that is reported too rather than returned as an empty success. ```ts runnable import { toV2Patient, ISSUE_CODES } from "@cosyte/transform"; import { parseResource } from "@cosyte/fhir"; const { resource } = parseResource('{"resourceType":"Patient"}'); const { value, issues } = toV2Patient(resource, "A28"); value; // => undefined issues[0]?.code === ISSUE_CODES.TRANSFORM_NO_V2_MESSAGE_EMITTED; // => true ``` ## Planned guides Not yet written: assembling a full `Patient`/`Encounter`/`Observation` graph from a message, applying a BYO `ConceptMap` for local codes, and targeting US Core profiles. --- URL: https://docs.cosyte.com/transform/api Markdown: https://docs.cosyte.com/transform/api.md # @cosyte/transform Public entry point for `@cosyte/transform`: the HL7 v2 → FHIR R4 transformation library. `@cosyte/transform` is **not** a parser; it is a **consumer** one tier above the parser suite. It takes already-parsed `@cosyte/hl7` composites and produces validated `@cosyte/fhir` model nodes, grounded on the official **HL7 Version 2 to FHIR** Implementation Guide (`hl7.fhir.uv.v2mappings`, STU Edition 1). Its whole promise is narrow and honest: **IG-grounded, fail-safe transformation with typed, value-free diagnostics, never a confident wrong FHIR value.** This module ships the six safety-critical datatype converters, the immutable `OperationOutcome`-shaped diagnostic channel and the NamingSystem resolver they consult, the message-level assembly (HL7 v2 **ADT → FHIR Patient + Encounter**, the **ORU^R01 → DiagnosticReport + Observation** results graph, the order-entry graph (**ORM_O01 / OML_O21 → ServiceRequest** and **RXO → MedicationRequest**), and the thin IG singles (**VXU_V04 → Immunization**, **SIU_S12 → Appointment**, **MDM_T02 → DocumentReference**), all through `toFhir(msg)`), and the **terminology value-translation** layer: a `$translate`-shaped [toFhirCodeableConceptVia](#tofhircodeableconceptvia) engine applying the license-clean IG value ConceptMaps to the previously structural-only coded fields (route/site, appointment type, order priority, substitution). It also ships a **narrow reverse path**, FHIR → v2: [toV2Patient](#tov2patient) and [toV2Observation](#tov2observation) emit a complete v2 message carrying a `PID` or an `OBX` built from the subset of the IG segment maps whose inverse is defensible. Each requires the caller to supply the v2 trigger, because no FHIR resource carries one. It is **lossy by design and not a round-trip**: a value the inverse of the IG map cannot ground is flagged and left absent, never guessed. Terminology **depth beyond these maps**, profiles, and any wider FHIR → v2 conversion are not implemented. ## Classes ### SegmentReachLedger Tracks, across one message-level assembly, which segment occurrences CONTRIBUTED to a resource the returned bundle contains, and turns the rest into one value-free issue each. The assembly marks an occurrence only where a resource has just cleared the emit gate and joined the bundle, so "reached" means contributed-and-present by construction: an occurrence that was read and refused, or that built a resource the gate withheld, is never marked and is therefore reported. #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; import { SegmentReachLedger } from "@cosyte/transform"; // const ledger = new SegmentReachLedger(parseHL7(raw)); // ledger.markFirstOfType("MSH"); // ledger.issues(); // one issue per occurrence that reached nothing ``` #### Constructors ##### Constructor > **new SegmentReachLedger**(`msg`): [`SegmentReachLedger`](#segmentreachledger) ###### Parameters ###### msg `Hl7Message` The parsed message whose occurrences are being tracked. ###### Returns [`SegmentReachLedger`](#segmentreachledger) #### Methods ##### issues() > **issues**(): readonly [`TransformIssue`](#transformissue)[] One value-free issue per reportable occurrence, in message order: the library-gap code when the IG publishes a segment map for the name, the standard-gap code otherwise, and the standard-gap code for every name that could not be classified at all. ###### Returns readonly [`TransformIssue`](#transformissue)[] ##### mark() > **mark**(...`segments`): `void` Record that each given segment contributed to a resource now present in the bundle. `undefined` entries are ignored, so a call site can pass an optional part of a group without branching. ###### Parameters ###### segments ...readonly `any`[] ###### Returns `void` ##### markFirstOfType() > **markFirstOfType**(`type`): `void` Record that the FIRST occurrence of `type` contributed. The parser's derived views (`patient`, `visit`, `meta`) are built from the first segment of their type, so this is how a resource built from a view marks the occurrence it came from. ###### Parameters ###### type `string` ###### Returns `void` ##### markNthOfType() > **markNthOfType**(`type`, `index`): `void` Record that the `index`-th (0-based) occurrence of `type` contributed: the counterpart of [SegmentReachLedger.markFirstOfType](#markfirstoftype) for the repeating views (`nextOfKin()`), whose entries are in document order. ###### Parameters ###### type `string` ###### index `number` ###### Returns `void` ##### reportable() > **reportable**(): readonly [`SegmentOccurrence`](#segmentoccurrence)[] The occurrences that contributed to no resource present in the bundle, in message order. ###### Returns readonly [`SegmentOccurrence`](#segmentoccurrence)[] ## Interfaces ### AllergyBuildOptions How much of the IG's AL1 mapping to carry: the alternate-codes extension is the one variable. #### Properties ##### carryAlternateCodes? > `readonly` `optional` **carryAlternateCodes?**: `boolean` Whether to carry the IG's `alternate-codes` extension on `category` / `criticality`. Defaults to `true`; the emit path builds a second draft with it off only when the first draft fails the conservative-emit gate. *** ### CodedTarget A translated FHIR target coding: the target CodeSystem URI + code, and the IG's target display. #### Properties ##### code > `readonly` **code**: `string` The FHIR target code. ##### display? > `readonly` `optional` **display?**: `string` The IG map's target display, when it carries one (absent for the identity table maps). ##### system > `readonly` **system**: `string` The FHIR target CodeSystem canonical URI. *** ### CodedValueMap A license-clean v2-table → FHIR value ConceptMap: it translates a **source table code** to its [CodedTarget](#codedtarget), or `undefined` when the code is not in the map's *mapped* group (the IG's `(unmapped)` group). Never fabricates a target. #### Properties ##### name > `readonly` **name**: `string` The IG ConceptMap id this map transcribes (its citation). ##### sourceMnemonics > `readonly` **sourceMnemonics**: `ReadonlySet`\<`string`\> The CWE.3 (`nameOfCodingSystem`) mnemonics that denote this map's **bound source table**: the HL7 forms (`"HL70162"`, `"0162"`, …). Translation applies **only** when the field's primary coding is from the bound table: CWE.3 absent/empty (a positionally-bound bare code) **or** CWE.3 ∈ this set. A CWE that declares a **different** coding system (a local `99…`, or SNOMED) is not a source-table code, so the map is not applied to it: the raw coding is carried structurally + its system flagged, never asserted as the standard concept. ##### sourceSystem > `readonly` **sourceSystem**: `string` The source table's canonical CodeSystem URI (what the raw v2 code is a member of when mapped). ##### translate > `readonly` **translate**: (`code`) => [`CodedTarget`](#codedtarget) \| `undefined` Translate a source table code to its FHIR target, or `undefined` when the IG map has no target. ###### Parameters ###### code `string` ###### Returns [`CodedTarget`](#codedtarget) \| `undefined` *** ### ConvertResult The result of one datatype conversion: the produced FHIR value (or `undefined` when the input was empty or un-emittable) plus the diagnostics raised. #### Example ```ts import { toFhirHumanName } from "@cosyte/transform"; const { value, issues } = toFhirHumanName({ familyName: "Public" }); // value is a FHIR HumanName node; issues is [] here void value; void issues; ``` #### Type Parameters ##### T `T` The produced FHIR value's type (a `FhirComplex` datatype node, or a lexical string for `dateTime`). #### Properties ##### issues > `readonly` **issues**: readonly [`TransformIssue`](#transformissue)[] The value-free diagnostics raised during the conversion, in emission order. ##### value > `readonly` **value**: `T` \| `undefined` The produced FHIR value, or `undefined` when nothing could be safely emitted. *** ### EntityIdentifier One EI field read as a FHIR `Identifier`: the value, and whether an authority went unused. #### Properties ##### authorityValued > `readonly` **authorityValued**: `boolean` Whether EI.2, EI.3 or EI.4 carried an assigning authority this library did not resolve. ##### identifier > `readonly` **identifier**: `any` The `Identifier` node built from EI.1, or `undefined` when EI.1 carries nothing. *** ### NamingSystemRegistry The resolver consulted by the identity- and code-conversion paths. #### Properties ##### resolveAssigningAuthority > `readonly` **resolveAssigningAuthority**: (`hd`) => `string` \| `undefined` Resolve an assigning authority (HD) to a FHIR `Identifier.system` URI, or `undefined` when it cannot be resolved safely. **Never** synthesizes a URI from HD.1 alone. ###### Parameters ###### hd `HD` ###### Returns `string` \| `undefined` ##### resolveCodeSystem > `readonly` **resolveCodeSystem**: (`mnemonic`) => `string` \| `undefined` Resolve a v2 coding-system mnemonic to a canonical URI, or `undefined` when unrecognized. ###### Parameters ###### mnemonic `string` ###### Returns `string` \| `undefined` *** ### NamingSystemSeed A seed for [createNamingSystem](#createnamingsystem): extra coding systems and explicit assigning authorities. #### Properties ##### authorities? > `readonly` `optional` **authorities?**: `Readonly`\<`Record`\<`string`, `string`\>\> Explicit assigning-authority entries, keyed by **HD.1 namespace mnemonic** and/or **HD.2 universal ID (OID)** → the `Identifier.system` URI. This is the safe, caller-vetted path the IG calls for; the registry never derives a system from a mnemonic on its own. ##### codeSystems? > `readonly` `optional` **codeSystems?**: `Readonly`\<`Record`\<`string`, `string`\>\> Extra v2 coding-system mnemonic → canonical URI entries (merged over the defaults). *** ### ReverseOptions Caller context for a reverse (FHIR to v2) conversion. Every entry is **caller-vetted**: nothing here is derived from resource content, and omitting all of it is safe (the conversion then flags what it cannot resolve rather than guessing it). #### Example ```ts import { toV2Patient } from "@cosyte/transform"; const options = { assigningAuthorities: { "urn:oid:1.2.840.114350": "HOSP" }, envelope: { sendingApp: "EHR", sendingFacility: "MAIN" }, }; void options; void toV2Patient; ``` #### Properties ##### assigningAuthorities? > `readonly` `optional` **assigningAuthorities?**: `Readonly`\<`Record`\<`string`, `string`\>\> `Identifier.system` URI to the v2 assigning-authority namespace (HD.1) that stands for it. There is no derivation from a URI to a namespace, so an identifier whose system is absent here is emitted with its value and **no** assigning authority, flagged rather than invented. ##### codeSystems? > `readonly` `optional` **codeSystems?**: `Readonly`\<`Record`\<`string`, `string`\>\> Extra v2 coding-system mnemonic to canonical URI entries, merged over [DEFAULT\_V2\_CODE\_SYSTEMS](#default_v2_code_systems) and then inverted. Same shape and direction as the forward registry's seed, so one declaration serves both directions. ##### envelope? > `readonly` `optional` **envelope?**: `Omit`\<`BuildMessageInit`, `"type"`\> MSH envelope fields for the emitted message (sending/receiving application and facility, control id, timestamp, version, processing id). The message type is never taken from here: it is fixed by the shape plus the caller's trigger argument. *** ### SegmentOccurrence One segment occurrence of a parsed message: a segment as it appears in the message, in message order, with everything needed to locate it in a value-free way and nothing else. #### Properties ##### name > `readonly` **name**: `string` \| `undefined` The renderable segment name, present only when it passed the shape test. ##### nameIndex > `readonly` **nameIndex**: `number` \| `undefined` 1-based position among the occurrences of [SegmentOccurrence.name](#name-1), when there is one. ##### ordinal > `readonly` **ordinal**: `number` 1-based position among **all** parsed positions, empty ones included, so it matches the wire. ##### segment > `readonly` **segment**: `Segment` The parsed segment this occurrence wraps. *** ### TransformContext The context a converter consults. Carries the [NamingSystemRegistry](#namingsystemregistry) used to resolve an assigning authority (HD) to an `Identifier.system` and a v2 coding-system mnemonic to a canonical URI, plus the [TransformOptions](#transformoptions). #### Properties ##### namingSystem? > `readonly` `optional` **namingSystem?**: [`NamingSystemRegistry`](#namingsystemregistry) The registry used to resolve HD → system and v2 mnemonic → canonical URI. ##### options? > `readonly` `optional` **options?**: [`TransformOptions`](#transformoptions) Conversion policy. *** ### TransformIssue A single value-free diagnostic. #### Example ```ts import { ISSUE_CODES } from "@cosyte/transform"; // issue.code === ISSUE_CODES.TRANSFORM_TIMESTAMP_NO_TIMEZONE // issue.v2Location === "TS.1"; issue.fhirPath === "dateTime" ``` #### Properties ##### code > `readonly` **code**: [`IssueCode`](#issuecode) The stable code. One of [ISSUE\_CODES](#issue_codes). ##### fhirPath? > `readonly` `optional` **fhirPath?**: `string` The FHIR path this issue concerns (e.g. `Identifier.system`), when applicable. ##### message > `readonly` **message**: `string` A static, value-free description drawn from [ISSUE\_REGISTRY](#issue_registry). ##### severity > `readonly` **severity**: [`TransformSeverity`](#transformseverity) The `OperationOutcome`-style severity, fixed per code. ##### v2Location > `readonly` **v2Location**: `string` The v2 source location: a segment / field / component **index**, never a value. *** ### TransformOptions Caller/profile policy for a conversion. #### Remarks `assumeTimezoneOffsetMinutes` is the **only** way a naked v2 timestamp acquires a time-of-day in FHIR: the caller asserts the sender's offset (in minutes east of UTC, e.g. `-300` for US Eastern standard time). It is a *sender-asserted* value, flagged as such; absent it, a naked timestamp is reduced to date precision, **never** assumed to be UTC. #### Properties ##### assumeTimezoneOffsetMinutes? > `readonly` `optional` **assumeTimezoneOffsetMinutes?**: `number` The sender's UTC offset in minutes, asserted by the caller to resolve naked timestamps. ##### generateId? > `readonly` `optional` **generateId?**: () => `string` Allocator for the `urn:uuid:` `Bundle.entry.fullUrl` / reference identities the assembler mints. Defaults to `crypto.randomUUID`. Inject a deterministic generator to make bundle output reproducible (e.g. for golden fixtures). It must return a fresh, unique value per call: the assembler relies on uniqueness for intra-bundle reference integrity. ###### Returns `string` ##### namingSystem? > `readonly` `optional` **namingSystem?**: [`NamingSystemRegistry`](#namingsystemregistry) The [NamingSystemRegistry](#namingsystemregistry) the message-level transform threads into every datatype conversion (HD → `Identifier.system`, v2 mnemonic → canonical URI). When omitted, a default registry (`createNamingSystem()`) is used: it resolves only the FHIR-core-fixed systems and the two unambiguous HD auto-derivations, so an un-seeded assigning authority surfaces a typed issue rather than a guessed system. *** ### TransformResult The immutable result of a message-level transform: the FHIR `Bundle` model and the value-free diagnostics raised assembling it. #### Properties ##### bundle > `readonly` **bundle**: `FhirComplex` The FHIR R4 `Bundle` resource node (a message Bundle: MessageHeader first, then focal resources). ##### issues > `readonly` **issues**: readonly [`TransformIssue`](#transformissue)[] The value-free diagnostics raised during assembly, in emission order. ## Type Aliases ### CodedElement > **CodedElement** = `Pick`\<`CWE`, `"identifier"` \| `"text"` \| `"nameOfCodingSystem"` \| `"alternateIdentifier"` \| `"alternateText"` \| `"nameOfAlternateCodingSystem"` \| `"codingSystemVersionId"` \| `"alternateCodingSystemVersionId"` \| `"originalText"`\> The subset of CWE fields a CE also provides, so `toFhirCodeableConcept` accepts both. *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] A value from [FATAL\_CODES](#fatal_codes): the type carried by a thrown fatal error. *** ### IssueCode > **IssueCode** = *typeof* [`ISSUE_CODES`](#issue_codes)\[keyof *typeof* [`ISSUE_CODES`](#issue_codes)\] A value from [ISSUE\_CODES](#issue_codes): the type consumers narrow `issue.code` against. *** ### ReverseResult > **ReverseResult** = [`ConvertResult`](#convertresult)\<`Hl7Message`\> What a reverse conversion returns: the complete `@cosyte/hl7` message it could faithfully build (or `undefined` when it could not), plus the value-free diagnostics it raised. The same fail-safe envelope the forward converters return, in the other direction. #### Example ```ts import { toV2Patient } from "@cosyte/transform"; // const { value, issues } = toV2Patient(patientNode, "A28"); // value?.toString() -> "MSH|^~\\&|...|ADT^A28|...\rPID|||MRN1\r" void toV2Patient; ``` *** ### TransformSeverity > **TransformSeverity** = `"error"` \| `"warning"` \| `"information"` The severity a [TransformIssue](#transformissue) carries: an `OperationOutcome.issue.severity` value. ## Variables ### ADDRESS\_TYPE\_MAP > `const` **ADDRESS\_TYPE\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0190 address-type code → FHIR `address-type`, exactly per the IG HL70190→address-type map. *** ### ADDRESS\_USE\_MAP > `const` **ADDRESS\_USE\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0190 address-type code → FHIR `address-use`, exactly per the IG HL70190→address-use map. *** ### ADDRESS\_USE\_TO\_V2 > `const` **ADDRESS\_USE\_TO\_V2**: `Readonly`\<`Record`\<`string`, `string`\>\> FHIR `address-use` to HL7 v2 Table 0190, the invertible rows of the IG's **HL70190 to address-use** map. `work` is absent on purpose (`B` and `O`), as is every `Address.type` row. #### Example ```ts import { ADDRESS_USE_TO_V2 } from "@cosyte/transform"; ADDRESS_USE_TO_V2["home"]; // => "H" ``` *** ### ADMINISTRATIVE\_GENDER\_MAP > `const` **ADMINISTRATIVE\_GENDER\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0001 (Administrative Sex) → FHIR `administrative-gender`, exactly per the IG **Table HL70001 to Administrative Gender** ConceptMap (every row "is equivalent to"). `A` (Ambiguous) and `N` (Not applicable) both narrow to `other`: the map's stated equivalence, carried as-is. A code absent here (the map defines no `unmapped` default) leaves `gender` absent and is flagged [ISSUE\_CODES.TRANSFORM\_CODE\_UNMAPPED](#transform_code_unmapped), never coerced to `unknown`. *** ### AL1\_ONSET\_WITHDRAWN\_AT > `const` **AL1\_ONSET\_WITHDRAWN\_AT**: readonly `number`[] The HL7 v2 version at which AL1-6 Identification Date was withdrawn, per the IG map's own comment on that row ("Withdrawn as of 2.7, Refer to IAM-11 Onset or IAM-13 Reported Date"). A message at this version or later carries no AL1-6 that this library will read as an onset. #### Example ```ts import { AL1_ONSET_WITHDRAWN_AT } from "@cosyte/transform"; AL1_ONSET_WITHDRAWN_AT; // [2, 7] ``` *** ### ALLERGY\_CATEGORY\_VALUE\_MAP > `const` **ALLERGY\_CATEGORY\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **AL1-2 Allergen Type to category**: IG `ConceptMap/table-hl70127-to-allergy-intolerance-category`. One **mapped** group of six rows into the FHIR R4 `AllergyIntoleranceCategory` code system (`DA` drug to `medication`, `FA` food to `food`, `EA` environmental to `environment`, `AA` animal to `biologic`, `PA` plant to `environment`, `LA` pollen to `environment`, each `equivalent`, with the IG target displays), and an `(unmapped)` group of two: **`MA` and `MC` have no category target**. The IG applies this map to `AllergyIntolerance.category` **only**; `AllergyIntolerance.type` has its own map ([ALLERGY\_TYPE\_VALUE\_MAP](#allergy_type_value_map)) over the same source table, and the two are resolved independently. `MA` therefore yields a type and no category, which is the guide's answer and not a gap to be filled: a category is never borrowed from the type map or from a neighbouring code. *** ### ALLERGY\_CLINICAL\_STATUS\_CODE > `const` **ALLERGY\_CLINICAL\_STATUS\_CODE**: `"active"` = `"active"` The fixed `clinicalStatus` code the IG assigns. The guide's own note explains why it is fixed rather than derived: constraint ait-1 requires a clinicalStatus unless the verificationStatus is `entered-in-error`, and no AL1 component can ground that retraction. #### Example ```ts import { ALLERGY_CLINICAL_STATUS_CODE } from "@cosyte/transform"; ALLERGY_CLINICAL_STATUS_CODE; // "active" ``` *** ### ALLERGY\_CLINICAL\_STATUS\_SYSTEM > `const` **ALLERGY\_CLINICAL\_STATUS\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"` = `"http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"` The code system of the fixed `clinicalStatus` the IG assigns to every AllergyIntolerance built from an AL1, transcribed from the segment map's own sort-order 0 row. #### Example ```ts import { ALLERGY_CLINICAL_STATUS_SYSTEM } from "@cosyte/transform"; ALLERGY_CLINICAL_STATUS_SYSTEM.endsWith("allergyintolerance-clinical"); // true ``` *** ### ALLERGY\_CRITICALITY\_VALUE\_MAP > `const` **ALLERGY\_CRITICALITY\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **AL1-4 Allergy Severity to criticality**: IG `ConceptMap/table-hl70128-to-allergy-intolerance-criticality`. One **mapped** group of two rows into the FHIR R4 `AllergyIntoleranceCriticality` code system (`SV` severe to `high`, `MI` mild to `low`, both `equivalent`, with the IG target displays), and an `(unmapped)` group of two: **`MO` moderate and `U` unknown have no criticality target**. The unmapped pair is the whole reason this field fails safe. `criticality` is an overall risk assessment a prescriber acts on, so a moderate severity is left absent and flagged rather than rounded to `low` or `high`, and `AllergyIntolerance.reaction.severity` is never populated from the same component instead: the IG offers that only as a local variation, conditioned on a severity that was **not** used equivalently to criticality, which no v2 message states. *** ### ALLERGY\_INTOLERANCE\_CATEGORY\_SYSTEM > `const` **ALLERGY\_INTOLERANCE\_CATEGORY\_SYSTEM**: `"http://hl7.org/fhir/allergy-intolerance-category"` = `"http://hl7.org/fhir/allergy-intolerance-category"` FHIR R4 `AllergyIntoleranceCategory` CodeSystem URI (the IG's AL1-2 category target). *** ### ALLERGY\_INTOLERANCE\_CRITICALITY\_SYSTEM > `const` **ALLERGY\_INTOLERANCE\_CRITICALITY\_SYSTEM**: `"http://hl7.org/fhir/allergy-intolerance-criticality"` = `"http://hl7.org/fhir/allergy-intolerance-criticality"` FHIR R4 `AllergyIntoleranceCriticality` CodeSystem URI (the IG's AL1-4 criticality target). *** ### ALLERGY\_INTOLERANCE\_TYPE\_SYSTEM > `const` **ALLERGY\_INTOLERANCE\_TYPE\_SYSTEM**: `"http://hl7.org/fhir/allergy-intolerance-type"` = `"http://hl7.org/fhir/allergy-intolerance-type"` FHIR R4 `AllergyIntoleranceType` CodeSystem URI (the IG's AL1-2 type target). *** ### ALLERGY\_ORIGINAL\_CATEGORY\_VALUE\_MAP > `const` **ALLERGY\_ORIGINAL\_CATEGORY\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **AL1-2 original code**: IG `ConceptMap/table-hl70127-to-v2-0127`. A single **identity** group of all eight Table 0127 codes (`DA`, `FA`, `MA`, `MC`, `EA`, `AA`, `PA`, `LA` to themselves, each `equivalent`, source and target both `v2-0127`). This is the map behind the `alternate-codes` extension the IG puts on `category`: whatever the category map does or does not translate, the code the sender actually wrote survives on the resource in its own code system, so a consumer can always see what the message said. *** ### ALLERGY\_ORIGINAL\_CRITICALITY\_VALUE\_MAP > `const` **ALLERGY\_ORIGINAL\_CRITICALITY\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **AL1-4 original code**: IG `ConceptMap/table-hl70128-original-to-v2-0128`. A single **identity** group of all four Table 0128 codes (`SV`, `MO`, `MI`, `U` to themselves, each `equivalent`, source and target both `v2-0128`), the map behind the `alternate-codes` extension on `criticality`. Its four codes are a superset of the two [ALLERGY\_CRITICALITY\_VALUE\_MAP](#allergy_criticality_value_map) translates, which is the point: a `MO` that yields no criticality still yields the original severity code here. *** ### ALLERGY\_TYPE\_VALUE\_MAP > `const` **ALLERGY\_TYPE\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **AL1-2 Allergen Type to type**: IG `ConceptMap/table-hl70127-to-allergy-intolerance-type`. One **mapped** group of seven rows into the FHIR R4 `AllergyIntoleranceType` code system (`DA`, `FA`, `MA`, `EA`, `AA`, `PA`, `LA` all to `allergy`, each `equivalent`, display `Allergy`), and an `(unmapped)` group of one: **`MC` miscellaneous contraindication has no type target**. Read beside [ALLERGY\_CATEGORY\_VALUE\_MAP](#allergy_category_value_map): the guide publishes two maps over Table 0127 with different unmapped sets, so a code can be mapped by one and not the other. `MC` is the only code neither map carries. *** ### ALTERNATE\_CODES\_EXTENSION\_URL > `const` **ALTERNATE\_CODES\_EXTENSION\_URL**: `"http://hl7.org/fhir/StructureDefinition/alternate-codes"` = `"http://hl7.org/fhir/StructureDefinition/alternate-codes"` The canonical URL of the FHIR `alternate-codes` extension, the IG's fixed assignment for the `category.extension.url` and `criticality.extension.url` rows. #### Example ```ts import { ALTERNATE_CODES_EXTENSION_URL } from "@cosyte/transform"; ALTERNATE_CODES_EXTENSION_URL; // "http://hl7.org/fhir/StructureDefinition/alternate-codes" ``` *** ### APPOINTMENT\_STATUS\_MAP > `const` **APPOINTMENT\_STATUS\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0278 (Filler Status Code) → FHIR `appointmentstatus` (`Appointment.status`), per the IG **Table HL70278 to AppointmentStatus** ConceptMap (each `is equivalent to`). The three source codes the IG leaves **unmatched** (`Discontinued`, `Blocked`, `Overbook`) are absent here on purpose: an SCH-25 carrying one of them (or any local code) leaves `Appointment.status` absent + flagged and the required-`status` emit gate withholds the Appointment. *** ### APPOINTMENT\_TYPE\_VALUE\_MAP > `const` **APPOINTMENT\_TYPE\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **SCH-8 Appointment Type**: IG `ConceptMap/table-hl70277-to-v2-0277`. A single **identity** group of the three v2-0277 codes (`Normal`, `Tentative`, `Complete` → themselves, each `equivalent`, source and target both `v2-0277`). Any other code is unmapped → raw coding preserved + flagged. *** ### CONDITION\_ENTERED\_IN\_ERROR > `const` **CONDITION\_ENTERED\_IN\_ERROR**: `"entered-in-error"` = `"entered-in-error"` The `verificationStatus` code the DG1-21 row assigns, transcribed from the segment map's own `Condition.verificationStatus.coding.code` assignment. #### Example ```ts import { CONDITION_ENTERED_IN_ERROR } from "@cosyte/transform"; CONDITION_ENTERED_IN_ERROR; // "entered-in-error" ``` *** ### CONDITION\_VERIFICATION\_STATUS\_SYSTEM > `const` **CONDITION\_VERIFICATION\_STATUS\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/condition-ver-status"` = `"http://terminology.hl7.org/CodeSystem/condition-ver-status"` The code system of the `verificationStatus` the DG1-21 row assigns, transcribed from the segment map's own `Condition.verificationStatus.coding.system` assignment. #### Example ```ts import { CONDITION_VERIFICATION_STATUS_SYSTEM } from "@cosyte/transform"; CONDITION_VERIFICATION_STATUS_SYSTEM.endsWith("condition-ver-status"); // true ``` *** ### COVERAGE\_STATUS\_UNKNOWN > `const` **COVERAGE\_STATUS\_UNKNOWN**: `"unknown"` = `"unknown"` The `data-absent-reason` code carried by `Coverage.status`, the one FHIR-required element of this resource that no published row of the segment map grounds. #### Example ```ts import { COVERAGE_STATUS_UNKNOWN } from "@cosyte/transform"; COVERAGE_STATUS_UNKNOWN; // "unknown" ``` *** ### COVERAGE\_SUBSCRIBER\_ID\_EXTENSION\_URL > `const` **COVERAGE\_SUBSCRIBER\_ID\_EXTENSION\_URL**: `"http://hl7.org/fhir/5.0/StructureDefinition/extension-subscriberId"` = `"http://hl7.org/fhir/5.0/StructureDefinition/extension-subscriberId"` The canonical URL of the subscriber-id extension the IN1-10 and IN1-49 rows assign, transcribed from the segment map's own `Coverage.extension.url` assignment. #### Example ```ts import { COVERAGE_SUBSCRIBER_ID_EXTENSION_URL } from "@cosyte/transform"; COVERAGE_SUBSCRIBER_ID_EXTENSION_URL.endsWith("extension-subscriberId"); // true ``` *** ### DEFAULT\_V2\_CODE\_SYSTEMS > `const` **DEFAULT\_V2\_CODE\_SYSTEMS**: `Readonly`\<`Record`\<`string`, `string`\>\> A small, license-clean seed of v2 Table 0396 coding-system mnemonic → canonical URI, for the FHIR-core-fixed systems whose URIs are universally cited (FHIR R4 terminologies-systems.html). The full HL7 THO NamingSystem crosswalk is not bundled; callers extend this via [createNamingSystem](#createnamingsystem). **No terminology *content* is bundled: only identity URIs.** *** ### DG1\_RETRACTION\_ACTION\_CODE > `const` **DG1\_RETRACTION\_ACTION\_CODE**: `"D"` = `"D"` The Table 0206 (Segment Action Code) code the DG1-21 assignment covers. The row assigns a retraction and names no source code; `D` is Delete, and the row's own comment excludes the rest. #### Example ```ts import { DG1_RETRACTION_ACTION_CODE } from "@cosyte/transform"; DG1_RETRACTION_ACTION_CODE; // "D" ``` *** ### DIAGNOSTIC\_REPORT\_STATUS\_MAP > `const` **DIAGNOSTIC\_REPORT\_STATUS\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0123 (Result Status) → FHIR `diagnostic-report-status` (`DiagnosticReport.status`), per the IG **Table HL70123 [Queries] to Diagnostic Report Status** ConceptMap (each `is equivalent to`). Only these **eight** source codes carry a target; the IG leaves `A`, `M`, `N`, `Y`, `Z` unmapped (verified firsthand against the published v1.0.0 ConceptMap: `N` "Procedure completed, results pending" is in the `(not mapped)` group, and the map declares no `unmapped` default), so an OBR-25 with one of them leaves `status` absent + flagged and the report is withheld. In particular a results-**pending** `N` is **never** emitted as the post-final `appended`. **`C`→`corrected` and `X`→`cancelled` guarantee a corrected/cancelled report never emits as `final`.** *** ### ED\_BASE64\_ENCODING > `const` **ED\_BASE64\_ENCODING**: `"Base64"` = `"Base64"` The OBX-5.4 encoding the OBX map conditions **both** of its `ED` rows on. Any other encoding (`A`, `Hex`, `Ascii`, a local token) leaves those rows unapplied, so no attachment is built. #### Example ```ts import { ED_BASE64_ENCODING } from "@cosyte/transform"; ED_BASE64_ENCODING; // "Base64" ``` *** ### ENCOUNTER\_CLASS\_V3\_MAP > `const` **ENCOUNTER\_CLASS\_V3\_MAP**: `Readonly`\<`Record`\<`string`, \{ `code`: `string`; `display`: `string`; \}\>\> HL7 v2 Table 0004 (Patient Class) → FHIR v3 ActCode (`Encounter.class`), per the IG **Table HL70004 to V3 ActCode** ConceptMap (each "is equivalent to"). Only these four v2 classes have a v3 ActCode equivalent; the remaining classes (R/B/C/N/U) map to themselves and stay in the v2-0004 system (V2\_0004\_SYSTEM) per the same map's V2→V2 rows. *** ### ENCOUNTER\_STATUS\_MAP > `const` **ENCOUNTER\_STATUS\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0004 (Patient Class) → FHIR `Encounter.status`, per the IG **Table HL70004 to Encounter Status** ConceptMap. Applied only when PV1-45 (discharge date) is *not* valued: a valued discharge sets `status = finished` per the PV1→Encounter map. *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable **fatal codes**: the reserved names for unrecoverable input conditions. **Nothing in this library throws one today**; every path, datatype and message level alike, fails safe to a value-free issue instead. They are declared so that a future one is an addition rather than a rename. #### Type Declaration ##### EMPTY\_INPUT > `readonly` **EMPTY\_INPUT**: `"EMPTY_INPUT"` = `"EMPTY_INPUT"` The input carried nothing to transform. ##### TRANSFORM\_UNSUPPORTED\_INPUT > `readonly` **TRANSFORM\_UNSUPPORTED\_INPUT**: `"TRANSFORM_UNSUPPORTED_INPUT"` = `"TRANSFORM_UNSUPPORTED_INPUT"` The input was not a shape this transform accepts. #### Example ```ts import { FATAL_CODES } from "@cosyte/transform"; void FATAL_CODES.EMPTY_INPUT; ``` *** ### GENDER\_TO\_V2 > `const` **GENDER\_TO\_V2**: `Readonly`\<`Record`\<`string`, `string`\>\> FHIR `administrative-gender` to HL7 v2 Table 0001, the invertible rows of the IG's **HL70001 to Administrative Gender** map. `other` is absent on purpose: three source codes carry it. #### Example ```ts import { GENDER_TO_V2 } from "@cosyte/transform"; GENDER_TO_V2["female"]; // => "F" GENDER_TO_V2["other"]; // => undefined (ambiguous inverse, flagged instead) ``` *** ### HL70078\_INTERPRETATION\_CODES > `const` **HL70078\_INTERPRETATION\_CODES**: `ReadonlySet`\<`string`\> HL7 v2 Table 0078 (Abnormal Flags) → FHIR v3 ObservationInterpretation (`Observation.interpretation`), per the IG **Table HL70078 to v3 ObservationInterpretation** ConceptMap. Every mapped row is `is equivalent to` an **identically-spelled** v3 code (the map is code-preserving), so this is the set of codes that carry a target: a flag **absent** from this set has no equivalent and is flagged [ISSUE\_CODES.TRANSFORM\_CODE\_UNMAPPED](#transform_code_unmapped), never coerced (the IG leaves `AC`/`HM`/`OBX`/`QCF`/`TOX` and any local flag unmapped, and declares no `unmapped` default). *** ### IG\_ALLERGY\_VALUE\_MAPS\_PUBLISHED > `const` **IG\_ALLERGY\_VALUE\_MAPS\_PUBLISHED**: `"2025-10-07"` = `"2025-10-07"` The publication date of [IG\_ALLERGY\_VALUE\_MAPS\_VERSION](#ig_allergy_value_maps_version), so a later release of the guide can be told apart from a defect in this library. #### Example ```ts import { IG_ALLERGY_VALUE_MAPS_PUBLISHED } from "@cosyte/transform"; IG_ALLERGY_VALUE_MAPS_PUBLISHED; // "2025-10-07" ``` *** ### IG\_ALLERGY\_VALUE\_MAPS\_RETRIEVED > `const` **IG\_ALLERGY\_VALUE\_MAPS\_RETRIEVED**: `"2026-08-28"` = `"2026-08-28"` The date the five AL1 allergy ConceptMaps were retrieved and transcribed into the maps below. A target that surprises a reader is answered by re-reading the guide at this date: each map is a transcription of a published document at a moment, not a live query. #### Example ```ts import { IG_ALLERGY_VALUE_MAPS_RETRIEVED } from "@cosyte/transform"; IG_ALLERGY_VALUE_MAPS_RETRIEVED; // "2026-08-28" ``` *** ### IG\_ALLERGY\_VALUE\_MAPS\_SOURCE > `const` **IG\_ALLERGY\_VALUE\_MAPS\_SOURCE**: `"https://hl7.org/fhir/uv/v2mappings/ConceptMap-segment-al1-to-allergyintolerance.html"` = `"https://hl7.org/fhir/uv/v2mappings/ConceptMap-segment-al1-to-allergyintolerance.html"` The IG segment map the five AL1 allergy value maps hang off, so a reader can audit any row: it is the page that names which vocabulary map applies to which AL1 field. #### Example ```ts import { IG_ALLERGY_VALUE_MAPS_SOURCE } from "@cosyte/transform"; IG_ALLERGY_VALUE_MAPS_SOURCE.startsWith("https://"); // true ``` *** ### IG\_ALLERGY\_VALUE\_MAPS\_VERSION > `const` **IG\_ALLERGY\_VALUE\_MAPS\_VERSION**: `"1.0.0"` = `"1.0.0"` The published version of the HL7 Version 2 to FHIR Implementation Guide the five **AL1 allergy** value maps below were transcribed from. Recorded per map family rather than once for the file, because the earlier value maps here were transcribed on their own date and this constant must not backdate or forward-date them. #### Example ```ts import { IG_ALLERGY_VALUE_MAPS_VERSION } from "@cosyte/transform"; IG_ALLERGY_VALUE_MAPS_VERSION; // "1.0.0" ``` *** ### IG\_MAPPED\_ADT\_TRIGGERS > `const` **IG\_MAPPED\_ADT\_TRIGGERS**: `ReadonlySet`\<`string`\> The ADT trigger events the IG ships a **message** map for. Other triggers still transform, from the reusable segment maps, but are flagged [ISSUE\_CODES.TRANSFORM\_SEGMENT\_ASSEMBLED](#transform_segment_assembled). *** ### IG\_MAPPED\_APPOINTMENT\_TRIGGERS > `const` **IG\_MAPPED\_APPOINTMENT\_TRIGGERS**: `ReadonlySet`\<`string`\> The scheduling trigger the IG ships a **message** map for. The IG maps only `SIU^S12`; other SIU triggers still assemble their Appointment from the reusable SCH/AIS/PID segment maps but are flagged [ISSUE\_CODES.TRANSFORM\_SEGMENT\_ASSEMBLED](#transform_segment_assembled). *** ### IG\_MAPPED\_DOCUMENT\_TRIGGERS > `const` **IG\_MAPPED\_DOCUMENT\_TRIGGERS**: `ReadonlySet`\<`string`\> The medical-document trigger the IG ships a **message** map for. The IG maps only `MDM^T02`; other MDM triggers still assemble their DocumentReference from the reusable TXA/OBX segment maps but are flagged [ISSUE\_CODES.TRANSFORM\_SEGMENT\_ASSEMBLED](#transform_segment_assembled). *** ### IG\_MAPPED\_IMMUNIZATION\_TRIGGERS > `const` **IG\_MAPPED\_IMMUNIZATION\_TRIGGERS**: `ReadonlySet`\<`string`\> The immunization trigger the IG ships a **message** map for. The IG maps only `VXU^V04`; other VXU triggers still assemble their Immunization graph from the reusable RXA/RXR/ORC segment maps but are flagged [ISSUE\_CODES.TRANSFORM\_SEGMENT\_ASSEMBLED](#transform_segment_assembled). *** ### IG\_MAPPED\_ORDER\_TRIGGERS > `const` **IG\_MAPPED\_ORDER\_TRIGGERS**: `ReadonlySet`\<`string`\> The order-message trigger events the IG ships a **message** map for, keyed `CODE^TRIGGER`. Only `ORM^O01` and `OML^O21` are message-map-grounded; other order families (`OMP`, `OMG`, `RDE`, …) still assemble their request graph from the reusable ORC/OBR/RXO/RXR segment maps but are flagged [ISSUE\_CODES.TRANSFORM\_SEGMENT\_ASSEMBLED](#transform_segment_assembled). *** ### IG\_MAPPED\_ORU\_TRIGGERS > `const` **IG\_MAPPED\_ORU\_TRIGGERS**: `ReadonlySet`\<`string`\> The ORU trigger events the IG ships a **message** map for. The IG maps only `R01`; other ORU triggers still assemble their results graph from the reusable OBR/OBX segment maps but are flagged [ISSUE\_CODES.TRANSFORM\_SEGMENT\_ASSEMBLED](#transform_segment_assembled). *** ### IG\_MAPPED\_SEGMENT\_NAMES > `const` **IG\_MAPPED\_SEGMENT\_NAMES**: `ReadonlySet`\<`string`\> Every v2 segment name the guide publishes at least one segment-to-resource map for, at [IG\_SEGMENT\_MAPS\_VERSION](#ig_segment_maps_version), as of [IG\_SEGMENT\_MAPS\_RETRIEVED](#ig_segment_maps_retrieved). Thirty-three names, transcribed chapter by chapter from [IG\_SEGMENT\_MAPS\_SOURCE](#ig_segment_maps_source): Control (`MSA`, `MSH`, `NTE`, `SFT`), Patient Administration (`AL1`, `EVN`, `IAM`, `MRG`, `NK1`, `PD1`, `PID`, `PV1`, `PV2`), Order Entry (`OBR`, `ORC`, `TQ1`), Pharmacy and Vaccination (`RXA`, `RXO`, `RXR`), Financial Management (`DG1`, `IN1`, `IN3`, `PR1`), Observation Reporting (`OBX`, `PRT`, `SPM`), Medical Records (`TXA`), Scheduling (`AIG`, `AIL`, `AIP`, `AIS`, `SCH`) and Personnel Management (`ROL`). Two absences a reader is most likely to check, because they look like oversights and are not: **`RXE` is not on this set** (the guide publishes no map for it, which is why this library counts an `RXE` and refuses to assemble one), and neither is **`GT1`**. #### Example ```ts import { IG_MAPPED_SEGMENT_NAMES } from "@cosyte/transform"; IG_MAPPED_SEGMENT_NAMES.has("IAM"); // true IG_MAPPED_SEGMENT_NAMES.has("RXE"); // false ``` *** ### IG\_SEGMENT\_MAPS\_PUBLISHED > `const` **IG\_SEGMENT\_MAPS\_PUBLISHED**: `"2025-10-07"` = `"2025-10-07"` The publication date of [IG\_SEGMENT\_MAPS\_VERSION](#ig_segment_maps_version), so a later release of the guide can be told apart from a defect in this library. #### Example ```ts import { IG_SEGMENT_MAPS_PUBLISHED } from "@cosyte/transform"; IG_SEGMENT_MAPS_PUBLISHED; // "2025-10-07" ``` *** ### IG\_SEGMENT\_MAPS\_RETRIEVED > `const` **IG\_SEGMENT\_MAPS\_RETRIEVED**: `"2026-08-22"` = `"2026-08-22"` The date the Segment Maps index was retrieved and transcribed into [IG\_MAPPED\_SEGMENT\_NAMES](#ig_mapped_segment_names). A classification that surprises a reader is answered by re-reading the index at this date: the set is a transcription of a published document at a moment, not a live query. #### Example ```ts import { IG_SEGMENT_MAPS_RETRIEVED } from "@cosyte/transform"; IG_SEGMENT_MAPS_RETRIEVED; // "2026-08-22" ``` *** ### IG\_SEGMENT\_MAPS\_SOURCE > `const` **IG\_SEGMENT\_MAPS\_SOURCE**: `"https://hl7.org/fhir/uv/v2mappings/segment_maps.html"` = `"https://hl7.org/fhir/uv/v2mappings/segment_maps.html"` The Segment Maps index this set was transcribed from, so a reader can audit any entry. #### Example ```ts import { IG_SEGMENT_MAPS_SOURCE } from "@cosyte/transform"; IG_SEGMENT_MAPS_SOURCE.startsWith("https://"); // true ``` *** ### IG\_SEGMENT\_MAPS\_VERSION > `const` **IG\_SEGMENT\_MAPS\_VERSION**: `"1.0.0"` = `"1.0.0"` The published version of the HL7 Version 2 to FHIR Implementation Guide this set was read from. #### Example ```ts import { IG_SEGMENT_MAPS_VERSION } from "@cosyte/transform"; IG_SEGMENT_MAPS_VERSION; // "1.0.0" ``` *** ### IMMUNIZATION\_STATUS\_MAP > `const` **IMMUNIZATION\_STATUS\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0322 (Completion Status) → FHIR `Immunization.status`, per the IG **Table HL70322 to Event Status** ConceptMap (each `is equivalent to`). Table 0322 has exactly these **four** source codes and every one carries a target; the two distinct targets (`completed`, `not-done`) are both valid members of the `immunization-status` required binding, so applying this map to `Immunization.status` is faithful. Consulted only for a **valued** RXA-20 when **RXA-21 ≠ "D"** (the IG's `mappedVia` condition on that row); the two other IG status rows assign fixed values directly (a delete → `entered-in-error`, an unvalued RXA-20 → `completed`) and do not consult this map. See buildStatus. *** ### IN1\_SUBSCRIBER\_NUMBER\_TYPE > `const` **IN1\_SUBSCRIBER\_NUMBER\_TYPE**: `"SN"` = `"SN"` The Table 0203 identifier-type code the IN1-10 row conditions its subscriber-id assignment on (`IF CX.5 IS "SN"`, Subscriber Number). Any other identifier type leaves that row unapplied. #### Example ```ts import { IN1_SUBSCRIBER_NUMBER_TYPE } from "@cosyte/transform"; IN1_SUBSCRIBER_NUMBER_TYPE; // "SN" ``` *** ### ISSUE\_CODES > `const` **ISSUE\_CODES**: `object` Stable **non-fatal issue codes**: the typed diagnostics the fail-safe rule emits. #### Type Declaration ##### TRANSFORM\_ADDRESS\_USE\_UNMAPPED > `readonly` **TRANSFORM\_ADDRESS\_USE\_UNMAPPED**: `"TRANSFORM_ADDRESS_USE_UNMAPPED"` = `"TRANSFORM_ADDRESS_USE_UNMAPPED"` An XAD.7 address-type code has no equivalent in the IG's HL70190 → address-use/type ConceptMaps; `Address.use`/`.type` are left absent rather than guessed. ##### TRANSFORM\_CODE\_NOT\_INVERTIBLE > `readonly` **TRANSFORM\_CODE\_NOT\_INVERTIBLE**: `"TRANSFORM_CODE_NOT_INVERTIBLE"` = `"TRANSFORM_CODE_NOT_INVERTIBLE"` A FHIR code has no usable inverse in the IG ConceptMap that governs its field: either several v2 source codes map onto it (so the inverse is ambiguous) or the map has no row for it at all. The v2 field is left absent, never resolved to one arbitrary member of the ambiguous set. ##### TRANSFORM\_CODE\_SYSTEM\_NOT\_V2 > `readonly` **TRANSFORM\_CODE\_SYSTEM\_NOT\_V2**: `"TRANSFORM_CODE_SYSTEM_NOT_V2"` = `"TRANSFORM_CODE_SYSTEM_NOT_V2"` A `Coding.system` is not a system this library can name with a v2 coding-system mnemonic, so the coding cannot be written into a v2 coded field. It is flagged rather than emitted with no table context or with a code from an unrelated table. ##### TRANSFORM\_CODE\_SYSTEM\_UNRESOLVED > `readonly` **TRANSFORM\_CODE\_SYSTEM\_UNRESOLVED**: `"TRANSFORM_CODE_SYSTEM_UNRESOLVED"` = `"TRANSFORM_CODE_SYSTEM_UNRESOLVED"` A coding-system mnemonic (CWE.3 / CWE.6) was not recognized, so no canonical system URI could be emitted. The code is preserved; a URI is **never** invented. ##### TRANSFORM\_CODE\_UNMAPPED > `readonly` **TRANSFORM\_CODE\_UNMAPPED**: `"TRANSFORM_CODE_UNMAPPED"` = `"TRANSFORM_CODE_UNMAPPED"` A coded value could not be mapped to a FHIR concept: either it carried no coding-system context (CWE.3 absent), or it is a table code the IG ConceptMap has **no target** for (e.g. an OBX-11/ OBR-25 result status the HL70085/HL70123 map does not carry, or an OBX-8 abnormal flag absent from HL70078). The original code is preserved / the FHIR value left absent; **never** coerced to a neighbor and **never** guessed to `final`/`normal`. ##### TRANSFORM\_ELEMENT\_DROPPED > `readonly` **TRANSFORM\_ELEMENT\_DROPPED**: `"TRANSFORM_ELEMENT_DROPPED"` = `"TRANSFORM_ELEMENT_DROPPED"` A populated source element was not carried to its FHIR target and was dropped, either because the IG map defines no target, or because that target's conversion is **not implemented** (e.g. XTN→ContactPoint telecom, PV1 participants). Surfaced here rather than silently discarded, never a fabricated value. ##### TRANSFORM\_IDENTIFIER\_SYSTEM\_UNRESOLVED > `readonly` **TRANSFORM\_IDENTIFIER\_SYSTEM\_UNRESOLVED**: `"TRANSFORM_IDENTIFIER_SYSTEM_UNRESOLVED"` = `"TRANSFORM_IDENTIFIER_SYSTEM_UNRESOLVED"` The assigning authority (HD) could not be resolved to a FHIR `Identifier.system` URI via the NamingSystem registry. The identifier is emitted with its value and **no** system: a system URI is **never** synthesized from HD.1 alone. ##### TRANSFORM\_MISSING\_TRIGGER > `readonly` **TRANSFORM\_MISSING\_TRIGGER**: `"TRANSFORM_MISSING_TRIGGER"` = `"TRANSFORM_MISSING_TRIGGER"` A reverse (FHIR to v2) conversion was called without the explicit message trigger it requires. No FHIR resource carries a v2 trigger, so it is never inferred from resource content and never defaulted: no message is built and no builder is called. ##### TRANSFORM\_NAME\_USE\_UNMAPPED > `readonly` **TRANSFORM\_NAME\_USE\_UNMAPPED**: `"TRANSFORM_NAME_USE_UNMAPPED"` = `"TRANSFORM_NAME_USE_UNMAPPED"` An XPN.7 name-type code has no equivalent in the IG's HL70200 → name-use ConceptMap; `HumanName.use` is left absent rather than guessed. ##### TRANSFORM\_NO\_V2\_MESSAGE\_EMITTED > `readonly` **TRANSFORM\_NO\_V2\_MESSAGE\_EMITTED**: `"TRANSFORM_NO_V2_MESSAGE_EMITTED"` = `"TRANSFORM_NO_V2_MESSAGE_EMITTED"` A reverse (FHIR to v2) conversion produced **no message at all**: nothing in the resource grounded a single field of the target segment, and an empty segment is never emitted. Raised so that an empty-handed conversion is never indistinguishable from a successful one, and separate from the refusals that name their own cause (an absent trigger, an unsupported resource type, a structurally malformed resource). ##### TRANSFORM\_NO\_V2\_TARGET > `readonly` **TRANSFORM\_NO\_V2\_TARGET**: `"TRANSFORM_NO_V2_TARGET"` = `"TRANSFORM_NO_V2_TARGET"` A populated FHIR element has no v2 field in the narrow reverse map that covers this shape (the IG map defines no source for it, or its inverse is not implemented here). The element is surfaced and left out, never approximated into a neighbouring v2 field. ##### TRANSFORM\_QUANTITY\_VALUE\_INVALID > `readonly` **TRANSFORM\_QUANTITY\_VALUE\_INVALID**: `"TRANSFORM_QUANTITY_VALUE_INVALID"` = `"TRANSFORM_QUANTITY_VALUE_INVALID"` A v2 numeric value (NM) could not be represented as a FHIR `decimal` without altering it, e.g. a leading `+`, a leading zero, or a trailing dot, which FHIR's `decimal` lexical form forbids. No `Quantity` value is emitted rather than a canonicalized (and therefore altered) magnitude. ##### TRANSFORM\_REQUIRED\_ELEMENT\_UNKNOWN > `readonly` **TRANSFORM\_REQUIRED\_ELEMENT\_UNKNOWN**: `"TRANSFORM_REQUIRED_ELEMENT_UNKNOWN"` = `"TRANSFORM_REQUIRED_ELEMENT_UNKNOWN"` A FHIR-required element's value could not be faithfully derived from the source, so it was emitted with a `data-absent-reason` extension (value `unknown`) rather than fabricated, e.g. `MessageHeader.source.endpoint` from an MSH-3 application namespace that is not a URL. ##### TRANSFORM\_RESOURCE\_INVALID > `readonly` **TRANSFORM\_RESOURCE\_INVALID**: `"TRANSFORM_RESOURCE_INVALID"` = `"TRANSFORM_RESOURCE_INVALID"` A produced FHIR resource failed `@cosyte/fhir` R4 structural validation and was **withheld** from the bundle rather than emitted as a silently-invalid resource: the conservative-emit gate. ##### TRANSFORM\_RESOURCE\_MALFORMED > `readonly` **TRANSFORM\_RESOURCE\_MALFORMED**: `"TRANSFORM_RESOURCE_MALFORMED"` = `"TRANSFORM_RESOURCE_MALFORMED"` A resource handed to a reverse (FHIR to v2) conversion is not structurally a FHIR resource of its expected type (not an object, no `resourceType`, or an element carrying the wrong node kind). Nothing is emitted and nothing is thrown. ##### TRANSFORM\_SEGMENT\_ASSEMBLED > `readonly` **TRANSFORM\_SEGMENT\_ASSEMBLED**: `"TRANSFORM_SEGMENT_ASSEMBLED"` = `"TRANSFORM_SEGMENT_ASSEMBLED"` The message's trigger event has no IG *message* map, so its resource graph was assembled from the reusable IG *segment* maps rather than a message map. The graph is best-effort and flagged as such, never a fabricated message map. ##### TRANSFORM\_SEGMENT\_NO\_IG\_MAP > `readonly` **TRANSFORM\_SEGMENT\_NO\_IG\_MAP**: `"TRANSFORM_SEGMENT_NO_IG_MAP"` = `"TRANSFORM_SEGMENT_NO_IG_MAP"` A segment the message carried did **not** reach any resource in the returned bundle, and its name is **not** one the IG publishes a segment map for: a **standard gap**, not fixable here. Custom Z-segments and `RXE` land on this code, and so does a name this library could not classify at all, because a damaged segment identifier is never re-derived from the line: an `AL1` whose identifier arrived as `AL11` reads as unclassifiable, not as an IG omission. Such an occurrence is located `[#n]` by its 1-based position among the segments of the message, with no name part at all. ##### TRANSFORM\_SEGMENT\_NOT\_EMITTED > `readonly` **TRANSFORM\_SEGMENT\_NOT\_EMITTED**: `"TRANSFORM_SEGMENT_NOT_EMITTED"` = `"TRANSFORM_SEGMENT_NOT_EMITTED"` A segment the message carried did **not** reach any resource in the returned bundle, and the IG *does* publish a segment map for its name: a **library gap**, fixable here. Raised once per occurrence, located `NAME[k]` with `k` counted 1-based among the occurrences of that name (`DG1[1]`, `DG1[2]`), so a consumer can tell how much of the message the bundle represents without diffing the two by hand. The occurrence may have been read and refused rather than never read: being counted, or already named by another issue, is not the same as reaching a resource. ##### TRANSFORM\_TIMESTAMP\_INVALID > `readonly` **TRANSFORM\_TIMESTAMP\_INVALID**: `"TRANSFORM_TIMESTAMP_INVALID"` = `"TRANSFORM_TIMESTAMP_INVALID"` A v2 DTM/TS could not be parsed into a valid FHIR date/time; no value was emitted. ##### TRANSFORM\_TIMESTAMP\_NO\_TIMEZONE > `readonly` **TRANSFORM\_TIMESTAMP\_NO\_TIMEZONE**: `"TRANSFORM_TIMESTAMP_NO_TIMEZONE"` = `"TRANSFORM_TIMESTAMP_NO_TIMEZONE"` A v2 DTM/TS carried a time-of-day but no timezone offset. FHIR forbids time-without-zone, so the value was reduced to date precision (or an explicit sender offset applied via options), **never** a guessed UTC. ##### TRANSFORM\_UNIT\_NOT\_UCUM > `readonly` **TRANSFORM\_UNIT\_NOT\_UCUM**: `"TRANSFORM_UNIT_NOT_UCUM"` = `"TRANSFORM_UNIT_NOT_UCUM"` A unit was not a valid UCUM code (or was not declared UCUM). It is preserved verbatim in `Quantity.unit`; `Quantity.code`/`.system` are left absent: a UCUM code is **never** fabricated and a magnitude is **never** converted. ##### TRANSFORM\_UNSUPPORTED\_RESOURCE > `readonly` **TRANSFORM\_UNSUPPORTED\_RESOURCE**: `"TRANSFORM_UNSUPPORTED_RESOURCE"` = `"TRANSFORM_UNSUPPORTED_RESOURCE"` A reverse (FHIR to v2) conversion was handed a resource outside the narrow set of shapes it supports. No v2 message is emitted and no segment layout is guessed. ##### TRANSFORM\_V2\_REQUIRED\_FIELD\_ABSENT > `readonly` **TRANSFORM\_V2\_REQUIRED\_FIELD\_ABSENT**: `"TRANSFORM_V2_REQUIRED_FIELD_ABSENT"` = `"TRANSFORM_V2_REQUIRED_FIELD_ABSENT"` A v2 field the segment's own attribute table marks **required** (usage `R`) is absent from an emitted segment, because the FHIR resource carried no source this reverse map could ground it from, or the source it carried had no faithful v2 form. The field is left absent per v2 optionality rules and **declared here**: a placeholder is never written to satisfy v2 structure, and the receiver is never left to discover the gap. Distinct from [ISSUE\_CODES.TRANSFORM\_REQUIRED\_ELEMENT\_UNKNOWN](#transform_required_element_unknown), which is the forward direction's FHIR-required element. ##### TRANSFORM\_VALUE\_NOT\_REPRESENTABLE > `readonly` **TRANSFORM\_VALUE\_NOT\_REPRESENTABLE**: `"TRANSFORM_VALUE_NOT_REPRESENTABLE"` = `"TRANSFORM_VALUE_NOT_REPRESENTABLE"` A FHIR value cannot be carried into its v2 target without altering it (a lexical form v2 has no representation for, or content that exceeds the target's component structure). The v2 field is left absent rather than truncated, rounded, or coerced. #### Example ```ts import { ISSUE_CODES } from "@cosyte/transform"; if (issue.code === ISSUE_CODES.TRANSFORM_TIMESTAMP_NO_TIMEZONE) { // a v2 timestamp had a time-of-day but no timezone offset: reduced to date precision } ``` *** ### ISSUE\_REGISTRY > `const` **ISSUE\_REGISTRY**: `Readonly`\<`Record`\<[`IssueCode`](#issuecode), `IssueMeta`\>\> The per-code registry: severity, FHIR `issue-type`, and a static message. Frozen and exhaustive over [ISSUE\_CODES](#issue_codes) (enforced by the `Record` type: a new code will not compile until it has an entry). *** ### NAME\_USE\_MAP > `const` **NAME\_USE\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0200 name-type code → FHIR `name-use`, exactly per the IG **HL70200 → name-use** ConceptMap (every row is "is equivalent to"). Codes absent here (A, B, C, F, I, K, NB, NOUSE, P, REL, S, T, U, …) have **no FHIR equivalent** and are surfaced, never guessed. *** ### NAME\_USE\_TO\_V2 > `const` **NAME\_USE\_TO\_V2**: `Readonly`\<`Record`\<`string`, `string`\>\> FHIR `name-use` to HL7 v2 Table 0200, the invertible rows of the IG's **HL70200 to name-use** map. `official` and `temp` are absent on purpose: two source codes each. #### Example ```ts import { NAME_USE_TO_V2 } from "@cosyte/transform"; NAME_USE_TO_V2["maiden"]; // => "M" ``` *** ### OBSERVATION\_STATUS\_MAP > `const` **OBSERVATION\_STATUS\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0085 (Observation Result Status) → FHIR `observation-status` (`Observation.status`), per the IG **Table HL70085 to Observation Status** ConceptMap (each `is equivalent to`). Only these seven source codes carry a target; the codes the IG leaves unmapped (`B`, `I`, `N`, `O`, `R`, `S`, `U`, `V`) are **absent here on purpose**: an OBX-11 with one of them (or any local code) leaves `Observation.status` absent + flagged, and the required-`status` emit gate withholds the Observation. **`C`→`corrected` and `X`→`cancelled` guarantee a corrected/cancelled result never emits as `final`.** *** ### OBSERVATION\_STATUS\_TO\_V2 > `const` **OBSERVATION\_STATUS\_TO\_V2**: `Readonly`\<`Record`\<`string`, `string`\>\> FHIR `observation-status` to HL7 v2 Table 0085, the invertible rows of the IG's **HL70085 to Observation Status** map. `entered-in-error` is absent on purpose (`D` and `W` both carry it), as are the FHIR statuses the map never targets (`registered`, `unknown`). #### Example ```ts import { OBSERVATION_STATUS_TO_V2 } from "@cosyte/transform"; OBSERVATION_STATUS_TO_V2["corrected"]; // => "C" OBSERVATION_STATUS_TO_V2["entered-in-error"]; // => undefined (ambiguous inverse, flagged instead) ``` *** ### OBSERVATION\_VALUE\_ATTACHMENT\_EXTENSION\_URL > `const` **OBSERVATION\_VALUE\_ATTACHMENT\_EXTENSION\_URL**: `"https://hl7.org/fhir/5.0/StructureDefinition/extension-Observation.valueAttachment"` = `"https://hl7.org/fhir/5.0/StructureDefinition/extension-Observation.valueAttachment"` The extension URL the OBX map **fixes** on the `IF OBX-2 EQUALS "ED" AND IF OBX-5.4 EQUALS "Base64"` rows, transcribed from the map's own `Observation.extension.url` assignment. R4 has no `Observation.valueAttachment`, so the guide carries the R5 element as this named extension. #### Example ```ts import { OBSERVATION_VALUE_ATTACHMENT_EXTENSION_URL } from "@cosyte/transform"; OBSERVATION_VALUE_ATTACHMENT_EXTENSION_URL.endsWith("extension-Observation.valueAttachment"); // true ``` *** ### PROCEDURE\_STATUS\_UNKNOWN > `const` **PROCEDURE\_STATUS\_UNKNOWN**: `"unknown"` = `"unknown"` The `Procedure.status` the PR1 map's own narrative row directs when the message context does not determine one. No PR1 component carries a procedure status, so this is the value every Procedure built from a PR1 carries. #### Example ```ts import { PROCEDURE_STATUS_UNKNOWN } from "@cosyte/transform"; PROCEDURE_STATUS_UNKNOWN; // "unknown" ``` *** ### REQUEST\_STATUS\_MAP > `const` **REQUEST\_STATUS\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0119 (Order Control Codes) → FHIR `request-status` (`ServiceRequest.status`), per the IG **Table HL70119 to Request Status** ConceptMap (each `is equivalent to`). Only these **19** source codes carry a target; every other v2-0119 code sits in the IG's explicit `(unmapped)` group (verified firsthand against the published v1.0.0 ConceptMap: CH/CN/CP/DE/LI/NA/OE/OF/OP/OR/PA/RE/ RF/RP/RR/RU/SC/SN/SR/SS/UA/UC/UD/UF/UH/UM/UN/UR/UX/XO/XR/XX/MC are all `unmatched`, and the map declares no `unmapped` default), so an ORC-1 with one of them leaves `status` absent + flagged and the request is withheld. `request-status` has no `cancelled`; the cancel/discontinue codes map to `revoked` and the hold codes to `on-hold`, exactly as the IG table specifies. *** ### ROUTE\_VALUE\_MAP > `const` **ROUTE\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **RXR-1 Route**: IG `ConceptMap/table-hl70162-to-v2-0162`. Two IG groups, transcribed verbatim: a 41-code **identity** group (source `v2-0162` → target `v2-0162`, each `equivalent`) and a 6-code **remap** group into `v3-RouteOfAdministration` (`ID→IDINJ`, `IM→IM`, `IV→IVINJ`, `PO→PO`, `SC→SQ`, `TD→TRNSDERM`, each `equivalent`, with the IG target displays). The two source-code sets are disjoint. Any other v2-0162 code (or a non-table code) is unmapped → the raw coding is preserved and flagged, never coerced. *** ### SAMPLED\_DATA\_ABSENT\_POINT > `const` **SAMPLED\_DATA\_ABSENT\_POINT**: `"E"` = `"E"` The `SampledData.data` token the Implementation Considerations chapter's own worked example directs for a data point a repetition did not carry (`"set .dimensions to 4 and use E for the data points not present"`). It is the FHIR-defined "error" token, and it is written **only** for an absent position, never for a value that arrived and could not be converted. #### Example ```ts import { SAMPLED_DATA_ABSENT_POINT } from "@cosyte/transform"; SAMPLED_DATA_ABSENT_POINT; // "E" ``` *** ### SAMPLED\_DATA\_UNGROUNDED > `const` **SAMPLED\_DATA\_UNGROUNDED**: `"unknown"` = `"unknown"` The `data-absent-reason` code carried by `SampledData.origin` and `SampledData.period`, the two R4-required elements of that datatype that **no row of the IG's NA map, and no line of the Implementation Considerations chapter it points at, supplies a source for**. #### Example ```ts import { SAMPLED_DATA_UNGROUNDED } from "@cosyte/transform"; SAMPLED_DATA_UNGROUNDED; // "unknown" ``` *** ### SEGMENT\_IDENTIFIER\_SHAPE > `const` **SEGMENT\_IDENTIFIER\_SHAPE**: `RegExp` The HL7 v2 Chapter 2 segment-identifier shape: three characters, a leading letter, applied to the name value ITSELF as a positive membership test that must pass before any name is rendered. Everything else is a malformed name without exception: the empty string, a withheld marker, a raw pre-delimiter token, a bare numeric like `202`, a four-character `AL11`. A malformed occurrence has no name in this library's vocabulary and is identified by position instead. #### Example ```ts import { SEGMENT_IDENTIFIER_SHAPE } from "@cosyte/transform"; SEGMENT_IDENTIFIER_SHAPE.test("ZAL"); // true SEGMENT_IDENTIFIER_SHAPE.test("202"); // false ``` *** ### SERVICE\_REQUEST\_PRIORITY\_MAP > `const` **SERVICE\_REQUEST\_PRIORITY\_MAP**: `Readonly`\<`Record`\<`string`, `string`\>\> HL7 v2 Table 0485 (Extended Priority Codes) → FHIR `request-priority` (`ServiceRequest.priority`), per the IG **Table HL70485 to Request Priority** ConceptMap (each `is equivalent to`; verified firsthand against the published v1.0.0 ConceptMap). Only these **three** source codes carry a target; every other v2-0485 code, namely `P` (Preop), `C` (Callback), `T` (Timing critical), the `T{S,M,H,D,W,L}` timing-critical family, and `PRN` (As needed), sits in the IG's `(unmapped)` group with no target, so an OBR-5 carrying one leaves `priority` absent + flagged. All three targets (`stat`/`asap`/`routine`) are valid `request-priority` members. *** ### SITE\_VALUE\_MAP > `const` **SITE\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **RXR-2 Administration Site**: IG `ConceptMap/table-hl70550-to-v2-0550`. A single **identity** group of 443 body-part codes (source `v2-0550` → target `v2-0550`, each `equivalent`), transcribed verbatim from the published STU1 ConceptMap, **including** its as-published data-quality artifacts (`CHESTÂ`, a lone `Â`, `KIDNÂ`, where a stray U+00C2 leaked from the IG's source encoding). They are preserved for source-fidelity; they are inert (a clean `CHEST`/`KIDN` simply falls through to unmapped). A code not in the table is unmapped → raw coding preserved + flagged. *** ### SPM\_SHIPMENT\_IDENTIFIER\_TYPE > `const` **SPM\_SHIPMENT\_IDENTIFIER\_TYPE**: `"SHIP"` = `"SHIP"` The Table 0203 identifier-type code the SPM-32 row **assigns** to the shipment identifier (`Specimen.identifier[3].type.coding.code = "SHIP"`, with the v2-0203 system beside it). #### Example ```ts import { SPM_SHIPMENT_IDENTIFIER_TYPE } from "@cosyte/transform"; SPM_SHIPMENT_IDENTIFIER_TYPE; // "SHIP" ``` *** ### SUBSTITUTION\_VALUE\_MAP > `const` **SUBSTITUTION\_VALUE\_MAP**: [`CodedValueMap`](#codedvaluemap) **RXO-9 Allow Substitutions**: IG `ConceptMap/table-hl70161-to-v2-0161`. A single **identity** group of the three v2-0161 codes (`N` NOT authorized, `G` generic, `T` therapeutic → themselves, each `equivalent`, source and target both `v2-0161`), the target for `MedicationRequest.substitution.allowedCodeableConcept`. Any other code is unmapped → the substitution is withheld + flagged, never a fabricated substitution permission. *** ### TRANSFORM\_ISSUE\_SYSTEM > `const` **TRANSFORM\_ISSUE\_SYSTEM**: `"https://cosyte.com/fhir/CodeSystem/transform-issue-codes"` = `"https://cosyte.com/fhir/CodeSystem/transform-issue-codes"` The code system under which the transform's stable issue codes are carried in `details.coding`. *** ### V2\_0127\_SYSTEM > `const` **V2\_0127\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v2-0127"` = `"http://terminology.hl7.org/CodeSystem/v2-0127"` HL7 v2 Table 0127 (Allergen Type) THO CodeSystem URI. *** ### V2\_0128\_SYSTEM > `const` **V2\_0128\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v2-0128"` = `"http://terminology.hl7.org/CodeSystem/v2-0128"` HL7 v2 Table 0128 (Allergy Severity) THO CodeSystem URI. *** ### V2\_0161\_SYSTEM > `const` **V2\_0161\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v2-0161"` = `"http://terminology.hl7.org/CodeSystem/v2-0161"` HL7 v2 Table 0161 (Allow Substitution) THO CodeSystem URI. *** ### V2\_0162\_SYSTEM > `const` **V2\_0162\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v2-0162"` = `"http://terminology.hl7.org/CodeSystem/v2-0162"` HL7 v2 Table 0162 (Route of Administration) THO CodeSystem URI. *** ### V2\_0203\_SYSTEM > `const` **V2\_0203\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v2-0203"` = `"http://terminology.hl7.org/CodeSystem/v2-0203"` The HL7 v2 Table 0203 identifier-type code system canonical URI: used to build `Identifier.type`. (FHIR R4 terminologies-systems.html) *** ### V2\_0277\_SYSTEM > `const` **V2\_0277\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v2-0277"` = `"http://terminology.hl7.org/CodeSystem/v2-0277"` HL7 v2 Table 0277 (Appointment Type Codes) THO CodeSystem URI. *** ### V2\_0550\_SYSTEM > `const` **V2\_0550\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v2-0550"` = `"http://terminology.hl7.org/CodeSystem/v2-0550"` HL7 v2 Table 0550 (Body Parts) THO CodeSystem URI. *** ### V3\_OBSERVATION\_INTERPRETATION\_SYSTEM > `const` **V3\_OBSERVATION\_INTERPRETATION\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"` = `"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"` The v3 ObservationInterpretation canonical system (FHIR `Observation.interpretation` binding). #### Example ```ts import { V3_OBSERVATION_INTERPRETATION_SYSTEM } from "@cosyte/transform"; V3_OBSERVATION_INTERPRETATION_SYSTEM.endsWith("v3-ObservationInterpretation"); // => true ``` *** ### V3\_ROUTE\_OF\_ADMINISTRATION\_SYSTEM > `const` **V3\_ROUTE\_OF\_ADMINISTRATION\_SYSTEM**: `"http://terminology.hl7.org/CodeSystem/v3-RouteOfAdministration"` = `"http://terminology.hl7.org/CodeSystem/v3-RouteOfAdministration"` HL7 v3 RouteOfAdministration CodeSystem URI (the IG's remap target for the six common routes). *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Package version marker exported from the `@cosyte/transform` root. Kept in lockstep with `package.json` by `scripts/sync-version.mjs`, which the `version` script runs immediately after `changeset version`. The `: string` annotation is deliberate: without it TypeScript infers the *literal* type (`declare const VERSION = "0.0.0"`), which leaks the current release into consumers' types and makes an equality check against any other version a compile error. #### Example ```typescript import { VERSION } from "@cosyte/transform"; console.log(VERSION); ``` ## Functions ### addMinutes() > **addMinutes**(`start`, `minutes`): `string` \| `undefined` Add whole minutes to a FHIR `dateTime` that carries a time of day, preserving its stated offset and its fractional seconds exactly. Returns `undefined` when the value is not a full zoned timestamp, because a date is not a start a duration can be added to. #### Parameters ##### start `string` The FHIR `dateTime` lexical value the conversion produced. ##### minutes `number` The number of minutes to add. #### Returns `string` \| `undefined` #### Example ```ts import { addMinutes } from "@cosyte/transform"; addMinutes("2026-01-15T10:30:00-05:00", 45); // "2026-01-15T11:15:00-05:00" addMinutes("2026-01-15", 45); // undefined ``` *** ### buildAnnotations() > **buildAnnotations**(`notes`, `ctx`, `issues`): `any` Every FHIR `Annotation` a run of `NTE` segments contributes, in message order: one per valued `NTE-3` repetition, each carrying that repetition's rendered text and the segment's `NTE-6` time. Returns `undefined` when the segments carry no comment text at all, so a caller adds no empty `note` element. The diagnostics raised are value-free: no byte of a note ever reaches one. #### Parameters ##### notes readonly `Segment`[] The `NTE` segments to read, in message order. ##### ctx [`TransformContext`](#transformcontext) The transform context (naming-system registry + timezone policy). ##### issues [`TransformIssue`](#transformissue)[] The issue sink for the deferred-row and timestamp diagnostics. #### Returns `any` #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // const nte = parseHL7(raw).segments("NTE"); // buildAnnotations(nte, {}, []); // a FHIR list of Annotation nodes, or undefined ``` *** ### buildCondition() > **buildCondition**(`dg1`, `patientFullUrl`, `ctx`, `v2Location`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Build a FHIR `Condition` resource node from one `DG1` occurrence, wired to the bundle's Patient. Always produces a Condition: a DG1 states a diagnosis, so an occurrence that grounds neither a code nor a description is emitted with its empty `code` declared rather than withheld. #### Parameters ##### dg1 `Segment` The `DG1` `@cosyte/hl7` `Segment`. ##### patientFullUrl `string` The bundle's Patient fullUrl, for `subject` (required 1..1). ##### ctx [`TransformContext`](#transformcontext) The transform context (naming-system registry + timezone policy). ##### v2Location `string` The occurrence's v2 location, for the codeless diagnostic (e.g. `DG1[0]`). #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // const dg1 = parseHL7(raw).segments("DG1")[0]; // const { value } = buildCondition(dg1!, "urn:uuid:pat", {}, "DG1[0]"); ``` *** ### buildCoverage() > **buildCoverage**(`in1`, `patientFullUrl`, `ctx`, `v2Location`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Build a FHIR `Coverage` resource node from one `IN1` occurrence, wired to the bundle's Patient. Returns `{ value: undefined }` when IN1-4 names no insurance company, because `payor` is required by R4 and no other row of the segment map grounds it. #### Parameters ##### in1 `Segment` The `IN1` `@cosyte/hl7` `Segment`. ##### patientFullUrl `string` The bundle's Patient fullUrl, for `beneficiary` (required 1..1). ##### ctx [`TransformContext`](#transformcontext) The transform context (naming-system registry + timezone policy). ##### v2Location `string` The occurrence's v2 location, for the withholding diagnostic (e.g. `IN1[0]`). #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // const in1 = parseHL7(raw).segments("IN1")[0]; // const { value } = buildCoverage(in1!, "urn:uuid:pat", {}, "IN1[0]"); ``` *** ### buildProcedure() > **buildProcedure**(`pr1`, `patientFullUrl`, `ctx`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Build a FHIR `Procedure` resource node from one `PR1` occurrence, wired to the bundle's Patient. Always produces a Procedure: a PR1 states that a procedure happened, and `status` is grounded on the map's own row rather than on the message, so nothing here can fail to be groundable. #### Parameters ##### pr1 `Segment` The `PR1` `@cosyte/hl7` `Segment`. ##### patientFullUrl `string` The bundle's Patient fullUrl, for `subject` (required 1..1). ##### ctx [`TransformContext`](#transformcontext) The transform context (naming-system registry + timezone policy). #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // const pr1 = parseHL7(raw).segments("PR1")[0]; // const { value } = buildProcedure(pr1!, "urn:uuid:pat", {}); ``` *** ### buildSpecimen() > **buildSpecimen**(`spm`, `subjectFullUrl`, `ctx`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Build a FHIR `Specimen` resource node from one parsed HL7 v2 SPM segment. Returns `{ value: undefined }` when the segment grounds no element of the map at all, because an empty `Specimen` in a bundle is a claim that a sample was described when nothing about it was sent. #### Parameters ##### spm `Segment` The SPM `@cosyte/hl7` `Segment`. ##### subjectFullUrl `string` \| `undefined` The `urn:uuid:` fullUrl of the bundle's Patient → `Specimen.subject`. ##### ctx [`TransformContext`](#transformcontext) The transform context (naming-system registry + timezone policy). #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // const spm = parseHL7(raw).segments("SPM")[0]; // const { value } = buildSpecimen(spm!, "urn:uuid:pat", {}); ``` *** ### carriesLegacyOnsetDate() > **carriesLegacyOnsetDate**(`version`): `boolean` Whether a message's MSH-12 version identifier is readable **and** earlier than the version at which AL1-6 was withdrawn ([AL1\_ONSET\_WITHDRAWN\_AT](#al1_onset_withdrawn_at)). Fails closed in both directions a version can be unusable: an absent MSH-12 and one this library cannot compare (a vendor string, an empty component, anything outside the dotted-numeric shape) both answer `false`, so a valued AL1-6 is dropped and flagged rather than read as an onset on a message that may never have meant one. #### Parameters ##### version `string` \| `undefined` The MSH-12 version identifier exactly as the parser published it. #### Returns `boolean` #### Example ```ts import { carriesLegacyOnsetDate } from "@cosyte/transform"; carriesLegacyOnsetDate("2.5.1"); // true carriesLegacyOnsetDate("2.7"); // false carriesLegacyOnsetDate(undefined); // false ``` *** ### codeableConceptFromTarget() > **codeableConceptFromTarget**(`target`, `text?`): `FhirComplex` Build a FHIR `CodeableConcept` node for a [CodedTarget](#codedtarget): the single translated coding, with an optional free-`text`. Used by translate-or-withhold callers (e.g. substitution) that build the CodeableConcept only on a successful translation. #### Parameters ##### target [`CodedTarget`](#codedtarget) The translated target coding. ##### text? `string` The original free text (`CWE.9`) to carry as `CodeableConcept.text`, if any. #### Returns `FhirComplex` #### Example ```ts import { codeableConceptFromTarget, V2_0161_SYSTEM } from "@cosyte/transform"; const cc = codeableConceptFromTarget({ system: V2_0161_SYSTEM, code: "G" }); void cc; ``` *** ### collectAllergies() > **collectAllergies**(`msg`): readonly `Segment`[] Every `AL1` occurrence a message carries, in message order. One AllergyIntolerance is created per occurrence, per the IG message map's `0..-1` cardinality on the AL1 row. #### Parameters ##### msg `Hl7Message` The parsed `@cosyte/hl7` message. #### Returns readonly `Segment`[] #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // collectAllergies(parseHL7(raw)).length; // one entry per AL1 in the message ``` *** ### collectCoverages() > **collectCoverages**(`msg`): readonly `Segment`[] Every `IN1` occurrence a message carries, in message order. One Coverage is created per occurrence, per the IG message map's INSURANCE group. #### Parameters ##### msg `Hl7Message` The parsed `@cosyte/hl7` message. #### Returns readonly `Segment`[] #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // collectCoverages(parseHL7(raw)).length; // one entry per IN1 in the message ``` *** ### collectDiagnoses() > **collectDiagnoses**(`msg`): readonly `Segment`[] Every `DG1` occurrence a message carries, in message order. One Condition is created per occurrence, per the IG message map's `0..-1` cardinality on the DG1 row. #### Parameters ##### msg `Hl7Message` The parsed `@cosyte/hl7` message. #### Returns readonly `Segment`[] #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // collectDiagnoses(parseHL7(raw)).length; // one entry per DG1 in the message ``` *** ### collectProcedures() > **collectProcedures**(`msg`): readonly `Segment`[] Every `PR1` occurrence a message carries, in message order. One Procedure is created per occurrence, per the IG message map's `0..-1` cardinality on the PROCEDURE group. #### Parameters ##### msg `Hl7Message` The parsed `@cosyte/hl7` message. #### Returns readonly `Segment`[] #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // collectProcedures(parseHL7(raw)).length; // one entry per PR1 in the message ``` *** ### createNamingSystem() > **createNamingSystem**(`seed?`): [`NamingSystemRegistry`](#namingsystemregistry) Create a [NamingSystemRegistry](#namingsystemregistry). With no seed it resolves the built-in FHIR-core code systems and the two unambiguous HD auto-derivations (OID/UUID); a seed adds caller-vetted code systems and explicit assigning-authority URIs. #### Parameters ##### seed? [`NamingSystemSeed`](#namingsystemseed) = `{}` #### Returns [`NamingSystemRegistry`](#namingsystemregistry) #### Example ```ts import { createNamingSystem } from "@cosyte/transform"; const registry = createNamingSystem({ authorities: { HOSPMRN: "urn:oid:1.2.840.114350.1.13.1" }, }); registry.resolveCodeSystem("LN"); // => "http://loinc.org" ``` *** ### deferredCoverageIssues() > **deferredCoverageIssues**(`in1`): readonly [`TransformIssue`](#transformissue)[] The value-free diagnostics for the IN1 rows this library reads and deliberately does not build: the payer address and the two policy-holder rows (each an Organization resource), the subscriber (a Patient or RelatedPerson reference), and the relationship, whose value translation needs a table this library does not carry. Raised for the occurrence whether or not the Coverage itself is emitted, so a deferred row a message actually valued is in the issues list rather than silently absent. #### Parameters ##### in1 `Segment` The `IN1` `@cosyte/hl7` `Segment`. #### Returns readonly [`TransformIssue`](#transformissue)[] #### Example ```ts // deferredCoverageIssues(in1).length; // one per valued deferred row ``` *** ### deferredDiagnosisIssues() > **deferredDiagnosisIssues**(`dg1`): readonly [`TransformIssue`](#transformissue)[] The value-free diagnostics for the DG1 rows this library reads and deliberately does not build: the diagnosing clinician (a Practitioner resource), and the parent diagnosis (a reference resolved by identifier rather than by bundle position). Raised for the occurrence whether or not the Condition itself is emitted, so a deferred row a message actually valued is in the issues list rather than silently absent. #### Parameters ##### dg1 `Segment` The `DG1` `@cosyte/hl7` `Segment`. #### Returns readonly [`TransformIssue`](#transformissue)[] #### Example ```ts // deferredDiagnosisIssues(dg1).length; // one per valued deferred row ``` *** ### deferredProcedureIssues() > **deferredProcedureIssues**(`pr1`): readonly [`TransformIssue`](#transformissue)[] The value-free diagnostics for the PR1 rows this library reads and deliberately does not build: the three practitioner roles, the treating organizational unit, the parent procedure, and the code modifier whose target is not an R4 element. Raised for the occurrence whether or not the Procedure itself is emitted, so a deferred row a message actually valued is in the issues list rather than silently absent. #### Parameters ##### pr1 `Segment` The `PR1` `@cosyte/hl7` `Segment`. #### Returns readonly [`TransformIssue`](#transformissue)[] #### Example ```ts // deferredProcedureIssues(pr1).length; // one per valued deferred row ``` *** ### emitEncounterDiagnosis() > **emitEncounterDiagnosis**(`encounter`, `conditionFullUrls`, `gate`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> The Encounter that should replace the emitted one once the message's diagnoses exist: the same resource carrying the back-references, put through the conservative-emit gate first. When the gate refuses the linked draft the ORIGINAL Encounter is returned unchanged and the link is declared [ISSUE\_CODES.TRANSFORM\_ELEMENT\_DROPPED](#transform_element_dropped), so a detail never costs the visit. The gate is a parameter rather than a call into the assembler, so what "the gate refuses this" means stays in one place and the refusal branch is reachable from a test without a hand-built invalid resource: the same shape the allergy emit path uses. #### Parameters ##### encounter `FhirComplex` The Encounter node already in the bundle. ##### conditionFullUrls readonly `string`[] The fullUrls of the Conditions emitted for the same message, in order. ##### gate (`resource`) => `boolean` The conservative-emit predicate: `true` when the linked Encounter may replace it. #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts // emitEncounterDiagnosis(encounter, ["urn:uuid:cond-1"], () => true).value; // the linked one ``` *** ### enumerateSegmentOccurrences() > **enumerateSegmentOccurrences**(`msg`): readonly [`SegmentOccurrence`](#segmentoccurrence)[] Every segment occurrence of a parsed message, in message order. A position that carries neither a name nor any field content is the parser's ordinal placeholder for a blank line: it is not a segment occurrence, it is left out, and it still advances the ordinal of everything after it. A position that carries field content is an occurrence even when its name is empty. #### Parameters ##### msg `Hl7Message` The parsed message. #### Returns readonly [`SegmentOccurrence`](#segmentoccurrence)[] #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; import { enumerateSegmentOccurrences } from "@cosyte/transform"; // enumerateSegmentOccurrences(parseHL7(raw))[0]?.name; // "MSH" ``` *** ### fhirIssueTypeFor() > **fhirIssueTypeFor**(`code`): `string` The FHIR `issue-type` code for an [IssueCode](#issuecode): used by the `OperationOutcome` renderer. #### Parameters ##### code [`IssueCode`](#issuecode) The stable [IssueCode](#issuecode). #### Returns `string` #### Example ```ts import { fhirIssueTypeFor, ISSUE_CODES } from "@cosyte/transform"; fhirIssueTypeFor(ISSUE_CODES.TRANSFORM_IDENTIFIER_SYSTEM_UNRESOLVED); // "not-found" ``` *** ### insuranceCompanyName() > **insuranceCompanyName**(`in1`): `string` The insurance company name an IN1 carries, read from XON.1 of IN1-4, or `""` when that component carries nothing. The only row of the segment map that grounds the required `payor`. #### Parameters ##### in1 `Segment` The `IN1` `@cosyte/hl7` `Segment`. #### Returns `string` #### Example ```ts // insuranceCompanyName(in1); // the XON.1 organization name, verbatim ``` *** ### invertCodeMap() > **invertCodeMap**(`forward`): `Readonly`\<`Record`\<`string`, `string`\>\> Invert a forward v2-to-FHIR code map, keeping **only** the targets exactly one v2 code maps to. This is the bijective-subset rule as code. Where several v2 codes are "equivalent to" one FHIR concept (Table 0001 `O`/`A`/`N` all map to `other`), the inverse is ambiguous: the target is dropped here, and its caller flags `TRANSFORM_CODE_NOT_INVERTIBLE` rather than picking one member of the set. #### Parameters ##### forward `Readonly`\<`Record`\<`string`, `string`\>\> A v2 code to FHIR code map, as published by the IG ConceptMap. #### Returns `Readonly`\<`Record`\<`string`, `string`\>\> #### Example ```ts import { invertCodeMap } from "@cosyte/transform"; invertCodeMap({ F: "female", O: "other", A: "other" }); // => { female: "F" } ``` *** ### isIgMappedSegmentName() > **isIgMappedSegmentName**(`name`): `boolean` Whether the guide publishes a segment map for `name`, decided against [IG\_MAPPED\_SEGMENT\_NAMES](#ig_mapped_segment_names) and against nothing else. The comparison is exact: a name this library could not classify is not in the set, and is reported as a standard gap for that reason rather than because the guide was consulted and published nothing. That conflation is deliberate and is documented on the code itself; the alternative would be re-deriving a name the parser refused to vouch for. #### Parameters ##### name `string` A v2 segment name, exactly as the parser published it. #### Returns `boolean` #### Example ```ts import { isIgMappedSegmentName } from "@cosyte/transform"; isIgMappedSegmentName("DG1"); // true isIgMappedSegmentName("ZAL"); // false ``` *** ### issue() > **issue**(`code`, `v2Location`, `fhirPath?`): [`TransformIssue`](#transformissue) Construct a value-free [TransformIssue](#transformissue). The severity and message come from [ISSUE\_REGISTRY](#issue_registry); the caller supplies only positional metadata, so an issue can carry no PHI. #### Parameters ##### code [`IssueCode`](#issuecode) The stable [IssueCode](#issuecode). ##### v2Location `string` The v2 source location (segment / field / component index), never a value. ##### fhirPath? `string` The FHIR path the issue concerns, when applicable. #### Returns [`TransformIssue`](#transformissue) #### Example ```ts import { issue, ISSUE_CODES } from "@cosyte/transform"; const i = issue(ISSUE_CODES.TRANSFORM_TIMESTAMP_NO_TIMEZONE, "TS.1", "dateTime"); void i.severity; // "warning" ``` *** ### isWellFormedSegmentName() > **isWellFormedSegmentName**(`name`): `boolean` Whether a segment name may be rendered: the shape test of [SEGMENT\_IDENTIFIER\_SHAPE](#segment_identifier_shape) applied to the value itself. Deliberately NOT an exclusion of known-bad literals. An exclusion test admits any value it has not heard of, which is the wrong direction for a rule whose whole job is to keep message bytes out of a diagnostic, and it would silently couple this library to one release of the parser package. #### Parameters ##### name `string` The value the parser published as the occurrence's segment type. #### Returns `boolean` #### Example ```ts import { isWellFormedSegmentName } from "@cosyte/transform"; isWellFormedSegmentName("ZAL"); // true isWellFormedSegmentName("AL11"); // false ``` *** ### segmentOccurrenceLocation() > **segmentOccurrenceLocation**(`occurrence`): `string` The v2 location of an occurrence: `NAME[k]` for a name that passed the shape test, `[#n]` for one that did not. Those are the only two shapes, and neither carries a field or component index. #### Parameters ##### occurrence [`SegmentOccurrence`](#segmentoccurrence) The occurrence to locate. #### Returns `string` #### Example ```ts import { segmentOccurrenceLocation } from "@cosyte/transform"; // segmentOccurrenceLocation(occ); // "DG1[2]" or "[#4]" ``` *** ### toFhir() > **toFhir**(`msg`, `opts?`): [`TransformResult`](#transformresult) Transform a parsed HL7 v2 message into a FHIR R4 message `Bundle`. Never throws for a well-formed Hl7Message; every ambiguity, unmapped code, dropped element, or withheld resource surfaces as a value-free [TransformIssue](#transformissue). #### Parameters ##### msg `Hl7Message` A parsed `@cosyte/hl7` message. ##### opts? [`TransformOptions`](#transformoptions) = `{}` Transform options: the [TransformOptions.namingSystem](#namingsystem-1) registry, the naked- timestamp [TransformOptions.assumeTimezoneOffsetMinutes](#assumetimezoneoffsetminutes) policy, and a [TransformOptions.generateId](#generateid) allocator for reproducible fullUrls. #### Returns [`TransformResult`](#transformresult) #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; import { toFhir, createNamingSystem } from "@cosyte/transform"; const { bundle, issues } = toFhir(parseHL7(raw), { namingSystem: createNamingSystem() }); void bundle; void issues; ``` *** ### toFhirAddress() > **toFhirAddress**(`xad`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Convert a parsed HL7 v2 XAD to a FHIR `Address` node. Returns `{ value: undefined }` when the address carries no emittable part. #### Parameters ##### xad `XAD` A parsed `@cosyte/hl7` `XAD`. #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { toFhirAddress } from "@cosyte/transform"; const { value } = toFhirAddress({ street: "1 Main St", city: "Boston", stateOrProvince: "MA", addressType: "H" }); // value === Address { use: "home", line: ["1 Main St"], city: "Boston", state: "MA" } void value; ``` *** ### toFhirCodeableConcept() > **toFhirCodeableConcept**(`cwe`, `ctx?`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Convert a parsed HL7 v2 CWE (or CE) to a FHIR `CodeableConcept` node, fail-safe on the coding system and on unmapped codes. Returns `{ value: undefined }` when the element is empty. #### Parameters ##### cwe [`CodedElement`](#codedelement) A parsed `@cosyte/hl7` `CWE` (a `CE` is accepted as its subset). ##### ctx? [`TransformContext`](#transformcontext) = `{}` The transform context; `ctx.namingSystem` resolves the coding-system mnemonic. #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { toFhirCodeableConcept, createNamingSystem } from "@cosyte/transform"; const { value } = toFhirCodeableConcept( { identifier: "789-8", text: "Hemoglobin", nameOfCodingSystem: "LN" }, { namingSystem: createNamingSystem() }, ); // coding[0] === { system: "http://loinc.org", code: "789-8", display: "Hemoglobin" } void value; ``` *** ### toFhirCodeableConceptVia() > **toFhirCodeableConceptVia**(`cwe`, `map`, `ctx?`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Value-translate a coded v2 element to a FHIR `CodeableConcept` **via** a license-clean [CodedValueMap](#codedvaluemap), **additively** and fail-safe, never mutating or discarding what the message said: - **Primary code recognized as a bound-table code** ([translateBound](#translatebound): CWE.3 absent or names the bound table, and the code is in the IG map's mapped group) → the **primary** coding is emitted in the recognized source-table system (`map.sourceSystem`, carrying `CWE.7` version) and the **derived** target coding is *appended* when it differs (a remap such as `SC→SQ`); the **alternate triplet** (`CWE.4/5/6`, via buildCoding, with its own system resolution + flags) and the original `CWE.9` text are preserved. No primary unmapped flag: the value was faithfully recognized. - **Otherwise**: the primary declares a **foreign** coding system (not the bound table), the code is empty, or it is in the IG's `(unmapped)` group → this defers entirely to the **structural** [toFhirCodeableConcept](#tofhircodeableconcept), which preserves the raw coding (and its `CWE.4/5/6` alternate and `CWE.7` version) and flags it ([ISSUE\_CODES.TRANSFORM\_CODE\_UNMAPPED](#transform_code_unmapped) / `TRANSFORM_CODE_SYSTEM_UNRESOLVED`). A target is **never** fabricated, and a code that declares a foreign system is **never** asserted to be the standard concept. #### Parameters ##### cwe [`CodedElement`](#codedelement) The parsed `@cosyte/hl7` coded element (its `identifier` is the candidate table code). ##### map [`CodedValueMap`](#codedvaluemap) The license-clean value map to apply (e.g. [ROUTE\_VALUE\_MAP](#route_value_map)). ##### ctx? [`TransformContext`](#transformcontext) = `{}` The transform context (threaded to the structural path + the alternate-triplet resolver). #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { toFhirCodeableConceptVia, ROUTE_VALUE_MAP } from "@cosyte/transform"; const { value } = toFhirCodeableConceptVia({ identifier: "IM" }, ROUTE_VALUE_MAP); // coding === [{ system: ".../v2-0162", code: "IM" }, // { system: ".../v3-RouteOfAdministration", code: "IM", display: "Injection, intramuscular" }] void value; ``` *** ### toFhirDateTime() > **toFhirDateTime**(`ts`, `options?`): [`ConvertResult`](#convertresult)\<`string`\> Convert a parsed HL7 v2 timestamp to a FHIR `dateTime` lexical string, fail-safe on timezone and precision. Returns `{ value: undefined }` only when the timestamp is unparseable. #### Parameters ##### ts `TS` A parsed `@cosyte/hl7` `TS` (`DtmParts`). ##### options? [`TransformOptions`](#transformoptions) = `{}` Conversion policy; `assumeTimezoneOffsetMinutes` supplies a sender-asserted offset. #### Returns [`ConvertResult`](#convertresult)\<`string`\> #### Example ```ts import { parseDtm } from "@cosyte/hl7"; import { toFhirDateTime } from "@cosyte/transform"; const { value } = toFhirDateTime(parseDtm("20260721")); // value === "2026-07-21" void value; ``` *** ### toFhirEntityIdentifier() > **toFhirEntityIdentifier**(`field`): [`EntityIdentifier`](#entityidentifier) Read one EI (Entity Identifier) field as a FHIR `Identifier`, carrying EI.1 as `Identifier.value` and nothing else. The same reading orderIdentifier applies to the placer/filler order numbers, without the v2-0203 identifier type those rows assign. `Identifier.system` is deliberately left absent even when EI.2 to EI.4 name an assigning authority: a system URI is never synthesized from a bare namespace, because two senders reusing one namespace would otherwise collide. The caller is told the authority went unused through [EntityIdentifier.authorityValued](#authorityvalued) so it can raise its own value-free diagnostic. #### Parameters ##### field `Field` The `EI` field, exactly as the parser published it. #### Returns [`EntityIdentifier`](#entityidentifier) #### Example ```ts import { parseHL7 } from "@cosyte/hl7"; // const dg1 = parseHL7(raw).segments("DG1")[0]; // toFhirEntityIdentifier(dg1!.field(20)).identifier; // { value: "" } or undefined ``` *** ### toFhirHumanName() > **toFhirHumanName**(`xpn`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Convert a parsed HL7 v2 XPN to a FHIR `HumanName` node. Returns `{ value: undefined }` when the name carries no emittable part. #### Parameters ##### xpn `XPN` A parsed `@cosyte/hl7` `XPN`. #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { toFhirHumanName } from "@cosyte/transform"; const { value } = toFhirHumanName({ familyName: "Public", givenName: "Jane", nameTypeCode: "L" }); // value === HumanName { use: "official", family: "Public", given: ["Jane"] } void value; ``` *** ### toFhirIdentifier() > **toFhirIdentifier**(`cx`, `ctx?`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Convert a parsed HL7 v2 CX to a FHIR `Identifier` node, fail-safe on the assigning authority. Returns `{ value: undefined }` when the CX carries no identifier value at all. #### Parameters ##### cx `CX` A parsed `@cosyte/hl7` `CX`. ##### ctx? [`TransformContext`](#transformcontext) = `{}` The transform context; `ctx.namingSystem` resolves the assigning authority (HD). #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { toFhirIdentifier, createNamingSystem } from "@cosyte/transform"; const { value, issues } = toFhirIdentifier( { idNumber: "12345", assigningAuthority: { namespaceId: "HOSPMRN" } }, { namingSystem: createNamingSystem() }, ); // no registry entry for a bare "HOSPMRN" namespace → value emitted, system absent, one issue void value; void issues; ``` *** ### toFhirQuantity() > **toFhirQuantity**(`value`, `units`, `ctx?`): [`ConvertResult`](#convertresult)\<`FhirComplex`\> Convert a parsed HL7 v2 numeric value plus its units to a FHIR `Quantity` node, fail-safe on the unit. Returns `{ value: undefined }` when the numeric magnitude is absent or non-numeric (a Quantity without a value cannot be safely emitted). #### Parameters ##### value `NM` A parsed `@cosyte/hl7` `NM` (its `.raw` carries the exact lexical value). ##### units `CWE` The units component (an OBX-6 `CWE`); its CWE.1 is the candidate UCUM code. ##### ctx? [`TransformContext`](#transformcontext) = `{}` The transform context; used to recognize the UCUM coding system. #### Returns [`ConvertResult`](#convertresult)\<`FhirComplex`\> #### Example ```ts import { toFhirQuantity } from "@cosyte/transform"; const { value } = toFhirQuantity( { raw: "5.4", value: 5.4 }, { identifier: "mg/dL", nameOfCodingSystem: "UCUM" }, ); // value === Quantity { value: 5.4, unit: "mg/dL", system: "http://unitsofmeasure.org", code: "mg/dL" } void value; ``` *** ### toOperationOutcome() > **toOperationOutcome**(`issues`): `FhirComplex` Build a FHIR `OperationOutcome` FhirComplex from the given issues. An empty list still produces a structurally-valid `OperationOutcome` with a single `information` "all clear" issue (FHIR requires `OperationOutcome.issue` to be non-empty). #### Parameters ##### issues readonly [`TransformIssue`](#transformissue)[] The value-free diagnostics to render. #### Returns `FhirComplex` #### Example ```ts import { toOperationOutcome } from "@cosyte/transform"; const oo = toOperationOutcome([]); // oo is a FhirComplex resource node with resourceType "OperationOutcome" void oo; ``` *** ### toV2Observation() > **toV2Observation**(`resource`, `trigger`, `options?`): [`ReverseResult`](#reverseresult) Convert a FHIR R4 `Observation` into a **complete** v2 message carrying an `OBX` segment, in the result-report (`ORU`) shape. The `trigger` argument is **required and never inferred**: no `Observation` element maps to a v2 message trigger, so the caller supplies it, and a missing, empty or non-string one returns `{ value: undefined }` plus [ISSUE\_CODES.TRANSFORM\_MISSING\_TRIGGER](#transform_missing_trigger) without calling the message builder at all. It is used verbatim as MSH-9.2 under the `ORU` message code this shape fixes. The message carries the `OBX` alone: this shape's input is an `Observation`, which names no patient, and a subject segment assembled from a reference would be fabricated. Lossy by design and never round-trip-safe. An `OBX` field v2 requires (OBX-11 Observation Result Status) that this resource gives no source for stays absent and is declared with [ISSUE\_CODES.TRANSFORM\_V2\_REQUIRED\_FIELD\_ABSENT](#transform_v2_required_field_absent), rather than defaulted to `F`: a result reported as final that the sender never called final is the confidently wrong value this library exists to refuse. An `Observation` that grounds no `OBX` field at all yields no message and [ISSUE\_CODES.TRANSFORM\_NO\_V2\_MESSAGE\_EMITTED](#transform_no_v2_message_emitted). #### Parameters ##### resource `FhirNode` The FHIR `Observation` node. ##### trigger `string` The bare v2 trigger, e.g. `"R01"`. Required; never derived from the resource. ##### options? [`ReverseOptions`](#reverseoptions) = `{}` Caller-vetted reverse context: code systems and the MSH envelope. #### Returns [`ReverseResult`](#reverseresult) #### Example ```ts import { parseResource } from "@cosyte/fhir"; import { toV2Observation } from "@cosyte/transform"; const { resource } = parseResource( '{"resourceType":"Observation","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"789-8"}]}}', ); const { value, issues } = toV2Observation(resource, "R01"); // value.toString() carries "ORU^R01" in MSH-9, then an OBX whose OBX-3 is "789-8^^LN" void value; void issues; ``` *** ### toV2Patient() > **toV2Patient**(`resource`, `trigger`, `options?`): [`ReverseResult`](#reverseresult) Convert a FHIR R4 `Patient` into a **complete** v2 message carrying a `PID` segment, for the demographics-only trigger events (`A28`, `A31`, `A29`, ...) that carry a patient with no visit. The `trigger` argument is **required and never inferred**: no `Patient` element maps to a v2 message trigger, so the caller supplies it, and a missing, empty or non-string one returns `{ value: undefined }` plus [ISSUE\_CODES.TRANSFORM\_MISSING\_TRIGGER](#transform_missing_trigger) without calling the message builder at all. It is used verbatim as MSH-9.2 under the `ADT` message code this shape fixes; the caller never supplies the code and it never varies per call. Lossy by design and never round-trip-safe: a value the inverse of the IG map cannot ground is flagged and left absent, never guessed, and a resource of another type is refused outright. A `PID` field v2 requires (PID-3 Patient Identifier List, PID-5 Patient Name) that this resource gives no source for stays absent and is declared with [ISSUE\_CODES.TRANSFORM\_V2\_REQUIRED\_FIELD\_ABSENT](#transform_v2_required_field_absent): the emitted message is honest about what it lacks rather than padded to look conformant. A `Patient` that grounds no `PID` field at all yields no message and [ISSUE\_CODES.TRANSFORM\_NO\_V2\_MESSAGE\_EMITTED](#transform_no_v2_message_emitted). #### Parameters ##### resource `FhirNode` The FHIR `Patient` node (build one with `@cosyte/fhir`'s `parseResource`). ##### trigger `string` The bare v2 trigger, e.g. `"A28"`. Required; never derived from the resource. ##### options? [`ReverseOptions`](#reverseoptions) = `{}` Caller-vetted reverse context: assigning authorities, code systems, MSH envelope. #### Returns [`ReverseResult`](#reverseresult) #### Example ```ts import { parseResource } from "@cosyte/fhir"; import { toV2Patient } from "@cosyte/transform"; const { resource } = parseResource('{"resourceType":"Patient","gender":"female"}'); const { value, issues } = toV2Patient(resource, "A28"); // value.toString() starts "MSH|^~\\&|" and carries "ADT^A28" in MSH-9, then a PID segment void value; void issues; ``` *** ### translateBound() > **translateBound**(`cwe`, `map`): [`CodedTarget`](#codedtarget) \| `undefined` The bound-table target for a coded element's **primary** coding, or `undefined` when the map does not faithfully apply, i.e. the primary declares a coding system that is **not** this map's bound table (CWE.3 present and not in [CodedValueMap.sourceMnemonics](#sourcemnemonics)), the primary code is empty, or the code is in the IG map's `(unmapped)` group. Never asserts a standard translation for a foreign coding system, and never fabricates a target. #### Parameters ##### cwe [`CodedElement`](#codedelement) The parsed coded element (its `identifier` is the candidate source-table code). ##### map [`CodedValueMap`](#codedvaluemap) The license-clean value map to consider applying. #### Returns [`CodedTarget`](#codedtarget) \| `undefined` #### Example ```ts import { translateBound, ROUTE_VALUE_MAP } from "@cosyte/transform"; translateBound({ identifier: "IM" }, ROUTE_VALUE_MAP)?.code; // "IM" (v3 route) translateBound({ identifier: "IM", nameOfCodingSystem: "99LOCAL" }, ROUTE_VALUE_MAP); // undefined ``` *** ### withCodeableText() > **withCodeableText**(`concept`, `textValue`): `FhirComplex` The same `CodeableConcept` with its `text` replaced by `textValue`, so a segment row that targets `code.text` directly can override the original text the datatype map read out of the coded field. #### Parameters ##### concept `FhirComplex` The `CodeableConcept` node the datatype converter produced. ##### textValue `string` The text the segment map's own row assigns. #### Returns `FhirComplex` #### Example ```ts // withCodeableText(concept, "Diabetes mellitus") -> the same codings, that text ``` *** ### withEncounterDiagnosis() > **withEncounterDiagnosis**(`encounter`, `conditionFullUrls`): `FhirComplex` The same Encounter carrying one `diagnosis` entry per Condition the bundle holds, per the ADT_A01 message map's `Encounter[1].diagnosis.reference = Condition[1].id` row. Each entry's `condition` points at that Condition's `fullUrl`, so the link resolves inside the bundle. Applied after the diagnoses are built, because the Encounter joins the bundle before them and the produced nodes are immutable: this returns a new node rather than mutating the emitted one. #### Parameters ##### encounter `FhirComplex` The Encounter node already in the bundle. ##### conditionFullUrls readonly `string`[] The fullUrls of the Conditions emitted for the same message, in order. #### Returns `FhirComplex` #### Example ```ts // withEncounterDiagnosis(encounter, ["urn:uuid:cond-1"]); // adds one diagnosis entry ``` --- URL: https://docs.cosyte.com/transform/troubleshooting Markdown: https://docs.cosyte.com/transform/troubleshooting.md # Troubleshooting Common symptoms when converting v2 → FHIR, and how to read what the transform is telling you. ## A converter returned `value: undefined` Nothing could be **safely** emitted. This is by design, not an error: it happens for an empty input composite, an unparseable timestamp, or a numeric value that wasn't numeric. Check `issues` for the typed reason (e.g. `TRANSFORM_TIMESTAMP_INVALID`). ## A field I expected is missing from the output A missing FHIR element can come with a diagnostic explaining the refusal: - **`Identifier.system` is absent** → `TRANSFORM_IDENTIFIER_SYSTEM_UNRESOLVED`: the assigning authority wasn't resolvable. Seed it via `createNamingSystem({ authorities: { … } })`. The value is never attached to a guessed system. - **`Coding.system` is absent** → `TRANSFORM_CODE_SYSTEM_UNRESOLVED` (unknown mnemonic) or `TRANSFORM_CODE_UNMAPPED` (no coding system at all). The code is preserved verbatim, never invented. - **`HumanName.use` / `Address.use` is absent** → the v2 code has no equivalent in the IG's table map (`TRANSFORM_NAME_USE_UNMAPPED` / `TRANSFORM_ADDRESS_USE_UNMAPPED`). It is surfaced, never guessed. ## My timestamp lost its time-of-day `TRANSFORM_TIMESTAMP_NO_TIMEZONE`: the v2 timestamp had a time but no offset, and FHIR forbids a time without a zone, so it was reduced to date precision rather than assuming UTC (which would shift the clinical instant by hours). Supply `assumeTimezoneOffsetMinutes` if you know the sender's offset. ## My unit didn't populate `Quantity.code` `TRANSFORM_UNIT_NOT_UCUM`: the unit wasn't declared UCUM or failed the UCUM shape check, so it was preserved verbatim in `Quantity.unit` with `code`/`system` absent. Magnitudes are **never** converted (mg/dL ↔ mmol/L is analyte-dependent and unsafe to automate). ## My order has a TQ1 but the request carries no schedule The whole `Timing` is withheld the moment any part of the TQ1 cannot be grounded, and never partially built: a half-built schedule reads to the receiving system as a complete dosing instruction, so "every 4 hours, but only between meals" must not arrive as "every 4 hours". Look for a `TRANSFORM_ELEMENT_DROPPED` or `TRANSFORM_CODE_UNMAPPED` whose `v2Location` names the exact TQ1 field or repeat-pattern component that caused it (`TQ1.3.2`, `TQ1.6`, `TQ1.8`, …); the full list of causes is under [Known limitations](#known-limitations). Note that no `boundsPeriod` survives a refusal either, even when TQ1-7 and TQ1-8 were both perfectly usable: an unanchored pair of dates would read as an open regimen the message never authorized. ## My TQ1 free-text instruction didn't reach the resource TQ1-10 and TQ1-11 are `TX`, a v2 primitive with no component structure, so they are read **whole**: a raw `^`, `&` or `~` inside one is content, and a taper written `2 tabs^then 1 tab` arrives intact rather than truncated at the first delimiter. That holds when the delimiters are all the row carries: `^&~` is the same characters as `^leading` with the letters removed, and it arrives as the text it is rather than vanishing. Three cases still write nothing. An **absent** field and the HL7 **explicit null** (`""`, the wire saying the field carries no value) both write nothing and say nothing, because neither carried anything to drop. A field that **did** carry content whose whole projection resolves away (display markup such as a `\H\` / `\N\` highlight pair with nothing between, or, for TQ1-11 alone, nothing but whitespace, which R4's `txt-2` forbids in a narrative) writes nothing and raises a `TRANSFORM_ELEMENT_DROPPED` naming the row, so it is never confused with a field that was never sent. ## Are diagnostics safe to log? Yes. A `TransformIssue` carries only a stable code, a severity, a **positional** v2 location, and a FHIR path, **never a value**. Its `message` is static. Do not log the raw v2 message or the produced resource values; those carry PHI. ## Known limitations - **Message families: the IG-covered set.** `toFhir(msg)` assembles ADT → Patient + Encounter, ORU^R01 → DiagnosticReport + Observation, ORM_O01 / OML_O21 → ServiceRequest and RXO → MedicationRequest, and the thin IG singles VXU_V04 → Immunization, SIU_S12 → Appointment, and MDM_T02 → DocumentReference. An AL1 in any of them becomes an AllergyIntolerance, a DG1 a Condition, a PR1 a Procedure and an IN1 a Coverage, and a TQ1 accompanying an order becomes that order's schedule. The v2→FHIR direction is feature-complete for the IG-covered message set; terminology depth and profiles are not implemented. - **Schedule scope: TQ1 only, and four of its repeat-pattern components.** A TQ1 on an order group builds `dosageInstruction.timing` on a MedicationRequest and `occurrenceTiming` on a ServiceRequest, from TQ1-3's repeat-pattern code (HL70335), period quantity, period units and event code (the HL70528 rows the guide gives a `v3-TimingEvent` target), plus TQ1-7 / TQ1-8 as `repeat.boundsPeriod`. TQ2 is not read. TQ1-10 and TQ1-11 reach the two different targets the guide names on the medication path (`dosageInstruction.additionalInstruction.text` and the resource's own `text` narrative) and are flagged as dropped on the service path, where the guide targets an extension and an annotation this tier does not build. **A schedule is fully grounded or absent and flagged**, so the whole `Timing` is withheld with a `TRANSFORM_ELEMENT_DROPPED` or `TRANSFORM_CODE_UNMAPPED` naming the cause when: a repeat component has no target in the guide (calendar alignment, either end of the day-of-week range, institution-specified time, event offset, general timing specification, or a component past the eleven the datatype defines); a code sits outside its published table, or was sent under a coding system that is not that table (a site's local `AC` is not asserted to be the published "before meal" concept); a period arrives without its units, or written with a minus sign (R4's `tim-2` and `tim-5` reject both); a field narrows the schedule (TQ1-4, TQ1-5, TQ1-6, TQ1-12, TQ1-13, TQ1-14, each needing a rescale, an invented date or an unbuilt element); a bound is unusable or inverted; or more than one TQ1 accompanies one order, or one TQ1-3 carries more than one repeat pattern. A repetition of TQ1-3 that carries no value is not a second pattern: `Q4H~`, `~Q4H` and a second repetition the sender explicitly nulled each send one schedule, and it is read from whichever repetition carries it. TQ1-2 and TQ1-9 are the exception: they are flagged and the schedule still ships, because the dose and the priority already come from the RXO/OBR path and are left exactly as they were. - **Allergy scope: AL1 only, and `criticality` only.** `IAM` is not read (it keeps reporting `TRANSFORM_SEGMENT_NOT_EMITTED`), and `AllergyIntolerance.reaction.severity` is never populated: the IG names `criticality` the base target for AL1-4 and offers `reaction.severity` only as a local variation, conditioned on a severity that was not used equivalently to criticality, which no v2 message states. So an AL1-4 of `MO` or `U` leaves `criticality` absent with a `TRANSFORM_CODE_UNMAPPED`, rather than reappearing as a reaction grading. AL1-2 resolves `category` and `type` against two separate IG maps with different unmapped sets: `MA` yields a type and no category, `MC` yields neither, and each absence is flagged on its own. The original AL1-2 / AL1-4 code is always carried in the IG's `alternate-codes` extension, so an untranslated code is still on the resource. Two conditions withhold the whole allergy, each with a `TRANSFORM_ELEMENT_DROPPED` naming it: no Patient in the bundle to anchor `patient`, and an AL1-3 that grounds no allergen code and no allergen text. AL1-6 is read as `onsetDateTime` only for a message whose version identifier is readable and earlier than 2.7, the version that withdrew the field; on 2.7 or later, or when the version cannot be read, it is dropped and flagged. - **Diagnosis, procedure and coverage scope: what the three segment maps ground, and no more.** A DG1 becomes a Condition, a PR1 a Procedure and an IN1 a Coverage, each wired to the bundle Patient; with no Patient in the bundle every one of them is withheld and declared `TRANSFORM_ELEMENT_DROPPED`, naming the occurrence and the reference it could not anchor. **`Coverage.status` is never asserted.** No published row of the IN1 map grounds one, and R4's binding has no neutral member, so the element ships value-absent with a `data-absent-reason` of `unknown` and a `TRANSFORM_REQUIRED_ELEMENT_UNKNOWN`: read it as unknown, never as active coverage. **`Coverage.payor` names the insurer and resolves to nothing**: IN1-4.1 becomes a reference `display` with no literal reference, because no Organization resource is built, and an IN1 that names no insurance company is withheld entirely, since `payor` is required and nothing else grounds it. `Procedure.status` is the `unknown` the map's own row directs where the message context determines none; `completed` is never selected from a message that did not say so. A DG1-21 of `D` sets `verificationStatus` to `entered-in-error`, the one value the map assigns; every other Table 0206 action code leaves the element absent with a `TRANSFORM_CODE_UNMAPPED`. A DG1 that grounds neither a diagnosis code nor a description still becomes a Condition, with its empty `code` flagged, so it is visible rather than reading as an intentionally empty one. `Condition.clinicalStatus` is absent because no row grounds it, which can fail R4's `con-3` invariant under a full profile validator. **The rows that need a resource this tier does not build are declared, never silently dropped**: `Condition.asserter` (DG1-16), `Procedure.performer.actor` (PR1-8, PR1-11, PR1-12), `Procedure.location` (PR1-23), `Coverage.payer` (IN1-5), `Coverage.policyHolder` (IN1-10, IN1-11) and `Coverage.subscriber` (IN1-16) each raise a `TRANSFORM_ELEMENT_DROPPED` when their field is valued, and so do the rows whose reference resolves by identifier rather than by position (DG1-22, PR1-25), the CPT modifier concatenation whose target is not an R4 element (PR1-16), and the relationship whose value translation needs a table this library does not carry (IN1-17). A message carrying a DG1 raises one more, for the `EpisodeOfCare` its diagnoses are not tied to. **The visit's financial class (PV1-20) is declared only on an insured message**: the guide routes that field into the same `Coverage` an IN1 creates, so a message carrying both gets a `TRANSFORM_ELEMENT_DROPPED` naming it, while a message carrying no DG1, PR1 or IN1 gains nothing at all and produces exactly the bundle and issue list it produced before. - **Reverse (FHIR → v2) scope: two shapes, deliberately.** `toV2Patient` emits an `ADT`-shaped message carrying a `PID`, `toV2Observation` an `ORU`-shaped message carrying an `OBX`. Both require the caller to pass the v2 trigger (no resource carries one, so it is never inferred: a missing one returns no message and a `TRANSFORM_MISSING_TRIGGER` diagnostic). The direction is **lossy by design and not a round-trip**: a mapping row whose inverse is ambiguous is refused (`TRANSFORM_CODE_NOT_INVERTIBLE`), an element with no v2 field in this map is flagged (`TRANSFORM_NO_V2_TARGET`), and a value v2 cannot carry unchanged is left out (`TRANSFORM_VALUE_NOT_REPRESENTABLE`). Emitting a `Patient` **and** an `Encounter` together as a visit-carrying ADT is not implemented. - **An emitted message can be missing a field v2 requires, and it tells you so.** PID-3 (Patient Identifier List), PID-5 (Patient Name) and OBX-11 (Observation Result Status) are required fields with no safe default: a resource that carries no source for one leaves it absent, never a fabricated placeholder, and raises one `TRANSFORM_V2_REQUIRED_FIELD_ABSENT` per field naming the v2 location and the FHIR path it would have come from. Supply the missing element on the resource, or repair the message before you send it. If nothing in the resource grounds any field of the target segment, there is no message to repair: the call returns `value: undefined` and one `TRANSFORM_NO_V2_MESSAGE_EMITTED`, which is how an empty-handed conversion is told apart from a successful one. - **Thin-IG-single scope**: each family covers the single trigger the IG maps and the resource-internal fields; references to resources this tier does not yet build (Immunization performer/manufacturer/location, Appointment practitioner/location participants, DocumentReference author/authenticator) are deferred and flagged, never dangling. `Immunization.status` follows the IG's three conditioned rows (RXA-21 = `D` → `entered-in-error`, unvalued RXA-20 → `completed`, else the HL70322 map); a required status the IG cannot ground withholds the resource (a valued-but-unmapped RXA-20, an IG-unmatched SCH-25, or a non-`AV` TXA-19). The Appointment patient participant's IG-unsourced required `status` is a `data-absent-reason` primitive, and the MDM document body is base64-encoded verbatim (the IG-assigned `application/text` / `text/hl7v2` contentType), carried and never interpreted. - **ORU scope**: `DiagnosticReport.category` is not defaulted (the IG segment map sets none; it is realm-dependent), the results graph uses the first PID/PV1 (multiple patient result groups are not handled), and OBR performers, the OBR-sourced specimen row and `basedOn` ServiceRequest are deferred. An OBX value type with no settled FHIR `value[x]` (`RP`, whose extension target the guide itself marks unresolved, and any type the guide does not map) preserves the raw value as `valueString` and flags it, never a fabricated typed value. `DR`, `NR`, `TM` and `NA` reach `valuePeriod`, `valueRange`, `valueTime` and `valueSampledData`, and an `ED` whose OBX-5.4 is `Base64` reaches the guide's named `valueAttachment` extension with its payload carried byte-for-byte, undecoded; each of them still falls back to the raw text plus a flagged drop when the value cannot be carried faithfully (a `TM` with a UTC offset, a magnitude FHIR `decimal` cannot hold unaltered, an `ED` under any other encoding). A `SampledData` never asserts an `origin` or a `period`: the guide sources neither, so both ship value-absent with a `data-absent-reason` and a `TRANSFORM_REQUIRED_ELEMENT_UNKNOWN`. An `SPM` in an ORU becomes a `Specimen` the scoping `DiagnosticReport` references, withheld with that report rather than left orphaned, and its `Specimen.status` is never asserted (the guide reaches it only through a Table 0136 value map this library does not carry). An `NTE` inside the OBSERVATION group becomes `Observation.note`; the PATIENT-level and ORDER_OBSERVATION-level `NTE` rows have no published target and reach nothing. - **Terminology value translation**: coded fields with an IG `mappedVia` value ConceptMap are value-translated via `toFhirCodeableConceptVia`, covering RXR route/site (HL70162/HL70550), SCH-8 appointment type (HL70277), RXO-9 substitution (HL70161), OBR-5 priority (HL70485), and the AL1 allergy tables (HL70127 to category and to type, HL70128 to criticality, plus the two original-code identity maps). Each map is transcribed and verified firsthand against the raw published IG ConceptMap JSON; a source code the IG leaves in its `(unmapped)` group is flagged (`TRANSFORM_CODE_UNMAPPED`), never coerced. Two fields the IG maps into **SNOMED CT** (RXR-4 method, SCH-7 reason) stay structural (SNOMED is not bundled; BYO ConceptMap), and fields the IG ships no value map for (TXA-2 document type, RXA-5 vaccine code) are carried as-is. The built-in NamingSystem code-system seed is still the FHIR-core-fixed systems; the full HL7 THO crosswalk beyond these maps is not implemented. - **No terminology content, no unit conversion, R4-only.** --- # @cosyte/terminology > Healthcare terminology engine: validate and map codes across code systems. These are the 7 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/terminology/intro Markdown: https://docs.cosyte.com/terminology/intro.md # @cosyte/terminology Answer FHIR terminology questions over your own code-system data, in one line: without standing up a terminology server. `@cosyte/terminology` is a **zero-dependency terminology engine** for US healthcare code systems. It is **not a wire parser**; it mirrors the FHIR **Terminology Module** (`$translate`, `$lookup`, `$validate-code`, `$expand`, …), operating over **consumer-supplied** FHIR resources. It ships the **engine, and no code-system release**: SNOMED CT, CPT, LOINC, UMLS/RxNorm and VSAC value sets are strictly bring-your-own. It is not content-free, though. What is bundled includes the following, with its copyright: - the **UCUM unit table** (`ucum-essence.xml` v2.2), copyright ©1999-2024 Regenstrief Institute, Inc., reproduced verbatim under the UCUM Copyright Notice and License ([https://ucum.org/license](https://ucum.org/license)) and embedded byte-for-byte in the published build. The UCUM Specification is provided "as is" without warranty of any kind; the full notice ships with the package at `vendor/ucum/NOTICE.md`; - the code-system **identities** (the OID ↔ canonical-URI pairings), encoded and grounded firsthand against the HL7/FHIR registry: these identify the systems, they are not any system's code list; - the **SNOMED CT concepts the crosswalk resolver names**, each with its description: the four map-category concepts a complex-map row's `mapCategoryId` refers to, and the two gender findings a gender `IFA` rule is written against. SNOMED CT is copyright © International Health Terminology Standards Development Organisation; no SNOMED CT refset or release is bundled; - **RxNorm's relationship and term-type names** the drug-graph API is written against (`RELA`, `RELA_INVERSE`, `TERM_TYPES`), each with its name as published by the U.S. National Library of Medicine, which produces RxNorm. Individual RxNorm identifiers also appear in the API documentation examples, with their RxNorm names and term types. No RxNorm release is bundled. That states what is distributed and under whose copyright. It is not legal advice. > **Status:** `0.1.0`, published on npm. The public API is settled: below `1.0.0` a breaking change > ships in a new minor version and is called out in the changelog. This release ships the code-system > identity resolver, the ConceptMap `$translate` engine, the CodeSystem `$lookup`/`$validate-code` > loaders, ValueSet `$expand`/binding, UCUM validation, the crosswalk resolvers (GEMs + > SNOMED→ICD-10-CM), and the RxNorm drug graph. ## Install ```bash npm install @cosyte/terminology ``` ## Translate a code through a ConceptMap ```ts import { loadConceptMap, translate } from "@cosyte/terminology"; const map = loadConceptMap(myConceptMapJson); // a standard FHIR R4 ConceptMap resource const result = translate({ system: "http://loinc.org", code: "2160-0" }, map); if (result.unmapped) { // A first-class, surfaced outcome: never a guessed target. } else { for (const m of result.matches) { // m.target (a Coding), m.relationship, m.equivalence, m.comment } } ``` The engine is **conservative on assertion**: an unmapped source is a typed `unmapped`, never a fabricated target, and a directional map is never silently inverted. ## Next - [Quickstart](https://docs.cosyte.com/terminology/quickstart.md): resolve a system and translate a code, runnable. - [Core Concepts](https://docs.cosyte.com/terminology/concepts-archetype.md): the engine model and the never-fabricate invariant. - Read the **API reference** for every export, generated from source. --- URL: https://docs.cosyte.com/terminology/installation Markdown: https://docs.cosyte.com/terminology/installation.md # Installation `@cosyte/terminology` is a zero-dependency TypeScript package for Node.js. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration. > **Status:** `0.1.0`, published on npm. The command below installs the real package. ## Prerequisites - **Node.js >= 22** (the whole `@cosyte/*` suite targets ES2023 / Node 22+). - A package manager: `pnpm`, `npm`, or `yarn`. ## Install ```bash npm install @cosyte/terminology ``` ## Smoke test Confirm the package resolves and its version symbol is present: ```ts import { VERSION } from "@cosyte/terminology"; console.log(VERSION); ``` If that prints a version string, the install is good: head to the [Quickstart](https://docs.cosyte.com/terminology/quickstart.md). --- URL: https://docs.cosyte.com/terminology/quickstart Markdown: https://docs.cosyte.com/terminology/quickstart.md # Quickstart `@cosyte/terminology` is a **terminology engine**, not a wire parser: it answers FHIR terminology questions over **consumer-supplied** resources (the code-system identity resolver, the ConceptMap `$translate` engine, the CodeSystem `$lookup` / `$validate-code` operations, and the ValueSet `$expand` / binding operations) with one non-negotiable rule: **it never fabricates a code, and never treats an answer it could not compute as if it had**. ## Resolve a code system to its canonical URI A `Coding` off any parser (HL7 v2, C-CDA, FHIR) might name its system by OID, mnemonic, or URI. `resolveSystem` canonicalizes it, and returns a **typed unknown** rather than guessing when it does not recognize the identifier. ```ts runnable import { resolveSystem, isUnknownSystem } from "@cosyte/terminology"; const loinc = resolveSystem("2.16.840.1.113883.6.1"); isUnknownSystem(loinc) ? "?" : loinc.url; // => "http://loinc.org" ``` ## Translate a code through a ConceptMap Feed the engine a standard FHIR `ConceptMap` resource, then translate a `Coding` through it. The data is **bring-your-own**: the engine ships no map content. ```ts runnable import { loadConceptMap, translate } from "@cosyte/terminology"; const map = loadConceptMap({ resourceType: "ConceptMap", group: [ { source: "http://hl7.org/fhir/administrative-gender", target: "http://terminology.hl7.org/CodeSystem/v2-0001", element: [{ code: "male", target: [{ code: "M", equivalence: "equivalent" }] }], }, ], }); const result = translate( { system: "http://hl7.org/fhir/administrative-gender", code: "male" }, map, ); result.unmapped; // => false ``` ## An unmapped source is surfaced, never guessed When a source does not map, the result is a **typed `unmapped`** carrying the source and any declared fallback mode: never a fabricated target. ```ts runnable import { loadConceptMap, translate } from "@cosyte/terminology"; const map = loadConceptMap({ resourceType: "ConceptMap", group: [] }); const result = translate({ system: "http://loinc.org", code: "2160-0" }, map); result.unmapped; // => true ``` ## Look up a code in a code system Load a **consumer-supplied** release (RRF, CSV, fixed-width, or a FHIR `CodeSystem` JSON, the engine ships no code-system release) and resolve a code to its display and status. An unknown code is a typed `{ found: false }`, never a fabricated display. ```ts runnable import { loadCodeSystem, lookup } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", url: "http://example.org/labs", concept: [{ code: "2160-0", display: "Creatinine [Mass/volume] in Serum or Plasma" }], }, }); const hit = lookup(cs, "2160-0"); hit.found ? hit.display : "?"; // => "Creatinine [Mass/volume] in Serum or Plasma" ``` An unknown code never yields a guessed display: ```ts runnable import { loadCodeSystem, lookup, validateCode } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", concept: [{ code: "A", display: "Alpha" }] }, }); lookup(cs, "ZZZ").found; // => false validateCode(cs, "ZZZ").valid; // => false ``` ## Expand a value set and test membership Load a **consumer-supplied** FHIR `ValueSet` and expand its `compose` over the code systems you supply. `$expand` returns the flattened membership plus an honest `complete` flag; binding (`validateCodeInValueSet`) answers whether a code is allowed in a slot. ```ts runnable import { loadCodeSystem, loadValueSet, expand } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", url: "http://example.org/cs", concept: [{ code: "a" }, { code: "b" }, { code: "c" }], }, }); const vs = loadValueSet({ resourceType: "ValueSet", compose: { include: [{ system: "http://example.org/cs", concept: [{ code: "a" }, { code: "b" }] }], }, }); const result = expand(vs, { codeSystems: new Map([["http://example.org/cs", cs]]) }); result.complete; // => true ``` ```ts runnable import { loadValueSet, validateCodeInValueSet } from "@cosyte/terminology"; const vs = loadValueSet({ resourceType: "ValueSet", compose: { include: [{ system: "http://example.org/cs", concept: [{ code: "a" }] }] }, }); const member = validateCodeInValueSet({ system: "http://example.org/cs", code: "a" }, vs); member.undetermined === false && member.result; // => true ``` ## A value set it cannot expand is surfaced, never guessed When a part of the value set cannot be computed (a code system you did not supply, a truncated server expansion, an unimplemented filter), the answer is a typed **`undetermined`**, never a fabricated "not a member" (a false negative on a binding is a clinical error). ```ts runnable import { loadValueSet, validateCodeInValueSet } from "@cosyte/terminology"; // An intensional `is-a` filter whose code system was not supplied cannot be evaluated. const vs = loadValueSet({ resourceType: "ValueSet", compose: { include: [ { system: "http://example.org/cs", filter: [{ property: "concept", op: "is-a", value: "root" }] }, ], }, }); const check = validateCodeInValueSet({ system: "http://example.org/cs", code: "x" }, vs); check.undetermined; // => true ``` ## Crosswalk between classifications (ICD-9↔ICD-10 GEMs) The CMS **GEMs** are *public-domain* reference mappings between ICD-9-CM and ICD-10-CM. Load the file (bring-your-own, it is a free CMS download) in its authored direction and apply it. A 1:many source returns the **full** candidate set; a No-Map source is a typed outcome, never a fabricated code. ```ts runnable import { loadGems, applyGem } from "@cosyte/terminology"; // Synthetic rows in the CMS GEM format: `source target flags` (flags: approximate|no-map|combination|scenario|choice-list). const gems = loadGems({ direction: "9-to-10", version: "2018", content: "25000 E119 10000\nV290 NoDx 11000\n", }); const hit = applyGem(gems, "25000"); hit.mapped; // => true ``` A source the steward marked **No-Map** is surfaced as a typed result, never a guessed target: ```ts runnable import { loadGems, applyGem } from "@cosyte/terminology"; const gems = loadGems({ direction: "9-to-10", content: "V290 NoDx 11000\n" }); const r = applyGem(gems, "V290"); !r.mapped && r.noMap && r.code; // => "TERM_CROSSWALK_NO_MAP" ``` The forward (9→10) and backward (10→9) GEM files are **separate, non-inverse** artifacts: asking the engine to invert one throws, by design: ```ts runnable throws import { loadGems, invertGem } from "@cosyte/terminology"; const gems = loadGems({ direction: "9-to-10", content: "0010 A000 00000\n" }); invertGem(gems); // throws TERM_MAP_NOT_INVERTIBLE ``` ## Resolve a SNOMED CT → ICD-10-CM complex map (BYO) The NLM SNOMED→ICD-10-CM map is *rule-based* and **context-dependent**. The engine ships the rule machinery; the map rows are **yours** (SNOMED CT is licensed, the engine bundles no SNOMED CT refset or release, only the individual concepts the resolver names: the map categories and the two gender findings). Each map group resolves against the patient context you supply; a rule needing context you did **not** supply is surfaced as `context-required`, never a guessed branch. ```ts runnable import { loadComplexMap, applyComplexMap } from "@cosyte/terminology"; const map = loadComplexMap({ format: "rows", rows: [ { source: "72098002", group: 1, priority: 1, rule: "IFA 248152002 | Female (finding) |", advice: "MAP IS CONTEXT DEPENDENT FOR GENDER", target: "O00.9", category: "447639009" }, { source: "72098002", group: 1, priority: 2, rule: "OTHERWISE TRUE", advice: "CANNOT BE CLASSIFIED WITHOUT GENDER", category: "447638001" }, ], }); // No gender in context ⇒ the engine refuses to pick a branch. const stalled = applyComplexMap(map, "72098002", {}); stalled.mapped && stalled.groups[0]?.outcome; // => "context-required" ``` ## Navigate the RxNorm drug graph (BYO release) RxNorm's drug graph (ingredient → clinical drug → branded drug, with brand and dose-form cross-links) is loaded from a **bring-your-own** RxNorm RRF release (`RXNCONSO` + `RXNREL`, and `RXNSAT` for NDCs; the engine bundles **no** RxNorm release). Relationships are read in RxNorm's documented direction (`RELA` is the relationship the **second** `RXCUI` has to the **first**), and navigation follows only **authored** edges: the engine never synthesizes an inverse. ```ts runnable import { loadRxNormGraph, ingredientsOf } from "@cosyte/terminology"; // Synthetic rows in the real RxNorm RRF wire format (RXNCONSO 18 cols, RXNREL 16 cols). // "SCDC(2) has_ingredient IN(1)": subject=RXCUI2=2, object=RXCUI1=1. const graph = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n" + "2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||", rel: "1|||RN|2|||has_ingredient|||RXNORM||||||", }); const r = ingredientsOf(graph, "2"); r.found && r.targets[0]?.name; // => "lisinopril" ``` The ingredient edge is authored on the **component**, not on the clinical drug. That is RxNorm's own topology, not a simplification of the example. `has_ingredient` is authored from the **clinical** side (`SCDC`/`SCDF`/`SCDG`) to the ingredient `IN`, and from the **branded** side (`SBD`/`SBDC`/`SBDF`/`SBDG`) to the **brand name** `BN` rather than to the active ingredient. RxNorm authors **no** `SCD ⟶ IN` edge at all, so asking a clinical drug for its ingredients is an honest empty answer rather than a guessed one. The **precise** forms (`SCDFP`, `SBDFP`, `SCDGP`) answer empty too: an `SCDFP` reaches its basis-of-strength substance(s) by `has_boss`, a one-to-many relation whose targets can be a `PIN` or a plain `IN` and which `ingredientsOf` does not follow, and the other two author no ingredient edge at all. A concept takes its term type from its **defining** atom, wherever that sits in `RXNCONSO`. `PSN`, `SY` and `TMSY` type a *name* rather than a concept, so they never establish one. An `RXCUI` no defining atom could type is left out of the graph and reported on `graph.warnings` as `TERM_RXNORM_UNTYPED_CONCEPT`, rather than surfacing under a term type it does not have. To reach an active ingredient, walk to the **clinical** component and take its edge. From an `SCD` that is two hops through `consists_of`: ```ts runnable import { loadRxNormGraph, consistsOf, ingredientsOf } from "@cosyte/terminology"; const graph = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n" + "2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||\n" + "3|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||", rel: "1|||RN|2|||has_ingredient|||RXNORM||||||\n" + "2|||RN|3|||consists_of|||RXNORM||||||", }); // The clinical drug carries no ingredient edge of its own. const direct = ingredientsOf(graph, "3"); direct.found && direct.targets.length; // => 0 // Hop 1: the drug's component. Hop 2: that component's ingredient. const parts = consistsOf(graph, "3"); const component = parts.found ? parts.targets[0]?.rxcui : undefined; const viaComponent = ingredientsOf(graph, component ?? ""); viaComponent.found && viaComponent.targets[0]?.name; // => "lisinopril" ``` From a **branded** drug the same walk needs one more decision, so do not take the first component blindly. An `SBD`'s `consists_of` returns **both** its branded component (an `SBDC`, whose own ingredient edge is the brand name again) and the clinical `SCDC`; it is the `SCDC` that leads to the `IN`. Select on the component's `tty`: ```ts runnable import { loadRxNormGraph, consistsOf, ingredientsOf } from "@cosyte/terminology"; const graph = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n" + "2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||\n" + "4|ENG||||||||||RXNORM|BN||Zestril||N||\n" + "5|ENG||||||||||RXNORM|SBDC||lisinopril 10 MG [Zestril]||N||\n" + "6|ENG||||||||||RXNORM|SBD||lisinopril 10 MG Oral Tablet [Zestril]||N||", rel: "1|||RN|2|||has_ingredient|||RXNORM||||||\n" + "4|||RN|5|||has_ingredient|||RXNORM||||||\n" + "4|||RN|6|||has_ingredient|||RXNORM||||||\n" + "5|||RN|6|||consists_of|||RXNORM||||||\n" + "2|||RN|6|||consists_of|||RXNORM||||||", }); // The branded drug's own ingredient edge is its BRAND NAME, not the active ingredient. const direct = ingredientsOf(graph, "6"); direct.found && direct.targets[0]?.tty; // => "BN" // So pick the CLINICAL component, then take its ingredient edge. const parts = consistsOf(graph, "6"); const clinical = parts.found ? parts.targets.find((c) => c.tty === "SCDC")?.rxcui : undefined; const active = ingredientsOf(graph, clinical ?? ""); active.found && active.targets[0]?.name; // => "lisinopril" ``` An `RXCUI` absent from the loaded release is a typed unknown, never a guessed concept: ```ts runnable import { loadRxNormGraph, ingredientsOf } from "@cosyte/terminology"; const graph = loadRxNormGraph({ conso: "", rel: "" }); const r = ingredientsOf(graph, "99999999"); !r.found && r.code; // => "TERM_RXNORM_UNKNOWN_RXCUI" ``` Approximate name matching is an **opt-in, explicitly labeled** path: never the default, and never an exact code assertion (every candidate is marked `approximate: true`): ```ts runnable import { loadRxNormGraph, approximateMatch } from "@cosyte/terminology"; const graph = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||", rel: "", }); approximateMatch(graph, "lisinopril")[0]?.approximate; // => true ``` > **About runnable examples.** Blocks tagged ```` ```ts runnable ```` are extracted, run against the > package, and their `// =>` results asserted, so a documented example can never silently drift from > the code. A ```` ```ts runnable throws ```` block must throw. ## Next - [Core Concepts](https://docs.cosyte.com/terminology/concepts-archetype.md): the engine model and the never-fabricate invariant. - **API Reference**: every export, generated from source. --- URL: https://docs.cosyte.com/terminology/concepts-archetype Markdown: https://docs.cosyte.com/terminology/concepts-archetype.md # Core Concepts `@cosyte/terminology` is a **terminology engine**, not a wire-format parser. It mirrors the FHIR **Terminology Module**, the industry's own architecture for keeping terminology a _swappable service_ separate from the data that uses it, so its operations (`$translate`, `$lookup`, `$validate-code`, ValueSet `$expand` / binding, UCUM unit validation, and the published crosswalk resolvers) speak the FHIR shape every downstream tool already understands. ## Bring-your-own data, engine-only The engine ships **no code-system release**. You feed it standard FHIR resources (a `ConceptMap`, and later a `CodeSystem`/`ValueSet`), and it answers questions over them. SNOMED CT, CPT, LOINC, the UMLS/RxNorm release, and VSAC value sets are **strictly consumer-supplied** (a licensing wall, not a feature gap). What is bundled, named with its copyright, includes the UCUM unit table (copyright ©1999-2024 Regenstrief Institute, Inc., verbatim under [https://ucum.org/license](https://ucum.org/license); notice shipped at `vendor/ucum/NOTICE.md`), the SNOMED CT concepts the crosswalk resolver names (the map-category concepts and the two gender findings; SNOMED CT is copyright © International Health Terminology Standards Development Organisation), the code-system **identities**, the OID ↔ canonical-URI ↔ mnemonic pairings, which identify the systems rather than listing any system's codes, and RxNorm's relationship and term-type names (`RELA`, `RELA_INVERSE`, `TERM_TYPES`), published by the U.S. National Library of Medicine. Individual SNOMED CT, ICD-10-CM and RxNorm identifiers also appear in the API documentation examples. The full list is in the package's `LICENSE`. ## The never-fabricate invariant The single rule the whole library is built around: **the engine never invents a code, display, unit, or map target.** - A `$translate` over an **unmapped** source returns a typed `unmapped` result (surfaced, carrying any declared `group.unmapped` fallback mode), **never** a guessed target. - Every target `translate` returns is drawn **verbatim** from the supplied map. - An unrecognized code system resolves to a typed `unknown`, never a guessed URI. - A ValueSet `$expand` whose parts cannot all be computed (a missing code system, an unimplemented filter, a truncated server expansion, a component pinned to a code system version the release you supplied does not agree with) returns `complete: false` with a typed `TERM_VALUESET_CANNOT_EXPAND` / `TERM_VALUESET_EXPANSION_TRUNCATED` diagnostic: the `contains` set is a **lower bound**, never a silently-empty membership. - A code a `compose` component **enumerates** that the release you supplied for that component's system does not define is **not** a member: it is left out of `contains`, and the drop is a typed `TERM_VALUESET_ENUMERATED_CODE_UNDEFINED` located on that component rather than a silent one. Enumerating a code is the value set's claim that it exists, and returning it while holding the very release that shows it does not would be the engine deciding against its own evidence. That answer stays `complete: true`: the engine decided it, so `contains` is the whole of it. Where you supplied no usable release for that system, the engine holds no contrary evidence and every enumerated code stands, unchanged. - ValueSet binding (`validateCodeInValueSet`) returns a **decided** `result` only when it can prove membership; otherwise a typed `undetermined`: never a fabricated "not a member", because a false negative on a binding is a clinical error. ## The never-invert invariant Real steward maps (SNOMED→ICD-10-CM, the GEMs) are approximate, 1:many, and **non-invertible**. `translate` reads a map in its **authored direction only**: it matches a source coding against the map's _source-side_ codes and never against targets, so a directional map cannot be run backwards. Reverse translation requires an explicit inverse map; it is never synthesized. The CMS GEMs make this explicit: the forward (9→10) and backward (10→9) files are **separate artifacts**, and `invertGem` throws `TERM_MAP_NOT_INVERTIBLE` rather than fabricate a transpose. ## The crosswalk resolvers The published reference maps are the library's highest-risk surface, and the stewards say so in as many words. CMS: _"GEMs are not crosswalks. They are reference mappings"_; NLM: the SNOMED→ICD-10-CM map is _"semi-automated."_ The resolvers honour that. - **ICD-9↔ICD-10 GEMs** (`loadGems` / `applyGem`): CMS **public-domain**. Each entry carries the steward's 5-position flags (**approximate | no-map | combination | scenario | choice-list**), surfaced verbatim. A 1:many source returns the **whole** candidate set; a combination source surfaces its scenario→choice-list structure so a caller can build valid clusters (one target per choice list); a `NoDx` **No-Map** source is the typed `TERM_CROSSWALK_NO_MAP`, distinct from a source simply **absent** from the file (`TERM_CROSSWALK_UNMAPPED`). - **SNOMED CT → ICD-10-CM complex map** (`loadComplexMap` / `applyComplexMap`): **BYO** (SNOMED is licensed; the engine bundles **no** SNOMED CT refset or release, but it bundles the rule machinery and the individual concepts that machinery names, the map categories and the two gender findings). A source's **map groups** are an AND (a manifestation code _and_ an etiology code → two groups); within a group, **priorities** are an if-then-else chain of `IFA` rules evaluated against caller-supplied `PatientContext` (age band, gender). A group whose decision needs context the caller did **not** supply is the typed `TERM_CROSSWALK_CONTEXT_REQUIRED`: the candidate rules and Map Advice ride through, and the engine refuses to pick a branch it lacks the data for. Steward Map Categories (`447638001` cannot-classify, `447639009` context-dependent, `447640006` ambiguous) are carried verbatim. Both are **directional and never inverted**, and both extend the same never-fabricate rule: a crosswalk never invents a target, and "No-Map" is a first-class typed outcome, never an empty success. ## The RxNorm drug graph RxNorm's drug graph, ingredient (`IN`) → clinical-drug component (`SCDC`) → semantic clinical drug (`SCD`) → semantic branded drug (`SBD`), with brand (`BN`) and dose-form (`DF`) cross-links, is navigated over a **bring-your-own** RxNorm RRF release (`loadRxNormGraph` reads `RXNCONSO` concepts, `RXNREL` relationships, and `RXNSAT` NDC attributes). The engine bundles **no** RxNorm release; the graph is entirely the caller's release, and a resolution is release-scoped. - **Direction is a documented trap, grounded firsthand.** RxNorm's `RXNREL` stores each relationship as _"the relationship which the **second** concept (`RXCUI2`) HAS TO the **first** (`RXCUI1`)"_ (NLM RxNorm Technical Documentation §12.7; UMLS Reference Manual). So a row is read `RXCUI2 ⟶RELA⟶ RXCUI1`, and the loader normalizes every edge to `subject = RXCUI2`, `object = RXCUI1`. A `has_ingredient` row therefore puts the concept that _has_ the ingredient in `RXCUI2` and what it has in `RXCUI1`. Getting this backwards is a wrong-medication bug, so it is pinned by fixtures. - **Direction is not topology.** Knowing how to read a row does not tell you which rows exist, and RxNorm's ingredient topology is not the obvious one. `has_ingredient` is authored from the **clinical** side (`SCDC`/`SCDF`/`SCDG`) to the ingredient `IN`, and from the **branded** side (`SBD`/`SBDC`/`SBDF`/`SBDG`) to the **brand name** `BN` rather than to the active ingredient. There is **no** `SCD ⟶ IN` edge in any release, so `ingredientsOf` on a clinical drug answers with an honest empty set, and an active ingredient is reached by walking to the **clinical** component and taking its edge (`SCD ⟶ SCDC ⟶ IN`; from an `SBD`, `consists_of` returns the `SBDC` as well as the `SCDC`, and it is the `SCDC` that leads to the `IN`). Likewise `IN ingredient_of` reaches components and dose forms, never the clinical drug. The **precise** forms (`SCDFP`/`SBDFP`/`SCDGP`) carry no ingredient edge either: an `SCDFP` reaches its basis-of-strength substance(s) by `has_boss`, a one-to-many relation whose targets can be a `PIN` or a plain `IN` and which `ingredientsOf` does not follow, and the other two author none at all. Read the `tty` of what a traversal returns rather than assuming it from the one you queried. - **A concept is typed by its defining atom, never by file position.** An `RXCUI` carries one defining atom (its normalized name: `IN`, `SCD`, `SCDF`, `BN`, and so on) plus any number of synonym atoms, whose `TTY` (`PSN`, `SY`, `TMSY`) the NLM defines as a _"synonym of another TTY"_: it types a **name**, not a concept. RxNorm documents no ordering between an `RXCUI`'s atoms, so the loader takes the defining one wherever it sits and never falls back to a synonym. An `RXCUI` that no defining atom could type is left out of the graph and surfaced as `TERM_RXNORM_UNTYPED_CONCEPT`. Refusing is the safer direction: an absent concept is already a first-class typed answer (`TERM_RXNORM_UNKNOWN_RXCUI`), while a concept whose `tty` reads `TMSY` is a fabricated claim about what the drug is, and a caller cannot tell it from a real one. - **Authored edges only: never inverted.** RxNorm ships both directions of an asymmetric relationship as separate rows, so `genericFor` follows the authored `tradename_of` and `brandsFor` follows the authored `has_tradename`; the engine **never** synthesizes a reverse edge. Convenience resolvers (`ingredientsOf`, `doseFormsOf`, `consistsOf`) and the generic `relatedByRela` all follow authored predicates. - **Never fabricate.** A queried `RXCUI` absent from the release is a typed `TERM_RXNORM_UNKNOWN_RXCUI`; a _present_ concept with no such relationship is a found result with an **empty** target set (an honest "no such edge", distinct from "unknown concept"). An NDC not in the release is a typed `TERM_RXNORM_NDC_UNMAPPED`, never a guessed `RXCUI`. - **NDC↔RXCUI is many:1 and temporal.** `resolveNdc` carries the temporal status (`active`/`obsolete`/`alien`/`unknown`) and the **as-of release**: an NDC present in the loaded release is `active` as of that release; obsolete/alien statuses come from RxNav NDC-history data (a differential/BYO source, not the base RRF concept files) and are never fabricated. - **Coverage is not correctness.** Approximate name matching (`approximateMatch`) is an **opt-in, explicitly labeled** path: never the default resolution and never an exact assertion (every candidate carries `approximate: true` and a derived score). A "no match" is an empty array, never a nearest guess. > **Content posture.** This release ships the graph _mechanism_ over the real RRF format, grounded > firsthand on the RxNorm technical documentation; it bundles RxNorm's relationship and term-type > _names_ but **no** RxNorm release. The > public-domain Current Prescribable Content pack is not bundled either: bundling one requires a > genuine, verbatim RxNorm release, and none is fabricated to fill the gap. ## Liberal load, conservative assertion Loading is **liberal**: `loadConceptMap` accepts an untrusted `unknown` and degrades a malformed resource to a typed fatal (`TERM_CONCEPTMAP_MALFORMED`) rather than crashing, but it refuses to load a structurally partial, misleading map. Assertion is **conservative**: a 1:many mapping returns the full candidate set (never collapsed to one), and steward advice comments ride through verbatim. ## Stable diagnostic + fatal codes Outcomes carry **stable codes**: `DIAGNOSTIC_CODES` (typed, surfaced, non-throwing outcomes like `TERM_TRANSLATE_UNMAPPED`) and `FATAL_CODES` (thrown, like `TERM_CONCEPTMAP_MALFORMED`). Consumers branch on these, so a code's name is part of the public contract: renaming or removing one is a **breaking change**. Diagnostic and fatal **messages** are **value-free**: nothing you configured and nothing your release or resource contained reaches a `message` or an `err.stack`, and the locus beside one is a line number, an index path into your own resource, or a fixed token. The **objects** are the opposite by design: a typed `unknown`/`unmapped` outcome names what you asked about, and a code _in patient context_ can be PHI, so log the code and the locus rather than the whole result. ## Immutability A loaded `ConceptMap`, a `Coding` and a `TranslateResult` are deep-frozen. So is the bundled UCUM unit table, atom by atom: one table is shared by every caller and by the engine's own reducer, so a definition rewritten in place would change what a unit means for the whole process. An index a **release you load** exposes as a `ReadonlyMap` (the concepts of a code system, a GEM's or a complex map's entries, a drug graph's concepts, edges and NDCs) is a **read-only view** of a map rather than the map itself. Reads are exactly a `Map`'s (`get`, `has`, `size`, `keys`, `values`, `entries`, `forEach`, iteration, and `new Map(view)`), and adding, deleting or clearing is refused whichever way it is attempted. Why it matters is the never-fabricate invariant: without it, whatever holds a loaded release could empty a medication's ingredient edges, delete a diagnosis mapping, or add a target the steward's file never authored, and the engine would answer from it while `count` went on reporting what was loaded. Three consequences to know before you write against one. `readonly` in the type is a compile-time claim and this is the run-time one, so the refusal happens whether or not you type-check. A view is **not** a `Map` instance: `instanceof Map` is `false`, it prints as a plain object, `Object.keys` lists its own methods, and `JSON.stringify` renders it as `{"size":2}` where a `Map` rendered `{}`, so a JSON round-trip of a loaded model carries a `size` over no entries, having never carried the entries either. And a model carrying a view **cannot be structured-cloned**: `structuredClone` and `worker.postMessage` raise a `DataCloneError`, `v8.serialize` a plain `Error`, both naming an uncloneable function. Copy out what the other side needs instead (`new Map(cs.concepts)` clones fine). That refusal is deliberate rather than incidental: the alternative made the clone succeed and hand back a model whose indexes were empty while its counts still reported the loaded figures. A loaded `ConceptMap`, `ValueSet` or unit table carries no view and clones as it always did. The **bundled UCUM table is the exception**, deliberately and as before: `atomByCode` and `prefixByCode` are real `Map`s whose `set`, `delete` and `clear` are replaced by a refusal, so an entry forced through `Map.prototype` still lands. What that reaches is bounded: `parseUcum` resolves an atom by scanning the frozen `atoms` array, never through a lookup map, so a forced entry changes no validation, no reduction and no `ucumEqual` answer; only your own `get` is lied to. What `parseUcum` and `reduce` hand back is deliberately **not** frozen, because it is yours rather than the engine's: both are built per call, and mutating one changes nothing for anyone else. --- URL: https://docs.cosyte.com/terminology/guides-overview Markdown: https://docs.cosyte.com/terminology/guides-overview.md # Guides Task-oriented recipes ("how do I X?") for `@cosyte/terminology`. Each guide is a short, copy-pasteable answer to one real integration question. > **Status:** this package ships the identity resolver, the ConceptMap `$translate` engine, the > CodeSystem `$lookup`/`$validate-code` loaders, ValueSet `$expand`/binding, UCUM validation, the > crosswalk resolvers, and the RxNorm drug graph. ## Available now - **Canonicalize a code system**: turn an OID or HL7 v2 mnemonic off a parsed message into a FHIR canonical URI with `resolveSystem`, handling the typed `unknown` case. - **Translate through a ConceptMap**: `loadConceptMap` + `translate`, reading the `matches` (with their relationship and steward comments) or the typed `unmapped` outcome. - **Look up a code's display and status**: `$lookup` / `$validate-code` over a BYO CodeSystem. - **Validate a code against a value set**: `$validate-code` / `$expand` over a BYO ValueSet. - **Validate a UCUM unit**: recognition/validation, never magnitude conversion. - **Resolve a SNOMED→ICD-10-CM crosswalk or the ICD-9↔ICD-10 GEMs**: carrying every steward No-Map/advice flag, never inverted. - **Navigate the RxNorm drug graph**: resolve a drug to its components and through them to its ingredient, a brand to its generic, or an NDC to its `RXCUI`, over a BYO RxNorm RRF release, never fabricating an edge. The [Quickstart](https://docs.cosyte.com/terminology/quickstart.md) covers the one-line calls, and the **API Reference** documents every shipped export. --- URL: https://docs.cosyte.com/terminology/api Markdown: https://docs.cosyte.com/terminology/api.md # @cosyte/terminology Public entry point for **`@cosyte/terminology`**: a zero-dependency, developer-focused **terminology engine** for US healthcare code systems. Unlike its sibling `@cosyte/*` packages, this is **not a wire-format parser**: it mirrors the **FHIR Terminology Module** (`$translate`, `$lookup`, `$validate-code`, `$expand`, …), operating over **consumer-supplied** FHIR resources. It ships the *engine* and **no code-system release** (SNOMED CT / CPT / LOINC / UMLS / RxNorm are strictly BYO). It is not content-free: see **What is bundled** at the end of this note, which includes the UCUM unit table, the code-system *identities* (OID ↔ URI), the SNOMED CT concepts the crosswalk resolver names and RxNorm's relationship and term-type names, each with its copyright. **Code-system identity and ConceptMap translation:** - [resolveSystem](#resolvesystem): the code-system identity / canonical-URI resolver (mnemonic | OID → URI). - [loadConceptMap](#loadconceptmap) + [translate](#translate): the ConceptMap `$translate` engine, with the **never-fabricate / never-invert** invariants: an unmapped source is a typed [TranslateUnmapped](#translateunmapped), never a guessed target; a directional map is never run backwards. A source whose every declared target asserts non-relation (R4 `disjoint` / `unmatched`) is not translated either: it reports as [TranslateUnmapped](#translateunmapped), carrying the `disjoint` rows the map asserted on `notRelated`. **The CodeSystem load layer**, with the FHIR `$lookup` / `$validate-code` operations: - [loadCodeSystem](#loadcodesystem): load a **consumer-supplied** release (RRF / CSV / fixed-width / FHIR `CodeSystem` JSON) into an immutable model. Ships the loader; **no code-system release**. - [lookup](#lookup): code → display + properties, carrying status (deprecated / header-not-billable). - [validateCode](#validatecode): is a code a valid member of the system, with its status flags. The never-fabricate invariant holds here too: an unknown code is a typed `unknown` / `valid: false`, never a fabricated display or a guessed `valid: true`. **The ValueSet binding layer**, FHIR `$expand` / `$validate-code` over a `compose`: - [loadValueSet](#loadvalueset): load a **consumer-supplied** FHIR `ValueSet` (intensional `compose` and/or a pre-computed `expansion`) into an immutable model. - [expand](#expand): flatten membership over the supplied [CodeSystem](#codesystem)s (`include`/`exclude`, explicit `concept` lists, `is-a`/property `filter`s, referenced value sets), with an honest `complete` flag: an unresolvable part is a typed `TERM_VALUESET_CANNOT_EXPAND`, never a guess. An enumerated code the release you supplied for that component does not define is not a member either: it is omitted, the answer stays `complete: true`, and the drop is a typed `TERM_VALUESET_ENUMERATED_CODE_UNDEFINED` rather than a silent one. - [validateCodeInValueSet](#validatecodeinvalueset): binding membership, returning a **decided** `result` only when proven and a typed `undetermined` otherwise (a truncated expansion never reads as complete). **The UCUM unit layer**: a hand-rolled UCUM grammar parser + validation + representation canonicalization (recognition only, **no** magnitude conversion): - [validateUcum](#validateucum): is a string a valid UCUM unit; if so, its canonical descriptor. An invalid unit is a typed `TERM_UCUM_INVALID`, never a guessed "nearest" unit. - [ucumEqual](#ucumequal): do two expressions denote the *same unit* (`N` ≡ `kg.m/s2`), by reducing both to base dimensions. Not magnitude conversion (`mg/dL` → `mmol/L` is refused). - [parseUcum](#parseucum) / [reduce](#reduce) / [loadUcumEssence](#loaducumessence): the underlying grammar parser, dimensional reducer, and the in-memory model of the vendored, verbatim UCUM table. **The crosswalk resolvers**: the never-fabricate/never-invert invariant applied to the published, directional reference maps: - [loadGems](#loadgems) + [applyGem](#applygem): the CMS **ICD-9↔ICD-10 GEMs** (public-domain reference mappings), honoring the steward's Approximate / No-Map / Combination (scenario→choice-list) flags. A No-Map source is a typed [CrosswalkNoMap](#crosswalknomap); a 1:many source returns the full candidate set. - [loadComplexMap](#loadcomplexmap) + [applyComplexMap](#applycomplexmap): the NLM **SNOMED CT → ICD-10-CM complex map** (BYO, **no SNOMED CT refset bundled**), evaluating Map Group / Priority / `IFA` Rule / Advice / Category against caller-supplied [PatientContext](#patientcontext); a rule needing context the caller lacks is a typed [ComplexMapContextRequired](#complexmapcontextrequired), never a guessed branch. - [invertGem](#invertgem): the never-invert refusal made a first-class, thrown contract ([FATAL\_CODES.TERM\_MAP\_NOT\_INVERTIBLE](#term_map_not_invertible)). **The RxNorm drug relationship graph**: ingredient / brand / clinical-drug / dose-form navigation over a **caller-supplied** RxNorm RRF release: - [loadRxNormGraph](#loadrxnormgraph): load `RXNCONSO` (concepts, typed by `TTY`), `RXNREL` (directed `RELA` edges, normalized to the documented `RXCUI2 ⟶RELA⟶ RXCUI1` direction), and optionally `RXNSAT` (NDC attributes) into an immutable graph. Ships **no** RxNorm release: BYO. - [ingredientsOf](#ingredientsof) / [genericFor](#genericfor) / [brandsFor](#brandsfor) / [doseFormsOf](#doseformsof) / [consistsOf](#consistsof) / [relatedByRela](#relatedbyrela): graph navigation following **authored** edges only (the engine never synthesizes an inverse). An absent `RXCUI` is a typed [RxNormUnknown](#rxnormunknown). - [resolveNdc](#resolvendc): NDC → `RXCUI` carrying the temporal status and the as-of release; an absent NDC is a typed [NdcUnmapped](#ndcunmapped), never a guess. - [approximateMatch](#approximatematch): the **opt-in, explicitly labeled** similarity path (never the default, never an exact code assertion). **What is bundled.** No code-system release is. What is bundled includes the following, named with its copyright: - the **UCUM unit table** (`ucum-essence.xml`, version 2.2, revision-date 2024-06-17): copyright ©1999-2024 Regenstrief Institute, Inc., all rights reserved, reproduced **verbatim** under the UCUM Copyright Notice and License (https://ucum.org/license), embedded byte-for-byte in the published build and parsed at runtime; no modified or derivative copy is distributed. The UCUM Specification is provided "as is" **without warranty of any kind**: see the License for the full disclaimer. The complete notice ships with this package at `vendor/ucum/NOTICE.md`. - the **code-system identity facts** ([SYSTEM\_IDENTITIES](#system_identities)): OID ↔ canonical-URI pairings, which identify the systems rather than listing any system's codes. - the **SNOMED CT concepts the crosswalk resolver names**, each with its description: the four map-category concepts ([MAP\_CATEGORIES](#map_categories)) a complex-map row's `mapCategoryId` refers to, and the two gender findings (`248152002` Female, `248153007` Male) a gender `IFA` rule is written against. Further SNOMED CT identifiers, and ICD-10-CM codes, appear in the API documentation examples, which are distributed in the compiled declaration files and in the build's sourcemaps. SNOMED CT is copyright © International Health Terminology Standards Development Organisation. - the **RxNorm relationship and term-type names** the drug-graph API is written against: the relationship names in [RELA](#rela) / [RELA\_INVERSE](#rela_inverse) (`has_ingredient`, `tradename_of`, …), and the term-type codes in [TERM\_TYPES](#term_types), each with its name as published by the U.S. National Library of Medicine (`SCD` reads "Semantic Clinical Drug"). Individual RxNorm identifiers also appear in the API documentation examples, with their RxNorm names and term types (for instance `RXCUI` `316151`, "lisinopril 10 MG"), and in `README.md` as well as the places named above. RxNorm is produced by the National Library of Medicine; no RxNorm release is bundled. The `LICENSE` file that ships with this package carries the MIT text for the package's own code and a third-party notices section naming each of the above. The content packs that would change this (the CMS GEM files, the RxNorm Current Prescribable subset) are not bundled; a caller supplies them, as they do a SNOMED CT, CPT, LOINC, UMLS or VSAC release, under whatever terms its steward sets. This states what is distributed and under whose copyright; it is not legal advice, and makes no claim about whether a particular use is permitted to you. ## Classes ### TerminologyError The engine's typed error. Carries a stable [FatalCode](#fatalcode) so callers branch on `err.code` without string-matching the message. Thrown only for [FATAL\_CODES](#fatal_codes) conditions. The `message` is **value-free**: it names the structural fault (a resource path, a missing field, the *role* of a configured column) and never echoes an input value, so it is safe to log, and safe in an `err.stack` that reaches an error reporter. The rule the strings are built by is in this module's docblock above. #### Example ```ts import { loadConceptMap, TerminologyError, FATAL_CODES } from "@cosyte/terminology"; try { loadConceptMap({ resourceType: "Patient" }); } catch (err) { (err as TerminologyError).code === FATAL_CODES.TERM_CONCEPTMAP_MALFORMED; // => true } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new TerminologyError**(`code`, `message`): [`TerminologyError`](#terminologyerror) ###### Parameters ###### code [`FatalCode`](#fatalcode) The stable [FatalCode](#fatalcode) describing the fault. ###### message `string` A **value-free** structural description (path + fault; never an input value). Every piece of it must be engine-owned: a literal, a frozen-table entry, or a locus this package built. This string reaches `err.stack`, so interpolating a caller-supplied or document-derived value into it is how a diagnostic becomes a leak. ###### Returns [`TerminologyError`](#terminologyerror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`FatalCode`](#fatalcode) The stable fatal code. Branch on this, not on message. ## Interfaces ### AnnotationNode A component that carries **only** an annotation (e.g. `{cells}`), dimensionless and inert. #### Properties ##### kind > `readonly` **kind**: `"annotation"` *** ### ApproximateMatchOptions Options for [approximateMatch](#approximatematch). #### Properties ##### limit? > `readonly` `optional` **limit?**: `number` Maximum number of candidates to return, best-first. Default `20`. ##### minScore? > `readonly` `optional` **minScore?**: `number` Minimum similarity (`0` to `1`) for a candidate to be returned. Default `0.34`. *** ### BillableFlag The billable-flag descriptor for a [FixedWidthFieldMap](#fixedwidthfieldmap). #### Properties ##### at > `readonly` **at**: `number` The 0-based char index of the flag. ##### billableValue > `readonly` **billableValue**: `string` The character value that means "billable/valid leaf" (ICD-10-CM: `"1"`; `"0"` is a header). *** ### CodeableConcept A concept described by one or more [Coding](#coding-1)s plus optional free text: FHIR R4 `CodeableConcept`. Included so the value layer is complete for downstream consumers even though ../conceptmap/translate.translate operates on a single [Coding](#coding-1). #### Example ```ts import { codeableConcept } from "@cosyte/terminology"; const cc = codeableConcept({ coding: [{ system: "http://loinc.org", code: "2160-0" }], text: "Serum creatinine", }); cc.coding[0].code; // => "2160-0" ``` #### Properties ##### coding > `readonly` **coding**: readonly [`Coding`](#coding-1)[] The codings for this concept, in order of preference. Frozen and never empty-by-fabrication. ##### text? > `readonly` `optional` **text?**: `string` Plain-text representation of the concept, when supplied. *** ### CodeSystem A loaded, immutable code-system release: the queryable model [loadCodeSystem](#loadcodesystem) produces and [lookup](#lookup) / [validateCode](#validatecode) run over. Concepts are keyed by code for O(1) identity, and every concept is frozen. #### Properties ##### concepts > `readonly` **concepts**: `ReadonlyMap`\<`string`, [`Concept`](#concept)\> The concepts, keyed by `code`. A **read-only view**, not a `Map` you were handed: reads and iteration behave as a `Map`'s do, adding / deleting / clearing is refused however it is attempted, and every value is frozen. A view is not a `Map` instance: `instanceof Map` is `false` and this model cannot be `structuredClone`d or posted to a worker; copy out what you need (`new Map(…)`). ##### count > `readonly` **count**: `number` The number of loaded concepts. ##### name? > `readonly` `optional` **name?**: `string` A short human-readable name, when supplied. ##### url? > `readonly` `optional` **url?**: `string` The code system's canonical URI, when supplied by the source. ##### version? > `readonly` `optional` **version?**: `string` The release version, when supplied: mappings and displays are release-scoped. ##### warnings > `readonly` **warnings**: readonly [`LoadWarning`](#loadwarning)[] The skipped-row warnings surfaced during load, in line order. Frozen; may be empty. *** ### Coding A single coded value: a `code` drawn from a code `system`, optionally versioned and displayed. Models FHIR R4 `Coding`. `system` is the code system's **canonical URI** (e.g. `http://loinc.org`), not a mnemonic or OID: use ../systems/resolve.resolveSystem to canonicalize before constructing one. `system` is optional because a raw wire code sometimes arrives without one; the engine surfaces that rather than inventing a system. #### Example ```ts import { coding } from "@cosyte/terminology"; const c = coding({ system: "http://loinc.org", code: "2160-0", display: "Creatinine" }); c.code; // => "2160-0" ``` #### Properties ##### code > `readonly` **code**: `string` The symbol in the code system's syntax. Required: a coding without a code is meaningless. ##### display? > `readonly` `optional` **display?**: `string` The human-readable label. Carried verbatim from the source; never fabricated by the engine. ##### system? > `readonly` `optional` **system?**: `string` The code system's canonical URI (e.g. `http://snomed.info/sct`). Optional when unknown. ##### version? > `readonly` `optional` **version?**: `string` The code system version this code is drawn from, when known (mappings are release-scoped). *** ### ComplexMap A loaded, immutable SNOMED→ICD-10-CM complex map. Entries are keyed by source concept id; within a source they are ordered by (group, priority). Applied **only** SNOMED→ICD-10-CM; never inverted. [entries](#entries) is a **read-only view**, not a `Map` you were handed, and each source's rule list is frozen: nothing that holds a loaded map can delete a source concept's rules, or add one your refset never carried, and have [applyComplexMap](#applycomplexmap) resolve from it. A view is not a `Map` instance: `instanceof Map` is `false` and this model cannot be `structuredClone`d or posted to a worker; copy out what you need (`new Map(…)`). #### Properties ##### count > `readonly` **count**: `number` The number of loaded rows. ##### entries > `readonly` **entries**: `ReadonlyMap`\<`string`, readonly [`ComplexMapEntry`](#complexmapentry)[]\> Rows keyed by SNOMED source concept id, each ordered by (group, priority). Frozen. ##### version? > `readonly` `optional` **version?**: `string` The map version/release, when supplied: mappings are release-scoped. ##### warnings > `readonly` **warnings**: readonly [`ComplexMapLoadWarning`](#complexmaploadwarning)[] The skipped-row warnings surfaced during load, in line order. Frozen; may be empty. *** ### ComplexMapContextRequired A group whose winning path is a **context-dependent** `IFA` rule (or `447639009`/`447640006` category) for which the caller supplied **no** matching context. The engine surfaces the rules + advice and refuses to pick a branch: the never-fabricate rule for runtime context. #### Properties ##### code > `readonly` **code**: `"TERM_CROSSWALK_CONTEXT_REQUIRED"` The stable diagnostic code. ##### group > `readonly` **group**: `number` The 1-based map group. ##### outcome > `readonly` **outcome**: `"context-required"` Discriminant. ##### rules > `readonly` **rules**: readonly [`ComplexMapEntry`](#complexmapentry)[] The unresolved candidate rules in this group (priority order), each with its target + advice. *** ### ComplexMapEntry One row of the SNOMED CT → ICD-10-CM **complex/extended map** refset: the caller-supplied, SNOMED-licensed content the resolver runs over (**no SNOMED CT refset is bundled**). The field names paraphrase the SNOMED RF2 extended-map refset columns. #### Properties ##### advice > `readonly` **advice**: `string` The **map advice**, verbatim (pipe-delimited in the source): steward instructions to a human (`CONSIDER LATERALITY`, `EPISODE OF CARE INFORMATION NEEDED`, `THIS IS A MANIFESTATION CODE…`). Carried through untouched: advice flags must **never** be swallowed. ##### category? > `readonly` `optional` **category?**: `string` The steward **map category** id (a `MAP_CATEGORIES` value, e.g. `447638001`), when supplied. ##### correlation? > `readonly` `optional` **correlation?**: `string` The map **correlation** id (SNOMED map-correlation concept), when supplied. Carried verbatim. ##### group > `readonly` **group**: `number` The **map group** (1-based). Distinct groups on one source are an **AND**: the source needs a target from *each* group (e.g. a manifestation code plus an etiology code). Every group resolves independently. ##### priority > `readonly` **priority**: `number` The **map priority** within a group (1-based). Rules are evaluated in ascending priority; the first whose [rule](#rule) matches the caller's context wins that group (an if-then-else chain). ##### rule > `readonly` **rule**: `string` The **map rule**, verbatim: `"TRUE"` / `"OTHERWISE TRUE"` (unconditional fall-through), or an `IFA` predicate (e.g. an age-band or gender test) evaluated against caller-supplied context. ##### source > `readonly` **source**: `string` The SNOMED CT source concept id (`referencedComponentId`), verbatim. ##### target? > `readonly` `optional` **target?**: `string` The ICD-10-CM **map target** code, verbatim: **absent** for a No-Map / empty-target row. *** ### ComplexMapGroupNoMap A group the steward marked **No-Map** (`447638001` "cannot be classified", or an empty target): a typed, surfaced outcome, never a fabricated target. #### Properties ##### advice > `readonly` **advice**: `string` The advice on the No-Map row, verbatim. ##### category? > `readonly` `optional` **category?**: `string` The steward map category, when supplied (`447638001`, or absent for an empty-target row). ##### code > `readonly` **code**: `"TERM_CROSSWALK_NO_MAP"` The stable diagnostic code. ##### group > `readonly` **group**: `number` The 1-based map group. ##### outcome > `readonly` **outcome**: `"no-map"` Discriminant. *** ### ComplexMapGroupResolved A group that resolved to a concrete ICD-10-CM target via its winning rule. #### Properties ##### advice > `readonly` **advice**: `string` The winning rule's advice, verbatim. ##### category? > `readonly` `optional` **category?**: `string` The steward map category, when supplied (e.g. `447637006`). ##### group > `readonly` **group**: `number` The 1-based map group. ##### outcome > `readonly` **outcome**: `"resolved"` Discriminant. ##### rule > `readonly` **rule**: `string` The winning rule text, verbatim (`"TRUE"`, an `IFA` predicate, …). ##### target > `readonly` **target**: [`Coding`](#coding-1) The resolved ICD-10-CM target coding (drawn verbatim from the map). *** ### ComplexMapLoadWarning A surfaced, non-fatal complex-map load warning: a malformed refset row that was skipped. #### Properties ##### code > `readonly` **code**: `"TERM_COMPLEX_MAP_MALFORMED_ROW"` The stable diagnostic code for a skipped complex-map row. ##### detail > `readonly` **detail**: `string` A value-free description of the structural fault. ##### line > `readonly` **line**: `number` The 1-based source line the fault was found on. *** ### ComplexMapMatched A successful complex-map application: one resolution **per map group**, in group order. #### Properties ##### groups > `readonly` **groups**: readonly [`ComplexMapGroupResult`](#complexmapgroupresult)[] One resolution per map group, ordered by group. A source needing N codes has N groups. Frozen. ##### mapped > `readonly` **mapped**: `true` Discriminant: the source concept is present in the map. ##### source > `readonly` **source**: `string` The SNOMED source concept id, echoed. *** ### Concept One loaded, immutable concept: the subset of a `CodeSystem.concept` the operations need. #### Properties ##### code > `readonly` **code**: `string` The code, exactly as it appears in the release. ##### definition? > `readonly` `optional` **definition?**: `string` The formal definition, when the release supplied one. ##### display? > `readonly` `optional` **display?**: `string` The preferred display, carried verbatim. Absent when the release supplied none (never invented). ##### properties > `readonly` **properties**: readonly [`Property`](#property-1)[] Additional properties, in load order. Frozen; may be empty, never fabricated. ##### status? > `readonly` `optional` **status?**: [`ConceptStatus`](#conceptstatus-1) The concept's status flags, when the release carried status information. *** ### ConceptMap A loaded, immutable ConceptMap: the subset of the FHIR R4 `ConceptMap` resource the `$translate` engine needs. Produced by [loadConceptMap](#loadconceptmap); every field frozen. #### Properties ##### group > `readonly` **group**: readonly [`ConceptMapGroup`](#conceptmapgroup-1)[] The translation groups. ##### sourceScope? > `readonly` `optional` **sourceScope?**: `string` The map's `sourceUri`/`sourceCanonical` scope, when declared. ##### targetScope? > `readonly` `optional` **targetScope?**: `string` The map's `targetUri`/`targetCanonical` scope, when declared. ##### url? > `readonly` `optional` **url?**: `string` The resource's canonical `url`, when declared. ##### version? > `readonly` `optional` **version?**: `string` The resource's business `version`, when declared. *** ### ConceptMapElement One `group.element`: a source concept and its candidate targets (FHIR R4). #### Properties ##### code? > `readonly` `optional` **code?**: `string` The source code. ##### display? > `readonly` `optional` **display?**: `string` The source display. ##### target > `readonly` **target**: readonly [`ConceptMapTarget`](#conceptmaptarget)[] The candidate targets. *** ### ConceptMapGroup One `group`: a source-system/target-system pair with its element mappings (FHIR R4). #### Properties ##### element > `readonly` **element**: readonly [`ConceptMapElement`](#conceptmapelement)[] The element mappings. ##### source? > `readonly` `optional` **source?**: `string` The group's source code system (canonical URI), when declared. ##### sourceVersion? > `readonly` `optional` **sourceVersion?**: `string` The group's source system version, when declared. ##### target? > `readonly` `optional` **target?**: `string` The group's target code system (canonical URI), when declared. ##### targetVersion? > `readonly` `optional` **targetVersion?**: `string` The group's target system version, when declared. ##### unmapped? > `readonly` `optional` **unmapped?**: [`ConceptMapUnmapped`](#conceptmapunmapped) The `group.unmapped` fallback directive, when declared. *** ### ConceptMapTarget One `group.element.target`: a candidate target for a source concept (FHIR R4). #### Properties ##### code? > `readonly` `optional` **code?**: `string` The target code (absent for a pure `unmatched` assertion). ##### comment? > `readonly` `optional` **comment?**: `string` Author's comment (e.g. steward map advice), carried verbatim. ##### display? > `readonly` `optional` **display?**: `string` The target display, carried verbatim when present. ##### equivalence > `readonly` **equivalence**: [`R4Equivalence`](#r4equivalence) The verbatim R4 equivalence token. *** ### ConceptMapUnmapped A single `group.unmapped` directive from a loaded ConceptMap group (FHIR R4). #### Properties ##### code? > `readonly` `optional` **code?**: `string` For `fixed`, the fixed target code. ##### display? > `readonly` `optional` **display?**: `string` For `fixed`, the fixed target display. ##### mode > `readonly` **mode**: [`UnmappedMode`](#unmappedmode) The fallback mode. ##### url? > `readonly` `optional` **url?**: `string` For `other-map`, the canonical URL of the map to consult. *** ### ConceptRef One explicit `ValueSet.compose.include.concept`: an enumerated code (extensional). #### Properties ##### code > `readonly` **code**: `string` The code, drawn verbatim from the value set. ##### display? > `readonly` `optional` **display?**: `string` The display, when the value set supplied one (carried verbatim, never fabricated). This is the value set's **own** display and it always wins. Where the value set supplies none, [expand](#expand) falls back to the display the supplied [CodeSystem](#codesystem) release carries for the same code, again **verbatim**: the engine still synthesizes nothing, so a member whose release was not supplied, or which that release carries with no display, comes back with no display at all. A code a *usable* release does not carry is a membership question rather than a display one: see [ConceptSetComponent.concept](#concept-1). *** ### ConceptSetComponent One `ValueSet.compose.include` / `.exclude` component: a `ConceptSet`. Selects codes by an explicit `concept` list, by `filter`s over a `system`, by referencing other value sets (`valueSet`), or a combination (the combination is an **intersection**, per FHIR). #### Properties ##### concept? > `readonly` `optional` **concept?**: readonly [`ConceptRef`](#conceptref)[] An explicit, enumerated code list (extensional). Mutually exclusive with [filter](#filter) in FHIR. An entry is admitted as a member only where the evidence the caller supplied does not contradict it. Where a release keyed by this component's [system](#system-1) is in hand and the declared [version](#version-4) (if any) agrees with it, a code that release does not define is **not** a member: [expand](#expand) omits it, reporting one typed DiagnosticCode.TERM\_VALUESET\_ENUMERATED\_CODE\_UNDEFINED for this component while staying `complete: true`, and [validateCodeInValueSet](#validatecodeinvalueset) decides it a non-member. Where no usable release was supplied, the engine holds no contrary evidence and every enumerated code stands. ##### filter? > `readonly` `optional` **filter?**: readonly [`ConceptSetFilter`](#conceptsetfilter)[] Intensional filters over [system](#system-1). Each code must satisfy **all** filters. ##### system? > `readonly` `optional` **system?**: `string` The code system these codes are drawn from (canonical URI), when the component names one. ##### valueSet? > `readonly` `optional` **valueSet?**: readonly `string`[] Canonical URLs of other value sets; a code must be a member of **every** one (intersection). ##### version? > `readonly` `optional` **version?**: `string` The code system version to pin the selection to, when declared. Wherever the selection actually resolves a supplied [CodeSystem](#codesystem) (a `filter`, or a whole-system component with no [concept](#concept-1) list), the pin is **checked** against that release's own `version`: a disagreement, or a release that declares no version to agree with, is a typed DiagnosticCode.TERM\_VALUESET\_CANNOT\_EXPAND that marks the outcome incomplete or undetermined, never membership computed from the other release. The check is that the two declarations agree, not that either is true: a mislabelled release is still trusted. *** ### ConceptSetFilter One `ValueSet.compose.include.filter`: a `property op value` predicate over a code system. #### Properties ##### op > `readonly` **op**: [`FilterOperator`](#filteroperator) The filter operator. ##### property > `readonly` **property**: `string` The concept property the predicate is on (e.g. `"concept"` for the code hierarchy, or a code). ##### value > `readonly` **value**: `string` The comparison value (for `in`/`not-in`, a comma-separated list; for `is-a`, an anchor code). *** ### ConceptStatus The status flags a `$lookup`/`$validate-code` carries on a concept: the safety surface that stops a non-current or non-billable code from being presented as clean. Present only when the loaded release supplied status information; the engine never fabricates a status. #### Properties ##### active > `readonly` **active**: `boolean` Convenience: `activity === "active"`. A caller can gate on this without a switch. ##### activity > `readonly` **activity**: [`ConceptActivity`](#conceptactivity) The normalized [ConceptActivity](#conceptactivity). ##### billable? > `readonly` `optional` **billable?**: `boolean` For classification systems (ICD-10-CM): is this a billable/valid leaf? A header is `false`. ##### code? > `readonly` `optional` **code?**: [`DiagnosticCode`](#diagnosticcode) A stable [DiagnosticCode](#diagnosticcode) naming the concern, when the concept is not clean-active. ##### raw? > `readonly` `optional` **raw?**: `string` The steward's raw status token, carried verbatim (e.g. `"DEPRECATED"`, `"O"`, `"0"`). *** ### CrosswalkNoMap A **No-Map** outcome: the source is present in the map but the steward declares it has no valid target ("cannot be classified with available data"). A first-class typed result, **never** a fabricated target. Distinct from [CrosswalkUnmapped](#crosswalkunmapped) (source absent entirely). #### Properties ##### code > `readonly` **code**: `"TERM_CROSSWALK_NO_MAP"` The stable diagnostic code. ##### entries > `readonly` **entries**: readonly [`GemEntry`](#gementry)[] The steward flags/advice that declared the No-Map, carried verbatim. ##### mapped > `readonly` **mapped**: `false` Discriminant: the source did not map. ##### noMap > `readonly` **noMap**: `true` Sub-discriminant: this is an authored No-Map, not a plain absence. ##### source > `readonly` **source**: `string` The source code, echoed. *** ### CrosswalkUnmapped An **unmapped** outcome: the source code is **not present in the map at all** (distinct from an authored No-Map). Surfaced as a typed result, never a guessed target and never a silent success. #### Properties ##### code > `readonly` **code**: `"TERM_CROSSWALK_UNMAPPED"` The stable diagnostic code. ##### mapped > `readonly` **mapped**: `false` Discriminant: the source did not map. ##### noMap > `readonly` **noMap**: `false` Sub-discriminant: a plain absence, not an authored No-Map. ##### source > `readonly` **source**: `string` The source code, echoed. *** ### CsvColumnMap The column mapping for a [CsvSource](#csvsource) (header names, matched against the header row). #### Properties ##### code > `readonly` **code**: `string` The header name of the code column (e.g. LOINC `LOINC_NUM`). ##### display > `readonly` **display**: `string` The header name of the display column (e.g. LOINC `LONG_COMMON_NAME`). ##### properties? > `readonly` `optional` **properties?**: readonly `string`[] Header names to surface as properties. ##### status? > `readonly` `optional` **status?**: `string` The header name of a status column (e.g. LOINC `STATUS`), when present. *** ### CsvSource An **RFC-4180 CSV** source (quoted fields, embedded commas/quotes/newlines): e.g. LOINC's `Loinc.csv`. The one release format that needs real quote handling; hand-rolled, zero-dep. BYO. #### Properties ##### columns > `readonly` **columns**: [`CsvColumnMap`](#csvcolumnmap) Which header-named columns hold the code/display/status/properties. ##### content > `readonly` **content**: `string` The raw CSV file content. ##### format > `readonly` **format**: `"csv"` Discriminant. ##### name? > `readonly` `optional` **name?**: `string` A human-readable name. ##### statusMap? > `readonly` `optional` **statusMap?**: [`StatusMapper`](#statusmapper) Override the default status interpretation of the `status` column's raw token. ##### url? > `readonly` `optional` **url?**: `string` The code system's canonical URI. ##### version? > `readonly` `optional` **version?**: `string` The release version. *** ### ExpandResult The result of [expand](#expand): the flattened membership plus an honest **completeness** signal. `complete` is `false` whenever any part could not be fully computed (a missing code system, an unresolved referenced value set, an unimplemented `filter` op, a truncated pre-computed expansion, or a component whose declared [ConceptSetComponent.version](#version-4) disagrees with the supplied release). A `false` here is the never-fabricate contract in action: the `contains` set is a **lower bound**, so it must never be read as exhaustive membership. An enumerated code the supplied release does not define does **not** set it: that member was decided, not left unresolved, so the answer stays `complete: true` and carries a DiagnosticCode.TERM\_VALUESET\_ENUMERATED\_CODE\_UNDEFINED diagnostic instead. A `complete` expansion can therefore carry diagnostics: read [diagnostics](#diagnostics) on both. #### Properties ##### complete > `readonly` **complete**: `boolean` Whether the expansion is exhaustive. When `false`, `contains` is a lower bound, not the whole set. ##### contains > `readonly` **contains**: readonly [`Coding`](#coding-1)[] The expanded, de-duplicated members, in first-seen order (frozen; every member frozen). ##### diagnostics > `readonly` **diagnostics**: readonly [`ExpansionDiagnostic`](#expansiondiagnostic)[] The surfaced concerns, in encounter order (frozen; may be empty). ##### total? > `readonly` `optional` **total?**: `number` The server-reported total, when the expansion came from a pre-computed snapshot. *** ### ExpansionContains One `ValueSet.expansion.contains` entry from a **pre-computed** expansion. #### Properties ##### code > `readonly` **code**: `string` The code. Required: a `contains` entry without a code is a malformed expansion. ##### display? > `readonly` `optional` **display?**: `string` The display, carried verbatim when present. ##### system? > `readonly` `optional` **system?**: `string` The code system URI, when the entry names one. ##### version? > `readonly` `optional` **version?**: `string` The code system version, when the entry pins one. *** ### ExpansionContext The data an [expand](#expand)/[validateCodeInValueSet](#validatecodeinvalueset) call draws on: the loaded [CodeSystem](#codesystem) releases (for intensional `filter`/`system` includes) and any referenced [ValueSet](#valueset-1)s (for `compose.include.valueSet`), each keyed by its **canonical URL**. The engine makes **no network call** and holds no bundled code-system release: everything intensional resolves through this map, and a system or value set the caller did not supply becomes a typed DiagnosticCode.TERM\_VALUESET\_CANNOT\_EXPAND, never a fabricated member. #### Properties ##### codeSystems? > `readonly` `optional` **codeSystems?**: `ReadonlyMap`\<`string`, [`CodeSystem`](#codesystem)\> Loaded code-system releases, keyed by canonical URI (e.g. `"http://loinc.org"`). ##### valueSets? > `readonly` `optional` **valueSets?**: `ReadonlyMap`\<`string`, [`ValueSet`](#valueset-1)\> Referenced value sets, keyed by canonical URL, for `compose.include.valueSet`. *** ### ExpansionDiagnostic A surfaced expansion concern: a part that could not be expanded, a truncated snapshot, or an enumerated code the supplied release does not define. #### Properties ##### code > `readonly` **code**: [`DiagnosticCode`](#diagnosticcode) The stable code (`TERM_VALUESET_CANNOT_EXPAND` / `TERM_VALUESET_EXPANSION_TRUNCATED` / `TERM_VALUESET_ENUMERATED_CODE_UNDEFINED`). Branch on it: the first two mean the answer is a lower bound, the third reports a **decided** answer the engine narrowed on the caller's own evidence. ##### detail > `readonly` **detail**: `string` A **value-free** structural description of the concern (never echoes a patient value). ##### path? > `readonly` `optional` **path?**: `string` A **value-free structural locus** into the caller's own resource: `"expansion"`, or a `compose`-relative index path such as `"compose.include[2]"` or `"compose.include[0].valueSet[1]"`. A diagnostic raised while expanding a *referenced* value set is prefixed with the reference that reached it (`"compose.include[0].valueSet[1]/compose.include[3]"`). Built entirely from integers and the engine's own field names, so nothing the caller or their resource supplied can reach it, at any length. It replaces the URI this field used to carry: a canonical URI is unbounded consumer-supplied text on a diagnostic surface, and an index path is strictly more precise anyway (two `include`s may name the same system). *** ### FactorNode A numeric factor component (a bare integer, e.g. the `4` in `4.[pi]`). #### Properties ##### kind > `readonly` **kind**: `"factor"` ##### value > `readonly` **value**: `number` The integer value. *** ### FhirCodeSystemSource A **FHIR R4 `CodeSystem`** JSON source (native, consumer-supplied model). The consumer supplies the untrusted resource; nested `concept` hierarchies are flattened, and the standard `inactive` / `deprecated` concept properties are read into [ConceptStatus](#conceptstatus-1). #### Properties ##### format > `readonly` **format**: `"fhir"` Discriminant. ##### resource > `readonly` **resource**: `unknown` The untrusted FHIR `CodeSystem` resource (typically `JSON.parse` output, hence `unknown`). *** ### FieldSlice A half-open `[start, end)` slice of a fixed-width line (0-based char indices; `end` optional). #### Properties ##### end? > `readonly` `optional` **end?**: `number` The 0-based end char index (exclusive). Omit to slice to end of line. ##### start > `readonly` **start**: `number` The 0-based start char index (inclusive). *** ### FilterMatch The outcome of evaluating a single filter against a single concept. #### Properties ##### matched > `readonly` **matched**: `boolean` Whether the concept satisfies the filter (meaningful only when [supported](#supported)). ##### supported > `readonly` **supported**: `boolean` Whether the engine implements this filter operator; `false` ⇒ the caller must surface it. *** ### FixedWidthFieldMap The field mapping for a [FixedWidthSource](#fixedwidthsource) (0-based char slices). #### Properties ##### billable? > `readonly` `optional` **billable?**: [`BillableFlag`](#billableflag) The billable/header flag (ICD-10-CM position-15), when the format has one. ##### code > `readonly` **code**: [`FieldSlice`](#fieldslice) The slice holding the concept code. ##### display > `readonly` **display**: [`FieldSlice`](#fieldslice) The slice holding the display string. ##### properties? > `readonly` `optional` **properties?**: readonly [`FixedWidthPropertyField`](#fixedwidthpropertyfield)[] Extra slices to surface as properties. ##### status? > `readonly` `optional` **status?**: [`FieldSlice`](#fieldslice) A slice holding a status token, when the format has one (alternative to `billable`). *** ### FixedWidthPropertyField One extra fixed-width field to capture as a [Property](#property-1). #### Properties ##### name > `readonly` **name**: `string` The property code to emit. ##### slice > `readonly` **slice**: [`FieldSlice`](#fieldslice) The field slice. *** ### FixedWidthSource A **fixed-width** source (slice by column): e.g. the ICD-10-CM order file. Fully parameterized: the consumer supplies the field slices (see [ICD10CM\_ORDER\_FILE\_FIELDS](#icd10cm_order_file_fields) for the documented ICD-10-CM preset, which they confirm against their release's README, since the byte offsets are not confirmed against an authoritative machine-readable source). BYO. #### Properties ##### content > `readonly` **content**: `string` The raw fixed-width file content. ##### fields > `readonly` **fields**: [`FixedWidthFieldMap`](#fixedwidthfieldmap) Which char slices hold the code/display/billable/status/properties. ##### format > `readonly` **format**: `"fixed-width"` Discriminant. ##### name? > `readonly` `optional` **name?**: `string` A human-readable name. ##### statusMap? > `readonly` `optional` **statusMap?**: [`StatusMapper`](#statusmapper) Override the default status interpretation of the `status` slice's raw token. ##### url? > `readonly` `optional` **url?**: `string` The code system's canonical URI. ##### version? > `readonly` `optional` **version?**: `string` The release version. *** ### GemChoiceList One choice list within a combination scenario: pick **one** of its targets to build a cluster. #### Properties ##### choiceList > `readonly` **choiceList**: `number` The choice-list number ([GemFlags.choiceList](#choicelist-1)). ##### targets > `readonly` **targets**: readonly `string`[] The candidate target codes for this choice list, in file order. Frozen; never empty. *** ### GemEntry One loaded GEM entry: a source code, its (optional) target, and the decoded [GemFlags](#gemflags). A No-Map entry ([GemFlags.noMap](#nomap-2)) carries **no** [target](#target-4): the source has no equivalent, and the engine never invents one. #### Properties ##### flags > `readonly` **flags**: [`GemFlags`](#gemflags) The decoded steward flags. ##### source > `readonly` **source**: `string` The source code (in the map's source classification), verbatim. ##### target? > `readonly` `optional` **target?**: `string` The target code (in the map's target classification), verbatim: **absent** for a No-Map entry. *** ### GemFlags The decoded **5-position flag field** of a GEM entry (CMS Dx GEM User's Guide). Each digit is a steward signal a caller must not ignore; the verbatim field rides through as [raw](#raw-1). Positions (left to right): `approximate | no-map | combination | scenario | choice-list`. #### Properties ##### approximate > `readonly` **approximate**: `boolean` Position 1: **approximate** flag. `true` when the mapping is approximate (the common case: the source and target are not identical in meaning); `false` when the entry is an exact identity. A GEM entry is a *reference* mapping either way: never an assertion of clinical equivalence. ##### choiceList > `readonly` **choiceList**: `number` Position 5: **choice-list** number (`0` for a non-combination entry). Within a scenario, a valid cluster is built by taking **one** target from each distinct choice list. ##### combination > `readonly` **combination**: `boolean` Position 3: **combination** flag. `true` when the source requires a **combination** of target codes to be fully represented; the entry is one candidate within a scenario/choice-list cluster (see [scenario](#scenario) / [choiceList](#choicelist-1)), never a standalone 1:1 target. ##### noMap > `readonly` **noMap**: `boolean` Position 2: **no-map** flag. `true` when the source code has **no valid target** in the other classification (the target field is the `NoDx`/`NoPCS` sentinel). A first-class typed outcome: surfaced as a [CrosswalkNoMap](#crosswalknomap), never a fabricated target. ##### raw > `readonly` **raw**: `string` The verbatim 5-character flag field, carried through untouched. ##### scenario > `readonly` **scenario**: `number` Position 4: **scenario** number (`0` for a non-combination entry). Within a combination, all entries sharing a scenario describe **one** valid way to represent the source; distinct scenarios are alternative representations. *** ### GemLoadWarning A surfaced, non-fatal GEM load warning: a malformed line that was **skipped**, reported with its line number and a value-free structural reason. Liberal on load (the parsers' posture). #### Properties ##### code > `readonly` **code**: `"TERM_GEM_MALFORMED_ROW"` The stable diagnostic code for a skipped GEM row. ##### detail > `readonly` **detail**: `string` A value-free description of the structural fault (never echoes a field value). ##### line > `readonly` **line**: `number` The 1-based source line the fault was found on. *** ### GemMap A loaded, immutable GEM map. Entries are keyed by source code (a source may have many entries, the maps are 1:many). Applied **only** in its [direction](#direction); never inverted. [entries](#entries-2) is a **read-only view**, not a `Map` you were handed, and each source's candidate list is frozen: nothing that holds a loaded map can delete a source's mapping, or add a target the steward's file never authored, and have [applyGem](#applygem) answer from it. A view is not a `Map` instance: `instanceof Map` is `false` and this model cannot be `structuredClone`d or posted to a worker; copy out what you need (`new Map(…)`). #### Properties ##### count > `readonly` **count**: `number` The number of loaded entries. ##### direction > `readonly` **direction**: [`GemDirection`](#gemdirection) Which direction this file maps. Applied only this way. ##### entries > `readonly` **entries**: `ReadonlyMap`\<`string`, readonly [`GemEntry`](#gementry)[]\> Entries keyed by source code, each list in file order. Each list frozen. ##### version? > `readonly` `optional` **version?**: `string` The GEM version/release (e.g. `"2018"`), when supplied: mappings are release-scoped. ##### warnings > `readonly` **warnings**: readonly [`GemLoadWarning`](#gemloadwarning)[] The skipped-row warnings surfaced during load, in line order. Frozen; may be empty. *** ### GemMatched A successful GEM application: one or more candidate targets for the source. The **full** candidate set is surfaced (never collapsed to one); when the source is a combination, [combinations](#combinations) carries the scenario/choice-list structure needed to build valid clusters. #### Properties ##### combinations? > `readonly` `optional` **combinations?**: readonly [`GemScenario`](#gemscenario)[] For a combination source, the scenario→choice-list structure. Absent for a plain 1:many source. ##### direction > `readonly` **direction**: [`GemDirection`](#gemdirection) The direction the map was applied in. ##### entries > `readonly` **entries**: readonly [`GemEntry`](#gementry)[] Every candidate entry for the source, in file order. Frozen; never empty here. ##### mapped > `readonly` **mapped**: `true` Discriminant: the source mapped to at least one target. ##### source > `readonly` **source**: `string` The source code, echoed. *** ### GemScenario One combination **scenario**: a single valid way to represent the source as a combination of target codes. A valid **cluster** is one target taken from each of its [choiceLists](#choicelists). #### Properties ##### choiceLists > `readonly` **choiceLists**: readonly [`GemChoiceList`](#gemchoicelist)[] The choice lists composing this scenario, ordered by choice-list number. Frozen. ##### scenario > `readonly` **scenario**: `number` The scenario number ([GemFlags.scenario](#scenario)). *** ### GemSource A raw GEM source file, plus the direction it maps and its release version. BYO: the caller supplies the public-domain CMS GEM file content. #### Properties ##### content > `readonly` **content**: `string` The raw GEM file content (whitespace-delimited: `source target flags`, one entry per line). ##### direction > `readonly` **direction**: [`GemDirection`](#gemdirection) Which direction this file maps (`"9-to-10"` for `…_I9gem.txt`, `"10-to-9"` for `…_I10gem.txt`). ##### version? > `readonly` `optional` **version?**: `string` The GEM release/version (e.g. `"2018"`), when known: mappings are release-scoped. *** ### GroupNode A parenthesised sub-term or an annotation-only component. #### Properties ##### kind > `readonly` **kind**: `"group"` ##### term > `readonly` **term**: [`UnitNode`](#unitnode) The parsed inner term. *** ### LinearReduction A unit reduced to base dimensions: a scalar `factor` times a map of dimension axis → exponent. Dimension keys are the seven UCUM base symbols (`m s g rad K C cd`) plus `arb:` axes for arbitrary units. Two `linear` forms are the **same unit** iff equal dims and equal factor. #### Properties ##### dims > `readonly` **dims**: `Readonly`\<`Record`\<`string`, `number`\>\> Dimension axis → non-zero exponent (zero exponents omitted). ##### factor > `readonly` **factor**: `number` The accumulated scalar factor in base units. ##### kind > `readonly` **kind**: `"linear"` *** ### LoadWarning A surfaced, non-fatal load warning: a malformed row that was **skipped**, reported with its line number and a **value-free** structural reason (never the row's content). Liberal on load. #### Properties ##### code > `readonly` **code**: [`DiagnosticCode`](#diagnosticcode) The stable diagnostic code (`TERM_RRF_MALFORMED_ROW` / `_CSV_MALFORMED` / `_FIXED_WIDTH_MALFORMED`). ##### detail > `readonly` **detail**: `string` A value-free description of the structural fault (never echoes a field value). ##### line > `readonly` **line**: `number` The 1-based source line the fault was found on (positional context; value-free). *** ### LookupResult A successful [lookup](#lookup): the concept's details plus its release provenance. #### Properties ##### code > `readonly` **code**: `string` The code, echoed. ##### definition? > `readonly` `optional` **definition?**: `string` The formal definition, when present. ##### display? > `readonly` `optional` **display?**: `string` The preferred display, when the release carried one (never fabricated). ##### found > `readonly` **found**: `true` Discriminant: the code was found. ##### properties > `readonly` **properties**: readonly [`Property`](#property-1)[] The concept's properties, in load order (frozen, may be empty). ##### status? > `readonly` `optional` **status?**: [`ConceptStatus`](#conceptstatus-1) The concept's status flags, when present. ##### system? > `readonly` `optional` **system?**: `string` The code system's canonical URI, when known. ##### version? > `readonly` `optional` **version?**: `string` The release version, when known. *** ### LookupUnknown A **fail-safe unknown** [lookup](#lookup): the never-fabricate outcome; no display is ever guessed. #### Properties ##### code > `readonly` **code**: `"TERM_CODE_UNKNOWN"` The stable diagnostic code for an unknown code. ##### found > `readonly` **found**: `false` Discriminant: the code was not found. ##### input > `readonly` **input**: `string` The code that was looked up, echoed so the caller can surface it. *** ### MapProvenance Provenance of a translation, which map (and which group's systems) produced it. #### Properties ##### conceptMapUrl? > `readonly` `optional` **conceptMapUrl?**: `string` The ConceptMap's canonical `url`, when declared. ##### conceptMapVersion? > `readonly` `optional` **conceptMapVersion?**: `string` The ConceptMap's business `version`, when declared (mappings are release-scoped). ##### sourceSystem? > `readonly` `optional` **sourceSystem?**: `string` The source code system for this translation. **This is an echo of your query, not reference data off the map**: it is the `system` on the `Coding` you passed to `translate`, verbatim and unbounded, whenever that coding carries one; the matched (or consulted) group's own `source` is only the fallback for a coding that does not. Log it only if you would log the code you passed in. ##### targetSystem? > `readonly` `optional` **targetSystem?**: `string` The target code system of the group that matched, when known. *** ### NdcResolution A resolved **NDC → RXCUI** mapping (from an `RXNSAT.RRF` `ATN=NDC` attribute), carrying the temporal [NdcStatus](#ndcstatus) and the **as-of release** it is valid for. NDC↔RXCUI is many:1 and changes across releases, so a resolution is never timeless: the release rides through on [asOf](#asof). #### Properties ##### asOf? > `readonly` `optional` **asOf?**: `string` The release version the mapping is valid **as of**, when the graph carries one. ##### ndc > `readonly` **ndc**: `string` The 11-digit NDC (as supplied), echoed. ##### resolved > `readonly` **resolved**: `true` Discriminant: the NDC resolved to a concept in the loaded release. ##### rxcui > `readonly` **rxcui**: `string` The `RXCUI` the NDC attribute is attached to in the loaded release. ##### status > `readonly` **status**: [`NdcStatus`](#ndcstatus) The temporal status of the NDC ([NdcStatus](#ndcstatus)). *** ### NdcUnmapped An NDC that could not be resolved to any `RXCUI` in the loaded release: a typed absence. #### Properties ##### code > `readonly` **code**: `"TERM_RXNORM_NDC_UNMAPPED"` The stable diagnostic code. ##### ndc > `readonly` **ndc**: `string` The NDC (as supplied), echoed. ##### resolved > `readonly` **resolved**: `false` Discriminant: the NDC is not present in the loaded release. *** ### ParseFailure A parse failure carrying a value-free structural reason. #### Properties ##### ok > `readonly` **ok**: `false` ##### reason > `readonly` **reason**: `string` A structural description of the fault (never echoes an input *value* beyond the offending token shape). *** ### PatientContext The **caller-supplied patient context** an `IFA` rule is evaluated against. The resolver evaluates only against what it is given: a rule needing context absent here surfaces as a [ComplexMapContextRequired](#complexmapcontextrequired), never a silently-picked branch. #### Properties ##### ageYears? > `readonly` `optional` **ageYears?**: `number` The patient's age **in years**, when known (for `IFA … Age …` bands). ##### gender? > `readonly` `optional` **gender?**: `"male"` \| `"female"` The patient's administrative gender, when known (for gender `IFA` rules). *** ### Property One property value on a concept: models FHIR R4 `$lookup` `property` ("one or more properties that contain additional information about the code, including status"). Carried verbatim from the release; never fabricated. #### Properties ##### code > `readonly` **code**: `string` Identifies the property (FHIR `property.code`), e.g. `"shortDescription"`, `"TTY"`, `"parent"`. ##### description? > `readonly` `optional` **description?**: `string` Optional human-readable rendering of the value (FHIR `property.description`). ##### value > `readonly` **value**: `string` \| `number` \| `boolean` The property value (FHIR `property.value[x]`), reduced to the primitive kinds the readers emit. *** ### RrfColumnMap The column mapping for an [RrfSource](#rrfsource) (0-based column indices into the pipe-split row). #### Properties ##### code > `readonly` **code**: `number` The column holding the concept code (e.g. RxNorm `RXCUI` = 0, or `CODE` = 13). ##### display > `readonly` **display**: `number` The column holding the display string (e.g. `STR` = 14). ##### properties? > `readonly` `optional` **properties?**: readonly [`RrfPropertyColumn`](#rrfpropertycolumn)[] Extra columns to surface as properties. ##### status? > `readonly` `optional` **status?**: `number` The column holding a status/suppress token (e.g. `SUPPRESS` = 16), when present. *** ### RrfPropertyColumn One extra RRF column to capture as a [Property](#property-1). #### Properties ##### column > `readonly` **column**: `number` The 0-based column index. ##### name > `readonly` **name**: `string` The property code to emit. *** ### RrfSource An **RRF** (Rich Release Format, pipe-delimited RxNorm/UMLS release) source. One parameterized reader serves both: RxNorm `RXNCONSO`/`RXNSAT` and UMLS `MRCONSO`/`MRSAT` share the RRF shape (pipe-delimited, one trailing pipe per line). BYO: the consumer supplies the file. #### Properties ##### columns > `readonly` **columns**: [`RrfColumnMap`](#rrfcolumnmap) Which columns hold the code/display/status/properties. ##### content > `readonly` **content**: `string` The raw RRF file content. ##### format > `readonly` **format**: `"rrf"` Discriminant. ##### name? > `readonly` `optional` **name?**: `string` A human-readable name. ##### statusMap? > `readonly` `optional` **statusMap?**: [`StatusMapper`](#statusmapper) Override the default status interpretation of the `status` column's raw token. ##### url? > `readonly` `optional` **url?**: `string` The code system's canonical URI, attached to the loaded release. ##### version? > `readonly` `optional` **version?**: `string` The release version (RRF carries none inline, supply it so displays stay release-scoped). *** ### RxNormApproximateMatch One **approximate**-match candidate (the opt-in, never-default path). Every field is marked so a caller can never mistake it for an exact identity: [approximate](#approximate-1) is always `true` and [score](#score) is the labeled, derived similarity. The concept itself is drawn verbatim from the loaded release. #### Properties ##### approximate > `readonly` **approximate**: `true` Always `true`: this is a similarity match, never an exact code resolution. ##### concept > `readonly` **concept**: [`RxNormConcept`](#rxnormconcept) The matched concept, verbatim from the loaded release. ##### score > `readonly` **score**: `number` A derived, labeled similarity score in `[0, 1]` (token overlap). Never a clinical assertion. *** ### RxNormConcept One RxNorm concept (`RXCUI`) as loaded from `RXNCONSO.RRF`: its normalized name and term type. Built from the concept's `SAB=RXNORM` atom (the normalized form); the verbatim `RXCUI`, `TTY`, and `STR` (name) ride through untouched. #### Properties ##### name > `readonly` **name**: `string` The concept's normalized name (`RXNCONSO.STR`), verbatim. ##### rxcui > `readonly` **rxcui**: `string` The RxNorm concept unique identifier (`RXCUI`), verbatim. ##### suppressed > `readonly` **suppressed**: `boolean` `true` when the source atom is suppressed (`RXNCONSO.SUPPRESS` ≠ `N`): surfaced, never hidden. ##### tty > `readonly` **tty**: [`TermType`](#termtype) The concept's term type ([TermType](#termtype)), verbatim from the `RXNCONSO.TTY` of a **defining** atom. Never a synonym-class `TTY` (`PSN`/`SY`/`TMSY`), which types a name rather than a concept. *** ### RxNormEdge One directed relationship edge, normalized from an `RXNREL.RRF` row under the documented direction convention (see the module doc): the row `(RXCUI1, …, RELA, RXCUI2, …)` becomes `subject = RXCUI2`, `predicate = RELA`, `object = RXCUI1`: read **`subject ⟶predicate⟶ object`**. #### Properties ##### object > `readonly` **object**: `string` The object `RXCUI`: `RXCUI1` in the source row. The concept the relationship is *to*. ##### predicate > `readonly` **predicate**: `string` The relationship label (`RXNREL.RELA`, e.g. `has_ingredient`), verbatim. ##### subject > `readonly` **subject**: `string` The subject `RXCUI`: `RXCUI2` in the source row. The concept the relationship is *from*. *** ### RxNormGraph A loaded, immutable RxNorm drug graph: concepts keyed by `RXCUI`, edges indexed by subject, and the NDC→RXCUI attribute index. Applied only in the authored edge direction; never inverted. Its three indexes are **read-only views**, not `Map`s you were handed: reads and iteration behave as a `Map`'s do, and adding / deleting / clearing is refused however it is attempted. What that buys is the never-fabricate invariant surviving the handover: nothing that holds a loaded graph can empty a medication's ingredient edges, or add an edge or concept the release never authored, and leave [conceptCount](#conceptcount) / [edgeCount](#edgecount) still reporting the loaded figures. A view is not a `Map` instance: `instanceof Map` is `false` and this model cannot be `structuredClone`d or posted to a worker; copy out what you need (`new Map(…)`). #### Properties ##### conceptCount > `readonly` **conceptCount**: `number` The number of loaded concepts. ##### concepts > `readonly` **concepts**: `ReadonlyMap`\<`string`, [`RxNormConcept`](#rxnormconcept)\> Concepts keyed by `RXCUI` (first `SAB=RXNORM` atom per `RXCUI` wins). Each frozen. ##### edgeCount > `readonly` **edgeCount**: `number` The number of loaded edges. ##### edges > `readonly` **edges**: `ReadonlyMap`\<`string`, readonly [`RxNormEdge`](#rxnormedge)[]\> Directed edges indexed by `subject` `RXCUI`, each list in file order. Each list frozen. ##### ndcs > `readonly` **ndcs**: `ReadonlyMap`\<`string`, \{ `rxcui`: `string`; `status`: [`NdcStatus`](#ndcstatus); \}\> NDC → \{ rxcui, status \} index from `RXNSAT` `ATN=NDC` attributes. Each frozen; may be empty. ##### version? > `readonly` `optional` **version?**: `string` The release version (e.g. `"RXNORM_2026AA"`), when supplied: mappings are release-scoped. ##### warnings > `readonly` **warnings**: readonly [`RxNormLoadWarning`](#rxnormloadwarning)[] The skipped-row warnings surfaced during load, in file/line order. Frozen; may be empty. *** ### RxNormGraphSource A raw RxNorm RRF release, BYO. The caller supplies the public-domain Current Prescribable Content subset or the full (licensed) release; the engine bundles **no** RxNorm release. #### Properties ##### conso > `readonly` **conso**: `string` The raw `RXNCONSO.RRF` content (pipe-delimited concept/atom names). ##### rel > `readonly` **rel**: `string` The raw `RXNREL.RRF` content (pipe-delimited relationships). ##### sat? > `readonly` `optional` **sat?**: `string` The raw `RXNSAT.RRF` content (pipe-delimited attributes): supply to resolve NDCs. Optional. ##### version? > `readonly` `optional` **version?**: `string` The release version (e.g. `"RXNORM_2026AA"`), when known: mappings are release-scoped. *** ### RxNormLoadWarning A surfaced, non-fatal RxNorm load warning: an `RXNCONSO` / `RXNREL` / `RXNSAT` row (or, for `TERM_RXNORM_UNTYPED_CONCEPT`, an `RXCUI`) that was **skipped**, reported with its line number and a value-free structural reason (liberal on load). Never echoes a field value. #### Properties ##### code > `readonly` **code**: `"TERM_RXNORM_MALFORMED_ROW"` \| `"TERM_RXNORM_UNTYPED_CONCEPT"` The stable diagnostic code for the skip. `TERM_RXNORM_MALFORMED_ROW` is a structurally unusable row; `TERM_RXNORM_UNTYPED_CONCEPT` is an `RXCUI` no defining atom could type. Both are in the package-wide ../common/diagnostics.DIAGNOSTIC\_CODES registry, which documents each. ##### detail > `readonly` **detail**: `string` A value-free description of the structural fault. ##### file > `readonly` **file**: `"RXNCONSO"` \| `"RXNREL"` \| `"RXNSAT"` Which source file the fault was in. ##### line > `readonly` **line**: `number` The 1-based source line the fault was found on. *** ### RxNormRelated A successful graph-navigation result: the concept, the predicate followed, and the **full** set of related concepts drawn verbatim from the loaded edges. `targets` may be **empty**: that is an honest, data-grounded "the release has no such edge for this concept", never a fabricated relation. #### Properties ##### found > `readonly` **found**: `true` Discriminant: the source `RXCUI` is present in the graph. ##### predicates > `readonly` **predicates**: readonly `string`[] The relationship predicate(s) that were followed. ##### rxcui > `readonly` **rxcui**: `string` The source `RXCUI`, echoed. ##### targets > `readonly` **targets**: readonly [`RxNormConcept`](#rxnormconcept)[] The related concepts, in edge order, each drawn verbatim from the loaded graph. May be empty. *** ### RxNormUnknown A typed **unknown** navigation result: the queried `RXCUI` is **absent from the loaded graph**. A first-class outcome, never a fabricated concept or an empty success (the never-fabricate invariant, applied to the drug graph). Distinct from a [RxNormRelated](#rxnormrelated) with empty `targets`, which means "present, but no such relationship". #### Properties ##### code > `readonly` **code**: `"TERM_RXNORM_UNKNOWN_RXCUI"` The stable diagnostic code. ##### found > `readonly` **found**: `false` Discriminant: the source `RXCUI` is not in the graph. ##### rxcui > `readonly` **rxcui**: `string` The queried `RXCUI`, echoed. *** ### SimpleUnitNode One parsed component of a unit term: a simple unit (prefix?+atom) with an optional exponent. #### Properties ##### atom > `readonly` **atom**: [`UcumAtom`](#ucumatom) The resolved atom. ##### exponent > `readonly` **exponent**: `number` The integer exponent (default 1). Negative for the denominator side. ##### kind > `readonly` **kind**: `"simple"` ##### prefix? > `readonly` `optional` **prefix?**: [`UcumPrefix`](#ucumprefix) The applied prefix, when present. *** ### SpecialReduction A **non-linear** (special) reduction: the expression involves a special unit (`Cel`, `B`, …) and cannot be reduced to a scalar×dimension form. Carried by its normalized representation `form`; two special reductions are the same unit iff their `form`s are identical (never equated with a linear unit: the never-fabricate posture applied to units). #### Properties ##### form > `readonly` **form**: `string` The normalized expression string (prefixes/atoms/exponents; annotations stripped). ##### kind > `readonly` **kind**: `"special"` *** ### Subsumption An immutable ancestor index over a [CodeSystem](#codesystem)'s `parent` edges. #### Methods ##### ancestorsOf() > **ancestorsOf**(`code`): `ReadonlySet`\<`string`\> All (transitive) ancestor codes of `code`, cycle-safe. Excludes `code` itself. ###### Parameters ###### code `string` ###### Returns `ReadonlySet`\<`string`\> *** ### SystemIdentity A resolved code-system identity: the canonical URI plus the alternate identifiers that name the same system. #### Properties ##### mnemonics > `readonly` **mnemonics**: readonly `string`[] HL7 v2 Table 0396 mnemonic(s) and other accepted short aliases for this system. ##### name > `readonly` **name**: `string` A short human-readable name for diagnostics/logs (value-free, a system name, never a value). ##### oid? > `readonly` `optional` **oid?**: `string` The registered OID (bare, no `urn:oid:` prefix), when the system has one. ##### url > `readonly` **url**: `string` The canonical URI: the single value the engine treats as the system's identity. *** ### TranslateMatch One target concept a [translate](#translate) call produced, with its relationship to the source and the verbatim R4 equivalence token. Never fabricated: every field is drawn from the loaded map. #### Properties ##### comment? > `readonly` `optional` **comment?**: `string` Author's free-text comment on this mapping, when present (carried verbatim, e.g. map advice). ##### equivalence > `readonly` **equivalence**: [`R4Equivalence`](#r4equivalence) The verbatim FHIR R4 `equivalence` token this match was declared with. ##### relationship > `readonly` **relationship**: [`Relationship`](#relationship-2) The normalized, version-neutral relationship (R5 vocabulary). ##### target > `readonly` **target**: [`Coding`](#coding-1) The target [Coding](#coding-1) exactly as declared in the map (frozen). *** ### TranslateMatched A successful translation: one or more matches, never empty (an empty result is `unmapped`), and **at least one of them asserts a relationship** to the source. A `disjoint` row still appears in `matches`, in its declared position, whenever some other target for the same source does assert a relationship: nothing the map said is dropped. When *every* declared target asserts non-relation there is no relationship to report, so the result is a [TranslateUnmapped](#translateunmapped) carrying those rows on `notRelated` instead. #### Properties ##### matches > `readonly` **matches**: readonly [`TranslateMatch`](#translatematch)[] The target concepts, in declared order. Non-empty and frozen. At least one carries a relationship other than `not-related-to`. ##### provenance > `readonly` **provenance**: [`MapProvenance`](#mapprovenance) Where this translation came from. ##### unmapped > `readonly` **unmapped**: `false` Discriminant: the source mapped. *** ### TranslateNotRelated One target the map explicitly asserted is **not related** to the source: an R4 `disjoint` row, carried verbatim on a [TranslateUnmapped](#translateunmapped) result so the assertion the map author made is not lost when the source reports as not translated. This is the opposite of a [TranslateMatch](#translatematch): a `disjoint` row is a declared non-mapping, so it never counts toward the translated verdict. It is surfaced because dropping it would discard what the steward said. A pure `unmatched` assertion carries no target code and is not surfaced here. #### Properties ##### comment? > `readonly` `optional` **comment?**: `string` Author's free-text comment on this assertion, when present (carried verbatim). ##### equivalence > `readonly` **equivalence**: `"disjoint"` Always `disjoint`: the verbatim FHIR R4 `equivalence` token this assertion was declared with. ##### relationship > `readonly` **relationship**: [`Relationship`](#relationship-2) The normalized, version-neutral relationship (R5 vocabulary), folded from the token below by the same table every match uses: always `not-related-to` for a `disjoint` row. ##### target > `readonly` **target**: [`Coding`](#coding-1) The target [Coding](#coding-1) exactly as declared in the map (frozen). *** ### TranslateUnmapped A **fail-safe unmapped** translation: the never-fabricate outcome. The source is surfaced as-is; **no target is ever guessed**. Any `group.unmapped` fallback the author declared is reported via [UnmappedMode](#unmappedmode) (and `fixedTarget`/`otherMapUrl`) for the caller to accept or reject: the engine does not silently substitute it. A source whose every declared target asserts non-relation (R4 `disjoint` or `unmatched`) reports here too: the map said these concepts are *not* related, which is an answer, not a translation. The `disjoint` rows it asserted are carried on `notRelated`, verbatim. Any target on this result was declared in the map; none of it is a mapping. #### Properties ##### code > `readonly` **code**: `"TERM_TRANSLATE_UNMAPPED"` The stable diagnostic code for an unmapped translation. ##### fixedTarget? > `readonly` `optional` **fixedTarget?**: [`Coding`](#coding-1) For `mode: "fixed"`, the author's fixed fallback coding: reported, not auto-applied. ##### mode > `readonly` **mode**: [`UnmappedMode`](#unmappedmode) The map author's declared fallback mode (or `none`). ##### notRelated? > `readonly` `optional` **notRelated?**: readonly [`TranslateNotRelated`](#translatenotrelated)[] The `disjoint` rows the map declared for this source, in declared order, when it declared any: explicit assertions that those targets are **not related** to the source. Present (frozen and non-empty) only when every declared target asserted non-relation, which is what put the result here; omitted otherwise. These are declared non-mappings, never candidates to fall back to. ##### otherMapUrl? > `readonly` `optional` **otherMapUrl?**: `string` For `mode: "other-map"`, the referenced map's URL: reported, not auto-followed. ##### provenance > `readonly` **provenance**: [`MapProvenance`](#mapprovenance) Where the (failed) translation was attempted. ##### source > `readonly` **source**: [`Coding`](#coding-1) The original source [Coding](#coding-1), surfaced untouched. ##### unmapped > `readonly` **unmapped**: `true` Discriminant: the source did not map. *** ### UcumAtom A UCUM unit atom, from the essence `` / `` table. An atom is a symbol like `m`, `L`, `[in_i]`, `10*`, or `Cel`, **before** any prefix or exponent is applied. #### Properties ##### arbitrary > `readonly` **arbitrary**: `boolean` An **arbitrary** unit (essence `isArbitrary`, e.g. `[IU]`, `[arb'U]`): not commensurable with any other unit. Modeled as its own dimension axis keyed by the atom `code`, so `[IU]/[IU]` reduces to unity but `[IU]` is never equated with a mass or another arbitrary unit. ##### base > `readonly` **base**: `boolean` A **base** unit (one of the 7: `m s g rad K C cd`), its own dimension, factor 1. ##### code > `readonly` **code**: `string` The case-sensitive atom symbol (the essence `Code`, e.g. `"m"`, `"[iU]"`, `"m[Hg]"`). ##### dim? > `readonly` `optional` **dim?**: `string` A base unit's dimension key (its own `code`), or `undefined` for derived atoms. ##### metric > `readonly` **metric**: `boolean` Whether a metric prefix may attach to this atom (essence `isMetric`). Only metric atoms prefix. ##### special > `readonly` **special**: `boolean` A **special** unit (essence `isSpecial`, e.g. `Cel`, `B`, `[pH]`): defined by a non-linear function, so it has **no** linear factor/dimension and is never reduced to one. Compared only by identity of its normalized representation (never claimed equal to a linear unit). ##### value? > `readonly` `optional` **value?**: `object` The linear definition: `1 = value × ` (essence ``), for derived atoms. `undefined` for base units (they define the dimensions) and for special units (non-linear). ###### factor > `readonly` **factor**: `number` ###### unit > `readonly` **unit**: `string` *** ### UcumEssence The parsed UCUM essence table: the engine's in-memory unit model, built lazily and cached. #### Properties ##### atomByCode > `readonly` **atomByCode**: `ReadonlyMap`\<`string`, [`UcumAtom`](#ucumatom)\> Atom lookup by exact `code`. ##### atoms > `readonly` **atoms**: readonly [`UcumAtom`](#ucumatom)[] Atom symbols → atom, longest symbols first (for greedy longest-match). ##### prefixByCode > `readonly` **prefixByCode**: `ReadonlyMap`\<`string`, [`UcumPrefix`](#ucumprefix)\> Prefix lookup by exact `code`. ##### prefixes > `readonly` **prefixes**: readonly [`UcumPrefix`](#ucumprefix)[] Prefix symbols → prefix, longest symbols first (for greedy matching). *** ### UcumPrefix A UCUM metric prefix (e.g. `k` = kilo, factor `1e3`), from the essence `` table. #### Properties ##### code > `readonly` **code**: `string` The case-sensitive prefix symbol (e.g. `"k"`, `"da"`, `"Ki"`). ##### factor > `readonly` **factor**: `number` The multiplicative factor (e.g. `1000` for `k`, `1024` for `Ki`). *** ### UnitNode A parsed UCUM term: an ordered list of components, each combined into the product with the given sign (+1 for `.` / the leading operand, -1 for the `/` denominator). #### Properties ##### factors > `readonly` **factors**: readonly `object`[] The components and their operator sign, in source order. ##### kind > `readonly` **kind**: `"term"` *** ### UnknownSystem The typed "not recognized" outcome from [resolveSystem](#resolvesystem): surfaced, never a guess. #### Properties ##### input > `readonly` **input**: `string` The identifier as supplied, echoed back so the caller can surface/log it. ##### unknown > `readonly` **unknown**: `true` Discriminant: the identifier was not recognized. *** ### ValidateCodeResult The result of [validateCode](#validatecode): models FHIR R4 `$validate-code`'s `result` boolean. A found concept is `valid: true` **carrying its status** (a deprecated or header code validates as present but is flagged, never presented clean); an absent code is `valid: false`, never a guessed `true`. #### Properties ##### code? > `readonly` `optional` **code?**: [`DiagnosticCode`](#diagnosticcode) For an invalid code, the stable diagnostic (`TERM_CODE_UNKNOWN`); absent when valid. ##### status? > `readonly` `optional` **status?**: [`ConceptStatus`](#conceptstatus-1) The concept's status flags, when the code was found and the release carried status. ##### valid > `readonly` **valid**: `boolean` FHIR `result`: true if the code is a valid member of the code system. *** ### ValueSet A loaded, immutable `ValueSet`: the subset of the FHIR R4 `ValueSet` resource the binding layer needs. Produced by [loadValueSet](#loadvalueset); every field deep-frozen. #### Properties ##### compose? > `readonly` `optional` **compose?**: [`ValueSetCompose`](#valuesetcompose-1) The intensional/extensional `compose`, when the resource defines membership by composition. ##### expansion? > `readonly` `optional` **expansion?**: [`ValueSetExpansion`](#valuesetexpansion-1) A **pre-computed** `expansion`, when the resource carries a cached membership snapshot. ##### name? > `readonly` `optional` **name?**: `string` A short human-readable name, when declared. ##### url? > `readonly` `optional` **url?**: `string` The resource's canonical `url`, when declared (the key referenced value sets resolve against). ##### version? > `readonly` `optional` **version?**: `string` The resource's business `version`, when declared. *** ### ValueSetCompose A `ValueSet.compose`: the intensional/extensional definition of membership. #### Properties ##### exclude > `readonly` **exclude**: readonly [`ConceptSetComponent`](#conceptsetcomponent)[] The `exclude` components: codes removed from the include union. ##### inactive? > `readonly` `optional` **inactive?**: `boolean` `ValueSet.compose.inactive`, when the resource declares one: whether codes the supplied release marks as **not active** belong to the membership. A declared `false` is an **active-only** value set, and the engine honours it: an `include` contributes only the codes the supplied [CodeSystem](#codesystem) release marks active. A release that carries no status for a selected concept is not evidence of inactivity, so that member is kept; a component whose activity cannot be checked against any usable release contributes **nothing** and marks the outcome incomplete, so `contains` stays a lower bound rather than a set that may hold inactive codes. Absent, or `true`, and no code is ever omitted on activity grounds. This is the value set's own declaration and nothing else: `$expand`'s `activeOnly` **request** parameter is not modelled. ##### include > `readonly` **include**: readonly [`ConceptSetComponent`](#conceptsetcomponent)[] The `include` components (their union, minus every [exclude](#exclude), is the membership). *** ### ValueSetExpansion A **pre-computed** `ValueSet.expansion`: a cached membership snapshot, used as-is (extensional). The engine never re-derives it; it only checks whether it is **complete** (a truncated expansion must never read as full membership). #### Properties ##### contains > `readonly` **contains**: readonly [`ExpansionContains`](#expansioncontains)[] The snapshot members, in expansion order (frozen). ##### total? > `readonly` `optional` **total?**: `number` The server-reported total membership, when present (FHIR `expansion.total`). ##### truncated > `readonly` **truncated**: `boolean` Derived at load: the expansion is **incomplete**. True when `total` exceeds `contains.length`, or the `http://hl7.org/fhir/StructureDefinition/valueset-toocostly` extension flagged it too-costly (the VSAC >1200-code truncation hazard), or [unclosed](#unclosed) is set. A truncated expansion never reads as complete membership. ##### unclosed > `readonly` **unclosed**: `boolean` Derived at load: the server marked the expansion `unclosed` (`http://hl7.org/fhir/StructureDefinition/valueset-unclosed`), meaning the value set is **unbounded** because it includes post-coordinated content (SNOMED CT, UCUM), so codes other than the ones listed may be valid and no expansion can enumerate its membership. A source of incompleteness in its own right: whenever this is `true` so is [truncated](#truncated). It is carried separately so a caller can tell an unbounded value set from a truncated page, and the mark is read **fail-safe**: an entry that names the extension but carries no readable boolean is taken as set, and only an explicit `valueBoolean: false` reads as absent. The engine only ever **reads** this off a supplied expansion. It never sets it on an expansion it computes: `expand` runs over consumer-supplied code systems and cannot assert post-coordination. *** ### ValueSetMemberDecided A definitive membership answer: the code is (or is not) a member of the value set. #### Properties ##### coding > `readonly` **coding**: [`Coding`](#coding-1) The coding that was tested, echoed. ##### result > `readonly` **result**: `boolean` FHIR `$validate-code` `result`: true iff the code is a member of the value set. ##### undetermined > `readonly` **undetermined**: `false` Discriminant: membership was decided. *** ### ValueSetMemberUndetermined A **fail-safe undetermined** membership: the never-fabricate outcome for a value set that could not be fully evaluated (a missing code system, a truncated expansion). The engine refuses to guess: it returns neither `true` nor `false`, so a caller can never mistake "we could not check" for "not a member" (a false "not a member" is a clinical error). #### Properties ##### code > `readonly` **code**: `"TERM_VALUESET_CANNOT_EXPAND"` The stable diagnostic code (`TERM_VALUESET_CANNOT_EXPAND`). ##### coding > `readonly` **coding**: [`Coding`](#coding-1) The coding that was tested, echoed. ##### diagnostics > `readonly` **diagnostics**: readonly [`ExpansionDiagnostic`](#expansiondiagnostic)[] The surfaced reasons the check could not be completed. ##### undetermined > `readonly` **undetermined**: `true` Discriminant: membership could not be decided. ## Type Aliases ### CodeSystemSource > **CodeSystemSource** = [`RrfSource`](#rrfsource) \| [`CsvSource`](#csvsource) \| [`FixedWidthSource`](#fixedwidthsource) \| [`FhirCodeSystemSource`](#fhircodesystemsource) The discriminated union [loadCodeSystem](#loadcodesystem) accepts. *** ### ComplexMapApplyResult > **ComplexMapApplyResult** = [`ComplexMapMatched`](#complexmapmatched) \| [`CrosswalkUnmapped`](#crosswalkunmapped) The result of ../crosswalk/complex-map.applyComplexMap: a per-group match, or a typed absence when the source concept is not in the map. *** ### ComplexMapGroupResult > **ComplexMapGroupResult** = [`ComplexMapGroupResolved`](#complexmapgroupresolved) \| [`ComplexMapGroupNoMap`](#complexmapgroupnomap) \| [`ComplexMapContextRequired`](#complexmapcontextrequired) One map group's resolution: a discriminated outcome. A group resolves to a target, a No-Map, an ambiguity (candidates surfaced), or a context-required stall; it is **never** a silently-picked single target when the steward flagged otherwise. *** ### ComplexMapInput > **ComplexMapInput** = \{ `format`: `"rows"`; `rows`: readonly [`ComplexMapEntry`](#complexmapentry)[]; `version?`: `string`; \} \| \{ `content`: `string`; `format`: `"rf2"`; `version?`: `string`; \} The discriminated input [loadComplexMap](#loadcomplexmap) accepts: already-structured refset rows, or a raw RF2 tab-delimited extended-map refset the caller exported from their SNOMED release. *** ### ComponentNode > **ComponentNode** = [`SimpleUnitNode`](#simpleunitnode) \| [`FactorNode`](#factornode) \| [`GroupNode`](#groupnode) \| [`AnnotationNode`](#annotationnode) A component: a simple unit, a factor, a group, or an annotation, each optionally exponentiated. *** ### ConceptActivity > **ConceptActivity** = `"active"` \| `"deprecated"` \| `"discouraged"` \| `"trial"` \| `"obsolete"` \| `"suppressed"` \| `"header"` \| `"unknown"` A concept's normalized activity: whether it is a clean, current, usable code, or carries a steward status that a caller must not ignore. `active` is the boolean convenience derived from it. - `active`: current and usable. - `deprecated`: no longer current (LOINC `DEPRECATED`; a FHIR `inactive`/`deprecated` concept). - `discouraged` / `trial`: LOINC lifecycle states (usable, but not settled). - `obsolete`: withdrawn (RxNorm `SUPPRESS = O`). - `suppressed`: editor-suppressed (RxNorm/UMLS `SUPPRESS = Y`/`E`). - `header`: a classification header, **not a billable/valid leaf** (ICD-10-CM flag `0`). - `unknown`: a status token the mapper did not recognize; carried verbatim, never guessed clean. *** ### DiagnosticCode > **DiagnosticCode** = *typeof* [`DIAGNOSTIC_CODES`](#diagnostic_codes)\[keyof *typeof* [`DIAGNOSTIC_CODES`](#diagnostic_codes)\] A value from [DIAGNOSTIC\_CODES](#diagnostic_codes): the type consumers narrow a diagnostic's `code` against. *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] A value from [FATAL\_CODES](#fatal_codes): the type carried by a thrown [TerminologyError](#terminologyerror). *** ### FilterOperator > **FilterOperator** = `"="` \| `"is-a"` \| `"descendent-of"` \| `"is-not-a"` \| `"regex"` \| `"in"` \| `"not-in"` \| `"generalizes"` \| `"exists"` A FHIR R4 `ValueSet.compose.include.filter` operator: the subset the engine implements plus the ones it explicitly does not. Grounded on the FHIR `filter-operator` code system (`https://hl7.org/fhir/R4/valueset-filter-operator.html`). **Implemented:** `is-a` / `descendent-of` / `is-not-a` (subsumption over the loaded release's hierarchy), `=` / `in` / `not-in` / `exists` (property predicates). **Not implemented** (surfaced as a typed DiagnosticCode.TERM\_VALUESET\_CANNOT\_EXPAND, never silently mis-expanded): `regex`, `generalizes`. Anything unrecognized is treated as not-implemented: never as "matches nothing". *** ### GemApplyResult > **GemApplyResult** = [`GemMatched`](#gemmatched) \| [`CrosswalkNoMap`](#crosswalknomap) \| [`CrosswalkUnmapped`](#crosswalkunmapped) The result of ../crosswalk/gems.applyGem: a match, an authored No-Map, or an absence. *** ### GemDirection > **GemDirection** = `"9-to-10"` \| `"10-to-9"` The **direction** of a loaded GEM file. CMS ships the forward (ICD-9→ICD-10, `…_I9gem.txt`) and backward (ICD-10→ICD-9, `…_I10gem.txt`) maps as **separate, non-inverse artifacts**: a forward map composed with a backward map is *"not a mirror image"*. A `GemMap` therefore records which direction it is and is only ever applied in that direction. *** ### LookupOutcome > **LookupOutcome** = [`LookupResult`](#lookupresult) \| [`LookupUnknown`](#lookupunknown) The result of [lookup](#lookup): a found concept or a typed [LookupUnknown](#lookupunknown). *** ### MapCategoryId > **MapCategoryId** = *typeof* [`MAP_CATEGORIES`](#map_categories)\[keyof *typeof* [`MAP_CATEGORIES`](#map_categories)\] A value from [MAP\_CATEGORIES](#map_categories): the SNOMED map-category id carried on a resolution. *** ### NdcResult > **NdcResult** = [`NdcResolution`](#ndcresolution) \| [`NdcUnmapped`](#ndcunmapped) The result of ../rxnorm/navigate.resolveNdc: a resolution or a typed absence. *** ### NdcStatus > **NdcStatus** = `"active"` \| `"obsolete"` \| `"alien"` \| `"unknown"` The temporal status of an NDC↔RXCUI mapping (RxNav `getNDCStatus`: `ACTIVE` / `OBSOLETE` / `ALIEN` / `UNKNOWN`). An NDC found in the loaded release's `RXNSAT` attributes is `active` **as of that release**; `obsolete` / `alien` come from the RxNav NDC-history data (a differential / BYO source, not the base RRF concept files) and are surfaced only when the caller supplies them: the engine **never fabricates** a non-current status. *** ### ParseResult > **ParseResult** = \{ `node`: [`UnitNode`](#unitnode); `ok`: `true`; \} \| [`ParseFailure`](#parsefailure) A successful parse (the AST) or a value-free failure reason. *** ### R4Equivalence > **R4Equivalence** = `"relatedto"` \| `"equivalent"` \| `"equal"` \| `"wider"` \| `"subsumes"` \| `"narrower"` \| `"specializes"` \| `"inexact"` \| `"unmatched"` \| `"disjoint"` The verbatim **FHIR R4** `ConceptMap.group.element.target.equivalence` value, carried through untouched so nothing the map author asserted is lost. (R4 enum: `relatedto | equivalent | equal | wider | subsumes | narrower | specializes | inexact | unmatched | disjoint`.) *** ### Reduction > **Reduction** = [`LinearReduction`](#linearreduction) \| [`SpecialReduction`](#specialreduction) The reduced canonical form of a unit expression. *** ### RelaName > **RelaName** = *typeof* [`RELA`](#rela)\[keyof *typeof* [`RELA`](#rela)\] A value from [RELA](#rela): a drug-graph relationship name. *** ### Relationship > **Relationship** = `"equivalent"` \| `"source-is-narrower-than-target"` \| `"source-is-broader-than-target"` \| `"related-to"` \| `"not-related-to"` The engine's **normalized, version-neutral relationship** between a source and a target concept: the FHIR **R5** vocabulary, which the R4 `equivalence` tokens fold into cleanly. Surfaced on every match alongside the verbatim R4 token. - `equivalent` ← R4 `equivalent` / `equal` - `source-is-narrower-than-target` ← R4 `wider` / `subsumes` (the source is-a the target) - `source-is-broader-than-target` ← R4 `narrower` / `specializes` (the target is-a the source) - `related-to` ← R4 `relatedto` / `inexact` - `not-related-to` ← R4 `disjoint` / `unmatched` *** ### ResolveSystemResult > **ResolveSystemResult** = [`SystemIdentity`](#systemidentity) \| [`UnknownSystem`](#unknownsystem) The result of [resolveSystem](#resolvesystem): a resolved [SystemIdentity](#systemidentity) or a typed unknown. *** ### RxNormNavResult > **RxNormNavResult** = [`RxNormRelated`](#rxnormrelated) \| [`RxNormUnknown`](#rxnormunknown) The result of a graph-navigation query: a match (possibly empty) or a typed unknown source. *** ### StatusMapper > **StatusMapper** = (`raw`) => [`ConceptActivity`](#conceptactivity) A mapper from a raw steward status token to a normalized [ConceptActivity](#conceptactivity). Overridable. #### Parameters ##### raw `string` #### Returns [`ConceptActivity`](#conceptactivity) *** ### TermType > **TermType** = `"IN"` \| `"PIN"` \| `"MIN"` \| `"BN"` \| `"SCDC"` \| `"SBDC"` \| `"SCDF"` \| `"SBDF"` \| `"SCDFP"` \| `"SBDFP"` \| `"SCD"` \| `"SBD"` \| `"SCDG"` \| `"SBDG"` \| `"SCDGP"` \| `"GPCK"` \| `"BPCK"` \| `"DF"` \| `"DFG"` \| `"PSN"` \| `"SY"` \| `"TMSY"` \| `"ET"` A RxNorm **term type** (`TTY`): the kind of drug concept an `RXCUI` names. These are the `RXNCONSO.RRF` `TTY` values relevant to the drug graph, grounded on the NLM RxNorm Technical Documentation (Appendix 5). Carried verbatim on a [RxNormConcept](#rxnormconcept); the engine classifies but never re-labels a concept. `PSN` / `SY` / `TMSY` are Appendix 5's **synonym** term types: each labels an alternate *name* on some other concept rather than a concept of its own, so they are part of this vocabulary but can never appear as a loaded [RxNormConcept.tty](#tty). See ../rxnorm/tty.isSynonymTermType. *** ### TranslateResult > **TranslateResult** = [`TranslateMatched`](#translatematched) \| [`TranslateUnmapped`](#translateunmapped) The result of [translate](#translate): a matched result or a typed [TranslateUnmapped](#translateunmapped). *** ### UcumValidation > **UcumValidation** = \{ `canonical`: `string`; `valid`: `true`; \} \| \{ `code`: `"TERM_UCUM_INVALID"`; `reason`: `string`; `valid`: `false`; \} The result of [validateUcum](#validateucum): a valid unit with its canonical descriptor, or a typed invalid carrying the stable TERM\_UCUM\_INVALID diagnostic and a value-free reason. #### Union Members ##### Type Literal \{ `canonical`: `string`; `valid`: `true`; \} ###### canonical > `readonly` **canonical**: `string` A stable, normalized **descriptor** of the unit's canonical form: equal iff the units are the same (see [ucumEqual](#ucumequal)). Note: this is a comparison/debug descriptor, **not** guaranteed to be a re-parseable UCUM expression (UCUM cannot express arbitrary numeric factors as literals). ###### valid > `readonly` **valid**: `true` The expression is a well-formed UCUM unit over known atoms. *** ##### Type Literal \{ `code`: `"TERM_UCUM_INVALID"`; `reason`: `string`; `valid`: `false`; \} ###### code > `readonly` **code**: `"TERM_UCUM_INVALID"` The stable diagnostic code: never a guessed "nearest" unit (the never-fabricate rule). ###### reason > `readonly` **reason**: `string` A **value-free** structural reason (a grammar fault or an unknown atom; never PHI). ###### valid > `readonly` **valid**: `false` The expression is not a valid UCUM unit. *** ### UnmappedMode > **UnmappedMode** = `"provided"` \| `"fixed"` \| `"other-map"` \| `"none"` The FHIR R4 `group.unmapped.mode` an unmapped source fell through to: the map author's declared fallback behavior. Surfaced (never silently acted on): the caller decides whether to trust a fallback. - `provided`: echo the source code as the target (the map declares no real mapping). - `fixed`: a single fixed fallback code the map declares for *all* unmapped sources. - `other-map`: consult the referenced ConceptMap (which this engine does not auto-follow). - `none`: no `group.unmapped` directive applied (plain no-match, or an authored non-mapping: an explicit `unmatched`, or a source whose every declared target is `disjoint`). *** ### ValueSetMembership > **ValueSetMembership** = [`ValueSetMemberDecided`](#valuesetmemberdecided) \| [`ValueSetMemberUndetermined`](#valuesetmemberundetermined) The result of [validateCodeInValueSet](#validatecodeinvalueset): a decided membership or a typed undetermined. ## Variables ### COMPLEX\_MAP\_SOURCE\_SYSTEM > `const` **COMPLEX\_MAP\_SOURCE\_SYSTEM**: `"http://snomed.info/sct"` = `SNOMEDCT_SYSTEM` The SNOMED CT source-system URI a complex-map source concept is drawn from (re-exported for docs). *** ### defaultStatusMapper > `const` **defaultStatusMapper**: [`StatusMapper`](#statusmapper) The default raw-token → [ConceptActivity](#conceptactivity) mapping, case-insensitive. Recognized (grounded in the format docs): `""`/`N`/`ACTIVE` → active; `DEPRECATED` → deprecated; `DISCOURAGED`; `TRIAL`; `O`/`OBSOLETE` → obsolete; `Y`/`E`/`SUPPRESSED` → suppressed. Anything else → `unknown` (surfaced with its raw token, never coerced to active). #### Param **raw** The steward's raw status token. #### Returns The normalized activity. #### Example ```ts import { defaultStatusMapper } from "@cosyte/terminology"; defaultStatusMapper("DEPRECATED"); // => "deprecated" defaultStatusMapper("N"); // => "active" ``` *** ### DIAGNOSTIC\_CODES > `const` **DIAGNOSTIC\_CODES**: `object` Stable **diagnostic codes**: typed, surfaced, non-throwing outcomes. These are not errors: they are first-class results a caller inspects (an unmapped translation, an unrecognized code system). `key === value` so `Object.values(...)` yields the snapshot set. #### Type Declaration ##### TERM\_CODE\_UNKNOWN > `readonly` **TERM\_CODE\_UNKNOWN**: `"TERM_CODE_UNKNOWN"` = `"TERM_CODE_UNKNOWN"` A `$lookup`/`$validate-code` found no concept for the code in the loaded release. A **first-class typed outcome, never a guess**: the engine returns `unknown`/`valid: false`, never a fabricated display or a coerced `valid: true` (the never-fabricate invariant, applied to code identity). ##### TERM\_COMPLEX\_MAP\_MALFORMED\_ROW > `readonly` **TERM\_COMPLEX\_MAP\_MALFORMED\_ROW**: `"TERM_COMPLEX_MAP_MALFORMED_ROW"` = `"TERM_COMPLEX_MAP_MALFORMED_ROW"` A SNOMED complex-map RF2 refset row was structurally unusable: missing its `referencedComponentId`/`mapGroup`/`mapPriority`. Skipped and surfaced, never partial. ##### TERM\_CONCEPT\_DEPRECATED > `readonly` **TERM\_CONCEPT\_DEPRECATED**: `"TERM_CONCEPT_DEPRECATED"` = `"TERM_CONCEPT_DEPRECATED"` A resolved concept is **deprecated / no longer current** (e.g. LOINC `STATUS = DEPRECATED`, or a FHIR concept flagged `inactive`). Surfaced on the concept's status so a caller never presents a non-current code as clean; the concept is still *found* (a lookup succeeds), it is just flagged. ##### TERM\_CONCEPT\_HEADER\_NOT\_BILLABLE > `readonly` **TERM\_CONCEPT\_HEADER\_NOT\_BILLABLE**: `"TERM_CONCEPT_HEADER_NOT_BILLABLE"` = `"TERM_CONCEPT_HEADER_NOT_BILLABLE"` A resolved concept is a **classification header, not a billable/valid leaf** (e.g. an ICD-10-CM order-file row with the position-15 flag `0`). Surfaced on the concept's status: a header code is found but is **not valid for claim submission**, and must never be presented as billable. ##### TERM\_CROSSWALK\_CONTEXT\_REQUIRED > `readonly` **TERM\_CROSSWALK\_CONTEXT\_REQUIRED**: `"TERM_CROSSWALK_CONTEXT_REQUIRED"` = `"TERM_CROSSWALK_CONTEXT_REQUIRED"` A SNOMED→ICD-10-CM complex-map group's decision needs **patient context** (an `IFA` age band or gender) the caller did not supply. The engine surfaces the candidate rules + advice and **refuses to pick a branch** it lacks the data for: never a silently-chosen target. ##### TERM\_CROSSWALK\_NO\_MAP > `readonly` **TERM\_CROSSWALK\_NO\_MAP**: `"TERM_CROSSWALK_NO_MAP"` = `"TERM_CROSSWALK_NO_MAP"` A crosswalk source code has an **authored No-Map**: the steward declares it has no valid target in the other classification (a GEM `NoDx`/`NoPCS` sentinel, or a SNOMED complex-map `447638001` "cannot be classified" category). A **first-class typed outcome, never an error and never a guess** (the never-fabricate invariant, applied to crosswalks). Distinct from [TERM\_CROSSWALK\_UNMAPPED](#term_crosswalk_unmapped): the source *is* in the map, and the map says "no target". ##### TERM\_CROSSWALK\_UNMAPPED > `readonly` **TERM\_CROSSWALK\_UNMAPPED**: `"TERM_CROSSWALK_UNMAPPED"` = `"TERM_CROSSWALK_UNMAPPED"` A crosswalk source code is **absent from the map entirely**: not an authored No-Map, simply not present. Surfaced as a typed outcome (never a fabricated target, never a silent success), and kept distinct from [TERM\_CROSSWALK\_NO\_MAP](#term_crosswalk_no_map) so a caller can tell "the steward said no target" from "this code was not in the file". ##### TERM\_CSV\_MALFORMED > `readonly` **TERM\_CSV\_MALFORMED**: `"TERM_CSV_MALFORMED"` = `"TERM_CSV_MALFORMED"` A CSV (RFC-4180, e.g. LOINC `Loinc.csv`) row was structurally unusable: too few fields to reach a configured column. The row is **skipped and surfaced** as a load warning, never partial. ##### TERM\_FHIR\_CONCEPT\_MALFORMED > `readonly` **TERM\_FHIR\_CONCEPT\_MALFORMED**: `"TERM_FHIR_CONCEPT_MALFORMED"` = `"TERM_FHIR_CONCEPT_MALFORMED"` A concept inside a FHIR `CodeSystem` resource was unusable: not an object, or missing its required `code`. Skipped and surfaced (the whole resource still loads); `line` is `0` since a JSON concept is not line-addressable. ##### TERM\_FIXED\_WIDTH\_MALFORMED > `readonly` **TERM\_FIXED\_WIDTH\_MALFORMED**: `"TERM_FIXED_WIDTH_MALFORMED"` = `"TERM_FIXED_WIDTH_MALFORMED"` A fixed-width (e.g. ICD-10-CM order file) row was structurally unusable: too short to contain the configured code field, or a missing code. Skipped and surfaced, never partial. ##### TERM\_GEM\_MALFORMED\_ROW > `readonly` **TERM\_GEM\_MALFORMED\_ROW**: `"TERM_GEM_MALFORMED_ROW"` = `"TERM_GEM_MALFORMED_ROW"` A GEM file line was structurally unusable: not three whitespace-delimited fields, or a flag field that is not a 5-digit code. The row is **skipped and surfaced** as a load warning (liberal on load), never kept as a partially-parsed entry. ##### TERM\_RRF\_MALFORMED\_ROW > `readonly` **TERM\_RRF\_MALFORMED\_ROW**: `"TERM_RRF_MALFORMED_ROW"` = `"TERM_RRF_MALFORMED_ROW"` An RRF (pipe-delimited RxNorm/UMLS release) row was structurally unusable: too few columns to reach a configured field, or a missing code. The row is **skipped and surfaced** as a load warning (liberal on load), never kept as a partially-parsed concept. ##### TERM\_RXNORM\_MALFORMED\_ROW > `readonly` **TERM\_RXNORM\_MALFORMED\_ROW**: `"TERM_RXNORM_MALFORMED_ROW"` = `"TERM_RXNORM_MALFORMED_ROW"` A RxNorm RRF row (`RXNCONSO` / `RXNREL` / `RXNSAT`) was structurally unusable: too few columns to reach a required field, or a missing `RXCUI`/NDC value. The row is **skipped and surfaced** as a load warning (liberal on load), never kept as a partial concept/edge. Rows that are simply *not of interest* (a non-`RXNORM` atom, an atom-level relationship, a non-`NDC` attribute) are skipped **silently**: they are expected, not faults. ##### TERM\_RXNORM\_NDC\_UNMAPPED > `readonly` **TERM\_RXNORM\_NDC\_UNMAPPED**: `"TERM_RXNORM_NDC_UNMAPPED"` = `"TERM_RXNORM_NDC_UNMAPPED"` An NDC could **not be resolved** to any `RXCUI` in the loaded RxNorm release: a typed, surfaced absence, never a guessed `RXCUI`. NDC↔RXCUI is many:1 and temporal, so a resolution is always release-scoped; an NDC not present in the loaded release is this diagnostic. ##### TERM\_RXNORM\_UNKNOWN\_RXCUI > `readonly` **TERM\_RXNORM\_UNKNOWN\_RXCUI**: `"TERM_RXNORM_UNKNOWN_RXCUI"` = `"TERM_RXNORM_UNKNOWN_RXCUI"` A RxNorm graph navigation was asked about an `RXCUI` **absent from the loaded release**: a first-class typed outcome, never a fabricated concept and never an empty "success" (the never-fabricate invariant, applied to the drug graph). Distinct from a *present* concept that simply has no edge of the requested relationship (that is a found result with empty targets). ##### TERM\_RXNORM\_UNTYPED\_CONCEPT > `readonly` **TERM\_RXNORM\_UNTYPED\_CONCEPT**: `"TERM_RXNORM_UNTYPED_CONCEPT"` = `"TERM_RXNORM_UNTYPED_CONCEPT"` An `RXCUI` appeared in the release's `SAB=RXNORM` atoms but **none of them could establish its term type**, so it is **not** in the graph. Either every atom carried a synonym-class `TTY` (`PSN`/`SY`/`TMSY`, which Appendix 5 defines as a "synonym of another TTY" and which therefore types a *name* rather than a concept) or every atom carried a `TTY` this engine does not model. The concept is skipped rather than typed from a synonym atom: an absent concept is already a first-class typed answer (`TERM_RXNORM_UNKNOWN_RXCUI`), while a concept whose term type reads `TMSY` is a fabricated claim about what a drug *is* that a caller cannot detect. ##### TERM\_SYSTEM\_UNRECOGNIZED > `readonly` **TERM\_SYSTEM\_UNRECOGNIZED**: `"TERM_SYSTEM_UNRECOGNIZED"` = `"TERM_SYSTEM_UNRECOGNIZED"` A code-system identifier (URI, OID, or mnemonic) was not recognized by the identity resolver. Surfaced as a typed `unknown`, never coerced to a guessed canonical URI. ##### TERM\_TRANSLATE\_UNMAPPED > `readonly` **TERM\_TRANSLATE\_UNMAPPED**: `"TERM_TRANSLATE_UNMAPPED"` = `"TERM_TRANSLATE_UNMAPPED"` A `$translate` found no target for the source code. A **first-class outcome, never an error**: the source is surfaced as `unmapped`, never replaced with a guessed target (the never-fabricate invariant). ##### TERM\_UCUM\_INVALID > `readonly` **TERM\_UCUM\_INVALID**: `"TERM_UCUM_INVALID"` = `"TERM_UCUM_INVALID"` A UCUM unit expression is **not valid**: it does not parse against the UCUM grammar, or it names an atom absent from the vendored UCUM table. A **first-class typed outcome, never an error and never a guess**: [validateUcum](#validateucum) returns `{ valid: false, reason }`, never a coerced or "nearest" unit (the never-fabricate invariant, applied to units). The `reason` is value-free (a grammar fault or an unknown-atom shape), never PHI. ##### TERM\_VALUESET\_CANNOT\_EXPAND > `readonly` **TERM\_VALUESET\_CANNOT\_EXPAND**: `"TERM_VALUESET_CANNOT_EXPAND"` = `"TERM_VALUESET_CANNOT_EXPAND"` A ValueSet `compose` part could **not be expanded**: an intensional `include`/`exclude` whose code system was not supplied, an unresolvable referenced value set, a `filter` operator the engine does not implement, or a component whose declared code system version disagrees with the supplied release (or names a pin that release carries no version to confirm). Surfaced as a typed outcome that marks the expansion **incomplete**; the engine **never** fabricates a member and **never** returns a silently-empty "no members" answer for a part it could not compute (a false "not a member" is a clinical error). ##### TERM\_VALUESET\_ENUMERATED\_CODE\_UNDEFINED > `readonly` **TERM\_VALUESET\_ENUMERATED\_CODE\_UNDEFINED**: `"TERM_VALUESET_ENUMERATED_CODE_UNDEFINED"` = `"TERM_VALUESET_ENUMERATED_CODE_UNDEFINED"` A `ValueSet.compose` component **enumerated** a code the release supplied for its `system` does not define, so the code was **not admitted** as a member. The never-fabricate invariant applied to an extensional selection: enumerating a code is the value set's claim that it exists, and asserting membership for it while holding the very release that shows it does not is the engine deciding against its own evidence. Raised **only** where the evidence is in hand: a release keyed by the component's `system` that the component's declared `version` (if any) agrees with. No release for that system, a declared version the supplied release does not agree with, or a component naming no `system` at all leaves the engine with no contrary evidence, so every enumerated code is carried and **no diagnostic is raised**: absence of evidence is not evidence the code is undefined. The answer stays **decided**, unlike [TERM\_VALUESET\_CANNOT\_EXPAND](#term_valueset_cannot_expand): the engine computed membership on the evidence it was given, so `expand` reports `complete: true` and `validateCodeInValueSet` reports a decided non-member for the dropped code, rather than a lower bound or a refusal. One diagnostic is raised per affected component, located by that component's own index path, however many of its codes were dropped. ##### TERM\_VALUESET\_EXPANSION\_TRUNCATED > `readonly` **TERM\_VALUESET\_EXPANSION\_TRUNCATED**: `"TERM_VALUESET_EXPANSION_TRUNCATED"` = `"TERM_VALUESET_EXPANSION_TRUNCATED"` A **pre-computed** `ValueSet.expansion` is **incomplete**: its `total` exceeds the number of `contains` entries, it is flagged too-costly (the VSAC `$expand` >1200-code truncation hazard), or it is marked `unclosed` (the value set is unbounded because it includes post-coordinated content, so no expansion can enumerate it). The engine **never treats an incomplete expansion as complete membership**: a code absent from one is `undetermined`, never a confident "not a member". The diagnostic's `detail` says which of those it was. #### Example ```ts import { DIAGNOSTIC_CODES } from "@cosyte/terminology"; DIAGNOSTIC_CODES.TERM_TRANSLATE_UNMAPPED; // => "TERM_TRANSLATE_UNMAPPED" ``` *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` Stable **fatal codes**: thrown (as a [TerminologyError](#terminologyerror)) when input cannot be loaded into a usable model at all. Always thrown, never swallowed. `key === value`, part of the contract. #### Type Declaration ##### TERM\_CODESYSTEM\_MALFORMED > `readonly` **TERM\_CODESYSTEM\_MALFORMED**: `"TERM_CODESYSTEM_MALFORMED"` = `"TERM_CODESYSTEM_MALFORMED"` A CodeSystem *source* was structurally unusable: a FHIR resource with the wrong `resourceType`, or a delimited source whose configured code/display column is absent from the header. Thrown rather than silently loading an empty or mis-keyed release. (Malformed individual **rows** are skipped-and-surfaced load warnings, not fatals: only an unusable *source* is fatal.) ##### TERM\_CONCEPTMAP\_MALFORMED > `readonly` **TERM\_CONCEPTMAP\_MALFORMED**: `"TERM_CONCEPTMAP_MALFORMED"` = `"TERM_CONCEPTMAP_MALFORMED"` A `ConceptMap` resource was structurally unusable: wrong `resourceType`, or a group/element missing a required field. Thrown rather than silently loading a partial, misleading map. ##### TERM\_CROSSWALK\_MALFORMED > `readonly` **TERM\_CROSSWALK\_MALFORMED**: `"TERM_CROSSWALK_MALFORMED"` = `"TERM_CROSSWALK_MALFORMED"` A crosswalk map *source* was structurally unusable: e.g. a SNOMED complex-map RF2 refset with empty content or a header missing a required column. Thrown rather than silently loading an empty or mis-keyed map. (Malformed individual **rows** are skipped-and-surfaced warnings, not fatals.) ##### TERM\_MAP\_NOT\_INVERTIBLE > `readonly` **TERM\_MAP\_NOT\_INVERTIBLE**: `"TERM_MAP_NOT_INVERTIBLE"` = `"TERM_MAP_NOT_INVERTIBLE"` An **inversion** of a directional crosswalk was requested: a forbidden operation. The GEMs and the SNOMED→ICD-10-CM map are authoritative in **one direction only**; a forward map is not the inverse of the backward map. Thrown by ../crosswalk/gems.invertGem so the never-invert refusal is a first-class, discoverable contract rather than a silent absence. ##### TERM\_VALUESET\_MALFORMED > `readonly` **TERM\_VALUESET\_MALFORMED**: `"TERM_VALUESET_MALFORMED"` = `"TERM_VALUESET_MALFORMED"` A `ValueSet` resource was structurally unusable: wrong `resourceType`, or a `compose`/`expansion` whose required shape (an `include` that is not an object, a `contains` entry missing its `code`) is corrupt. Thrown rather than silently loading a partial value set that would bind *some* codes and quietly drop others. (A `filter` operator the engine does not implement is **not** fatal; it is the surfaced `TERM_VALUESET_CANNOT_EXPAND` diagnostic at expansion time.) #### Example ```ts import { FATAL_CODES } from "@cosyte/terminology"; FATAL_CODES.TERM_CONCEPTMAP_MALFORMED; // => "TERM_CONCEPTMAP_MALFORMED" ``` *** ### HL7\_V2\_OID\_ROOT > `const` **HL7\_V2\_OID\_ROOT**: `"2.16.840.1.113883.12"` = `"2.16.840.1.113883.12"` The OID root under which every HL7 v2 table code system is registered: `2.16.840.1.113883.12.[table]`: grounded against the FHIR R4 terminology-systems registry. The table number is appended with no leading zeros (e.g. table 0203 → `…12.203`). *** ### HL7\_V2\_URL\_PREFIX > `const` **HL7\_V2\_URL\_PREFIX**: `"http://terminology.hl7.org/CodeSystem/v2-"` = `"http://terminology.hl7.org/CodeSystem/v2-"` The canonical URI prefix for HL7 v2 table code systems in HL7 Terminology (THO): `http://terminology.hl7.org/CodeSystem/v2-[XXXX]`, where `[XXXX]` is the zero-padded 4-digit table id: grounded against the FHIR R4 terminology-systems registry. *** ### ICD10CM\_ORDER\_FILE\_FIELDS > `const` **ICD10CM\_ORDER\_FILE\_FIELDS**: [`FixedWidthFieldMap`](#fixedwidthfieldmap) The documented **ICD-10-CM order file** field layout (0-based char indices). - `code`: the ICD-10-CM code (no decimal point). - `billable`, position-15 flag at index 14: `"1"` = billable/valid leaf, `"0"` = header. - `display`: the long description (to end of line). - property `shortDescription`: the 60-char short description. Grounded on the CMS/CDC order-file layout; **verify against your release's README** before relying on the exact offsets, which are not confirmed against an authoritative machine-readable source. The reader takes the layout as a parameter precisely so this preset is a starting point, not a hardcoded assumption. *** ### ICD10CM\_SYSTEM > `const` **ICD10CM\_SYSTEM**: `"http://hl7.org/fhir/sid/icd-10-cm"` = `"http://hl7.org/fhir/sid/icd-10-cm"` The canonical code-system URI for **ICD-10-CM**: the target system of both crosswalks. A published identity fact (mirrors ../systems/registry.SYSTEM\_IDENTITIES); attached to a resolved target ../common/coding.Coding so a translation is self-describing. *** ### ICD9CM\_SYSTEM > `const` **ICD9CM\_SYSTEM**: `"http://hl7.org/fhir/sid/icd-9-cm"` = `"http://hl7.org/fhir/sid/icd-9-cm"` The canonical code-system URI for **ICD-9-CM** (the legacy diagnosis classification the GEMs bridge to/from). A published identity fact; attached to a GEM target/source coding. *** ### MAP\_CATEGORIES > `const` **MAP\_CATEGORIES**: `object` The SNOMED CT complex-map **map category** concept ids: the `mapCategoryId` column of the SNOMED→ICD-10-CM extended-map refset. These four SNOMED CT concepts (id and steward description) are **bundled** in this package (SNOMED CT is copyright © International Health Terminology Standards Development Organisation); carried through a resolution verbatim so a caller sees the steward's own trust signal. #### Type Declaration ##### AMBIGUOUS > `readonly` **AMBIGUOUS**: `"447640006"` = `"447640006"` `447640006`: "Source concept is ambiguous" (surface candidates, never pick one). ##### CONTEXT\_DEPENDENT > `readonly` **CONTEXT\_DEPENDENT**: `"447639009"` = `"447639009"` `447639009`: "Map of source concept is context dependent" (needs runtime patient context). ##### NO\_MAP > `readonly` **NO\_MAP**: `"447638001"` = `"447638001"` `447638001`: "Map source concept cannot be classified with available data" (a **No-Map**). ##### PROPERLY\_CLASSIFIED > `readonly` **PROPERLY\_CLASSIFIED**: `"447637006"` = `"447637006"` `447637006`: "Map source concept is properly classified" (a normal, usable target). #### Example ```ts import { MAP_CATEGORIES } from "@cosyte/terminology"; MAP_CATEGORIES.NO_MAP; // => "447638001" ``` *** ### NO\_MAP\_CATEGORIES > `const` **NO\_MAP\_CATEGORIES**: `ReadonlySet`\<`string`\> The set of [MAP\_CATEGORIES](#map_categories) ids the resolver treats as a **No-Map** (a typed, surfaced "cannot be classified", never a fabricated target). *** ### RELA > `const` **RELA**: `object` The drug-graph `RELA` relationship names, grounded on the NLM RxNorm Technical Documentation (Appendix 1). Each is directional and read `subject ⟶RELA⟶ object` after normalization (see the module doc). RxNorm ships both members of an inverse pair as **separate rows**, so the graph traverses the authored predicate for the direction it wants: it **never synthesizes an inverse**. #### Type Declaration ##### CONSISTS\_OF > `readonly` **CONSISTS\_OF**: `"consists_of"` = `"consists_of"` `consists_of`: subject (an `SCD`/`SBD`) *consists of* object (its `SCDC`/`SBDC` component(s)). ##### CONSTITUTES > `readonly` **CONSTITUTES**: `"constitutes"` = `"constitutes"` `constitutes`: subject (a component) *constitutes* object (the drug). Inverse of `consists_of`. ##### CONTAINED\_IN > `readonly` **CONTAINED\_IN**: `"contained_in"` = `"contained_in"` `contained_in`: subject (a drug) *is contained in* object (a pack). Inverse of `contains`. ##### CONTAINS > `readonly` **CONTAINS**: `"contains"` = `"contains"` `contains`: subject (a pack) *contains* object (a member drug). ##### DOSE\_FORM\_GROUP\_OF > `readonly` **DOSE\_FORM\_GROUP\_OF**: `"doseformgroup_of"` = `"doseformgroup_of"` `doseformgroup_of`: inverse of `has_doseformgroup`. ##### DOSE\_FORM\_OF > `readonly` **DOSE\_FORM\_OF**: `"dose_form_of"` = `"dose_form_of"` `dose_form_of`: subject (a `DF`) *is the dose form of* object (a drug). ##### HAS\_DOSE\_FORM > `readonly` **HAS\_DOSE\_FORM**: `"has_dose_form"` = `"has_dose_form"` `has_dose_form`: subject (a drug) *has dose form* object (a `DF`). ##### HAS\_DOSE\_FORM\_GROUP > `readonly` **HAS\_DOSE\_FORM\_GROUP**: `"has_doseformgroup"` = `"has_doseformgroup"` `has_doseformgroup`: subject *has dose-form group* object (a `DFG`). ##### HAS\_INGREDIENT > `readonly` **HAS\_INGREDIENT**: `"has_ingredient"` = `"has_ingredient"` `has_ingredient`: subject *has ingredient* object. Authored from the clinical side (`SCDC`/`SCDF`/`SCDG`) to an `IN` and from the branded side (`SBD`/`SBDC`/`SBDF`/`SBDG`) to a `BN`; RxNorm authors no `SCD ⟶ IN` row. ##### HAS\_PRECISE\_INGREDIENT > `readonly` **HAS\_PRECISE\_INGREDIENT**: `"has_precise_ingredient"` = `"has_precise_ingredient"` `has_precise_ingredient`: subject *has precise ingredient* object (a `PIN`). ##### HAS\_QUANTIFIED\_FORM > `readonly` **HAS\_QUANTIFIED\_FORM**: `"has_quantified_form"` = `"has_quantified_form"` `has_quantified_form`: subject *has quantified form* object. ##### HAS\_TRADENAME > `readonly` **HAS\_TRADENAME**: `"has_tradename"` = `"has_tradename"` `has_tradename`: subject (a generic concept) *has tradename* object (a branded concept). ##### INGREDIENT\_OF > `readonly` **INGREDIENT\_OF**: `"ingredient_of"` = `"ingredient_of"` `ingredient_of`: subject *is an ingredient of* object. The authored inverse, so an `IN` reaches clinical components and dose forms and a `BN` reaches branded concepts (the `SBD` included). Neither reaches an `SCD`. ##### INVERSE\_ISA > `readonly` **INVERSE\_ISA**: `"inverse_isa"` = `"inverse_isa"` `inverse_isa`: subject *is a parent of* object. Inverse of `isa`. ##### ISA > `readonly` **ISA**: `"isa"` = `"isa"` `isa`: subject *is a* object (hierarchical, e.g. `SCD isa SCDG`). ##### PRECISE\_INGREDIENT\_OF > `readonly` **PRECISE\_INGREDIENT\_OF**: `"precise_ingredient_of"` = `"precise_ingredient_of"` `precise_ingredient_of`: inverse of `has_precise_ingredient`. ##### QUANTIFIED\_FORM\_OF > `readonly` **QUANTIFIED\_FORM\_OF**: `"quantified_form_of"` = `"quantified_form_of"` `quantified_form_of`: inverse of `has_quantified_form`. ##### TRADENAME\_OF > `readonly` **TRADENAME\_OF**: `"tradename_of"` = `"tradename_of"` `tradename_of`: subject (a branded concept) *is a tradename of* object (its generic). #### Example ```ts import { RELA } from "@cosyte/terminology"; RELA.HAS_INGREDIENT; // => "has_ingredient" ``` *** ### RELA\_INVERSE > `const` **RELA\_INVERSE**: `Readonly`\<`Record`\<`string`, `string`\>\> The authored **inverse pairs** of the drug-graph `RELA`s: a published fact (RxNorm Appendix 1), used only to *document* and *validate* that a caller is traversing a real relationship. The graph **never** uses this to synthesize a missing edge: RxNorm ships both directions, so ../rxnorm/navigate follows whichever authored predicate points the way it wants to go. #### Example ```ts import { RELA_INVERSE, RELA } from "@cosyte/terminology"; RELA_INVERSE[RELA.HAS_INGREDIENT]; // => "ingredient_of" ``` *** ### RXNORM\_SYSTEM > `const` **RXNORM\_SYSTEM**: `"http://www.nlm.nih.gov/research/umls/rxnorm"` = `"http://www.nlm.nih.gov/research/umls/rxnorm"` The canonical code-system URI for **RxNorm**: a published identity fact, attached to graph codings. *** ### SNOMEDCT\_SYSTEM > `const` **SNOMEDCT\_SYSTEM**: `"http://snomed.info/sct"` = `"http://snomed.info/sct"` The canonical code-system URI for **SNOMED CT**: the source system of the SNOMED→ICD-10-CM complex map. Recognised by URI only; **no SNOMED CT refset or release is bundled**. *** ### SYSTEM\_IDENTITIES > `const` **SYSTEM\_IDENTITIES**: readonly [`SystemIdentity`](#systemidentity)[] The seed identity table. Order is irrelevant; lookup indexes are built from it in ../systems/resolve. Each entry is one firsthand-grounded system (see the module doc). *** ### TERM\_TYPES > `const` **TERM\_TYPES**: `Readonly`\<`Record`\<[`TermType`](#termtype), `string`\>\> The drug-graph term types (`RXNCONSO.TTY`), each mapped to its **verbatim Appendix 5 name** (`ET` excepted: the appendix publishes no Name for it, so the UMLS expansion "Entry Term" is used). Encoded as an identity fact; a concept's `TTY` is carried verbatim on its ../rxnorm/types.RxNormConcept, never re-labeled. The keys are the full Appendix 5 vocabulary, defining and synonym alike. Only the **defining** ones can appear on a loaded concept (see [isSynonymTermType](#issynonymtermtype)). #### Example ```ts import { TERM_TYPES } from "@cosyte/terminology"; TERM_TYPES.SCD; // => "Semantic Clinical Drug" TERM_TYPES.SCDF; // => "Semantic Clinical Drug Form" ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, kept in lockstep with `package.json#version`. Changesets owns the bump and rewrites `package.json` only, so the release `version` script runs `scripts/sync-version.mjs` to rewrite this declaration in the same commit, and `test/sanity.test.ts` compares the two so a skipped sync goes red instead of shipping a version string that lies. That is not hypothetical: `0.0.1` went to the registry exporting `"0.0.0"`. #### Example ```ts import { VERSION } from "@cosyte/terminology"; typeof VERSION; // => "string" ``` ## Functions ### applyComplexMap() > **applyComplexMap**(`map`, `source`, `context?`): [`ComplexMapApplyResult`](#complexmapapplyresult) Apply a loaded SNOMED→ICD-10-CM complex map to a source concept and patient context. Resolves **each map group** independently (groups are an AND, a source needing multiple ICD-10-CM codes has multiple groups) by running its rules in priority order against `context`. A group whose decision needs `IFA` context the caller did not supply is a typed [ComplexMapContextRequired](#complexmapcontextrequired); a `447638001` row is a typed No-Map; every result carries the steward's advice verbatim. **Never** fabricates a target, **never** inverts the map. #### Parameters ##### map [`ComplexMap`](#complexmap) A [ComplexMap](#complexmap) from [loadComplexMap](#loadcomplexmap). ##### source `string` The SNOMED CT source concept id. ##### context? [`PatientContext`](#patientcontext) = `{}` The caller-supplied [PatientContext](#patientcontext) (age / gender). Optional; a missing field that a rule needs yields a `context-required` group, never a guessed branch. #### Returns [`ComplexMapApplyResult`](#complexmapapplyresult) A [ComplexMapApplyResult](#complexmapapplyresult): per-group resolutions, or a typed absence. #### Example ```ts import { loadComplexMap, applyComplexMap } from "@cosyte/terminology"; const map = loadComplexMap({ format: "rows", rows: [ { source: "72098002", group: 1, priority: 1, rule: "IFA 248152002 | Female (finding) |", advice: "MAP IS CONTEXT DEPENDENT FOR GENDER", target: "O00.9", category: "447639009" }, { source: "72098002", group: 1, priority: 2, rule: "OTHERWISE TRUE", advice: "CANNOT BE CLASSIFIED", category: "447638001" }, ], }); const r = applyComplexMap(map, "72098002", { gender: "female" }); r.mapped; // => true ``` *** ### applyGem() > **applyGem**(`map`, `source`): [`GemApplyResult`](#gemapplyresult) Apply a loaded GEM map to a source code, in the map's authored direction only. Returns the **full** candidate set (never collapsed to one), with each entry's steward flags; a combination source additionally surfaces its scenario/choice-list structure. A source the steward marked No-Map (`NoDx` sentinel) is a typed [CrosswalkNoMap](#crosswalknomap); a source **absent** from the map is a typed [CrosswalkUnmapped](#crosswalkunmapped): the two are distinct, and neither is ever a fabricated code. #### Parameters ##### map [`GemMap`](#gemmap) A [GemMap](#gemmap) from [loadGems](#loadgems). ##### source `string` The source code (in the map's source classification). #### Returns [`GemApplyResult`](#gemapplyresult) A [GemApplyResult](#gemapplyresult). #### Example ```ts import { loadGems, applyGem } from "@cosyte/terminology"; const gems = loadGems({ direction: "9-to-10", content: "V290 NoDx 11000\n" }); const r = applyGem(gems, "V290"); r.mapped; // => false ``` *** ### approximateMatch() > **approximateMatch**(`graph`, `query`, `options?`): readonly [`RxNormApproximateMatch`](#rxnormapproximatematch)[] **Opt-in, explicitly labeled** approximate name matching: find loaded concepts whose names are token-similar to `query`. This is **never** the default resolution path and its results are **never** an exact code assertion: every candidate is marked `approximate: true` with a derived [RxNormApproximateMatch.score](#score). A caller opts in by calling this function explicitly. Coverage is not correctness: a "no match" is an **empty** array (typed, honest), distinct from a fabricated nearest guess. The engine never promotes an approximate hit to an exact one. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### query `string` A free-text drug name to match against loaded concept names. ##### options? [`ApproximateMatchOptions`](#approximatematchoptions) = `{}` [ApproximateMatchOptions](#approximatematchoptions) (min score, limit). #### Returns readonly [`RxNormApproximateMatch`](#rxnormapproximatematch)[] The labeled approximate candidates, best-first (possibly empty). Never an exact resolution. #### Example ```ts import { loadRxNormGraph, approximateMatch } from "@cosyte/terminology"; const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||", rel: "", }); approximateMatch(g, "lisinopril")[0]?.approximate; // => true ``` *** ### asTermType() > **asTermType**(`tty`): [`TermType`](#termtype) \| `undefined` Narrow a raw `RXNCONSO.TTY` string to a known [TermType](#termtype), or `undefined` when it is not a drug-graph term type this engine models (the atom is then skipped at load, liberal-on-load, never coerced to a wrong type). #### Parameters ##### tty `string` The raw `TTY` value from an `RXNCONSO` row. #### Returns [`TermType`](#termtype) \| `undefined` The narrowed [TermType](#termtype), or `undefined` when unrecognized. #### Example ```ts import { asTermType } from "@cosyte/terminology"; asTermType("SCD"); // => "SCD" asTermType("ZZZ"); // => undefined ``` *** ### brandsFor() > **brandsFor**(`graph`, `rxcui`): [`RxNormNavResult`](#rxnormnavresult) The **branded** form(s) of a generic concept: an `SCD`/`IN`/`GPCK` that `has_tradename` a brand. The authored forward edge; not a synthesized inverse of `tradename_of`. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### rxcui `string` The generic concept. #### Returns [`RxNormNavResult`](#rxnormnavresult) The branded concept(s) (possibly empty), or a typed unknown source. #### Example ```ts import { loadRxNormGraph, brandsFor } from "@cosyte/terminology"; const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n2|ENG||||||||||RXNORM|SBD||lisinopril 10 MG Oral Tablet [Zestril]||N||", rel: "2|||RN|1|||has_tradename|||RXNORM||||||", }); brandsFor(g, "1").found; // => true ``` *** ### buildSubsumption() > **buildSubsumption**(`cs`): [`Subsumption`](#subsumption) Build a cycle-safe ancestor index from a loaded [CodeSystem](#codesystem)'s `parent` concept-properties. #### Parameters ##### cs [`CodeSystem`](#codesystem) The loaded code system. #### Returns [`Subsumption`](#subsumption) A [Subsumption](#subsumption) answering transitive-ancestor queries. #### Example ```ts import { loadCodeSystem, buildSubsumption } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", concept: [{ code: "p", concept: [{ code: "c" }] }] }, }); buildSubsumption(cs).ancestorsOf("c").has("p"); // => true ``` *** ### codeableConcept() > **codeableConcept**(`init`): [`CodeableConcept`](#codeableconcept) Construct a deep-frozen [CodeableConcept](#codeableconcept) (its `coding` array and every member frozen). #### Parameters ##### init [`CodeableConcept`](#codeableconcept) The concept fields; `coding` is required (may be empty). #### Returns [`CodeableConcept`](#codeableconcept) A frozen [CodeableConcept](#codeableconcept). #### Example ```ts import { codeableConcept } from "@cosyte/terminology"; const cc = codeableConcept({ coding: [{ code: "2160-0" }] }); Object.isFrozen(cc.coding); // => true ``` *** ### coding() > **coding**(`init`): [`Coding`](#coding-1) Construct a deep-frozen [Coding](#coding-1). The returned object is `Object.freeze`d so it cannot be mutated in place: parsed/translated values are immutable by default. Optional fields are only present when supplied (no `undefined` keys), so structural equality checks stay clean. #### Parameters ##### init [`Coding`](#coding-1) The coding fields; `code` is required. #### Returns [`Coding`](#coding-1) A frozen [Coding](#coding-1). #### Example ```ts import { coding } from "@cosyte/terminology"; const frozen = coding({ system: "http://loinc.org", code: "2160-0" }); Object.isFrozen(frozen); // => true ``` *** ### consistsOf() > **consistsOf**(`graph`, `rxcui`): [`RxNormNavResult`](#rxnormnavresult) The **components** a drug `consists_of`: an `SCD`/`SBD` → its `SCDC`/`SBDC` dose-form components. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### rxcui `string` The drug concept. #### Returns [`RxNormNavResult`](#rxnormnavresult) The component concept(s) (possibly empty), or a typed unknown source. #### Example ```ts import { loadRxNormGraph, consistsOf } from "@cosyte/terminology"; const g = loadRxNormGraph({ conso: "2|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n3|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||", rel: "3|||RN|2|||consists_of|||RXNORM||||||", }); consistsOf(g, "2").found; // => true ``` *** ### doseFormsOf() > **doseFormsOf**(`graph`, `rxcui`): [`RxNormNavResult`](#rxnormnavresult) The **dose form(s)** of a drug: the `DF`/`DFG` concepts it `has_dose_form` / `has_doseformgroup`. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### rxcui `string` The drug concept. #### Returns [`RxNormNavResult`](#rxnormnavresult) The dose-form concept(s) (possibly empty), or a typed unknown source. #### Example ```ts import { loadRxNormGraph, doseFormsOf } from "@cosyte/terminology"; const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n9|ENG||||||||||RXNORM|DF||Oral Tablet||N||", rel: "9|||RN|1|||has_dose_form|||RXNORM||||||", }); doseFormsOf(g, "1").found; // => true ``` *** ### expand() > **expand**(`vs`, `ctx?`): [`ExpandResult`](#expandresult) `$expand`: compute the flattened membership of a [ValueSet](#valueset-1) over the supplied code systems. #### Parameters ##### vs [`ValueSet`](#valueset-1) The loaded value set. ##### ctx? [`ExpansionContext`](#expansioncontext) = `{}` The loaded code systems / referenced value sets the intensional parts resolve against. #### Returns [`ExpandResult`](#expandresult) An [ExpandResult](#expandresult): the `contains` members plus an honest `complete` flag. When `complete` is `false`, `contains` is a lower bound (never treat it as exhaustive membership). #### Example ```ts import { loadValueSet, loadCodeSystem, expand } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", url: "http://example.org/cs", concept: [{ code: "a" }, { code: "b" }, { code: "c" }], }, }); const vs = loadValueSet({ resourceType: "ValueSet", compose: { include: [{ system: "http://example.org/cs", concept: [{ code: "a" }, { code: "b" }] }] }, }); const result = expand(vs, { codeSystems: new Map([["http://example.org/cs", cs]]) }); result.complete; // => true result.contains.length; // => 2 ``` *** ### genericFor() > **genericFor**(`graph`, `rxcui`): [`RxNormNavResult`](#rxnormnavresult) The **generic** form(s) of a branded concept: an `SBD`/`BN`/`BPCK` `tradename_of` its generic. The authored forward edge; the engine never inverts a `has_tradename` edge to synthesize this. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### rxcui `string` The branded concept. #### Returns [`RxNormNavResult`](#rxnormnavresult) The generic concept(s) (possibly empty), or a typed unknown source. #### Example ```ts import { loadRxNormGraph, genericFor } from "@cosyte/terminology"; // "SBD(2) tradename_of SCD(1)": subject=RXCUI2=2, object=RXCUI1=1. const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||\n2|ENG||||||||||RXNORM|SBD||lisinopril 10 MG Oral Tablet [Zestril]||N||", rel: "1|||RN|2|||tradename_of|||RXNORM||||||", }); genericFor(g, "2").found; // => true ``` *** ### getArray() > **getArray**(`obj`, `key`): readonly `unknown`[] \| `undefined` Read an **array** property from an untrusted object as a `readonly unknown[]`, or `undefined`. The elements are left as `unknown`: callers narrow each one. A non-array (including a missing key) yields `undefined`, which callers treat as "no entries" rather than an error. #### Parameters ##### obj `unknown` The source object (may be any `unknown`). ##### key `string` The property name to read. #### Returns readonly `unknown`[] \| `undefined` The array (elements untyped), or `undefined` when the key is missing or not an array. #### Example ```ts import { getArray } from "@cosyte/terminology"; getArray({ group: [{ source: "a" }] }, "group")?.length; // => 1 ``` *** ### getBoolean() > **getBoolean**(`obj`, `key`): `boolean` \| `undefined` Read a **boolean** property from an untrusted object, or `undefined` if absent/not a boolean. A non-boolean (including a missing key, or the strings `"true"`/`"false"`) yields `undefined`: the accessor never coerces, so a caller can distinguish "explicitly `false`" from "absent". #### Parameters ##### obj `unknown` The source object (may be any `unknown`). ##### key `string` The property name to read. #### Returns `boolean` \| `undefined` The boolean value, or `undefined` when the key is missing or not a boolean. #### Example ```ts import { getBoolean } from "@cosyte/terminology"; getBoolean({ inactive: true }, "inactive"); // => true ``` *** ### getConcept() > **getConcept**(`graph`, `rxcui`): [`RxNormConcept`](#rxnormconcept) \| `undefined` Look up a single concept by `RXCUI`. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### rxcui `string` The concept unique identifier. #### Returns [`RxNormConcept`](#rxnormconcept) \| `undefined` The [RxNormConcept](#rxnormconcept), or `undefined` when the `RXCUI` is not in the release. #### Example ```ts import { loadRxNormGraph, getConcept } from "@cosyte/terminology"; const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||aspirin||N||", rel: "" }); getConcept(g, "1")?.tty; // => "IN" ``` *** ### getNumber() > **getNumber**(`obj`, `key`): `number` \| `undefined` Read a **number** property from an untrusted object, or `undefined` if absent/not a finite number. #### Parameters ##### obj `unknown` The source object (may be any `unknown`). ##### key `string` The property name to read. #### Returns `number` \| `undefined` The number value, or `undefined` when the key is missing or not a finite number. #### Example ```ts import { getNumber } from "@cosyte/terminology"; getNumber({ valueInteger: 3 }, "valueInteger"); // => 3 ``` *** ### getString() > **getString**(`obj`, `key`): `string` \| `undefined` Read a **string** property from an untrusted object, or `undefined` if absent/not a string. #### Parameters ##### obj `unknown` The source object (may be any `unknown`). ##### key `string` The property name to read. #### Returns `string` \| `undefined` The string value, or `undefined` when the key is missing or not a string. #### Example ```ts import { getString } from "@cosyte/terminology"; getString({ url: "http://loinc.org" }, "url"); // => "http://loinc.org" ``` *** ### ingredientsOf() > **ingredientsOf**(`graph`, `rxcui`): [`RxNormNavResult`](#rxnormnavresult) The **ingredients** of a concept: the concepts it links to by a **direct** `has_ingredient` / `has_precise_ingredient` edge in the loaded release. This follows the *authored, direct* edge only (never-fabricate, never a synthesized path), and **RxNorm's ingredient topology is not the obvious one**. Always read the `TTY` of what comes back: - concepts on the **clinical** side (`SCDC`, `SCDF`, `SCDG`) link to the ingredient itself, an `IN`; - concepts on the **branded** side (`SBD`, `SBDC`, `SBDF`, `SBDG`) link to their **brand name**, a `BN`. That is what the release says and what is echoed back, term type included. It is *not* the active ingredient. (`has_precise_ingredient`, which this function also follows, likewise reaches a `PIN`.) - a **clinical drug** (`SCD`) carries **no** `has_ingredient` edge at all. RxNorm authors none, in any release, so this honestly returns found-with-empty-targets rather than walking a path on the caller's behalf. - the **precise** forms (`SCDFP`, `SBDFP`, `SCDGP`) carry **neither** edge, so this returns found-with-empty-targets for them too. They do **not** behave like their non-precise siblings: an `SCDFP` reaches its basis-of-strength substance(s) by `has_boss`, a one-to-many relation with a different meaning whose targets can be a `PIN` or a plain `IN`, and which this function deliberately does not follow; an `SBDFP` and an `SCDGP` author no ingredient-bearing edge at all and are reached instead through `form_of` / `tradename_of` / `isa`. Those pairings are the ones RxNorm authors, not a closed set a caller may rely on: check the returned `TTY` rather than assuming it from the one you queried. **To reach an active ingredient, walk to the CLINICAL component and take its edge.** From an `SCD` that is `SCD ⟶consists_of⟶ SCDC ⟶has_ingredient⟶ IN`, i.e. [consistsOf](#consistsof) then `ingredientsOf`. From an `SBD` it needs one more decision, because `consists_of` returns **both** its branded component (`SBDC`, whose own ingredient edge is the `BN` again) and the clinical `SCDC`: it is the `SCDC` that leads to the `IN`. The engine never invents the transitive edge and never picks that branch for you. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### rxcui `string` The concept (a drug or a clinical/branded component). #### Returns [`RxNormNavResult`](#rxnormnavresult) The directly-linked ingredient concepts (possibly empty), or a typed unknown source. #### Example ```ts import { loadRxNormGraph, ingredientsOf } from "@cosyte/terminology"; // The ingredient edge is authored on the COMPONENT, not on the clinical drug. const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||", rel: "1|||RN|2|||has_ingredient|||RXNORM||||||", }); ingredientsOf(g, "2").found; // => true ``` *** ### invertGem() > **invertGem**(`map`): `never` Refuse to invert a directional GEM map. **Always throws** [FATAL\_CODES.TERM\_MAP\_NOT\_INVERTIBLE](#term_map_not_invertible). The never-invert invariant made explicit and discoverable: a forward GEM (9→10) is **not** the inverse of the backward GEM (10→9): CMS ships them as separate artifacts and *"forward and backward mappings are not simply the reverse"*. To resolve the other direction, load the other file with [loadGems](#loadgems); the engine never synthesizes an inverse. #### Parameters ##### map [`GemMap`](#gemmap) The map an inversion was (incorrectly) requested for. #### Returns `never` Never: always throws. #### Throws `TERM_MAP_NOT_INVERTIBLE`, always. #### Example ```ts runnable throws import { loadGems, invertGem } from "@cosyte/terminology"; const gems = loadGems({ direction: "9-to-10", content: "0010 A000 00000\n" }); invertGem(gems); // throws TERM_MAP_NOT_INVERTIBLE ``` *** ### isA() > **isA**(`sub`, `code`, `anchor`): `boolean` True when `code` is-a `anchor`: the same code, or a (transitive) descendant of it. #### Parameters ##### sub [`Subsumption`](#subsumption) The subsumption index (from [buildSubsumption](#buildsubsumption)). ##### code `string` The candidate code. ##### anchor `string` The ancestor code to test against. #### Returns `boolean` Whether `code` is `anchor` or a descendant of it. #### Example ```ts import { loadCodeSystem, buildSubsumption, isA } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", concept: [{ code: "p", concept: [{ code: "c" }] }] }, }); isA(buildSubsumption(cs), "c", "p"); // => true ``` *** ### isJsonObject() > **isJsonObject**(`value`): `value is Record` Narrow an `unknown` to a plain JSON object (a non-null, non-array `object`). #### Parameters ##### value `unknown` The value to test. #### Returns `value is Record` `true` when `value` is a non-null, non-array object. #### Example ```ts import { isJsonObject } from "@cosyte/terminology"; isJsonObject({ resourceType: "ConceptMap" }); // => true ``` *** ### isSynonymTermType() > **isSynonymTermType**(`tty`): `boolean` Is this term type a **synonym-class** one (`PSN` / `SY` / `TMSY`)? Appendix 5 defines each as a *"synonym of another TTY"*: it labels an alternate **name** for a concept, never the concept's own kind. So a synonym atom can never establish an `RXCUI`'s term type, and ../rxnorm/load.loadRxNormGraph will not let one: an `RXCUI` whose release supplies only synonym atoms is skipped and surfaced, never typed `"TMSY"`. #### Parameters ##### tty [`TermType`](#termtype) A [TermType](#termtype). #### Returns `boolean` `true` for `PSN` / `SY` / `TMSY`; `false` for every defining term type. #### Example ```ts import { isSynonymTermType } from "@cosyte/terminology"; isSynonymTermType("TMSY"); // => true isSynonymTermType("SCDF"); // => false ``` *** ### isUnknownSystem() > **isUnknownSystem**(`result`): `result is UnknownSystem` Type guard: did [resolveSystem](#resolvesystem) fail to recognize the identifier? #### Parameters ##### result [`ResolveSystemResult`](#resolvesystemresult) A [resolveSystem](#resolvesystem) result. #### Returns `result is UnknownSystem` `true` when the result is a typed [UnknownSystem](#unknownsystem). #### Example ```ts import { resolveSystem, isUnknownSystem } from "@cosyte/terminology"; isUnknownSystem(resolveSystem("nope")); // => true ``` *** ### loadCodeSystem() > **loadCodeSystem**(`source`): [`CodeSystem`](#codesystem) Load a consumer-supplied code-system release into an immutable, queryable [CodeSystem](#codesystem). Dispatches on `source.format`: - `"rrf"`: pipe-delimited RxNorm/UMLS release (one parameterized reader for both). - `"csv"`: RFC-4180 CSV (e.g. LOINC `Loinc.csv`), with real quote handling. - `"fixed-width"`: slice-by-column order files (e.g. ICD-10-CM; see `ICD10CM_ORDER_FILE_FIELDS`). - `"fhir"`: a native FHIR R4 `CodeSystem` JSON resource. **Liberal on load:** a malformed *row* is skipped and surfaced in `warnings` (never a crash, never a partial concept). **Conservative on structure:** an unusable *source* (wrong FHIR `resourceType`, or a CSV whose configured columns are absent from the header) throws a typed `TerminologyError` carrying `TERM_CODESYSTEM_MALFORMED`. #### Parameters ##### source [`CodeSystemSource`](#codesystemsource) The release descriptor (a discriminated [CodeSystemSource](#codesystemsource)). #### Returns [`CodeSystem`](#codesystem) The immutable, deep-frozen [CodeSystem](#codesystem). #### Throws `TERM_CODESYSTEM_MALFORMED` when the source is structurally unusable. #### Example ```ts import { loadCodeSystem } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", url: "http://example.org/cs", concept: [{ code: "A", display: "Alpha" }], }, }); cs.count; // => 1 ``` *** ### loadComplexMap() > **loadComplexMap**(`input`): [`ComplexMap`](#complexmap) Load a SNOMED→ICD-10-CM complex map from **caller-supplied** rows or a raw RF2 refset into an immutable [ComplexMap](#complexmap), keyed by source concept and ordered by (group, priority). Liberal on load: a structurally unusable RF2 *row* is skipped and surfaced as a warning; only an unusable *source* (empty content, header missing required columns) is a fatal [FATAL\_CODES.TERM\_CROSSWALK\_MALFORMED](#term_crosswalk_malformed). Ships **no** SNOMED CT release or refset: the rows are the caller's, under their own licence. #### Parameters ##### input [`ComplexMapInput`](#complexmapinput) Structured `rows`, or a raw RF2 extended-map refset `content`. #### Returns [`ComplexMap`](#complexmap) The immutable [ComplexMap](#complexmap). #### Throws `TERM_CROSSWALK_MALFORMED` when the source is structurally unusable. #### Example ```ts import { loadComplexMap } from "@cosyte/terminology"; const map = loadComplexMap({ format: "rows", rows: [ { source: "195967001", group: 1, priority: 1, rule: "TRUE", advice: "ALWAYS J45.909", target: "J45.909", category: "447637006" }, ], }); map.count; // => 1 ``` *** ### loadConceptMap() > **loadConceptMap**(`json`): [`ConceptMap`](#conceptmap) Load an untrusted FHIR R4 `ConceptMap` JSON value into an immutable [ConceptMap](#conceptmap). Accepts the standard resource shape: a top-level object with `resourceType: "ConceptMap"` and an optional `group[]`. `source[x]`/`target[x]` scopes are read from either the `Uri` or `Canonical` variant. The result is deep-frozen. Anything structurally unusable throws a [TerminologyError](#terminologyerror) carrying [FATAL\_CODES.TERM\_CONCEPTMAP\_MALFORMED](#term_conceptmap_malformed). #### Parameters ##### json `unknown` The untrusted resource (typically `JSON.parse` output, hence `unknown`). #### Returns [`ConceptMap`](#conceptmap) The immutable, validated [ConceptMap](#conceptmap). #### Throws `TERM_CONCEPTMAP_MALFORMED` when the resource is not a usable ConceptMap. #### Example ```ts import { loadConceptMap } from "@cosyte/terminology"; const map = loadConceptMap({ resourceType: "ConceptMap", url: "http://example.org/cm/gender", group: [ { source: "http://hl7.org/fhir/administrative-gender", target: "http://terminology.hl7.org/CodeSystem/v2-0001", element: [{ code: "male", target: [{ code: "M", equivalence: "equivalent" }] }], }, ], }); map.group.length; // => 1 ``` *** ### loadGems() > **loadGems**(`source`): [`GemMap`](#gemmap) Load a CMS GEM file into an immutable [GemMap](#gemmap), keyed by source code. Liberal on load: a structurally unusable *line* (not three whitespace-delimited fields, or a malformed 5-digit flag) is **skipped and surfaced** as a [GemLoadWarning](#gemloadwarning), never a partially-parsed entry. An unusable *source* is not possible here (an all-malformed file loads to an empty map with warnings): there is no fatal for GEM load. Ships **no** GEM content: the entries are the caller's public-domain file. #### Parameters ##### source [`GemSource`](#gemsource) The [GemSource](#gemsource) (raw content + direction + version). #### Returns [`GemMap`](#gemmap) The immutable [GemMap](#gemmap). #### Example ```ts import { loadGems, applyGem } from "@cosyte/terminology"; // Synthetic rows in the CMS GEM format (source target flags): an exact map and a No-Map. const gems = loadGems({ direction: "9-to-10", version: "2018", content: "0010 A000 00000\nV290 NoDx 11000\n", }); applyGem(gems, "0010").mapped; // => true ``` *** ### loadRxNormGraph() > **loadRxNormGraph**(`source`): [`RxNormGraph`](#rxnormgraph) Load a **caller-supplied** RxNorm RRF release into an immutable [RxNormGraph](#rxnormgraph). Parses `RXNCONSO` (concepts), `RXNREL` (directed edges, normalized to the documented direction), and (when supplied) `RXNSAT` (NDC attributes). Liberal on load: a structurally unusable row is a skipped, surfaced [RxNormLoadWarning](#rxnormloadwarning), never partial. Ships **no** RxNorm release: the graph is entirely the caller's release. A concept is typed only by a **defining** atom, never by a synonym-class one and never by file order; an `RXCUI` no defining atom could type is skipped and surfaced as `TERM_RXNORM_UNTYPED_CONCEPT`. See the module documentation. #### Parameters ##### source [`RxNormGraphSource`](#rxnormgraphsource) The [RxNormGraphSource](#rxnormgraphsource) (RRF file contents + optional version). #### Returns [`RxNormGraph`](#rxnormgraph) The immutable [RxNormGraph](#rxnormgraph). #### Example ```ts import { loadRxNormGraph } from "@cosyte/terminology"; // Synthetic rows in the RxNorm RRF wire format (no RxNorm release is bundled). const graph = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||", // "SCDC(2) has_ingredient IN(1)": subject=RXCUI2=2, object=RXCUI1=1. rel: "1|||RN|2|||has_ingredient|||RXNORM||||||", }); graph.conceptCount; // => 2 ``` *** ### loadUcumEssence() > **loadUcumEssence**(): [`UcumEssence`](#ucumessence) Load the UCUM essence table, parsing the embedded verbatim XML on first call and caching it. One table is shared by every caller and by the engine itself, so it is handed out **frozen**: an atom's definition cannot be rewritten in place, and neither can the `atoms` array the parser scans. It used to be, and doing so before that atom's first reduction poisoned the reduction memo for the life of the process: a fabricated unit equivalence that survived putting the table back. A write is refused, which in strict mode means a `TypeError`. #### Returns [`UcumEssence`](#ucumessence) The shared, frozen in-memory [UcumEssence](#ucumessence) model. #### Example ```ts import { loadUcumEssence } from "@cosyte/terminology"; const essence = loadUcumEssence(); essence.atomByCode.has("m"); // => true ``` *** ### loadValueSet() > **loadValueSet**(`json`): [`ValueSet`](#valueset-1) Load an untrusted FHIR R4 `ValueSet` JSON value into an immutable [ValueSet](#valueset-1). Accepts the standard resource shape: `resourceType: "ValueSet"` with an optional intensional `compose` (`include`/`exclude` of `system`/`concept`/`filter`/`valueSet`, plus the `inactive` declaration that decides whether non-active codes belong to the membership) and/or a **pre-computed** `expansion` (`contains`, with `total` and the `valueset-toocostly` / `valueset-unclosed` extensions read into a derived `truncated` flag, the last of them also surfaced on its own `unclosed` flag). The result is deep-frozen. Anything structurally unusable throws a [TerminologyError](#terminologyerror) carrying [FATAL\_CODES.TERM\_VALUESET\_MALFORMED](#term_valueset_malformed). #### Parameters ##### json `unknown` The untrusted resource (typically `JSON.parse` output, hence `unknown`). #### Returns [`ValueSet`](#valueset-1) The immutable, validated [ValueSet](#valueset-1). #### Throws `TERM_VALUESET_MALFORMED` when the resource is not a usable ValueSet. #### Example ```ts import { loadValueSet } from "@cosyte/terminology"; const vs = loadValueSet({ resourceType: "ValueSet", url: "http://example.org/vs/colors", compose: { include: [{ system: "http://example.org/cs", concept: [{ code: "red" }, { code: "green" }] }], }, }); vs.compose?.include.length; // => 1 ``` *** ### lookup() > **lookup**(`cs`, `code`): [`LookupOutcome`](#lookupoutcome) `$lookup`: resolve a code to its display, definition, status, and properties in a loaded release. #### Parameters ##### cs [`CodeSystem`](#codesystem) The loaded [CodeSystem](#codesystem). ##### code `string` The code to look up. #### Returns [`LookupOutcome`](#lookupoutcome) A [LookupResult](#lookupresult) when found, or a typed [LookupUnknown](#lookupunknown): never a guess. #### Example ```ts import { loadCodeSystem, lookup } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", concept: [{ code: "A", display: "Alpha" }] }, }); const r = lookup(cs, "A"); r.found === true && r.display; // => "Alpha" ``` *** ### makeStatus() > **makeStatus**(`activity`, `raw?`, `billable?`): [`ConceptStatus`](#conceptstatus-1) Assemble a frozen [ConceptStatus](#conceptstatus-1) from a normalized activity, carrying the raw token and (for classification systems) the billable flag, and attaching the stable diagnostic code for the two concerns it distinguishes: `TERM_CONCEPT_DEPRECATED` and `TERM_CONCEPT_HEADER_NOT_BILLABLE`. #### Parameters ##### activity [`ConceptActivity`](#conceptactivity) The normalized activity. ##### raw? `string` The verbatim steward token, if any (omitted from the result when empty). ##### billable? `boolean` The billable flag for a classification system, if applicable. #### Returns [`ConceptStatus`](#conceptstatus-1) A frozen [ConceptStatus](#conceptstatus-1). #### Example ```ts import { makeStatus } from "@cosyte/terminology"; makeStatus("deprecated", "DEPRECATED").code; // => "TERM_CONCEPT_DEPRECATED" ``` *** ### matchesFilter() > **matchesFilter**(`concept`, `filter`, `sub`): [`FilterMatch`](#filtermatch) Evaluate one [ConceptSetFilter](#conceptsetfilter) against one [Concept](#concept), using the release's hierarchy. The `concept` filter property (and the hierarchy operators regardless of property) reads the code itself / its subsumption; other properties read the concept's `property` values. `CodeSystem .concept.property` is `0..*`, so **every** value the concept carries for the property is read: `=` and `in` match on ANY of them, `not-in` matches only where NONE of them is in the filter's set, and `exists` follows whether the concept carries any at all. An unimplemented operator returns `{ supported: false }`: never a coerced `matched`. #### Parameters ##### concept [`Concept`](#concept) The concept under test. ##### filter [`ConceptSetFilter`](#conceptsetfilter) The filter predicate. ##### sub [`Subsumption`](#subsumption) The subsumption index for the concept's code system. #### Returns [`FilterMatch`](#filtermatch) A [FilterMatch](#filtermatch). #### Example ```ts import { loadCodeSystem, buildSubsumption, matchesFilter } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", concept: [{ code: "p", concept: [{ code: "c" }] }] }, }); const dog = cs.concepts.get("c"); dog && matchesFilter(dog, { property: "concept", op: "is-a", value: "p" }, buildSubsumption(cs)).matched; // => true ``` *** ### parseCsv() > **parseCsv**(`content`): `string`[][] Parse RFC-4180 CSV content into rows of fields (the `.rows` of parseCsvRows). Lenient: a stray mid-field quote is literal, so a malformed quote never silently swallows subsequent rows. #### Parameters ##### content `string` The raw CSV text. #### Returns `string`[][] The rows, each an array of field strings (untrimmed). #### Example ```ts import { parseCsv } from "@cosyte/terminology"; parseCsv('a,b\n"x,y","z""q"'); // => [["a", "b"], ["x,y", 'z"q']] ``` *** ### parseCsvSource() > **parseCsvSource**(`source`): `object` Parse a [CsvSource](#csvsource) into concepts and skipped-row warnings. #### Parameters ##### source [`CsvSource`](#csvsource) The CSV source. #### Returns `object` The parsed concepts (by code) and the load warnings. ##### concepts > **concepts**: `Map`\<`string`, [`Concept`](#concept)\> ##### warnings > **warnings**: [`LoadWarning`](#loadwarning)[] #### Throws `TERM_CODESYSTEM_MALFORMED` when the file has no header row, or a configured `code`/`display`/`status`/property column is absent from the header. The message names the missing column's **role**, never the name you configured. #### Example ```ts import { parseCsvSource } from "@cosyte/terminology"; const { concepts } = parseCsvSource({ format: "csv", content: "CODE,NAME\n2160-0,Creatinine", columns: { code: "CODE", display: "NAME" }, }); concepts.get("2160-0")?.display; // => "Creatinine" ``` *** ### parseFhirCodeSystem() > **parseFhirCodeSystem**(`source`): `object` Parse a [FhirCodeSystemSource](#fhircodesystemsource) into concepts, a resolved url/version/name, and warnings. #### Parameters ##### source [`FhirCodeSystemSource`](#fhircodesystemsource) The FHIR CodeSystem source. #### Returns `object` The parsed concepts (by code), the load warnings, and the resource metadata. ##### concepts > **concepts**: `Map`\<`string`, [`Concept`](#concept)\> ##### name? > `optional` **name?**: `string` ##### url? > `optional` **url?**: `string` ##### version? > `optional` **version?**: `string` ##### warnings > **warnings**: [`LoadWarning`](#loadwarning)[] #### Throws `TERM_CODESYSTEM_MALFORMED` when the resource is not a FHIR `CodeSystem`. #### Example ```ts import { parseFhirCodeSystem } from "@cosyte/terminology"; const { concepts } = parseFhirCodeSystem({ resource: { resourceType: "CodeSystem", concept: [{ code: "A", display: "Alpha" }] }, format: "fhir", }); concepts.get("A")?.display; // => "Alpha" ``` *** ### parseFixedWidth() > **parseFixedWidth**(`source`): `object` Parse a [FixedWidthSource](#fixedwidthsource) into concepts and skipped-row warnings. #### Parameters ##### source [`FixedWidthSource`](#fixedwidthsource) The fixed-width source. #### Returns `object` The parsed concepts (by code) and the load warnings. ##### concepts > **concepts**: `Map`\<`string`, [`Concept`](#concept)\> ##### warnings > **warnings**: [`LoadWarning`](#loadwarning)[] #### Example ```ts import { parseFixedWidth } from "@cosyte/terminology"; const { concepts } = parseFixedWidth({ format: "fixed-width", content: "A00 1 Cholera", fields: { code: { start: 0, end: 3 }, display: { start: 6 } }, }); concepts.get("A00")?.display; // => "Cholera" ``` *** ### parseRrf() > **parseRrf**(`source`): `object` Parse an [RrfSource](#rrfsource) into concepts (first row per code wins) and skipped-row warnings. A code that recurs across rows (RRF repeats a code once per atom/attribute) keeps its **first** occurrence, a documented v1 limitation: preferred-atom selection (`TS`/`STT`/`ISPREF`) and attribute merging are later work. The result is data only; [loadCodeSystem](#loadcodesystem) freezes it. #### Parameters ##### source [`RrfSource`](#rrfsource) The RRF source. #### Returns `object` The parsed concepts (by code) and the load warnings. ##### concepts > **concepts**: `Map`\<`string`, [`Concept`](#concept)\> ##### warnings > **warnings**: [`LoadWarning`](#loadwarning)[] #### Example ```ts import { parseRrf } from "@cosyte/terminology"; const { concepts } = parseRrf({ format: "rrf", content: "1|Aspirin|", columns: { code: 0, display: 1 }, }); concepts.get("1")?.display; // => "Aspirin" ``` *** ### parseRrfLine() > **parseRrfLine**(`line`): readonly `string`[] Split one RRF line into its fields, dropping the single reserved trailing pipe. #### Parameters ##### line `string` One RRF line (no line terminator). #### Returns readonly `string`[] The fields, in column order. #### Example ```ts import { parseRrfLine } from "@cosyte/terminology"; parseRrfLine("316151|ENG|lisinopril 10 MG|"); // => ["316151", "ENG", "lisinopril 10 MG"] ``` *** ### parseUcum() > **parseUcum**(`unit`): [`ParseResult`](#parseresult) Parse a UCUM unit expression into an AST. #### Parameters ##### unit `string` The unit expression (e.g. `"mmol/L"`, `"kg.m/s2"`, `"10*3/ul"`). #### Returns [`ParseResult`](#parseresult) The parsed [UnitNode](#unitnode), or a typed [ParseFailure](#parsefailure) with a value-free reason. #### Example ```ts import { parseUcum } from "@cosyte/terminology"; parseUcum("mmol/L").ok; // => true parseUcum("m/").ok; // => false ``` *** ### reduce() > **reduce**(`node`): [`Reduction`](#reduction) Reduce a parsed UCUM unit to its canonical [Reduction](#reduction): a linear scalar×dimension form, or an opaque `special` form for expressions involving a non-linear special unit. The reduction of one atom is cached against **that atom object**, so a `UnitNode` you assembled yourself is answered from its own atoms and cannot change how a unit off [parseUcum](#parseucum) reduces, in this call or any later one. #### Parameters ##### node [`UnitNode`](#unitnode) The AST from [parseUcum](#parseucum). #### Returns [`Reduction`](#reduction) The reduced form. #### Throws If reducing `node` reaches an atom with no linear definition, or one whose definition does not parse. **No expression [parseUcum](#parseucum) accepts reaches either**; a node you assemble yourself can, including by defining an atom in terms of a special unit such as `Cel`, which lands on the table's own `Cel`. A third guard catches a definition that leads back to itself. Naming a table atom does not reach it (a definition is resolved against the loaded table, so your atom can name one but never be one), and neither does the table itself, which is frozen; what reaches it is a `value` accessor that re-enters `reduce` for the same atom while that atom is still being reduced. The message names the fault, never the atom. #### Example ```ts import { parseUcum, reduce } from "@cosyte/terminology"; const parsed = parseUcum("N"); if (parsed.ok) { const r = reduce(parsed.node); r.kind; // => "linear" (1000 × g·m·s⁻²) } ``` *** ### relatedByRela() > **relatedByRela**(`graph`, `rxcui`, `predicate`): [`RxNormNavResult`](#rxnormnavresult) Follow one or more `RELA` predicates out of a concept (the generic navigation primitive). Direction is the **authored** edge direction (`subject ⟶predicate⟶ object`); the engine never inverts. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph). ##### rxcui `string` The source concept. ##### predicate `string` \| readonly `string`[] A `RELA` name, or an array of them (any-of). #### Returns [`RxNormNavResult`](#rxnormnavresult) A [RxNormNavResult](#rxnormnavresult): the related concepts (possibly empty), or a typed unknown source. #### Example ```ts import { loadRxNormGraph, relatedByRela, RELA } from "@cosyte/terminology"; // "SCDC(2) has_ingredient IN(1)": subject=RXCUI2=2, object=RXCUI1=1. const g = loadRxNormGraph({ conso: "1|ENG||||||||||RXNORM|IN||lisinopril||N||\n2|ENG||||||||||RXNORM|SCDC||lisinopril 10 MG||N||", rel: "1|||RN|2|||has_ingredient|||RXNORM||||||", }); const r = relatedByRela(g, "2", RELA.HAS_INGREDIENT); r.found && r.targets[0]?.name; // => "lisinopril" ``` *** ### resolveNdc() > **resolveNdc**(`graph`, `ndc`): [`NdcResult`](#ndcresult) Resolve an **NDC** (National Drug Code) to its `RXCUI` in the loaded release, carrying the temporal status and the as-of release. NDC↔RXCUI is many:1 and changes across releases, so an NDC present in the release is `active` **as of that release**; an absent NDC is a typed [NdcUnmapped](#ndcunmapped), never a guessed `RXCUI`. #### Parameters ##### graph [`RxNormGraph`](#rxnormgraph) A loaded [RxNormGraph](#rxnormgraph) (load with `sat` supplied to populate NDC attributes). ##### ndc `string` The NDC (as it appears in the release, e.g. 11-digit). #### Returns [`NdcResult`](#ndcresult) An [NdcResult](#ndcresult): a resolution with status + `asOf`, or a typed absence. #### Example ```ts import { loadRxNormGraph, resolveNdc } from "@cosyte/terminology"; const g = loadRxNormGraph({ conso: "2|ENG||||||||||RXNORM|SCD||lisinopril 10 MG Oral Tablet||N||", rel: "", sat: "2||||||||NDC|RXNORM|00000000000|N||", version: "RXNORM_2026AA", }); const r = resolveNdc(g, "00000000000"); r.resolved && r.status; // => "active" ``` *** ### resolveSystem() > **resolveSystem**(`id`): [`ResolveSystemResult`](#resolvesystemresult) Resolve a code-system identifier to its canonical [SystemIdentity](#systemidentity). Accepts, in order of attempt: a canonical URI (case-insensitive), a `v2-XXXX` mnemonic or a v2 table URI/OID (resolved structurally), a bare or `urn:oid:`-prefixed OID, or an HL7 v2 Table 0396 mnemonic. Whitespace is trimmed. An empty or unrecognized identifier yields a typed [UnknownSystem](#unknownsystem): the resolver **never** invents a URI. #### Parameters ##### id `string` The identifier (canonical URI, OID, `urn:oid:` OID, or mnemonic). #### Returns [`ResolveSystemResult`](#resolvesystemresult) The resolved [SystemIdentity](#systemidentity), or `{ unknown: true, input }`. #### Example ```ts import { resolveSystem } from "@cosyte/terminology"; resolveSystem("2.16.840.1.113883.6.1"); // => { url: "http://loinc.org", … } resolveSystem("SCT"); // => { url: "http://snomed.info/sct", … } resolveSystem("http://example.com/nope"); // => { unknown: true, input: "http://example.com/nope" } ``` *** ### sliceField() > **sliceField**(`line`, `slice`): `string` Extract and trim a [FieldSlice](#fieldslice) from a line (`end` omitted slices to end of line). #### Parameters ##### line `string` The source line. ##### slice [`FieldSlice`](#fieldslice) The half-open `[start, end)` slice. #### Returns `string` The trimmed field text (empty when the slice is entirely past the line's end). #### Example ```ts import { sliceField } from "@cosyte/terminology"; sliceField("00001 A00 1 Cholera", { start: 6, end: 13 }); // => "A00" ``` *** ### translate() > **translate**(`sourceCoding`, `map`): [`TranslateResult`](#translateresult) Translate a source [Coding](#coding-1) through a loaded [ConceptMap](#conceptmap). Matches the coding's `code` against each applicable group's `element.code` (the **source** side, never targets, so the map is never inverted) and returns every declared target for it. A target with an `unmatched` equivalence, or with no `code`, is treated as "no target" (the FHIR way of asserting a non-mapping), and does not count as a match. A `disjoint` target is the map's explicit assertion that the target is *not related* to the source, so it does not count as a match either: when **every** declared target for the source asserts non-relation the result is `unmapped`, carrying those `disjoint` rows verbatim on `notRelated`. When at least one target does assert a relationship, the result is matched and carries every declared target, `disjoint` rows included, in declared order. When nothing matches, the map author's `group.unmapped` directive (if any) is *reported* via the result's [UnmappedMode](#unmappedmode) but never silently applied. #### Parameters ##### sourceCoding [`Coding`](#coding-1) The source concept to translate. ##### map [`ConceptMap`](#conceptmap) A [ConceptMap](#conceptmap) from [loadConceptMap](#loadconceptmap). #### Returns [`TranslateResult`](#translateresult) A [TranslateResult](#translateresult): matched targets, or a typed unmapped outcome. #### Example ```ts import { loadConceptMap, translate } from "@cosyte/terminology"; const map = loadConceptMap({ resourceType: "ConceptMap", group: [ { source: "http://hl7.org/fhir/administrative-gender", target: "http://terminology.hl7.org/CodeSystem/v2-0001", element: [{ code: "male", target: [{ code: "M", equivalence: "equivalent" }] }], }, ], }); const r = translate( { system: "http://hl7.org/fhir/administrative-gender", code: "male" }, map, ); r.unmapped; // => false ``` *** ### ucumEqual() > **ucumEqual**(`a`, `b`): `boolean` Whether two UCUM expressions denote the **same unit**: i.e. they reduce to the same canonical form (equal base dimensions *and* equal scale). This recognizes representational equivalences (`N` ≡ `kg.m/s2`, `mmol/L` ≡ `mmol.L-1`, annotations inert) but is **not** magnitude conversion: `mg` and `g` are different units and compare unequal. A unit is never equal to an invalid expression, and a special (non-linear) unit (`Cel`, `[pH]`) is equal only to a structurally identical special unit: never to a linear unit (the never-fabricate posture: the engine will not assert an equivalence it cannot prove). #### Parameters ##### a `string` The first UCUM unit expression. ##### b `string` The second UCUM unit expression. #### Returns `boolean` `true` iff both are valid and denote the same unit. #### Example ```ts import { ucumEqual } from "@cosyte/terminology"; ucumEqual("N", "kg.m/s2"); // => true ucumEqual("mmol/L", "mmol.L-1"); // => true ucumEqual("mg", "g"); // => false ``` *** ### unsupportedOps() > **unsupportedOps**(`filters`): readonly [`ConceptSetFilter`](#conceptsetfilter)[] The filters whose operator the engine does **not** implement (`regex` / `generalizes` / unknown): a non-empty result means the caller must surface a typed cannot-expand, never mis-expand. #### Parameters ##### filters readonly [`ConceptSetFilter`](#conceptsetfilter)[] The component's filters. #### Returns readonly [`ConceptSetFilter`](#conceptsetfilter)[] The subset with an unimplemented operator (empty when all are supported). #### Example ```ts import { unsupportedOps } from "@cosyte/terminology"; unsupportedOps([{ property: "concept", op: "is-a", value: "x" }]).length; // => 0 ``` *** ### validateCode() > **validateCode**(`cs`, `code`): [`ValidateCodeResult`](#validatecoderesult) `$validate-code`: is `code` a valid member of the loaded code system? A found code is `valid: true`, **carrying its status** (a deprecated or header code is present but flagged: never silently clean). An absent code is `valid: false` with `TERM_CODE_UNKNOWN`, never a guessed `true`. #### Parameters ##### cs [`CodeSystem`](#codesystem) The loaded [CodeSystem](#codesystem). ##### code `string` The code to validate. #### Returns [`ValidateCodeResult`](#validatecoderesult) A [ValidateCodeResult](#validatecoderesult). #### Example ```ts import { loadCodeSystem, validateCode } from "@cosyte/terminology"; const cs = loadCodeSystem({ format: "fhir", resource: { resourceType: "CodeSystem", concept: [{ code: "A", display: "Alpha" }] }, }); validateCode(cs, "A").valid; // => true validateCode(cs, "ZZZ").valid; // => false ``` *** ### validateCodeInValueSet() > **validateCodeInValueSet**(`coding`, `valueSet`, `ctx?`): [`ValueSetMembership`](#valuesetmembership) `$validate-code` (ValueSet): is `coding` a member of `valueSet`? #### Parameters ##### coding [`Coding`](#coding-1) The code to test (system + code). ##### valueSet [`ValueSet`](#valueset-1) The loaded value set. ##### ctx? [`ExpansionContext`](#expansioncontext) = `{}` The loaded code systems / referenced value sets the intensional parts resolve against. #### Returns [`ValueSetMembership`](#valuesetmembership) A decided [ValueSetMembership](#valuesetmembership) (`result: true`/`false`), or a typed [ValueSetMemberUndetermined](#valuesetmemberundetermined) when membership could not be proven: never a fabricated `false`. #### Example ```ts import { loadValueSet, validateCodeInValueSet } from "@cosyte/terminology"; const vs = loadValueSet({ resourceType: "ValueSet", compose: { include: [{ system: "http://example.org/cs", concept: [{ code: "a" }, { code: "b" }] }] }, }); const r = validateCodeInValueSet({ system: "http://example.org/cs", code: "a" }, vs); r.undetermined === false && r.result; // => true ``` *** ### validateUcum() > **validateUcum**(`unit`): [`UcumValidation`](#ucumvalidation) Validate a UCUM unit expression and, when valid, return its canonical descriptor. Validation **is** parsing against the UCUM grammar over the known atom table: an expression is valid iff it parses completely. An invalid expression returns a typed TERM\_UCUM\_INVALID with a value-free reason: never a coerced or guessed unit. #### Parameters ##### unit `string` The UCUM unit expression (e.g. `"mmol/L"`, `"kg.m/s2"`, `"Cel"`). #### Returns [`UcumValidation`](#ucumvalidation) `{ valid: true, canonical }` or `{ valid: false, code: "TERM_UCUM_INVALID", reason }`. #### Example ```ts import { validateUcum } from "@cosyte/terminology"; validateUcum("mmol/L").valid; // => true const bad = validateUcum("mg/dl/"); // trailing operator bad.valid; // => false ``` --- URL: https://docs.cosyte.com/terminology/troubleshooting Markdown: https://docs.cosyte.com/terminology/troubleshooting.md # Troubleshooting Common symptoms when integrating `@cosyte/terminology`, and how to read what the engine is telling you. ## `translate` returned `unmapped`: where's my code? That is the engine working as designed. When a source concept has no target in the supplied map, the result is a **typed `unmapped`**, never a guessed code: ```ts const result = translate(coding, map); if (result.unmapped) { result.code; // "TERM_TRANSLATE_UNMAPPED" result.mode; // "fixed" | "provided" | "other-map" | "none" result.source; // your original coding, surfaced untouched } ``` If the map declares a `group.unmapped` fallback, its `mode` (and, for `fixed`, `fixedTarget`, or for `other-map`, `otherMapUrl`) is **reported** on the result, but never silently applied. You decide whether to trust a fallback. ## `resolveSystem` returned `{ unknown: true }` The identifier (URI, OID, or mnemonic) is not one the engine recognizes. It returns a typed unknown rather than guessing a canonical URI. Check the value, and canonicalize known systems by their OID or HL7 v2 mnemonic (`LN`, `SCT`, `I10C`, …). ## `loadConceptMap` threw `loadConceptMap` throws a `TerminologyError` carrying `TERM_CONCEPTMAP_MALFORMED` when the resource is not a usable ConceptMap (wrong `resourceType`, a target missing its `equivalence`, and so on). Catch it and branch on `err.code`. This is deliberate: a structurally broken map is refused rather than loaded partially and translating some codes while silently dropping others. ## Can I translate a target code back to its source? Not through a forward map. Steward maps are directional and **non-invertible**, so `translate` only matches the map's source side. Reverse translation needs an explicit inverse `ConceptMap`. ## `validateCodeInValueSet` returned `undetermined`: is my code a member or not? The engine could not **prove** membership either way, so it refuses to guess. This happens when a part of the value set cannot be evaluated: an intensional `filter`/`system` include whose code system you did not pass in the `ExpansionContext`, a referenced value set that is not supplied, an unimplemented filter operator (`regex` / `generalizes`), a **truncated** pre-computed expansion, a pre-computed expansion the server marked **`unclosed`**, or a component that pins a `version` the `CodeSystem` you supplied for that system does not agree with. The result carries `code: "TERM_VALUESET_CANNOT_EXPAND"` and a `diagnostics` list naming each gap. ```ts const r = validateCodeInValueSet(coding, valueSet, { codeSystems }); if (r.undetermined) { r.diagnostics; // each carries a stable code and a `path` into your own compose } ``` Supply the missing `CodeSystem` (or referenced `ValueSet`) in the context and re-check. A truncated server expansion must be re-fetched in full: the engine never treats a truncated snapshot as complete membership (a false "not a member" is a clinical error). An `unclosed` expansion cannot be re-fetched into completeness at all: the marker says the value set is unbounded because it includes post-coordinated content (SNOMED CT, UCUM), so codes beyond the listed ones may be valid and only a terminology server that understands that content can decide the ones it did not list. A code that IS in such an expansion still validates `true`: the marker bounds what **absence** means, not presence. For a version disagreement, supply the release the value set actually pins: re-checking against the release you already have is what the diagnostic exists to stop. ## `expand` dropped a code my value set lists explicitly A `compose` component that enumerates codes is admitted only where the evidence you supplied does not contradict it. When you pass a `CodeSystem` for that component's `system` (and the component's declared `version`, if it has one, agrees with that release), a code the release does not define is **not** a member: `expand` leaves it out of `contains` and reports one `TERM_VALUESET_ENUMERATED_CODE_UNDEFINED` diagnostic on that component, and `validateCodeInValueSet` decides it a non-member rather than a member. The result is still `complete: true`, so this is a **decided** answer rather than a lower bound, and a complete expansion can carry diagnostics: read `diagnostics` on both. ```ts const r = expand(valueSet, { codeSystems }); r.complete; // => true r.diagnostics.filter((d) => d.code === "TERM_VALUESET_ENUMERATED_CODE_UNDEFINED"); // each carries a `path` into your own compose, such as "compose.include[0]" ``` If the release you passed is the incomplete party rather than the value set, supply the release the value set was written against: the rule reads the evidence you hand it and nothing else. If you want the enumeration honored as written, do not pass a release for that system, or pass one whose `version` the component does not pin to: with no usable release the engine holds no contrary evidence and every enumerated code is carried, exactly as before. ## `expand` returned `complete: false` The `contains` set is a **lower bound**, not exhaustive membership: some intensional part could not be computed (see above). Read each diagnostic's `path` (`"compose.include[2]"`, `"expansion"`) to find the part that did not resolve in your own resource; do **not** treat the partial `contains` as the whole value set. ## Diagnostics and logs The `message`, `detail` and `reason` **fields** are **value-free**: nothing you configured and nothing your release or resource contained reaches one, or an `err.stack`, at any length. The locus beside them is a line number, an index path into your own resource, or a fixed token: never a URI or a column name. `String(err)` is safe. **The objects those codes ride on are not the same thing, and this is the part to get right.** A `TERM_CODE_UNKNOWN`, `TERM_TRANSLATE_UNMAPPED`, `TERM_CROSSWALK_UNMAPPED`, `TERM_RXNORM_UNKNOWN_RXCUI` or `TERM_RXNORM_NDC_UNMAPPED` outcome names **what you asked about** (`input`, `source`, `rxcui`, `ndc`, `coding`) because that is how a never-fabricate answer says which thing it refused to guess. A loaded model likewise carries the displays and canonical URIs from your release. So `JSON.stringify(result)` into a log is a decision about PHI, and a **code in patient context** can be PHI: log the `code` and the locus, log a value only if you would log the code you passed in, and never the surrounding record. ## Known limitations (this release) > **Status:** `@cosyte/terminology` ships the code-system identity resolver, the ConceptMap > `$translate` engine, the CodeSystem load layer with `$lookup` / `$validate-code`, the ValueSet > `compose` / `$expand` / binding operations, UCUM validation, the crosswalk resolvers, and the > RxNorm drug graph. - **BYO data**: `$expand` and binding operate over the `CodeSystem` releases and referenced `ValueSet`s you supply in the `ExpansionContext`; an intensional part with no supplied code system is a typed `TERM_VALUESET_CANNOT_EXPAND`, never a fabricated member. - **An enumeration is checked against the release you supplied, where there is one**: an explicit `concept` entry a usable release does not define is not admitted, reported as a typed `TERM_VALUESET_ENUMERATED_CODE_UNDEFINED` on that component while the answer stays `complete: true`. Bounded to that case on purpose: with no release for the system, or a declared `version` the supplied release does not agree with, the engine has no contrary evidence and every enumerated code is carried. - **A declared code system version is checked, not selected on**: where an intensional part resolves a supplied release, a component's `version` is compared against that release's own `version`. A disagreement (or a release that declares no version to confirm the pin against) is a typed `TERM_VALUESET_CANNOT_EXPAND` located on that component, and its members are withheld or shipped without the unconfirmed version stamp: never membership computed from the other release. The `ExpansionContext` holds **one release per system URI**, so the engine cannot hold several releases and pick the one a component asks for. What is compared is that the two declarations agree, never that either is true: a mislabelled release is still trusted. - **Subsumption is the release's own hierarchy**: `is-a` / `descendent-of` read the loaded code system's `parent` edges (nested `concept`s or an explicit `parent` property). Subsumption across two separate releases is not computed. - **Intensional filters are best-effort**: `is-a` / `descendent-of` / `is-not-a` / `=` / `in` / `not-in` / `exists` are implemented; `regex` / `generalizes` surface as `TERM_VALUESET_CANNOT_EXPAND`. - **RxNorm graph is BYO**: `loadRxNormGraph` operates over the RxNorm RRF release you supply; the engine bundles **no** RxNorm release. NDC↔RXCUI resolution is release-scoped and carries the as-of release; obsolete/alien NDC statuses come from RxNav NDC-history data (a differential source), not the base RRF concept files, and are never fabricated. Approximate matching is opt-in and always labeled. An `RXCUI` whose supplied atoms could not establish a term type is left out of the graph and reported as `TERM_RXNORM_UNTYPED_CONCEPT` on `graph.warnings`, never loaded under a synonym's `TTY`; check that list if a concept you expected reads as `TERM_RXNORM_UNKNOWN_RXCUI`. - **No bundled code-system release**: every code-system release is bring-your-own, including the content packs (RxNorm Prescribable, ICD-10-CM) and SNOMED CT / CPT / LOINC / UMLS / VSAC. - **What *is* bundled** includes the UCUM unit table (`ucum-essence.xml` v2.2, copyright © Regenstrief Institute, Inc., reproduced verbatim under [https://ucum.org/license](https://ucum.org/license); notice at `vendor/ucum/NOTICE.md`, which ships with the package), the code-system identity pairings, the SNOMED CT concepts the crosswalk resolver names: the map-category concepts and the two gender findings (SNOMED CT is copyright © International Health Terminology Standards Development Organisation), and RxNorm's relationship and term-type names (`RELA`, `RELA_INVERSE`, `TERM_TYPES`), published by the U.S. National Library of Medicine. Individual SNOMED CT, ICD-10-CM and RxNorm identifiers also appear in the API documentation examples. The full list, with its copyright, is in the package's `LICENSE`. So the UCUM operations and `resolveSystem` work with no release at all; `$lookup`, `$validate-code`, `$expand`, `$translate` and the crosswalk and RxNorm resolvers need one you supply. The **API Reference** always reflects exactly what this release ships: treat it as the source of truth over any prose above. --- # @cosyte/cli > Command-line and MCP front door to the Cosyte healthcare parsers. These are the 10 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/cli/intro Markdown: https://docs.cosyte.com/cli/intro.md # @cosyte/cli `cosyte` is a **PHI-safe developer CLI** over the `@cosyte/*` healthcare parsers. Pipe a raw message from a hospital feed into the terminal and get typed, structured JSON back in one line, without writing code and without reading the spec. ```bash cat adt.hl7 | cosyte parse - ``` `@cosyte/cli` is a **`bin` package**, not a library: its primary artifact is the `cosyte` command on your `PATH`. It is a thin, honest skin over libraries that already own correctness: it routes, reads, shapes output, and owns two disciplines of its own: a documented **exit-code contract** and a **value-free diagnostic** posture (never a field value on stderr). > **Status:** `0.1`, published to npm. While the package is below 1.0, a breaking change ships in a > minor version and is called out in the changelog. `0.0.1` and `0.0.2` cannot be installed; every > later version installs: see [Installation](https://docs.cosyte.com/cli/installation.md). `parse`, `validate`, `inspect`, and `fmt` > are wired for **HL7 v2** and **FHIR R4**; `convert` and `map-codes` wrap the higher-layer libraries. > A **`cosyte-mcp` MCP server** exposes the same core to an LLM/agent. All eight Cosyte formats are > wired, with support honest per (format, operation) and NDJSON streaming for MLLP and `--ndjson`. ## Parse a message `parse` reads a file (or stdin via `-`), **autodetects the format by content**, and prints the parsed model as typed JSON on stdout: ```bash cosyte parse message.hl7 # autodetected → HL7 v2 cosyte parse patient.json # autodetected → FHIR cat message.hl7 | cosyte parse - # from a pipeline cosyte parse --format hl7 msg.txt # override autodetection ``` The exit code carries the outcome (`0` success, `65` unparseable/undetected, `66` missing file) so `cosyte parse` is safe to branch on in CI. See [Quickstart](https://docs.cosyte.com/cli/quickstart.md). ## Next - [Installation](https://docs.cosyte.com/cli/installation.md): `npx`, global install, prerequisites. - [Quickstart](https://docs.cosyte.com/cli/quickstart.md): the one-line parse, stdin, and the programmatic API. - [MCP server](https://docs.cosyte.com/cli/mcp.md): expose the same parse/validate/convert core to an LLM/agent. - **API Reference**: every programmatic export, generated from source. --- URL: https://docs.cosyte.com/cli/installation Markdown: https://docs.cosyte.com/cli/installation.md # Installation `@cosyte/cli` ships the `cosyte` command as a Node.js executable, alongside `cosyte-mcp`. A global install is the simplest route; `npx` works too, with one flag noted under [Run it](#run-it). > **Status:** `0.1`, published to npm, and the install commands below are live. While the package is > below 1.0, a breaking change ships in a minor version and is called out in the changelog. `0.0.1` > and `0.0.2` are on npm and cannot be installed: see > [If you are on 0.0.1 or 0.0.2](#if-you-are-on-001-or-002). Every later version installs. ## If you are on 0.0.1 or 0.0.2 Both are on npm, and `npm install @cosyte/cli` fails on either with an `ENOENT`: ``` npm error code ENOENT npm error path node_modules/@cosyte/cli/vendor/cosyte-fhir-0.0.0.tgz npm error enoent ENOENT: no such file or directory ``` `npx` and `npm install -g` fail identically. There is no consumer-side workaround, and nothing is wrong with your environment. Those manifests declared the ten `@cosyte/*` sibling packages as local file paths (`file:vendor/*.tgz`) instead of npm version ranges. The tarballs are not part of the published package, so npm resolved the paths against a directory that is not there. A published version is immutable, so both stay broken. **Every later version installs**: `npm install -g @cosyte/cli` installs the current one. ## What a default install includes The siblings the CLI wraps are real npm ranges. HL7 v2 (`@cosyte/hl7`) and `map-codes` (`@cosyte/terminology`) run on the two hard dependencies. The six breadth formats X12, C-CDA, DICOM, NCPDP, ASTM and MLLP, and `@cosyte/transform`, are optional dependencies, and `@cosyte/fhir` arrives as the peer dependency of `@cosyte/transform`, so a default install has every one of them. If one is still missing, for example a peer your package manager did not install, the commands that need it report `CLI_PARSER_UNAVAILABLE` and exit `69`. They do not guess, and they do not fail as though your input were bad. > **Do not install with `--omit=optional`.** It succeeds, but the `cosyte` command then fails to start > at all on a missing `@modelcontextprotocol/sdk`, before it reaches any command. Known defect, > tracked separately. ## Prerequisites - **Node.js >= 22 and < 26** (the whole `@cosyte/*` suite targets ES2023 / Node 22+). The upper bound is deliberate: this package declares support only for the release lines its own test suite runs on, so a line nobody has exercised is stated as unsupported rather than implied to work. - A package manager: `pnpm`, `npm`, or `yarn`. ## Run it Install globally to put `cosyte` on your `PATH`: ```bash npm install -g @cosyte/cli cosyte --help ``` Or run it without installing, naming the executable explicitly: ```bash npx --package @cosyte/cli cosyte parse message.hl7 ``` > **The short form `npx @cosyte/cli …` does not work**, and it is a separate matter from the > packaging defect above: it fails with `could not determine executable to run`. When a package ships > more than one executable, `npx` runs the one whose name matches the package name's last segment, > which would be `cli`; this package ships `cosyte` and `cosyte-mcp`. Use `--package` as above. A > `cli` alias is deliberately not added, because `npm install -g` would then claim a command called > `cli` on your `PATH`. ## Programmatic API The same `core` the CLI uses is available as a small library (the `.` subpath): the format autodetector, the exit-code contract, and the value-free diagnostic types: ```ts runnable import { VERSION } from "@cosyte/cli"; VERSION; // => "0.1.1" ``` If that resolves and prints the release you installed, the install is good: head to the [Quickstart](https://docs.cosyte.com/cli/quickstart.md). > **`VERSION` was wrong in `0.0.1` and `0.0.2`.** Both shipped exporting `0.0.0`, and `cosyte > --version` printed that too. This page asserted only `typeof VERSION` back then, which is true of > every wrong value, so it stayed green across both. The constant is now kept in lockstep with the > manifest by the release tooling and compared against it in the test suite, and the literal above is > rewritten by that same step. A published version is never re-published, so those two copies stay > wrong on the registry; if you have one, read the manifest instead, which was always correct: > `node -p "require('@cosyte/cli/package.json').version"`. --- URL: https://docs.cosyte.com/cli/quickstart Markdown: https://docs.cosyte.com/cli/quickstart.md # Quickstart Turn an opaque healthcare message into typed JSON in one line. `cosyte parse` reads a file (or stdin via `-`), **autodetects the format by content**, and prints the parsed model to stdout. ## Parse a file or a pipeline ```bash cosyte parse message.hl7 # → { "format": "hl7", "model": …, "warnings": [] } cat patient.json | cosyte parse - # from stdin cosyte parse --json message.hl7 | jq '.model' # compact output for a pipeline ``` The output is a stable envelope: the detected `format`, the parsed `model`, and any value-free `warnings` the wrapped parser recovered (each a stable code plus a position, never a field value). ## Validate, inspect, format Three more commands wrap the same parsers, each value-free by default: ```bash cosyte validate message.hl7 # verdict in the exit code: 0 valid · 1 invalid · 65 unparseable cosyte inspect message.hl7 # a value-free structural summary (type + segment/entry counts) cat messy.json | cosyte fmt - # canonical re-serialization via the library's spec-clean serializer ``` `validate` is the CI gate: a validation failure is **never** exit `0`. Its findings (and `inspect`'s summary) carry only codes, severities, and positional locators, never a field value. ## Autodetection is conservative: it never guesses Detection sniffs the leading bytes' content, not the file extension: an `MSH|…` message is HL7, a JSON object with a `resourceType` is FHIR. If nothing matches, `parse` does **not** guess: it exits with a data error (`65`) and asks for `--format`: ```ts runnable import { detectFormat } from "@cosyte/cli"; const enc = new TextEncoder(); detectFormat(enc.encode("MSH|^~\\&|Sender|Facility\r")).format; // => "hl7" detectFormat(enc.encode('{"resourceType":"Patient","id":"example"}')).format; // => "fhir" detectFormat(enc.encode("not a healthcare message")).confidence; // => "none" ``` ## The exit-code contract Every command is safe to branch on in CI. The exit code carries the outcome: | Code | Meaning | |------|------------------------------------------------------------| | `0` | success / valid (`validate`) | | `1` | invalid: `validate` found a parseable-but-bad message, or `redact` could not de-identify every locus | | `2` | usage error (unknown flag, missing argument) | | `65` | data error (unparseable input, or format undetected) | | `66` | no input (missing/unreadable file) | | `69` | unavailable (a capability is not wired for this input, e.g. `redact` on `astm`) | | `70` | internal error (a bug) | | `74` | output error (the output stream closed before the CLI had finished writing to it, e.g. piping into a reader that exits early) | ```ts runnable import { EXIT } from "@cosyte/cli"; EXIT.OK; // => 0 EXIT.INVALID; // => 1 EXIT.DATAERR; // => 65 EXIT.NOINPUT; // => 66 ``` ## Next - [Core Concepts](https://docs.cosyte.com/cli/concepts-archetype.md): the wrapper boundary, the exit-code contract, the PHI posture. - **API Reference**: every programmatic export, generated from source. --- URL: https://docs.cosyte.com/cli/concepts-archetype Markdown: https://docs.cosyte.com/cli/concepts-archetype.md # Core Concepts `@cosyte/cli` is a **thin, honest skin** over the `@cosyte/*` parsers. Understanding its three load-bearing disciplines (the wrapper boundary, the exit-code contract, and the PHI posture) tells you exactly what to trust it for. ## The wrapper boundary The CLI's correctness surface is **narrow by design**, because most correctness lives one layer down. **The CLI owns:** format autodetection routing, the exit-code contract, the value-free PHI posture, argument/stdin handling, output shaping, and faithful pass-through of the wrapped library's warnings. **The CLI does _not_ re-do:** any wire-format parsing, tolerance, or warning semantics. That is the wrapped parser's job, graded by its own conformance gate. `cosyte parse` equals the library's programmatic parse; a bug in a value is the library's, while a mis-route, a wrong exit code, or a leaked value is the CLI's. ## Fail-safe autodetection Detection sniffs content, never the file extension, and is **conservative**: a single confident signature match parses; zero or more than one match is a typed data error asking for `--format`, **never a guess**. A wrong sniff would route bytes to the wrong parser and yield confident garbage, so the CLI refuses to guess at the routing layer. ## The exit-code contract Exit codes are a **designed surface** CI depends on: `0` success, `1` an operation-level failure (an invalid `validate` verdict, a `redact` the de-identifier could not complete), `2` usage error, `65` data error (unparseable / undetected), `66` no input, `69` a capability not wired for this input, `70` internal error, `74` an output error (the stream the CLI was writing to closed before the output was delivered, the shape a downstream consumer that exits early produces). The codes from `65` up are the values the Unix `sysexits.h` header assigns to the conditions they name; `1` and `2` are this CLI's own values, because that header defines no constant for either. The load-bearing rule: the CLI **never prints a reassuring line and exits `0`** on input it could not handle, or on output that never reached its consumer. ## The PHI posture A CLI operates on real files a developer points at: the moment Cosyte code touches un-synthetic PHI. So the channels are split: - **stdout is the data channel.** `parse` emits the parsed model there because that is your explicit request, going to the sink you chose (a pipe, a redirect, your screen). - **Every other surface is value-free.** stderr, errors, and diagnostics carry **only** positional context (a segment/field index, a byte offset, a file path, a stable code), **never** a name, DOB, MRN, or result value. An error never echoes the offending bytes. Diagnostic codes are stable (`CLI_CODES`): scripts branch on them, so renaming one is a breaking change. ### `--unsafe-show-values`: the single door to a value Value-free-by-default has one explicit escape hatch: `--unsafe-show-values`. With it set, a `CLI_PARSE_FAILED` diagnostic appends a bounded excerpt of the offending input to aid local debugging. It is off by default, PHI-exposing by design (the flag name carries the warning), and it is the **only** configuration under which a value reaches a secondary surface: resolved once, globally, and funnelled through a single chokepoint so the "a value appears on stderr **iff** the flag is set" property is provable in one place. A successful parse never puts values on stderr regardless of the flag. ### `redact` / `deid`: delegated, never approximated The one command whose job is to strip identifiers **owns no policy**. De-identification belongs to `@cosyte/deid`: the CLI locates nothing, transforms nothing, and never falls back to a partial Safe-Harbor scrub, which would leave PHI behind while _looking_ de-identified (a false-safety impression). It delegates for the formats that library covers and this CLI can serialize, which are `ccda`, `fhir`, `hl7`, `x12`, and refuses everything else with a typed, value-free code: `astm`, `mllp` and `ncpdp` have no adapter there (`CLI_NOT_IMPLEMENTED`, `69`), and `dicom` is covered but its de-identified form is a binary stream this text stdout cannot carry (`CLI_FORMAT_UNSUPPORTED`, `65`, the CLI's own limit rather than the library's). The fail-closed half is the point: if the library reports **any** locus it could not handle, the run exits `1` and emits **nothing**, because a partial pass offered as a de-identified copy is the exact hazard this command exists to avoid. What reaches stderr is the library's own value-free manifest and its own published label and version; the CLI asserts no de-identification standard of its own. Same discipline as the wrapper boundary everywhere else: the CLI never invents a capability its ground layer lacks, and never claims one it cannot back. --- URL: https://docs.cosyte.com/cli/guides-overview Markdown: https://docs.cosyte.com/cli/guides-overview.md # Guides Task-oriented recipes for the `cosyte` command. Each is a short, copy-pasteable answer to one real question. > **Status:** `0.1`, published to npm. While the package is below 1.0, a breaking change ships in a > minor version and is called out in the changelog. `0.0.1` and `0.0.2` cannot be installed; every > later version installs: see [Installation](https://docs.cosyte.com/cli/installation.md). The `cosyte` command wraps **all eight > Cosyte formats** (HL7 v2, FHIR R4, X12, ASTM, NCPDP SCRIPT, C-CDA, DICOM, MLLP) plus the > `@cosyte/transform` and `@cosyte/terminology` higher-layer libraries: `parse` (autodetect → typed JSON, > with NDJSON streaming for MLLP frames and `--ndjson`), `validate` (verdict in the exit code), `inspect` > (a value-free structural summary), `fmt` (canonical re-serialization), `convert` (HL7 v2 → FHIR R4 via > `@cosyte/transform`), `map-codes` (ConceptMap `$translate` via `@cosyte/terminology`), `redact`/`deid` > (de-identification delegated whole to `@cosyte/deid`, covering `ccda`, `fhir`, `hl7`, `x12` and > refusing every other format rather than approximating it), and `completion` (a shell completion > script). Support is honest **per (format, operation)**: a parser that does not faithfully support a > command (DICOM `parse`/`fmt`, C-CDA `parse`, MLLP `fmt`/`validate`) is a value-free > `CLI_FORMAT_UNSUPPORTED`, never a fake. PHI discipline runs throughout: value-free by default, the > opt-in `--unsafe-show-values`, never a PHI temp file. A `cosyte-mcp` MCP server exposes the same core > to an LLM/agent. A command is only documented here once its behavior ships and its example passes the > doc/code-agreement check. ## Parse from a pipeline and select a field `parse` is pipeline-first. Use `--json` for compact output and pipe it to `jq`: ```bash cat adt.hl7 | cosyte parse - --json | jq '.model.segments[0]' ``` ## Branch on the outcome in CI The exit code is the contract, no need to grep stdout: ```bash if cosyte parse "$file" > /dev/null 2> err.log; then echo "parsed OK" else code=$? # 65 = unparseable/undetected, 66 = missing file, 2 = usage echo "parse failed with exit $code"; cat err.log # err.log is value-free fi ``` ## Force a format when autodetection can't A `.txt` that is really HL7, or an ambiguous input, takes an explicit `--format`: ```bash cosyte parse --format hl7 weird-extension.txt ``` ## Debug a rejected message (and mind the PHI) A `CLI_PARSE_FAILED` line is value-free by default: a code and position, never the bytes. When you are working locally and need to see what the parser choked on, add the loud, opt-in `--unsafe-show-values` (it is **PHI-exposing**, never on stderr you will share): ```bash cosyte parse broken.hl7 --format hl7 # value-free diagnostic cosyte parse broken.hl7 --format hl7 --unsafe-show-values # appends a bounded input excerpt ``` ## Convert an HL7 v2 feed to FHIR in a pipeline `convert` wraps `@cosyte/transform`: the FHIR `Bundle` is on stdout, the value-free conversion issues on stderr, and an error-severity issue sets a non-zero exit: ```bash cat adt.hl7 | cosyte convert - --to fhir | jq '.entry[].resource.resourceType' ``` ## Translate a code through a bring-your-own ConceptMap `map-codes` wraps `@cosyte/terminology`. A code and a ConceptMap are reference data (not PHI), so the target coding lands on stdout; an unmapped code is a value-free signal and a non-zero exit: ```bash cosyte map-codes gender.conceptmap.json \ --system http://hl7.org/fhir/administrative-gender --code male --json ``` ## Use the programmatic core The same autodetection and exit-code contract are importable, useful when embedding the routing logic: ```ts runnable import { detectFormat, EXIT } from "@cosyte/cli"; const enc = new TextEncoder(); const detected = detectFormat(enc.encode('{"resourceType":"Bundle"}')); detected.format; // => "fhir" detected.confidence; // => "certain" EXIT.USAGE; // => 2 ``` Until more commands ship, the [Quickstart](https://docs.cosyte.com/cli/quickstart.md) covers the one-line parse and the **API Reference** documents every export. --- URL: https://docs.cosyte.com/cli/mcp Markdown: https://docs.cosyte.com/cli/mcp.md # The Cosyte MCP server `@cosyte/cli` ships a second front door over the **same** core as the `cosyte` command: a [Model Context Protocol](https://modelcontextprotocol.io) server that lets an LLM/agent parse, validate, inspect, and convert healthcare messages as **callable tools**. The terminal and the agent share one codebase, one PHI posture, and one set of results: the MCP `parse` tool returns exactly what `cosyte parse` returns. > The server is a **local stdio subprocess**, implicitly trusted by whoever launches it, not a hosted > network endpoint. It is stateless per call. ## Register it Add the server to your MCP client's configuration (Claude Desktop, an IDE agent, etc.): ```json { "mcpServers": { "cosyte": { "command": "npx", "args": ["-y", "--package", "@cosyte/cli", "cosyte-mcp"] } } } ``` > **`--package` is required here, and the shorter `["-y", "@cosyte/cli", "mcp"]` does not work.** It > fails with `could not determine executable to run`, because `npx` picks the executable whose name > matches the package name's last segment (`cli`) and this package ships `cosyte` and `cosyte-mcp`. > Naming `cosyte-mcp` explicitly is the supported form. > > This registration also requires a version that can be installed at all: `0.0.1` and `0.0.2` cannot > be. See [If you are on 0.0.1 or 0.0.2](https://docs.cosyte.com/cli/installation.md#if-you-are-on-001-or-002). `cosyte mcp` starts the stdio server; the standalone `cosyte-mcp` bin is equivalent. ```bash cosyte mcp # start the server on stdio (also: cosyte-mcp) ``` ## The tools | Tool | What it does | | ---------- | ----------------------------------------------------------------------------- | | `parse` | Parse an HL7 v2 / FHIR R4 message to typed JSON (format autodetected). | | `validate` | Validate a message; the result carries the verdict (valid / invalid findings).| | `inspect` | Return a value-free structural summary (type + segment/entry counts). | | `convert` | Convert an HL7 v2 message to a FHIR R4 `Bundle` via `@cosyte/transform`. | Every tool takes a `content` string (the raw message); `parse`/`validate`/`inspect` accept an optional `format` override. ## The result contract **Every tool publishes an `outputSchema`**, so a client validates a result against a declared contract instead of pattern-matching a text blob to work out what it is holding. `tools/list` carries that schema next to each tool's input schema and title; every reply from that tool conforms to it. The structured result has the same three outcome properties for every tool, plus that tool's own payload: | Property | Type | What it says | | -------- | --------- | --------------------------------------------------------------------------------- | | `ok` | boolean | Whether the call produced data. A negative verdict about the message is still `true`. | | `status` | string | `success`, `verdict`, or `failed`: the one property to branch on. | | `exit` | integer | The exit code from the documented [exit-code contract](https://docs.cosyte.com/cli/reference-commands.md#exit-codes). | | `code` | string | On a failed call, the stable diagnostic code. Absent when data was produced. | | `data` | object | The tool's own payload. Absent on a failed call, which produced none. | `status` is the distinction a text blob could never make reliably: - **`success`** the operation completed cleanly. - **`verdict`** the tool ran and reports a negative finding _about the message_: a resource that parsed but is not conformant, or a conversion with an error-severity issue. The payload is present and the call is not an error. - **`failed`** the call produced no data: a usage mistake, unparseable input, an unavailable parser, or an internal error. `code` says which, `data` is absent. `data` is the payload the terminal command puts on stdout: `parse` gives `format` + `model` + `warnings` (or `records`, one entry per record, for a multi-record input), `validate` gives `format` + `valid` + `findings`, `inspect` gives the value-free structural summary, and `convert` gives `format` + `bundle` + `findings`. ```json { "ok": true, "status": "verdict", "exit": 1, "data": { "format": "fhir", "valid": false, "findings": [{ "code": "value-not-in-set", "severity": "error", "location": "Patient.gender" }] } } ``` **The text content block carries the serialized JSON of that same structured result**, so a client that reads only text sees exactly the value a schema-aware client validates. There is one value, serialized once: the two channels cannot disagree. ## PHI posture on the agent surface The value-free discipline is **hardened** for agents: there is **no `--unsafe-show-values` door** over MCP. A tool _result_ carries the requested data (the parsed model, the converted Bundle: the explicit request). A tool _error_ carries only a value-free diagnostic: a stable code and a position, never a name, DOB, MRN, or field value. A parsed-but-invalid `validate` is a **successful** call reporting the verdict (not a tool error); only a hard failure (unparseable input, a usage mistake) is flagged as an error. ## Isolation The MCP SDK (`@modelcontextprotocol/sdk`) is the CLI's only third-party runtime dependency. It is declared **optional**, and the programmatic core (the `.` subpath) loads with the SDK absent. The server surface is also importable programmatically via the `@cosyte/cli/mcp` subpath. --- URL: https://docs.cosyte.com/cli/reference-commands Markdown: https://docs.cosyte.com/cli/reference-commands.md # `cosyte` command reference A man-page-style reference for every `cosyte` command, its arguments, and its flags. This mirrors `cosyte --help`; the **API Reference** documents the programmatic (`.` subpath) surface. Every command reads a `` argument or `-` for stdin, autodetects the format by content unless `--format` overrides it, and resolves to a code in the [exit-code contract](#exit-codes). ## Synopsis ``` cosyte [options] cosyte-mcp # the stdio MCP server (also: cosyte mcp) ``` ## Global options | Flag | Effect | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `-h`, `--help` | Print help and exit `0`. | | `-V`, `--version` | Print the CLI version and exit `0`. | | `--unsafe-show-values` | Permit a bounded input excerpt in a **failure** diagnostic on stderr. PHI-exposing; off by default; the single door to a value on a secondary surface. | ## Common options (parse / validate / inspect / fmt) | Flag | Effect | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--format ` | Override autodetection: `hl7 \| fhir \| dicom \| x12 \| ccda \| ncpdp \| astm \| mllp`. Support is per (format, operation): see [Limitations](https://docs.cosyte.com/cli/limitations.md). | | `--json` | Machine-readable JSON output. | | `--quiet` | Suppress value-free notes on stderr (the exit code becomes the whole signal). | | `--no-color` | Disable ANSI colour. | ## Commands ### `cosyte parse ` Parse a healthcare message to typed JSON on stdout (the data channel). Autodetects the format. `--ndjson` treats each non-empty input line as a record and emits NDJSON; an MLLP stream is parsed one record per frame with per-record isolation (a failed record becomes a value-free `{ record, error }` line and the stream continues; any failure → exit `65`). Multi-record output is **written record by record, as each record is parsed**, so the first line reaches stdout long before the last byte of input has been read and a large batch can be piped straight into another process. ```bash cosyte parse adt.hl7 cat adt.hl7 | cosyte parse - cosyte parse --ndjson bulk.ndjson | jq . ``` #### Input size limit `parse` reads at most **67108864 bytes (64 MiB)** of input per invocation. A larger input is refused with a value-free `CLI_INPUT_TOO_LARGE` diagnostic naming that limit and the **data-error** exit code (`65`), never the internal-error code (`70`): a large input is not a bug in the tool. Split the input and re-run. The same number is printed by `cosyte --help`. Because records are emitted as they are parsed, a refusal can arrive **after** some records have already been written to stdout. The exit code, not the output, is the signal that the run did not complete: a partial record stream always carries a non-zero exit, never `0`. ### `cosyte validate [--profile ]` Validate a message; **the exit code carries the verdict**: `0` valid, `1` invalid (parseable but non-conformant), `65` unparseable. Findings are value-free (a code + a positional location). `--profile` is reserved but gated to `CLI_NOT_IMPLEMENTED` (exit `69`), no profiles are bundled. ```bash cosyte validate patient.json && echo "valid" # branches on exit 0/1 ``` ### `cosyte inspect ` Print a **value-free** structural summary: the message/resource type, segment/entry counts, and a warning/issue count. Never emits a field value on either channel. ### `cosyte fmt ` Canonically re-serialize via the wrapped library's spec-clean serializer. stdout is the data channel; an unparseable input is a data error (`65`) with **no partial emit**. ### `cosyte convert --to fhir` Convert an HL7 v2 message to a FHIR R4 `Bundle` via `@cosyte/transform`. `--to fhir` is required (the only target today). An error-severity conversion issue drives exit `1`; a non-HL7 source is `CLI_FORMAT_UNSUPPORTED` (`65`). ### `cosyte map-codes --code [--system ] [--version ] [--display ]` Translate a code through a **bring-your-own** FHIR ConceptMap via `@cosyte/terminology` (`$translate`). A match prints the target coding(s) and exits `0`; an unmapped code is `TERM_TRANSLATE_UNMAPPED` (exit `1`); an unloadable map is `CLI_MAP_INVALID` (`65`). The positional is reference data, and a code is not PHI. ### `cosyte redact ` (alias: `deid`) De-identify a message. **The whole de-identification policy is `@cosyte/deid`'s**; the CLI adds no locus map, no transform and no fallback scrub, so it either delegates or refuses. The covered formats are `ccda`, `fhir`, `hl7`, `x12`. stdout is the de-identified document, serialized exactly as `cosyte fmt` serializes that format. stderr carries the library's own **value-free manifest**, one line per locus it acted on (category, transform, the structural path, the count, the disposition and its stable code), plus the library's own published output label and version. The CLI claims **no de-identification standard of its own**. Anything short of a clean, fully-handled pass emits **nothing** on stdout: | Outcome | Diagnostic | Exit | | ---------------------------------------------------------------- | ------------------------ | ---- | | Every locus handled | the manifest | `0` | | The library reports a locus it could not handle | `CLI_DEID_INCOMPLETE` | `1` | | `astm`, `mllp`, `ncpdp`: the library ships no adapter for them | `CLI_NOT_IMPLEMENTED` | `69` | | `dicom`: covered by the library, but its de-identified form is a binary stream this text stdout cannot carry | `CLI_FORMAT_UNSUPPORTED` | `65` | | `@cosyte/deid` is not installed (it is an optional dependency) | `CLI_PARSER_UNAVAILABLE` | `69` | An absent library is decided **before the input is read**. What the library's **default policy** does, applied unmodified (the CLI passes it no policy, profile or retention set of its own): - **Medical record, account and member numbers are removed**, not replaced by a surrogate. Their manifest lines carry the disposition `removed` and the code `DEID_CATEGORY_REMOVED`. - **A visit or order number is blocked, and the run is refused.** An HL7 visit number (`PV1-19`) and a placer or filler order number (`ORC-2`/`ORC-3`, `OBR-2`/`OBR-3`) fall to the library's catch-all identifier category, which its default policy blocks. The run ends `CLI_DEID_INCOMPLETE` with exit `1` and nothing on stdout, and stderr names each blocked locus with the library's `DEID_LOCUS_BLOCKED` code. An ADT message that carries `PV1-19` therefore produces no output. The CLI keys every run with a **per-invocation ephemeral key** and discloses it on stderr: any identifier surrogate is consistent within one output and deliberately not stable across runs, so two runs cannot be linked by their surrogates. ```bash cosyte redact adt.hl7 > clean.hl7 # stdout is the document (or nothing, on exit 1), stderr is the manifest cosyte redact adt.hl7 2>/dev/null # the manifest suppressed; the exit code still carries the verdict ``` ### `cosyte completion ` Print a static, value-free shell-completion script generated from the command tree. ### `cosyte mcp` / `cosyte-mcp` Start the stdio MCP server: the agent front door, exposing `parse`/`validate`/`inspect`/`convert` as tools over the same core. See [MCP server](https://docs.cosyte.com/cli/mcp.md). ## Exit codes | Code | Meaning | | ---- | --------------------------------------------------------------------------------------- | | `0` | success / `validate` found the input **valid** | | `1` | operation-level failure: `validate` found the input **invalid**, or `redact` could not de-identify every locus (a real CI signal) | | `2` | usage error: unknown command, bad flag, missing argument | | `65` | data error: input could not be parsed / format not detected / (format, op) unsupported / input larger than the size limit | | `66` | no input: the file does not exist or is unreadable | | `69` | unavailable: a capability is not wired here (e.g. `redact` on a format `@cosyte/deid` has no adapter for, `--profile`) | | `70` | internal error: an unexpected exception (a bug) | | `74` | output error: the output stream closed before the CLI had finished writing to it, the shape a downstream consumer that exits early produces (`cosyte parse bulk.ndjson --ndjson \| head -3`, a pager you quit). Never a bug in the CLI, and never a silent `0`: the answer did not reach the consumer | A closed output stream terminates the run quietly under `74` with the stable `CLI_OUTPUT_WRITE_FAILED` code on stderr: no stack trace, no platform error text, and no byte of the input. It reads the same whether the output was a record stream or a single write. The contract is a **stability surface**: renaming a code or a stable `CLI_*` diagnostic is a breaking change, and it is locked by an exit-code golden matrix in the test suite. --- URL: https://docs.cosyte.com/cli/api Markdown: https://docs.cosyte.com/cli/api.md # @cosyte/cli Public programmatic entry point for `@cosyte/cli` (the `.` subpath export). `@cosyte/cli` is a **`bin` package**: its primary artifact is the `cosyte` command on your `PATH` (`npx @cosyte/cli parse …`), not an import surface. This module is the small, honest **programmatic API** over the same `core` the CLI uses: the format autodetector, the exit-code contract, the value-free diagnostic types, and the dispatcher, so another adapter (the MCP server, or a web playground) can drive the same logic without shelling out. The CLI is a **thin, PHI-safe skin** over the wrapped `@cosyte/*` parsers: it routes, reads, shapes output, and owns the exit-code + value-free-diagnostic disciplines; it re-implements no wire-format parsing. ## Classes ### CliError A typed, PHI-safe CLI error. Its CliError.message \| message is **value-free by construction**: callers must build it from codes, indices, offsets, format names, and file paths, never from an input value. It carries the [ExitCode](#exitcode) the invocation resolves to. #### Example ```ts import { CliError, CLI_CODES, EXIT } from "@cosyte/cli"; throw new CliError(CLI_CODES.CLI_NO_INPUT, EXIT.NOINPUT, "cannot read the named file"); ``` #### Extends - `Error` #### Constructors ##### Constructor > **new CliError**(`code`, `exit`, `message`): [`CliError`](#clierror) ###### Parameters ###### code [`CliCode`](#clicode) A stable [CliCode](#clicode). ###### exit [`ExitCode`](#exitcode) The [ExitCode](#exitcode) to resolve to. ###### message `string` A **value-free** explanation (positional/structural context only, never PHI). ###### Returns [`CliError`](#clierror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`CliCode`](#clicode) The stable diagnostic code. ##### exit > `readonly` **exit**: [`ExitCode`](#exitcode) The exit code this error resolves the invocation to. ## Interfaces ### DeidAvailability Whether de-identification is available, and (while it is not) the value-free reason why. The one function that answers "is the ground layer here?", consulted **before any input is read** so a `redact` invocation never touches PHI it cannot strip. #### Properties ##### available > `readonly` **available**: `boolean` `true` when `@cosyte/deid` resolves in this install. ##### reason > `readonly` **reason**: `string` A value-free explanation: the delegate's own label and version, or why it is unavailable. *** ### DeidDelegate The delegate, reduced to what the command needs: its own published honesty label and version, and one call that de-identifies an input. Deliberately CLI-owned shapes, so the delegate's types never reach this package's published surface. #### Properties ##### label > `readonly` **label**: `string` The delegate's own output label, quoted verbatim; the CLI asserts no standard of its own. ##### redact > `readonly` **redact**: (`format`, `bytes`) => `Promise`\<[`RedactOutcome`](#redactoutcome)\> De-identify one input under the delegate's default policy. ###### Parameters ###### format [`DeidCoveredFormat`](#deidcoveredformat) A covered format. ###### bytes `Uint8Array` The input bytes. ###### Returns `Promise`\<[`RedactOutcome`](#redactoutcome)\> The serialized de-identified document and the value-free manifest. ##### version > `readonly` **version**: `string` The delegate's own version string. *** ### DeidManifestRecord One entry of the delegate's value-free manifest, narrowed to the fields the CLI renders. The `locus` is a **path, never a value**, by the delegate's own contract; nothing here carries an input value, which is what lets the manifest go on the diagnostic channel at all. #### Properties ##### category > `readonly` **category**: `string` The identifier category acted on. ##### code > `readonly` **code**: `string` The delegate's stable disposition code. ##### count > `readonly` **count**: `number` How many values at this locus were acted on. ##### disposition > `readonly` **disposition**: `string` What happened: `transformed`, `removed`, or the fail-closed `blocked`. ##### locus > `readonly` **locus**: `string` The structural path acted on: never a value. ##### transform > `readonly` **transform**: `string` The transform applied. *** ### DetectResult How confident autodetection is: - `certain`: exactly one signature matched (`format` names it); - `ambiguous`: more than one matched (`format` is `null`; `candidates` names them); - `none`: nothing matched (`format` is `null`; `candidates` is empty). `format` is `null` unless `confidence` is `certain`, so a non-certain result can never be mistaken for a routable format. #### Properties ##### candidates > `readonly` **candidates**: readonly [`CosyteFormat`](#cosyteformat)[] The matching candidate formats (0 for `none`, 1 for `certain`, ≥2 for `ambiguous`). Value-free. ##### confidence > `readonly` **confidence**: `"certain"` \| `"ambiguous"` \| `"none"` The detection confidence. ##### format > `readonly` **format**: [`CosyteFormat`](#cosyteformat) \| `null` The detected format, or `null` when `confidence` is not `certain`. *** ### Finding A single value-free finding: a stable code, a severity, and a positional locator. Every field is safe to print, log, or serialize. None carries a name, DOB, MRN, or field value. #### Properties ##### code > `readonly` **code**: `string` The stable diagnostic/warning code (the wrapped library's or the CLI's). ##### location > `readonly` **location**: `string` A value-free positional locator: a FHIRPath expression, or an HL7 segment/field index path. ##### severity > `readonly` **severity**: `string` The R4-style severity (`fatal` | `error` | `warning` | `information`), or `warning` for HL7. *** ### FmtResult The result of a canonical re-serialization (`fmt`): the spec-clean text + a value-free warning count. #### Properties ##### output > `readonly` **output**: `string` The wrapped serializer's output text. ##### warningCount > `readonly` **warningCount**: `number` How many parse warnings the round-trip surfaced (value-free count only). *** ### ParseResult The result of wrapping a parser's `parse`: the library's parsed model + its value-free warnings. #### Properties ##### model > `readonly` **model**: `unknown` The wrapped library's parsed model: emitted verbatim as JSON on the data channel. ##### warnings > `readonly` **warnings**: readonly [`ParseWarning`](#parsewarning)[] The parser's value-free warnings (code + position). *** ### PhiPosture The resolved PHI display posture for one invocation. `showValues` is `true` **only** when the user passed `--unsafe-show-values`; every value-echoing decision reads this one flag. #### Properties ##### showValues > `readonly` **showValues**: `boolean` `true` iff `--unsafe-show-values` was given: a value may appear on a secondary surface. *** ### RedactOutcome A completed de-identification pass: the serialized document plus the delegate's manifest. #### Properties ##### manifest > `readonly` **manifest**: readonly [`DeidManifestRecord`](#deidmanifestrecord)[] The delegate's value-free manifest, in locus order. ##### output > `readonly` **output**: `string` The de-identified document, serialized exactly as this format's wrapped serializer emits it. *** ### ResolvedInput A successfully-resolved input: the format (guaranteed to support the requested op) and the bytes. #### Properties ##### bytes > `readonly` **bytes**: `Uint8Array` The input bytes (a whole file or a drained stdin buffer); guaranteed non-empty. ##### format > `readonly` **format**: [`CosyteFormat`](#cosyteformat) The resolved format: guaranteed to satisfy `supportsOp(format, op)` for the requested op. *** ### ResolvedInputStream A successfully-resolved **streaming** input: the format, plus the input as a stream of chunks with the leading bytes detection consumed put back in front, so the consumer still sees the whole input in order. #### Properties ##### chunks > `readonly` **chunks**: [`ByteChunks`](#bytechunks) The whole input as chunks, size-limited: reading past the limit raises `CLI_INPUT_TOO_LARGE`. ##### format > `readonly` **format**: [`CosyteFormat`](#cosyteformat) The resolved format: guaranteed to satisfy `supportsOp(format, op)` for the requested op. *** ### RunDeps The injectable I/O side-effects the dispatcher needs. Kept tiny and pure-ish so tests drive the CLI end to end with in-memory fakes and no real filesystem, stdin or stdout. The two whole-input readers are required; the chunk readers and the output sink are **optional**, and a caller that omits them still gets identical output. They are the difference between reading a stream as it arrives and being handed one buffer: with them, a multi-record `parse` emits each record as it is produced and refuses an over-limit input while it is still arriving; without them, the same code path runs over a single chunk and returns its output on the result's `stdout` as before. #### Properties ##### openFile? > `readonly` `optional` **openFile?**: (`path`) => `AsyncIterable`\<`Uint8Array`\<`ArrayBufferLike`\>\> Optional: open a file as a stream of byte chunks (see [fileChunks](#filechunks)). ###### Parameters ###### path `string` ###### Returns `AsyncIterable`\<`Uint8Array`\<`ArrayBufferLike`\>\> ##### openStdin? > `readonly` `optional` **openStdin?**: () => `AsyncIterable`\<`Uint8Array`\<`ArrayBufferLike`\>\> Optional: open stdin as a stream of byte chunks (see [streamChunks](#streamchunks)). ###### Returns `AsyncIterable`\<`Uint8Array`\<`ArrayBufferLike`\>\> ##### readFile > `readonly` **readFile**: (`path`) => `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> Read a file's bytes, or raise a `CLI_NO_INPUT` [CliError](#clierror) if it cannot be read. ###### Parameters ###### path `string` ###### Returns `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> ##### readStdin > `readonly` **readStdin**: () => `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> Read all of stdin's bytes. ###### Returns `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> ##### writeStdout? > `readonly` `optional` **writeStdout?**: (`chunk`) => `void` Optional: write one piece of the data channel **as it is produced**. When present, a multi-record `parse` writes each record's NDJSON line through here instead of accumulating them, and the returned result's `stdout` is empty because the output has already been delivered. May throw (a consumer that closed the pipe); the caller turns that into a value-free `CLI_OUTPUT_WRITE_FAILED`. ###### Parameters ###### chunk `string` ###### Returns `void` *** ### RunResult A fully-resolved CLI invocation. #### Example ```ts import { EXIT, type RunResult } from "@cosyte/cli"; const r: RunResult = { stdout: "{}\n", stderr: "", exit: EXIT.OK }; r.exit; // => 0 ``` #### Properties ##### exit > `readonly` **exit**: [`ExitCode`](#exitcode) The exit code the invocation resolves to. ##### stderr > `readonly` **stderr**: `string` The diagnostic channel is **value-free**: codes, positions, file paths, format names only. ##### stdout > `readonly` **stdout**: `string` The data channel: the parsed model or requested output. May contain the user's data by design. *** ### Verdict The verdict of running a format's validation surface: value-free findings + the validity boolean. #### Properties ##### findings > `readonly` **findings**: readonly [`Finding`](#finding)[] The value-free findings (code + severity + positional locator). ##### valid > `readonly` **valid**: `boolean` `true` iff the input is valid (no error/fatal-severity finding). ## Type Aliases ### ByteChunks > **ByteChunks** = `AsyncIterable`\<`Uint8Array`\> A stream of input byte chunks, in arrival order: what every function here consumes. *** ### CliCode > **CliCode** = *typeof* [`CLI_CODES`](#cli_codes)\[keyof *typeof* [`CLI_CODES`](#cli_codes)\] A value from [CLI\_CODES](#cli_codes): the stable code carried by a [CliError](#clierror). *** ### CosyteFormat > **CosyteFormat** = `"hl7"` \| `"fhir"` \| `"dicom"` \| `"x12"` \| `"ccda"` \| `"ncpdp"` \| `"astm"` \| `"mllp"` The set of formats the `cosyte` command names. *** ### DeidCoverage > **DeidCoverage** = `"covered"` \| `"no-adapter"` \| `"unserializable"` What the delegate can do for one format, from this CLI's point of view: - `covered`: an adapter for the standard this CLI resolves, serializable onto the text channel. - `no-adapter`: the delegate does not de-identify this format (`CLI_NOT_IMPLEMENTED`, exit `69`). - `unserializable`: the delegate covers it, but the result cannot ride a text stdout (`CLI_FORMAT_UNSUPPORTED`, exit `65`: the CLI's own limit, not the delegate's). *** ### DeidCoveredFormat > **DeidCoveredFormat** = `"hl7"` \| `"fhir"` \| `"x12"` \| `"ccda"` A format the delegate de-identifies and the CLI can serialize. *** ### ExitCode > **ExitCode** = *typeof* [`EXIT`](#exit-2)\[keyof *typeof* [`EXIT`](#exit-2)\] A value from [EXIT](#exit-2): the exit code a CLI invocation resolves to. *** ### InputResolution > **InputResolution** = \{ `input`: [`ResolvedInput`](#resolvedinput); `ok`: `true`; \} \| \{ `ok`: `false`; `result`: [`RunResult`](#runresult); \} The outcome of [resolveInput](#resolveinput): either the resolved input, or a ready-to-return value-free [RunResult](#runresult) carrying the diagnostic + exit code for whatever went wrong. A discriminated union so a command reads `if (!r.ok) return r.result;` and then works with `r.input`. *** ### InputStreamResolution > **InputStreamResolution** = \{ `input`: [`ResolvedInputStream`](#resolvedinputstream); `ok`: `true`; \} \| \{ `ok`: `false`; `result`: [`RunResult`](#runresult); \} The outcome of [resolveInputStream](#resolveinputstream): the resolved streaming input, or a ready-to-return value-free [RunResult](#runresult). The same discriminated-union shape as [InputResolution](#inputresolution). *** ### InspectSummary > **InspectSummary** = `Hl7Summary` \| `FhirSummary` \| `X12Summary` \| `AstmSummary` \| `CcdaSummary` \| `DicomSummary` \| `NcpdpSummary` \| `MllpSummary` The discriminated value-free structural summary `inspect` renders. *** ### Op > **Op** = `"parse"` \| `"inspect"` \| `"fmt"` \| `"validate"` The four wrapping operations a command can ask a format's adapter to perform. *** ### ParseWarning > **ParseWarning** = `object` & `Readonly`\<`Record`\<`string`, `unknown`\>\> A value-free parse warning: a stable code plus the parser's positional (index-only) context. #### Type Declaration ##### code > `readonly` **code**: `string` ## Variables ### CLI\_CODES > `const` **CLI\_CODES**: `object` Stable **CLI diagnostic code** registry: errors the CLI owns (routing, I/O, argument handling), distinct from the wrapped library's own warning/issue codes which are passed through unchanged. Each code is its own value (`key === value`) so the set survives `Object.values(...)`; renaming or removing one is a **breaking change** because scripts branch on the stderr text. #### Type Declaration ##### CLI\_DEID\_INCOMPLETE > `readonly` **CLI\_DEID\_INCOMPLETE**: `"CLI_DEID_INCOMPLETE"` = `"CLI_DEID_INCOMPLETE"` The delegated de-identifier reported that it could not handle every element of the input (a fail-closed `blocked` locus, or its own fatal). The run is **not** a de-identified copy, so no output is emitted: an operation-level failure, never a bug. Names loci and codes, never a value. Exit `1`. ##### CLI\_EMPTY\_INPUT > `readonly` **CLI\_EMPTY\_INPUT**: `"CLI_EMPTY_INPUT"` = `"CLI_EMPTY_INPUT"` The input was empty, no bytes to detect or parse. Exit `65`. ##### CLI\_FORMAT\_AMBIGUOUS > `readonly` **CLI\_FORMAT\_AMBIGUOUS**: `"CLI_FORMAT_AMBIGUOUS"` = `"CLI_FORMAT_AMBIGUOUS"` More than one format signature matched; the CLI will not guess. Names the candidates, never the bytes. Exit `65`. ##### CLI\_FORMAT\_UNDETECTED > `readonly` **CLI\_FORMAT\_UNDETECTED**: `"CLI_FORMAT_UNDETECTED"` = `"CLI_FORMAT_UNDETECTED"` No format signature matched; the CLI will not guess. Names the candidates, never the bytes. Exit `65`. ##### CLI\_FORMAT\_UNSUPPORTED > `readonly` **CLI\_FORMAT\_UNSUPPORTED**: `"CLI_FORMAT_UNSUPPORTED"` = `"CLI_FORMAT_UNSUPPORTED"` A recognised format whose parser does not support the requested operation (e.g. `parse` on a format wired only for `inspect`). Names the format + operation, never the bytes. Exit `65`. ##### CLI\_INPUT\_TOO\_LARGE > `readonly` **CLI\_INPUT\_TOO\_LARGE**: `"CLI_INPUT_TOO_LARGE"` = `"CLI_INPUT_TOO_LARGE"` The input is larger than the documented size limit the CLI reads in one invocation. Raised against the running byte count while the input is read, so the refusal always arrives before an allocation ceiling could. Names the limit, never the input. Exit `65`, never `70`. ##### CLI\_INTERNAL > `readonly` **CLI\_INTERNAL**: `"CLI_INTERNAL"` = `"CLI_INTERNAL"` An unexpected internal error (a bug). Exit `70`. ##### CLI\_MAP\_INVALID > `readonly` **CLI\_MAP\_INVALID**: `"CLI_MAP_INVALID"` = `"CLI_MAP_INVALID"` The BYO ConceptMap supplied to `map-codes` is not valid JSON or not a loadable FHIR ConceptMap. Names the stable terminology-loader code, never the map's bytes. Exit `65`. ##### CLI\_NO\_INPUT > `readonly` **CLI\_NO\_INPUT**: `"CLI_NO_INPUT"` = `"CLI_NO_INPUT"` The named file does not exist or could not be read. Exit `66`. ##### CLI\_NOT\_IMPLEMENTED > `readonly` **CLI\_NOT\_IMPLEMENTED**: `"CLI_NOT_IMPLEMENTED"` = `"CLI_NOT_IMPLEMENTED"` A command whose ground-layer library does not cover the resolved format (e.g. `redact` on a format `@cosyte/deid` ships no adapter for). Never a fake success: a distinct, value-free "unavailable" signal. Exit `69`. ##### CLI\_OUTPUT\_WRITE\_FAILED > `readonly` **CLI\_OUTPUT\_WRITE\_FAILED**: `"CLI_OUTPUT_WRITE_FAILED"` = `"CLI_OUTPUT_WRITE_FAILED"` The output stream could not be written: a consumer that closes the pipe ends the invocation here rather than as an unhandled platform error, whether it went away part way through a record stream or before a single-result write was delivered. Names no input, and never the platform's own error text. Exit `74`. ##### CLI\_PARSE\_FAILED > `readonly` **CLI\_PARSE\_FAILED**: `"CLI_PARSE_FAILED"` = `"CLI_PARSE_FAILED"` The wrapped parser rejected the input. Positional context only, never the offending bytes. Exit `65`. ##### CLI\_PARSER\_UNAVAILABLE > `readonly` **CLI\_PARSER\_UNAVAILABLE**: `"CLI_PARSER_UNAVAILABLE"` = `"CLI_PARSER_UNAVAILABLE"` The optional parser package for a recognised format is not installed. The six breadth parsers (dicom/x12/ccda/ncpdp/astm/mllp) are `optionalDependencies`: normally installed, but if one is absent the CLI degrades to this value-free signal rather than crashing. Exit `69`. ##### CLI\_USAGE > `readonly` **CLI\_USAGE**: `"CLI_USAGE"` = `"CLI_USAGE"` A required argument was missing or a flag was invalid. Exit `2`. #### Example ```ts import { CLI_CODES } from "@cosyte/cli"; if (diagnostic.code === CLI_CODES.CLI_FORMAT_UNDETECTED) { // ask the user for --format } ``` *** ### DEID\_BLOCKED > `const` **DEID\_BLOCKED**: `"blocked"` = `"blocked"` The fail-closed disposition: the delegate reports it could not handle the element. *** ### DEID\_COVERAGE > `const` **DEID\_COVERAGE**: `Readonly`\<`Record`\<[`CosyteFormat`](#cosyteformat), [`DeidCoverage`](#deidcoverage)\>\> The honest per-format de-identification matrix, the `redact` analogue of the parser registry's operation matrix. Every one of the eight formats has an entry, so a new format cannot be added without answering for it here. #### Example ```ts import { DEID_COVERAGE } from "@cosyte/cli"; DEID_COVERAGE.hl7; // => "covered" DEID_COVERAGE.dicom; // => "unserializable" ``` *** ### DEID\_PACKAGE > `const` **DEID\_PACKAGE**: `"@cosyte/deid"` = `"@cosyte/deid"` The npm package the whole de-identification capability is delegated to. *** ### DEID\_UNAVAILABLE\_REASON > `const` **DEID\_UNAVAILABLE\_REASON**: `string` The value-free reason `redact`/`deid` reports when the de-identification library is absent from the install. It is an `optionalDependency`, so a copy without it degrades rather than crashing. *** ### DETECT\_PREFIX\_BYTES > `const` **DETECT\_PREFIX\_BYTES**: `512` = `512` How many leading bytes detection reads. Small: every signature lives in the first line (the deepest is DICOM's magic at byte 128). Exported because a **streaming** caller has to know how much of the input to hold back before it can detect the format, and reading a different amount than detection uses would make the two disagree. #### Example ```ts import { DETECT_PREFIX_BYTES } from "@cosyte/cli"; DETECT_PREFIX_BYTES; // => 512 ``` *** ### DETECTABLE\_FORMATS > `const` **DETECTABLE\_FORMATS**: readonly [`CosyteFormat`](#cosyteformat)[] The formats content-autodetection can recognise (every format now carries a signature). *** ### EXIT > `const` **EXIT**: `object` The stable exit-code map. Adding a code is a documented, tested change to the CLI's contract; renaming or repurposing one is a breaking change. #### Type Declaration ##### DATAERR > `readonly` **DATAERR**: `65` = `65` Data error: input could not be parsed or its format could not be detected (`EX_DATAERR`). ##### INVALID > `readonly` **INVALID**: `1` = `1` Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant), or `redact` could not de-identify every locus: a real, expected CI signal that the tool worked and the operation did not complete. Never emitted for unparseable input (that is `DATAERR`). ##### IOERR > `readonly` **IOERR**: `74` = `74` Output error: the stream the CLI was writing to closed before the output had been delivered, so the consumer holds nothing or holds only part of the answer. The shape a downstream consumer that exits early produces (a pager quit, a `head` that has seen enough). A handled condition the consumer owns, never a defect in the CLI, so it is deliberately distinct from `SOFTWARE` (`EX_IOERR`). ##### NOINPUT > `readonly` **NOINPUT**: `66` = `66` No input: the named file does not exist or is unreadable (`EX_NOINPUT`). ##### OK > `readonly` **OK**: `0` = `0` Success: the operation completed; `validate` found the input **valid**. ##### SOFTWARE > `readonly` **SOFTWARE**: `70` = `70` Internal error: an unexpected exception (a bug), distinct from a handled bad input (`EX_SOFTWARE`). ##### UNAVAILABLE > `readonly` **UNAVAILABLE**: `69` = `69` Unavailable: a required capability is not wired for this input (e.g. `redact` on a format `@cosyte/deid` has no adapter for, or with that optional library absent), a distinct non-zero signal that is never a fake success (`EX_UNAVAILABLE`). ##### USAGE > `readonly` **USAGE**: `2` = `2` Usage error: unknown command, bad flag, missing argument. A value specific to this CLI, not a `sysexits.h` constant. #### Example ```ts import { EXIT } from "@cosyte/cli"; process.exitCode = EXIT.OK; // => 0 ``` *** ### KNOWN\_FORMATS > `const` **KNOWN\_FORMATS**: readonly [`CosyteFormat`](#cosyteformat)[] The full set of format names `--format` accepts as syntactically valid (a bad value is a usage error). *** ### MAX\_INPUT\_BYTES > `const` **MAX\_INPUT\_BYTES**: `number` The maximum number of input bytes `cosyte parse` reads in one invocation: **67108864 bytes (64 MiB)**. Chosen to sit far below the smaller of Node's two allocation ceilings (`buffer.constants.MAX_STRING_LENGTH`, 536870888 characters), because the bytes read are not the peak: a parsed model rendered as JSON is routinely several times the size of the input that produced it, so the limit has to leave room for the output as well as the input. A power of two, so the byte count and its mebibyte rendering are both exact. #### Example ```ts import { MAX_INPUT_BYTES } from "@cosyte/cli"; MAX_INPUT_BYTES; // => 67108864 ``` *** ### OP\_SUPPORT > `const` **OP\_SUPPORT**: `Readonly`\<`Record`\<[`CosyteFormat`](#cosyteformat), `ReadonlySet`\<[`Op`](#op)\>\>\> The honest per-format operation matrix. A (format, op) pair absent here is a value-free `CLI_FORMAT_UNSUPPORTED`, never a faked result. The unsupported cells and why: - **dicom** `parse`/`fmt`. The model is binary (no faithful JSON view; `serializeDicom` emits a Part-10 byte stream, not text). `inspect`/`validate` are supported. - **ccda** `parse`: the canonical form is XML; there is no library-blessed JSON model, so `fmt` (XML re-serialize) is the faithful surface. `inspect`/`fmt`/`validate` are supported. - **mllp** `fmt`/`validate`: MLLP is a transport container the CLI de-frames to HL7; `parse` yields the enclosed HL7 message(s) and `inspect` reports the frame count. #### Example ```ts import { OP_SUPPORT } from "@cosyte/cli"; OP_SUPPORT.dicom.has("parse"); // => false OP_SUPPORT.hl7.has("validate"); // => true ``` *** ### SHOW\_VALUES > `const` **SHOW\_VALUES**: [`PhiPosture`](#phiposture) The opted-in, PHI-exposing posture selected by `--unsafe-show-values`. *** ### UNSAFE\_EXCERPT\_MAX > `const` **UNSAFE\_EXCERPT\_MAX**: `200` = `200` The maximum number of leading input bytes an `--unsafe-show-values` diagnostic may echo. Bounded so an unsafe excerpt stays a single, readable diagnostic line rather than dumping a whole message. *** ### UNSAFE\_SHOW\_VALUES\_FLAG > `const` **UNSAFE\_SHOW\_VALUES\_FLAG**: `"--unsafe-show-values"` = `"--unsafe-show-values"` The single global flag token that opts into showing values on secondary surfaces. *** ### VALUE\_FREE > `const` **VALUE\_FREE**: [`PhiPosture`](#phiposture) The default, safe posture: no value ever reaches a secondary surface. *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` The `@cosyte/cli` version. On the uniform `v0.0.x-until-first-alpha` ladder. Read by `cosyte --version` and by the MCP server's advertised `SERVER_INFO`, so it is a user-visible surface on both front doors, not merely an export. #### Example ```ts import { VERSION } from "@cosyte/cli"; console.log(`cosyte ${VERSION}`); ``` ## Functions ### asCosyteFormat() > **asCosyteFormat**(`value`): [`CosyteFormat`](#cosyteformat) \| `null` Narrow an arbitrary `--format` string to a [CosyteFormat](#cosyteformat), or `null` if it is not a known format name (which the caller turns into a usage error, exit `2`). #### Parameters ##### value `string` The raw `--format` argument. #### Returns [`CosyteFormat`](#cosyteformat) \| `null` The narrowed format, or `null`. #### Example ```ts import { asCosyteFormat } from "@cosyte/cli"; asCosyteFormat("hl7"); // => "hl7" asCosyteFormat("nope"); // => null ``` *** ### classifyCandidates() > **classifyCandidates**(`candidates`): [`DetectResult`](#detectresult) Classify a list of matched candidate formats into a [DetectResult](#detectresult): exactly one → `certain`, zero → `none`, two-or-more → `ambiguous` (all with `format` `null` unless certain). Split out and exported so the **ambiguity** contract is directly testable. It is otherwise unreachable while every signature is disjoint, and it must stay a *detected* ambiguity, never a mis-route. #### Parameters ##### candidates readonly [`CosyteFormat`](#cosyteformat)[] The formats whose signatures matched the input. #### Returns [`DetectResult`](#detectresult) The classified [DetectResult](#detectresult). #### Example ```ts import { classifyCandidates } from "@cosyte/cli"; classifyCandidates(["hl7"]).confidence; // => "certain" classifyCandidates([]).confidence; // => "none" classifyCandidates(["hl7", "fhir"]).confidence; // => "ambiguous" ``` *** ### collectChunks() > **collectChunks**(`chunks`): `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> Drain a chunk stream into one buffer: the single-message path, where the parser needs the whole message anyway. The size limit is already applied by [withinLimit](#withinlimit) upstream, so this cannot accumulate more than the limit allows. #### Parameters ##### chunks [`ByteChunks`](#bytechunks) The input chunk stream. #### Returns `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> The concatenated bytes. #### Example ```ts import { collectChunks, oneChunk } from "@cosyte/cli"; (await collectChunks(oneChunk(new TextEncoder().encode("MSH|")))).length; // => 4 ``` *** ### completionCommand() > **completionCommand**(`args`): [`RunResult`](#runresult) Run the `completion` command. #### Parameters ##### args `string`[] The arguments after the `completion` subcommand token; the first is the shell name. #### Returns [`RunResult`](#runresult) A [RunResult](#runresult): the completion script on `stdout` (exit `0`), or a value-free usage error (exit `2`) when the shell is missing or unrecognised. #### Example ```ts import { completionCommand } from "@cosyte/cli"; completionCommand(["bash"]).exit; // => 0 completionCommand(["powershell"]).exit; // => 2 ``` *** ### convertCommand() > **convertCommand**(`args`, `deps`, `posture?`): `Promise`\<[`RunResult`](#runresult)\> Run the `convert` command. #### Parameters ##### args `string`[] The arguments after the `convert` subcommand token. ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)). ##### posture? [`PhiPosture`](#phiposture) = `VALUE_FREE` The resolved [PhiPosture](#phiposture) (governs only the opt-in unsafe excerpt on an HL7 parse-failure diagnostic; the converted bundle and the value-free findings are unaffected). #### Returns `Promise`\<[`RunResult`](#runresult)\> A [RunResult](#runresult): the converted FHIR `Bundle` on `stdout` (the data channel), value-free findings on `stderr` (or JSON under `--json`), and an exit code that carries the outcome: `0` clean · `1` an error-severity conversion issue · `65` the HL7 input could not be parsed or is not an HL7 v2 source · `66` no input · `2` usage. #### Throws Never [CliError](#clierror); may propagate a truly unexpected error for the dispatcher to map to `CLI_INTERNAL`. #### Example ```ts import { convertCommand } from "@cosyte/cli"; const deps = { readFile: async () => new TextEncoder().encode("MSH|^~\\&|A|B|C|D|20240101||ADT^A01|1|P|2.5\r"), readStdin: async () => new Uint8Array(), }; (await convertCommand(["adt.hl7", "--to", "fhir"], deps)).exit; // => 0 ``` *** ### convertOutcome() > **convertOutcome**(`findings`): `object` Decide the conversion outcome from the library's value-free findings: whether any is **error-severity** (which drives a non-zero exit: the load-bearing "a conversion error is never exit 0" rule) and the value-free human report. The severity classification is the library's: `@cosyte/transform` fixes each issue's severity; the CLI only reads it. Exported so the exit-verdict and report logic is unit-testable with synthetic findings, independent of which HL7 message happens to produce an error. #### Parameters ##### findings readonly [`Finding`](#finding)[] The value-free [Finding](#finding)s the conversion produced. #### Returns `object` `hasError` (true iff any finding is error/fatal severity) and the rendered value-free `report` (one line per finding + a summary line). ##### hasError > `readonly` **hasError**: `boolean` ##### report > `readonly` **report**: `string` #### Example ```ts import { convertOutcome } from "@cosyte/cli"; convertOutcome([{ code: "TRANSFORM_RESOURCE_INVALID", severity: "error", location: "PID" }]) .hasError; // => true convertOutcome([{ code: "TRANSFORM_ELEMENT_DROPPED", severity: "information", location: "PID.13" }]) .hasError; // => false ``` *** ### deframeMllp() > **deframeMllp**(`bytes`): `Promise`\<\{ `payloads`: `Buffer`\<`ArrayBufferLike`\>[]; `warningCount`: `number`; \}\> De-frame an MLLP byte stream into its enclosed HL7 payloads via `@cosyte/mllp`'s `FrameReader`. MLLP is a transport container, not a document format: each frame's payload is an HL7 v2 message the CLI then parses/inspects with the `hl7` adapter. Multi-frame streams are the CLI's multi-message surface. #### Parameters ##### bytes `Uint8Array` The MLLP-framed input. #### Returns `Promise`\<\{ `payloads`: `Buffer`\<`ArrayBufferLike`\>[]; `warningCount`: `number`; \}\> The de-framed HL7 payloads (in stream order) + a value-free framing-warning count. **Truncation is a data error, never a silent drop.** The `FrameReader` is a streaming decoder: an unterminated trailing frame (a VT opened with no closing FS/CR) is left buffered and delivered by *no* callback, so feeding a whole file and reading only `onFrame` would silently lose the partial message with a green exit. We therefore detect an open trailing frame at the byte level (the last VT sits after the last FS: MLLP payloads are HL7 v2 text and never carry the `0x0B`/`0x1C` framing bytes) and reject the whole stream as a value-free `CLI_PARSE_FAILED` data error. #### Throws `CLI_PARSER_UNAVAILABLE` if `@cosyte/mllp` is absent; `CLI_PARSE_FAILED` (exit `65`) on a truncated stream; a hard framing error propagates for the caller's value-free boundary. #### Example ```ts import { deframeMllp } from "@cosyte/cli"; // a VT-framed "MSH|..." message → one payload (await deframeMllp(new Uint8Array([0x0b, 0x4d, 0x53, 0x48, 0x1c, 0x0d]))).payloads.length; // => 1 ``` *** ### deidCoverage() > **deidCoverage**(`format`): [`DeidCoverage`](#deidcoverage) What the delegate can do for `format`. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) The resolved format. #### Returns [`DeidCoverage`](#deidcoverage) That format's [DeidCoverage](#deidcoverage). #### Example ```ts import { deidCoverage } from "@cosyte/cli"; deidCoverage("ncpdp"); // => "no-adapter" ``` *** ### deidCoveredFormats() > **deidCoveredFormats**(): readonly [`CosyteFormat`](#cosyteformat)[] The formats `redact` produces output for, sorted: the single source the help text, the published documentation and the tests all read, so the three cannot drift. #### Returns readonly [`CosyteFormat`](#cosyteformat)[] The covered format names, sorted. #### Example ```ts import { deidCoveredFormats } from "@cosyte/cli"; deidCoveredFormats(); // => ["ccda", "fhir", "hl7", "x12"] ``` *** ### deidStatus() > **deidStatus**(`load?`): `Promise`\<[`DeidAvailability`](#deidavailability)\> Report the current de-identification availability. This is the single documented flip point: it resolves the delegated library rather than asserting anything about it. #### Parameters ##### load? () => `Promise`\<[`DeidDelegate`](#deiddelegate)\> The delegate loader; injectable so a test can drive the unavailable branch. #### Returns `Promise`\<[`DeidAvailability`](#deidavailability)\> `{ available: true }` with the delegate's own label and version, else `{ available: false }` with the value-free [DEID\_UNAVAILABLE\_REASON](#deid_unavailable_reason). #### Example ```ts import { deidStatus } from "@cosyte/cli"; (await deidStatus()).available; // => true when @cosyte/deid is installed ``` *** ### describeByteLimit() > **describeByteLimit**(`limit?`): `string` Render a byte limit as a **concrete number with an explicit byte-based unit**: `"67108864 bytes (64 MiB)"`. Every surface that states the limit (the refusal diagnostic, `cosyte --help`, the command reference) renders it through here, so the number a user reads is always the number the code enforces, and never a vague description of one. #### Parameters ##### limit? `number` = `MAX_INPUT_BYTES` The limit in bytes. Defaults to [MAX\_INPUT\_BYTES](#max_input_bytes). #### Returns `string` The limit as bytes, plus a mebibyte rendering when it is a whole number of mebibytes. #### Example ```ts import { describeByteLimit } from "@cosyte/cli"; describeByteLimit(67108864); // => "67108864 bytes (64 MiB)" describeByteLimit(1000); // => "1000 bytes" ``` *** ### detectFormat() > **detectFormat**(`bytes`): [`DetectResult`](#detectresult) Detect the healthcare format of `bytes` by content, conservatively. #### Parameters ##### bytes `Uint8Array` The input bytes (a whole file or a stdin buffer). Only the leading prefix is sniffed. #### Returns [`DetectResult`](#detectresult) A [DetectResult](#detectresult): `certain` + a `format` on a single match, else `ambiguous`/`none` with `format: null` and the value-free `candidates` list. **Never guesses.** #### Example ```ts import { detectFormat } from "@cosyte/cli"; const enc = new TextEncoder(); detectFormat(enc.encode("MSH|^~\\&|A|B\r")).format; // => "hl7" detectFormat(enc.encode('{"resourceType":"Patient"}')).format; // => "fhir" detectFormat(enc.encode("hello")).confidence; // => "none" ``` *** ### detectionError() > **detectionError**(`detected`): [`CliError`](#clierror) Build the value-free [CliError](#clierror) for a **non-certain** detection: the data error the caller returns instead of guessing a parser. `ambiguous` names the matching candidates (a value-free code list); `none` asks for `--format`. Both map to the data-error exit (`65`). Never echoes input bytes. #### Parameters ##### detected [`DetectResult`](#detectresult) A [DetectResult](#detectresult) whose `format` is `null` (i.e. not `certain`). #### Returns [`CliError`](#clierror) A value-free `CLI_FORMAT_AMBIGUOUS` or `CLI_FORMAT_UNDETECTED` error. #### Example ```ts import { classifyCandidates, detectionError } from "@cosyte/cli"; detectionError(classifyCandidates([])).code; // => "CLI_FORMAT_UNDETECTED" detectionError(classifyCandidates(["hl7", "fhir"])).code; // => "CLI_FORMAT_AMBIGUOUS" ``` *** ### errorResult() > **errorResult**(`err`): [`RunResult`](#runresult) Resolve a [CliError](#clierror) into a value-free [RunResult](#runresult): empty `stdout` (nothing reaches the data channel on an error), the rendered diagnostic on `stderr`, and the error's exit code. The single place the CLI turns an owned error into a result, so every command and the dispatcher render errors identically. #### Parameters ##### err [`CliError`](#clierror) The CLI error to resolve. #### Returns [`RunResult`](#runresult) The value-free [RunResult](#runresult). #### Example ```ts import { CliError, CLI_CODES, EXIT, errorResult } from "@cosyte/cli"; errorResult(new CliError(CLI_CODES.CLI_USAGE, EXIT.USAGE, "missing ")).exit; // => 2 ``` *** ### extractPhiPosture() > **extractPhiPosture**(`argv`): `object` Resolve the global `--unsafe-show-values` flag out of an argument vector, **order-independently** (it may appear before or after the subcommand), and return the posture plus the argv with every occurrence of the flag removed, so each command's own `parseArgs` never sees it and cannot reject it as unknown. This is the one place the flag is recognised. #### Parameters ##### argv readonly `string`[] The raw arguments (after the program name). #### Returns `object` The resolved [PhiPosture](#phiposture) and the flag-stripped argv. ##### argv > **argv**: `string`[] ##### posture > **posture**: [`PhiPosture`](#phiposture) #### Example ```ts import { extractPhiPosture } from "@cosyte/cli"; extractPhiPosture(["parse", "x.hl7"]).posture.showValues; // => false extractPhiPosture(["--unsafe-show-values", "parse", "x.hl7"]).posture.showValues; // => true extractPhiPosture(["parse", "x.hl7", "--unsafe-show-values"]).argv; // => ["parse", "x.hl7"] ``` *** ### extractStableCode() > **extractStableCode**(`e`): `string` \| `null` Pull a `code` off a thrown value **only** when it is a PHI-free constant token (`^[A-Z][A-Z0-9_]*$`: a letter-led UPPER_SNAKE code like `MALFORMED_JSON`), e.g. a wrapped parser's stable fatal code. Anything else (no `code`, a non-string, a non-token, or a **pure-digit** value that could be a raw identifier) yields `null`, so a parser exception that embedded input bytes in a `code`-shaped field can never reach a diagnostic. #### Parameters ##### e `unknown` A caught value. #### Returns `string` \| `null` The stable code token, or `null`. #### Example ```ts import { extractStableCode } from "@cosyte/cli"; extractStableCode({ code: "MALFORMED_JSON" }); // => "MALFORMED_JSON" extractStableCode(new Error("boom")); // => null ``` *** ### fileChunks() > **fileChunks**(`path`): `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> Open a file as a stream of byte **chunks**, with the same value-free failure mode as [readFileBytes](#readfilebytes): a missing, unreadable or non-file path is a `CLI_NO_INPUT` error, never a raw filesystem exception. #### Parameters ##### path `string` The file path to read. #### Returns `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> An async iterable of the file's chunks. #### Throws `CLI_NO_INPUT` (exit `66`) when the file cannot be opened or read. #### Example ```ts throws import { fileChunks } from "@cosyte/cli"; for await (const chunk of fileChunks("/no/such/file")) chunk.length; // throws CLI_NO_INPUT ``` *** ### fmtCommand() > **fmtCommand**(`args`, `deps`, `posture?`): `Promise`\<[`RunResult`](#runresult)\> Run the `fmt` command. #### Parameters ##### args `string`[] The arguments after the `fmt` subcommand token. ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)). ##### posture? [`PhiPosture`](#phiposture) = `VALUE_FREE` The resolved [PhiPosture](#phiposture) (governs only the opt-in unsafe excerpt on a parse-failure diagnostic). #### Returns `Promise`\<[`RunResult`](#runresult)\> A [RunResult](#runresult): the canonical re-serialization on `stdout` (exit `0`), a value-free warning-count note on `stderr` unless `--quiet`; an unparseable input is a data error (`65`) with no partial emit, an unreadable file `66`, a bad flag `2`. #### Throws Never [CliError](#clierror); may propagate a truly unexpected error for the dispatcher to map. #### Example ```ts import { fmtCommand } from "@cosyte/cli"; const deps = { readFile: async () => new TextEncoder().encode('{ "resourceType":"Patient" , "id":"x" }'), readStdin: async () => new Uint8Array(), }; (await fmtCommand(["patient.json"], deps)).exit; // => 0 ``` *** ### fmtFormat() > **fmtFormat**(`format`, `bytes`): `Promise`\<[`FmtResult`](#fmtresult)\> Canonically re-serialize one input of `format` via the wrapped library's spec-clean serializer. The CLI never re-canonicalizes on its own; the output is exactly the serializer's. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) A format for which `supportsOp(format, "fmt")` is true. ##### bytes `Uint8Array` The input bytes. #### Returns `Promise`\<[`FmtResult`](#fmtresult)\> The serialized text + a value-free warning count. #### Throws `CLI_PARSER_UNAVAILABLE` if the optional parser is absent; the wrapped parser's rejection propagates for the command's value-free failure boundary (no partial emit). #### Example ```ts import { fmtFormat } from "@cosyte/cli"; const bytes = new TextEncoder().encode('{ "resourceType":"Patient" , "id":"x" }'); (await fmtFormat("fhir", bytes)).output.startsWith("{"); // => true ``` *** ### formatDiagnostic() > **formatDiagnostic**(`err`): `string` Render a [CliError](#clierror) as a single value-free `stderr` line: `cosyte: : `. The message is already value-free by the [CliError](#clierror) contract; this only prefixes the tool name and the code so scripts and humans can branch on a stable token. #### Parameters ##### err [`CliError`](#clierror) The CLI error to render. #### Returns `string` The stderr line (no trailing newline). #### Example ```ts import { CliError, CLI_CODES, EXIT, formatDiagnostic } from "@cosyte/cli"; formatDiagnostic(new CliError(CLI_CODES.CLI_USAGE, EXIT.USAGE, "missing argument")); // => "cosyte: CLI_USAGE: missing argument" ``` *** ### formatHl7Position() > **formatHl7Position**(`pos`): `string` Render an HL7 warning position as a **value-free** locator string built only from its numeric indices: e.g. `seg[3].field[5].comp[1]`. No segment content, no field value, ever appears; only the structural coordinates the parser reported. #### Parameters ##### pos `Hl7PositionLike` The HL7 position (segment/field/repetition/component/subcomponent indices). #### Returns `string` A value-free locator, e.g. `"seg[3].field[5]"`. #### Example ```ts import { formatHl7Position } from "@cosyte/cli"; formatHl7Position({ segmentIndex: 3, fieldIndex: 5 }); // => "seg[3].field[5]" formatHl7Position({ segmentIndex: 0 }); // => "seg[0]" ``` *** ### formatsSupporting() > **formatsSupporting**(`op`): readonly [`CosyteFormat`](#cosyteformat)[] The formats that support `op`, as a sorted, value-free list: used to build the "does not support" diagnostic so the user is told which formats *do* support the operation they asked for. #### Parameters ##### op [`Op`](#op) The wrapping operation. #### Returns readonly [`CosyteFormat`](#cosyteformat)[] The supporting format names, sorted. #### Example ```ts import { formatsSupporting } from "@cosyte/cli"; formatsSupporting("fmt"); // => ["astm", "ccda", "fhir", "hl7", "ncpdp", "x12"] ``` *** ### helpText() > **helpText**(`limitBytes?`): `string` Build the value-free `--help` text. Names commands, flags, the input-size limit, and exit codes, never any input. The limit is **rendered from the constant the code enforces**, not typed in: the help output and the published command reference state one number because there is only one number to state. #### Parameters ##### limitBytes? `number` = `MAX_INPUT_BYTES` The input-size limit to state. Defaults to [MAX\_INPUT\_BYTES](#max_input_bytes). #### Returns `string` The full help text. #### Example ```ts import { helpText } from "@cosyte/cli"; helpText(1024).includes("1024 bytes"); // => true ``` *** ### inputTooLargeError() > **inputTooLargeError**(`limit?`): [`CliError`](#clierror) Build the value-free over-limit refusal: a stable `CLI_INPUT_TOO_LARGE` code, the **data-error** exit (`65`, never the internal-error `70`), and a message that names the limit and nothing else. The offending input's own size is deliberately not reported either: the limit is the actionable fact, and a size read off the input is a property of the input. #### Parameters ##### limit? `number` = `MAX_INPUT_BYTES` The limit that was exceeded, in bytes. Defaults to [MAX\_INPUT\_BYTES](#max_input_bytes). #### Returns [`CliError`](#clierror) The value-free [CliError](#clierror) to resolve the invocation with. #### Example ```ts import { inputTooLargeError } from "@cosyte/cli"; inputTooLargeError().exit; // => 65 inputTooLargeError().code; // => "CLI_INPUT_TOO_LARGE" ``` *** ### inspectCommand() > **inspectCommand**(`args`, `deps`, `posture?`): `Promise`\<[`RunResult`](#runresult)\> Run the `inspect` command. #### Parameters ##### args `string`[] The arguments after the `inspect` subcommand token. ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)). ##### posture? [`PhiPosture`](#phiposture) = `VALUE_FREE` The resolved [PhiPosture](#phiposture) (governs only the opt-in unsafe excerpt on a parse-failure diagnostic; the summary itself is always value-free). #### Returns `Promise`\<[`RunResult`](#runresult)\> A [RunResult](#runresult): the value-free structural summary on `stdout` (human, or JSON under `--json`), exit `0`; a parse failure is a data error (`65`), an unreadable file `66`, a bad flag `2`. #### Throws Never [CliError](#clierror); may propagate a truly unexpected error for the dispatcher to map. #### Example ```ts import { inspectCommand } from "@cosyte/cli"; const deps = { readFile: async () => new TextEncoder().encode('{"resourceType":"Patient"}'), readStdin: async () => new Uint8Array(), }; (await inspectCommand(["patient.json", "--json"], deps)).exit; // => 0 ``` *** ### inspectFormat() > **inspectFormat**(`format`, `bytes`): `Promise`\<[`InspectSummary`](#inspectsummary)\> Build the value-free structural summary of one input of `format`: counts and structural type codes only, never a field value. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) A format for which `supportsOp(format, "inspect")` is true. ##### bytes `Uint8Array` The input bytes. #### Returns `Promise`\<[`InspectSummary`](#inspectsummary)\> A [InspectSummary](#inspectsummary) variant for the format. #### Throws `CLI_PARSER_UNAVAILABLE` if the optional parser is absent; the wrapped parser's rejection propagates for the command's value-free failure boundary. #### Example ```ts import { inspectFormat } from "@cosyte/cli"; const bytes = new TextEncoder().encode('{"resourceType":"Patient"}'); (await inspectFormat("fhir", bytes)).format; // => "fhir" ``` *** ### isDeidCovered() > **isDeidCovered**(`format`): `format is DeidCoveredFormat` Whether `format` is one the delegate de-identifies and the CLI can serialize. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) The resolved format. #### Returns `format is DeidCoveredFormat` `true` iff `redact` produces output for it. #### Example ```ts import { isDeidCovered } from "@cosyte/cli"; isDeidCovered("x12"); // => true ``` *** ### loadOptional() > **loadOptional**\<`T`\>(`format`, `load`): `Promise`\<`T`\> Lazy-import an **optional** parser package, mapping an *absent* package to a value-free `CLI_PARSER_UNAVAILABLE` (exit `69`): the graceful-degradation path for the `optionalDependencies` breadth parsers. A genuine import that succeeds passes straight through; any error whose shape is "module not found" becomes the value-free CLI error. Other errors propagate unchanged. #### Type Parameters ##### T `T` The imported module's type. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) The format whose optional parser is being loaded (named in the diagnostic). ##### load () => `Promise`\<`T`\> A thunk performing the dynamic import. #### Returns `Promise`\<`T`\> The imported module. #### Throws `CLI_PARSER_UNAVAILABLE` (exit `69`) when the package is absent; any other error propagates unchanged. #### Example ```ts import { loadOptional } from "@cosyte/cli"; // a present parser resolves; an absent one throws CLI_PARSER_UNAVAILABLE await loadOptional("x12", () => import("@cosyte/x12")); ``` *** ### loadOptionalPackage() > **loadOptionalPackage**\<`T`\>(`detail`, `load`): `Promise`\<`T`\> The same graceful-degradation path as [loadOptional](#loadoptional), keyed on a **package name** rather than a [CosyteFormat](#cosyteformat), with the diagnostic supplied by the caller. Two of the packages the CLI lazy-loads are not wire-format parsers and so cannot go through [loadOptional](#loadoptional): `@cosyte/transform` (a conversion library, not one of the eight formats, so calling it a "parser" in the diagnostic would be wrong) and `@cosyte/fhir` (a format, but absent for a reason no reinstall fixes, which the message has to say). Both are reachable from a published install only if the user already has them, so both must degrade to a value-free `CLI_PARSER_UNAVAILABLE` (exit `69`) instead of surfacing a raw resolver error and a stack frame. #### Type Parameters ##### T `T` The imported module's type. #### Parameters ##### detail `string` The value-free diagnostic detail shown when the package is absent. ##### load () => `Promise`\<`T`\> A thunk performing the dynamic import. #### Returns `Promise`\<`T`\> The imported module. #### Throws `CLI_PARSER_UNAVAILABLE` (exit `69`) when the package is absent; any other error propagates unchanged. #### Example ```ts import { loadOptionalPackage } from "@cosyte/cli"; await loadOptionalPackage("the conversion library is not installed", () => import("@cosyte/transform"), ); ``` *** ### mapCodesCommand() > **mapCodesCommand**(`args`, `deps`): `Promise`\<[`RunResult`](#runresult)\> Run the `map-codes` command. #### Parameters ##### args `string`[] The arguments after the `map-codes` subcommand token. ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)); the positional argument (or `-`) is the BYO ConceptMap document. #### Returns `Promise`\<[`RunResult`](#runresult)\> A [RunResult](#runresult): the translation result on `stdout` (matched target coding(s) or the value-free unmapped signal), a value-free note on `stderr` (unless `--quiet`), and an exit code carrying the outcome: `0` mapped · `1` unmapped · `65` unloadable ConceptMap · `66` no input · `2` usage. #### Throws Never [CliError](#clierror); may propagate a truly unexpected error for the dispatcher to map to `CLI_INTERNAL`. #### Example ```ts import { mapCodesCommand } from "@cosyte/cli"; const cm = JSON.stringify({ resourceType: "ConceptMap", group: [ { source: "http://hl7.org/fhir/administrative-gender", target: "http://terminology.hl7.org/CodeSystem/v2-0001", element: [{ code: "male", target: [{ code: "M", equivalence: "equivalent" }] }], }, ], }); const deps = { readFile: async () => new TextEncoder().encode(cm), readStdin: async () => new Uint8Array(), }; const r = await mapCodesCommand( ["gender.json", "--system", "http://hl7.org/fhir/administrative-gender", "--code", "male"], deps, ); r.exit; // => 0 ``` *** ### mllpFrames() > **mllpFrames**(`chunks`): `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> De-frame a chunk stream into **MLLP frame payloads** (each an enclosed HL7 v2 message), yielded as each frame completes. The framing is `@cosyte/mllp`'s own streaming `FrameReader`, fed chunk by chunk; the CLI only tracks where the frame bytes fell so it can tell a **truncated** stream from a complete one. **Truncation is a data error, never a silent drop.** An unterminated trailing frame (a start-of-block byte after the last end-of-block byte) is delivered by no callback, so it would otherwise vanish with a green exit. It is detected at end of stream and raised, after the frames that did complete have already been yielded: their output stands, and the invocation still resolves to the data error. #### Parameters ##### chunks [`ByteChunks`](#bytechunks) The input chunk stream. #### Returns `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> A stream of enclosed HL7 payloads, in frame order. #### Throws `CLI_PARSER_UNAVAILABLE` (exit `69`) if `@cosyte/mllp` is absent; `CLI_PARSE_FAILED` (exit `65`) at end of stream on a truncated final frame. #### Example ```ts import { mllpFrames, oneChunk } from "@cosyte/cli"; const framed = new Uint8Array([0x0b, 0x4d, 0x53, 0x48, 0x1c, 0x0d]); const seen: number[] = []; for await (const p of mllpFrames(oneChunk(framed))) seen.push(p.length); seen.length; // => 1 ``` *** ### ndjsonRecords() > **ndjsonRecords**(`chunks`): `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> Split a chunk stream into **NDJSON records**: each non-empty, newline-terminated line, yielded as soon as its terminator arrives. A record split across two chunks is carried over; a final record with no trailing newline is yielded at end of stream; a blank line carries no record and is skipped. #### Parameters ##### chunks [`ByteChunks`](#bytechunks) The input chunk stream. #### Returns `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> A stream of record bytes, in input order. #### Example ```ts import { ndjsonRecords, oneChunk } from "@cosyte/cli"; const src = oneChunk(new TextEncoder().encode('{"a":1}\n\n{"b":2}\n')); const out: string[] = []; for await (const r of ndjsonRecords(src)) out.push(new TextDecoder().decode(r)); out.length; // => 2 ``` *** ### oneChunk() > **oneChunk**(`bytes`): `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> Wrap `bytes` as a one-chunk stream, so the streaming record path can also serve a caller that hands the CLI a whole buffer (an injected reader, or the programmatic API). The path is the same; only the granularity the input arrives at differs. #### Parameters ##### bytes `Uint8Array` The whole input. #### Returns `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> A stream yielding exactly one chunk (or none, when `bytes` is empty). #### Example ```ts import { oneChunk } from "@cosyte/cli"; let n = 0; for await (const c of oneChunk(new Uint8Array([1, 2, 3]))) n += c.length; n; // => 3 ``` *** ### parseCommand() > **parseCommand**(`args`, `deps`, `posture?`): `Promise`\<[`RunResult`](#runresult)\> Run the `parse` command. #### Parameters ##### args `string`[] The arguments after the `parse` subcommand token. ##### deps [`RunDeps`](#rundeps) Injected I/O ([RunDeps](#rundeps)). When it carries the chunk readers and the output sink, a multi-record input is read and emitted incrementally; when it does not, the same path runs over one chunk and the output comes back on `stdout` instead. ##### posture? [`PhiPosture`](#phiposture) = `VALUE_FREE` The resolved [PhiPosture](#phiposture). Defaults to [VALUE\_FREE](#value_free); under `--unsafe-show-values` a bounded excerpt of the offending input is appended to a `CLI_PARSE_FAILED` diagnostic (the single, opt-in value-echoing surface): single-record mode only. #### Returns `Promise`\<[`RunResult`](#runresult)\> A [RunResult](#runresult): the typed-JSON model (or NDJSON records) on `stdout`, a value-free note (or nothing) on `stderr`, and the resolved exit code. An input past the documented size limit is a value-free data error naming the limit. Never throws a [CliError](#clierror): it resolves it to a result; unexpected exceptions are caught by the dispatcher and mapped to `CLI_INTERNAL`. #### Throws Never [CliError](#clierror); may propagate a truly unexpected error for the dispatcher to map. #### Example ```ts import { parseCommand } from "@cosyte/cli"; const enc = new TextEncoder(); const deps = { readFile: async () => enc.encode('{"resourceType":"Patient","id":"x"}'), readStdin: async () => new Uint8Array(), }; (await parseCommand(["patient.json"], deps)).exit; // => 0 ``` *** ### parseFailureResult() > **parseFailureResult**(`format`, `bytes`, `posture`, `e`): [`RunResult`](#runresult) Build the value-free [RunResult](#runresult) for a **wrapped-parser rejection**: a `CLI_PARSE_FAILED` data error (exit `65`). The stderr line names the format and, when the thrown value carried one, a stable code token in parentheses; it **never** echoes the parser's message or the input bytes. Under `--unsafe-show-values` (and only then) a bounded excerpt of the offending input is appended via the single [unsafeInputSuffix](#unsafeinputsuffix) chokepoint. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) The format whose parser rejected the input. ##### bytes `Uint8Array` The offending input (only consulted for the opt-in unsafe excerpt). ##### posture [`PhiPosture`](#phiposture) The resolved [PhiPosture](#phiposture); the excerpt is appended only when it opts in. ##### e `unknown` The caught exception (its `code`, if a stable token, is surfaced: nothing else). #### Returns [`RunResult`](#runresult) A value-free `CLI_PARSE_FAILED` / exit-`65` [RunResult](#runresult). #### Example ```ts import { parseFailureResult, VALUE_FREE } from "@cosyte/cli"; const bytes = new TextEncoder().encode("not hl7"); parseFailureResult("hl7", bytes, VALUE_FREE, { code: "MISSING_MSH" }).exit; // => 65 ``` *** ### parseFormat() > **parseFormat**(`format`, `bytes`): `Promise`\<[`ParseResult`](#parseresult)\> Parse one record of `format` into the wrapped library's model + its value-free warnings. The model is emitted verbatim on the data channel; the CLI adds no parsing of its own. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) A format for which `supportsOp(format, "parse")` is true. ##### bytes `Uint8Array` The single record's bytes (already de-framed for MLLP: callers pass `"hl7"` there). #### Returns `Promise`\<[`ParseResult`](#parseresult)\> The parsed model + value-free warnings. #### Throws `CLI_PARSER_UNAVAILABLE` if the optional parser is absent; the wrapped parser's own rejection propagates for the command's value-free failure boundary to catch. #### Example ```ts import { parseFormat } from "@cosyte/cli"; const bytes = new TextEncoder().encode('{"resourceType":"Patient","id":"x"}'); (await parseFormat("fhir", bytes)).warnings.length; // => 0 ``` *** ### readFileBytes() > **readFileBytes**(`path`, `limit?`): `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> Read a file into bytes, mapping any read failure to a **value-free** `CLI_NO_INPUT` / [EXIT.NOINPUT](#noinput) error. The path is structural context (the user supplied it), so it may appear in the message; the file *contents* never do. A file whose size already exceeds `limit` is refused **before it is read**, with the value-free over-limit data error, so an oversized file is never allocated in the first place. The size is read from the **open descriptor** rather than from the path, so what is measured and what is read are the same file even if the path is replaced in between. #### Parameters ##### path `string` The file path to read. ##### limit? `number` = `MAX_INPUT_BYTES` The maximum number of bytes to accept. Defaults to [MAX\_INPUT\_BYTES](#max_input_bytes). #### Returns `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> The file bytes. #### Throws `CLI_NO_INPUT` (exit `66`) when the file is missing, a directory, or unreadable; `CLI_INPUT_TOO_LARGE` (exit `65`) when it is larger than `limit`. #### Example ```ts throws import { readFileBytes } from "@cosyte/cli"; await readFileBytes("/no/such/file"); // throws CliError CLI_NO_INPUT ``` *** ### readStreamBytes() > **readStreamBytes**(`stream`, `limit?`): `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> Drain a readable stream (e.g. `process.stdin`) into a single byte buffer, refusing an over-limit stream against the **running total**: the read is abandoned the moment the accumulated size passes `limit`, so the bytes past it are never allocated and no platform allocation ceiling is ever reached. #### Parameters ##### stream `Readable` The readable stream to drain. ##### limit? `number` = `MAX_INPUT_BYTES` The maximum number of bytes to accept. Defaults to [MAX\_INPUT\_BYTES](#max_input_bytes). #### Returns `Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\> The concatenated bytes. #### Throws `CLI_INPUT_TOO_LARGE` (exit `65`) as soon as the running total passes `limit`. #### Example ```ts import { Readable } from "node:stream"; import { readStreamBytes } from "@cosyte/cli"; const bytes = await readStreamBytes(Readable.from([Buffer.from("MSH|")])); bytes.length; // => 4 ``` *** ### redactCommand() > **redactCommand**(`args`, `deps`, `loadDelegate?`): `Promise`\<[`RunResult`](#runresult)\> Run the `redact` / `deid` command. #### Parameters ##### args `string`[] The arguments after the `redact`/`deid` subcommand token. ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)). ##### loadDelegate? () => `Promise`\<[`DeidDelegate`](#deiddelegate)\> The de-identification delegate loader; injectable so a test can drive the library-absent path without uninstalling it. #### Returns `Promise`\<[`RunResult`](#runresult)\> A [RunResult](#runresult). A clean pass over a covered format: the de-identified document on `stdout`, the delegate's attribution and value-free manifest on `stderr`, exit `0`. Otherwise empty `stdout` and a typed non-zero exit: `1` when the delegate could not handle an element, `69` when it has no adapter for the format or is absent, `65` when the format cannot ride a text stdout or the input did not parse, `66` for an unreadable file, `2` for a bad flag. #### Throws Never [CliError](#clierror); may propagate a truly unexpected error for the dispatcher to map. #### Example ```ts import { redactCommand } from "@cosyte/cli"; const deps = { readFile: async () => new TextEncoder().encode('{"resourceType":"Patient","id":"x"}'), readStdin: async () => new Uint8Array(), }; (await redactCommand(["patient.json"], deps)).exit; // => 0 ``` *** ### resolveInput() > **resolveInput**(`source`, `formatOverride`, `deps`, `op`): `Promise`\<[`InputResolution`](#inputresolution)\> Resolve and read the input, and resolve its format, for a file-consuming command. #### Parameters ##### source `string` \| `undefined` The positional `` argument (or `undefined` when it was omitted). ##### formatOverride `string` \| `undefined` The raw `--format` value, or `undefined` to autodetect by content. ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)). ##### op [`Op`](#op) \| `null` The wrapping operation the caller will run; the resolved format is confirmed to support it (else a value-free `CLI_FORMAT_UNSUPPORTED` naming the supporting formats). Pass `null` when the caller answers for capability itself: `redact` delegates to `@cosyte/deid`, whose per-format coverage is a different matrix with its own two distinct refusals, so applying the parser registry's matrix first would answer with the wrong code. #### Returns `Promise`\<[`InputResolution`](#inputresolution)\> `{ ok: true, input }` when the bytes read and the format resolved to a parser supporting `op`; else `{ ok: false, result }` with a value-free usage/no-input/data-error [RunResult](#runresult). #### Throws Propagates a **non-`CliError`** read failure unchanged, so the dispatcher maps it to `CLI_INTERNAL` (a `CliError` read failure, e.g. a missing file, is caught and returned). #### Example ```ts import { resolveInput } from "@cosyte/cli"; const deps = { readFile: async () => new TextEncoder().encode('{"resourceType":"Patient"}'), readStdin: async () => new Uint8Array(), }; const r = await resolveInput("patient.json", undefined, deps, "parse"); if (r.ok) r.input.format; // => "fhir" ``` *** ### resolveInputStream() > **resolveInputStream**(`source`, `formatOverride`, `deps`, `op`, `limit?`): `Promise`\<[`InputStreamResolution`](#inputstreamresolution)\> Resolve the input as a **stream of chunks** rather than one buffer, for a command that can act on a record before the whole input has arrived. It reads only as far as the detection window ([DETECT\_PREFIX\_BYTES](#detect_prefix_bytes)) before deciding the format, then hands back those bytes followed by the rest of the stream, unread. The size limit is applied to the running total from the first chunk, so an over-limit input is refused while it is still arriving and never assembled. When the caller's [RunDeps](#rundeps) carry no chunk readers, the whole-input readers are wrapped as a single chunk: the same code path, the same output, only the granularity differs. #### Parameters ##### source `string` \| `undefined` The positional `` argument (or `undefined` when it was omitted). ##### formatOverride `string` \| `undefined` The raw `--format` value, or `undefined` to autodetect by content. ##### deps [`RunDeps`](#rundeps) Injected readers ([RunDeps](#rundeps)); the chunk readers are used when present. ##### op [`Op`](#op) The wrapping operation the caller will run; the resolved format is confirmed to support it. ##### limit? `number` = `MAX_INPUT_BYTES` The maximum number of input bytes to accept. Defaults to [MAX\_INPUT\_BYTES](#max_input_bytes). #### Returns `Promise`\<[`InputStreamResolution`](#inputstreamresolution)\> `{ ok: true, input }` with the format and the chunk stream, else `{ ok: false, result }` carrying the value-free usage / no-input / data-error [RunResult](#runresult). #### Throws Propagates a **non-`CliError`** read failure unchanged, so the dispatcher maps it to `CLI_INTERNAL`. #### Example ```ts import { resolveInputStream } from "@cosyte/cli"; const deps = { readFile: async () => new TextEncoder().encode('{"resourceType":"Patient"}'), readStdin: async () => new Uint8Array(), }; const r = await resolveInputStream("patient.json", undefined, deps, "parse"); if (r.ok) r.input.format; // => "fhir" ``` *** ### run() > **run**(`argv`, `deps`): `Promise`\<[`RunResult`](#runresult)\> Run a full `cosyte` invocation as data. #### Parameters ##### argv `string`[] The arguments after the program name (i.e. `process.argv.slice(2)`). ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)). #### Returns `Promise`\<[`RunResult`](#runresult)\> The [RunResult](#runresult) to write to the process streams and exit with. #### Example ```ts import { run } from "@cosyte/cli"; const deps = { readFile: async () => new Uint8Array(), readStdin: async () => new Uint8Array() }; const { exit } = await run(["--version"], deps); exit; // => 0 ``` *** ### streamChunks() > **streamChunks**(`stream`): `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> Open a readable stream (e.g. `process.stdin`) as a stream of byte **chunks**, in arrival order. Unlike [readStreamBytes](#readstreambytes) this never concatenates: the consumer decides what to keep, which is what lets a multi-record command emit a record before the rest of the input has been read. #### Parameters ##### stream `Readable` The readable stream to iterate. #### Returns `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> An async iterable of the stream's chunks, string chunks encoded as UTF-8. #### Example ```ts import { Readable } from "node:stream"; import { streamChunks } from "@cosyte/cli"; const seen: number[] = []; for await (const c of streamChunks(Readable.from([Buffer.from("MS"), Buffer.from("H|")]))) { seen.push(c.length); } seen.length; // => 2 ``` *** ### supportsOp() > **supportsOp**(`format`, `op`): `boolean` True iff `format`'s wrapped parser supports `op` in this build (see [OP\_SUPPORT](#op_support)). #### Parameters ##### format [`CosyteFormat`](#cosyteformat) The resolved format. ##### op [`Op`](#op) The wrapping operation. #### Returns `boolean` Whether the (format, op) pair is wired. #### Example ```ts import { supportsOp } from "@cosyte/cli"; supportsOp("x12", "fmt"); // => true supportsOp("dicom", "parse"); // => false ``` *** ### toCliError() > **toCliError**(`err`): [`CliError`](#clierror) Coerce an unknown thrown value into a [CliError](#clierror). A [CliError](#clierror) passes through; anything else becomes a `CLI_INTERNAL` / [EXIT.SOFTWARE](#software) error whose message is a **fixed, value-free string**: the original message is deliberately discarded so a parser exception that embedded input bytes can never reach stderr. #### Parameters ##### err `unknown` The caught value. #### Returns [`CliError`](#clierror) A [CliError](#clierror) safe to render to stderr. #### Example ```ts import { toCliError } from "@cosyte/cli"; toCliError(new Error("boom")).code; // => "CLI_INTERNAL" ``` *** ### truncatedMllpError() > **truncatedMllpError**(): [`CliError`](#clierror) The value-free data error for a **truncated MLLP stream**: a frame opened and never closed, which the streaming reader buffers and delivers to no callback. Built here so the whole-buffer de-framer above and the chunk-by-chunk one in `core/records.ts` refuse the same input with the same code and the same words. #### Returns [`CliError`](#clierror) A `CLI_PARSE_FAILED` (exit `65`) [CliError](#clierror). #### Example ```ts import { truncatedMllpError } from "@cosyte/cli"; truncatedMllpError().exit; // => 65 ``` *** ### unsafeInputSuffix() > **unsafeInputSuffix**(`bytes`, `posture`): `string` Build the **only** value-bearing addition the CLI ever appends to a secondary (stderr) surface: a bounded, single-line excerpt of the offending input, shown **iff** `--unsafe-show-values` is set. Under the default value-free posture it returns the empty string, so a diagnostic stays value-free unless the user explicitly opted in. This is the single chokepoint the PHI-leak matrix pins. #### Parameters ##### bytes `Uint8Array` The input the CLI is operating on (only the leading [UNSAFE\_EXCERPT\_MAX](#unsafe_excerpt_max) bytes are considered). ##### posture [`PhiPosture`](#phiposture) The resolved [PhiPosture](#phiposture). #### Returns `string` `""` under the value-free default; otherwise a ` [unsafe-show-values] …` suffix carrying a bounded, newline-flattened prefix of `bytes`. #### Example ```ts import { unsafeInputSuffix, VALUE_FREE, SHOW_VALUES } from "@cosyte/cli"; const bytes = new TextEncoder().encode("bad message"); unsafeInputSuffix(bytes, VALUE_FREE); // => "" unsafeInputSuffix(bytes, SHOW_VALUES); // => " [unsafe-show-values] offending input …: bad message" ``` *** ### validateCommand() > **validateCommand**(`args`, `deps`, `posture?`): `Promise`\<[`RunResult`](#runresult)\> Run the `validate` command. #### Parameters ##### args `string`[] The arguments after the `validate` subcommand token. ##### deps [`RunDeps`](#rundeps) Injected input readers ([RunDeps](#rundeps)). ##### posture? [`PhiPosture`](#phiposture) = `VALUE_FREE` The resolved [PhiPosture](#phiposture) (governs only the opt-in unsafe excerpt on a parse-failure diagnostic; a validation verdict is always value-free). #### Returns `Promise`\<[`RunResult`](#runresult)\> A [RunResult](#runresult) whose **exit code carries the verdict** (`0` valid / `1` invalid / `65` unparseable / `66` no input / `2` usage). Value-free findings on `stderr`, or value-free JSON on stdout under `--json`. #### Throws Never [CliError](#clierror); may propagate a truly unexpected error for the dispatcher to map. #### Example ```ts import { validateCommand } from "@cosyte/cli"; const deps = { readFile: async () => new TextEncoder().encode('{"resourceType":"Patient","gender":"male"}'), readStdin: async () => new Uint8Array(), }; (await validateCommand(["patient.json"], deps)).exit; // => 0 ``` *** ### validateFormat() > **validateFormat**(`format`, `bytes`): `Promise`\<[`Verdict`](#verdict)\> Run the format's validation surface and return a value-free [Verdict](#verdict). For the Postel's-Law lenient parsers, a **parseable** input is valid: recovered deviations are non-fatal warnings the library surfaces, never a stricter verdict the CLI invents. FHIR additionally runs `validateResource` and is invalid on any error/fatal-severity issue. #### Parameters ##### format [`CosyteFormat`](#cosyteformat) A format for which `supportsOp(format, "validate")` is true. ##### bytes `Uint8Array` The input bytes. #### Returns `Promise`\<[`Verdict`](#verdict)\> The value-free verdict (findings + validity). #### Throws `CLI_PARSER_UNAVAILABLE` if the optional parser is absent; an unparseable input propagates as the wrapped parser's throw for the command to map to a data error (`65`). #### Example ```ts import { validateFormat } from "@cosyte/cli"; const bytes = new TextEncoder().encode('{"resourceType":"Patient","gender":"male"}'); (await validateFormat("fhir", bytes)).valid; // => true ``` *** ### valueFreeLocator() > **valueFreeLocator**(`pos`): `string` Render an arbitrary parser **position** object as a value-free locator string, keeping **only** number-valued own properties (the parsers' positions are index-only): e.g. `{ segmentIndex: 3, elementIndex: 2 }` → `segmentIndex[3].elementIndex[2]`. A non-number field (which a position should never carry) is dropped, so a stray value can never reach a diagnostic. Falls back to `"?"` when no numeric index is present. #### Parameters ##### pos `unknown` A parser position object (index-only by the parser's contract). #### Returns `string` A value-free locator built solely from numeric indices. #### Example ```ts import { valueFreeLocator } from "@cosyte/cli"; valueFreeLocator({ segmentIndex: 3, elementIndex: 2 }); // => "segmentIndex[3].elementIndex[2]" valueFreeLocator({}); // => "?" ``` *** ### withinLimit() > **withinLimit**(`chunks`, `limit?`): `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> Pass chunks through while counting them against the CLI's own size limit, and **refuse the moment the running total passes it**: the chunk that crosses the limit is never yielded, the source is never pulled again, and nothing downstream ever allocates memory proportional to an oversized input. This is why the refusal a caller sees is the CLI's own data error rather than a platform allocation failure reported as an internal error. #### Parameters ##### chunks [`ByteChunks`](#bytechunks) The input chunk stream. ##### limit? `number` = `MAX_INPUT_BYTES` The maximum number of bytes to accept. Defaults to [MAX\_INPUT\_BYTES](#max_input_bytes). #### Returns `AsyncGenerator`\<`Uint8Array`\<`ArrayBufferLike`\>\> The same chunks, up to the limit. #### Throws `CLI_INPUT_TOO_LARGE` (exit `65`) as soon as the running total passes `limit`. #### Example ```ts import { oneChunk, withinLimit } from "@cosyte/cli"; const src = withinLimit(oneChunk(new Uint8Array(10)), 4); await src.next().catch((e: unknown) => (e as { code: string }).code); // => "CLI_INPUT_TOO_LARGE" ``` --- URL: https://docs.cosyte.com/cli/troubleshooting Markdown: https://docs.cosyte.com/cli/troubleshooting.md # Troubleshooting Common symptoms with `cosyte parse`, and how to read what the CLI is telling you. Every diagnostic is a value-free line on stderr, `cosyte: : `, a stable code plus positional context, never a field value. ## `CLI_FORMAT_UNDETECTED` (exit 65) No format signature matched the content, and the CLI **will not guess**. Re-run with an explicit format: ```bash cosyte parse --format hl7 message.txt ``` Detection sniffs the leading bytes: HL7 needs an `MSH|…` start, FHIR needs a JSON object with a `resourceType`. A file with a misleading extension is fine: content is what matters. ## `CLI_FORMAT_AMBIGUOUS` (exit 65) More than one signature matched. Disambiguate with `--format`. (With only HL7 and FHIR wired this cannot yet occur. The branch exists so a future overlapping signature is a _detected_ ambiguity, not a silent mis-route.) ## `CLI_FORMAT_UNSUPPORTED` (exit 65) The format was recognised, but its parser does not support the operation you asked for. Support is **per (format, operation)**: `x12`/`astm`/`ncpdp` support parse/inspect/fmt/validate; `ccda` supports inspect/fmt/validate (parse is deferred: XML is the canonical `fmt` form); `dicom` supports inspect/validate (parse/fmt deferred. The model is binary); `mllp` supports parse/inspect. The message names which formats _do_ support the operation. The command is never faked to a success it cannot deliver. ## `CLI_PARSER_UNAVAILABLE` (exit 69) The library for a recognised format is not installed. The CLI degrades to this value-free signal rather than crashing, and it never falls back to a guess. Two different causes: - **The six breadth parsers** (`dicom`/`x12`/`ccda`/`ncpdp`/`astm`/`mllp`) are `optionalDependencies` that resolve, so a default install has all six; you see this only if one was removed. Install the named `@cosyte/` package to get it back. Note that `--omit=optional` is **not** a supported way to slim the install: it also removes `@modelcontextprotocol/sdk`, and the `cosyte` command then fails to start at all rather than reaching this diagnostic. Known defect, tracked separately. - **FHIR** comes from `@cosyte/fhir`, the peer dependency of `@cosyte/transform`, so a default install has it; you see this on FHIR input only if your package manager did not install that peer or it was removed. `convert` reports the same when `@cosyte/fhir` or `@cosyte/transform` is missing. Install the named package to get it back. ## `CLI_NO_INPUT` (exit 66) The file does not exist or is unreadable. Check the path; use `-` to read stdin instead of a file. ## `CLI_PARSE_FAILED` (exit 65) The wrapped parser rejected the input as unrecoverable. The stderr line carries the format and a stable code token only, **not** the offending bytes. To see a bounded excerpt of the input while debugging locally, add the loud, opt-in `--unsafe-show-values` (below); by default the CLI will not echo it for you. ## `CLI_NOT_IMPLEMENTED` (exit 69) The capability the command needs is not wired for this input, so the command is **unavailable**, never a fake success. From `redact`/`deid` it means `@cosyte/deid` ships no de-identification adapter for the format you gave it: `astm`, `mllp` and `ncpdp`. The covered formats are `ccda`, `fhir`, `hl7`, `x12`. The CLI will not fall back to a partial scrub that looks de-identified while leaving PHI behind, so nothing is emitted. `validate --profile` reports the same code, for the same reason. ## `CLI_DEID_INCOMPLETE` (exit 1) `redact`/`deid` ran, and `@cosyte/deid` reported at least one locus it could **not** handle (its fail-closed `blocked` disposition). The run is therefore not a de-identified copy and is not offered as one: **no output is emitted**, and the stderr manifest names each blocked path and its stable code so you can see exactly where the gap is. This is an operation-level failure, like an invalid `validate` verdict: the tool worked, the input could not be fully handled. It is never exit `70`, which means a bug. The most common cause is an HL7 visit number. Under the library's default policy an encounter (visit) number such as `PV1-19`, and a placer or filler order number (`ORC-2`/`ORC-3`, `OBR-2`/`OBR-3`), is blocked as the catch-all identifier category, so a message carrying one is refused. The manifest line for it reads like this (a structural path and codes, never the value): ```text cosyte: redact: OTHER_UNIQUE_ID block PV1-19[0] x1 blocked DEID_LOCUS_BLOCKED ``` The CLI passes the library no policy of its own and has no flag to change that, so there is no setting that turns the refusal into output. Medical record, account and member numbers are not a cause: the default policy removes them (`DEID_CATEGORY_REMOVED`) and the run continues. ## `CLI_PARSER_UNAVAILABLE` from `redact` (exit 69) `@cosyte/deid` is an **optional dependency**: an install without it degrades rather than failing. The command decides this **before reading your input**, so a copy that cannot de-identify never touches the bytes it cannot strip. Install `@cosyte/deid` (and the parser for your format) to enable the command. ## `CLI_USAGE` (exit 2) An unknown flag or command, or a missing `` argument. Run `cosyte --help`. ## Is the output safe to share? **stdout is the data channel**: the parsed `model` it prints is your real data and may contain PHI; treat it as you would the source message. **stderr is value-free**: safe to paste into a bug report, **unless** you ran with `--unsafe-show-values` (below), which deliberately puts a bounded input excerpt into a failure diagnostic. The CLI never writes a temp file and never logs to a file. ## `--unsafe-show-values` (the one exception to value-free stderr) By default every diagnostic is value-free. When you need to see the bytes a parser rejected, add the loud, opt-in `--unsafe-show-values`: it appends a bounded excerpt of the offending input to a `CLI_PARSE_FAILED` line. It is **PHI-exposing**; do not use it on stderr you intend to share. It is the only setting under which a value reaches a secondary surface, and it affects failure diagnostics only: a successful parse still keeps values on stdout alone. ## Known limitations - All eight formats are wired, but **per (format, operation)**: `x12`/`astm`/`ncpdp` support parse/inspect/fmt/validate; `ccda` supports inspect/fmt/validate (no `parse`: XML is the canonical `fmt` form); `dicom` supports inspect/validate (no `parse`/`fmt`: binary model); `mllp` supports parse/inspect. A deferred cell is a value-free `CLI_FORMAT_UNSUPPORTED`, never a fake. - Streaming is **multi-record NDJSON**: MLLP de-frames to one record per frame, and `--ndjson` treats each non-empty line as a record (FHIR bulk data). A failed record is isolated as a value-free `{ record, error }` line; any failure makes the overall exit `65`. - `convert` reads **HL7 v2** and writes **FHIR R4** only (`--to fhir`); its coverage is bounded by `@cosyte/transform` (the IG-mapped ADT/ORU/order/… message families). A non-HL7 source is a data error (`65`), never a fake conversion. - `map-codes` translates a **single** source coding through a **bring-your-own** ConceptMap: the CLI ships no terminology content and does not scan a message for codes (that would re-implement the parser/transform layer). An unmapped code is a value-free signal + exit `1`, never a fabricated target. - `validate --profile` is reserved but gated: the CLI bundles no profiles yet, so it reports an honest `CLI_NOT_IMPLEMENTED` (exit `69`) rather than fake a profile verdict. - `redact`/`deid` de-identifies `ccda`, `fhir`, `hl7`, `x12` by delegating to `@cosyte/deid`, and refuses everything else rather than approximating it: `astm`/`mllp`/`ncpdp` are `CLI_NOT_IMPLEMENTED` (`69`), `dicom` is `CLI_FORMAT_UNSUPPORTED` (`65`, its de-identified form is binary and this stdout is text), and a locus the library could not handle is `CLI_DEID_INCOMPLETE` (`1`) with no output: under the library's default policy that includes any HL7 message carrying a visit number (`PV1-19`) or an order number. Its stderr manifest is the library's own; the CLI asserts no standard. - `redact` does **not** honour `--unsafe-show-values`. An excerpt of the input you asked to have stripped is exactly the leak that command exists to prevent, so its diagnostics stay value-free under every flag. The **API Reference** always reflects exactly what this release ships. --- URL: https://docs.cosyte.com/cli/limitations Markdown: https://docs.cosyte.com/cli/limitations.md # What `@cosyte/cli` does, and does not do The `cosyte` CLI touches **real files a developer points at**: the moment Cosyte code meets un-synthetic PHI on a real disk. So this page is deliberately blunt about the promise and its edges. Read it before you rely on the tool: the **API Reference** and `cosyte --help` are always the exact truth of what a given release ships; this page is the honest shape of the whole. ## The promise (narrow, on purpose) `@cosyte/cli` is a **thin, PHI-safe, correct-exit-code terminal-and-agent front door over libraries that already own correctness**, and **never a mis-routed parse, a green exit on an invalid message, or a leaked value in a diagnostic.** - **It wraps; it does not re-implement.** Every wire-format guarantee is the wrapped `@cosyte/*` library's, graded by _its_ own conformance gate. The CLI owns only routing, I/O, output shaping, the exit-code contract, and the value-free PHI posture. A bug in a parsed _value_ is the library's; a mis-route, a wrong exit code, or a leaked value is the CLI's. - **Fail-safe routing.** Autodetection sniffs content, not the file extension. A confident single match parses; an ambiguous or unrecognised input is a typed data error (exit `65`) asking for `--format`, **never a guessed parser** and never a confident wrong parse. - **The exit code is a designed surface.** `0` success / valid · `1` invalid verdict · `2` usage · `65` data error · `66` no input · `69` unavailable · `70` internal. The load-bearing rule: the CLI **never prints a reassuring line and exits `0` on input it could not handle.** A `validate` failure is exit `1`, never `0`. That is the property the CI use case rests on. - **Value-free by default.** `parse`/`convert`/`fmt` emit the requested data to **stdout** (the sink you chose: that is the tool doing its job). **Every other surface** (stderr, warnings, errors, `inspect`'s summary) carries only positional context (a segment/field index, a code, a file path), **never** a name, DOB, MRN, or result value. `--unsafe-show-values` is the single, loud, opt-in door to a value in a failure diagnostic. ## What it does **not** do These are **non-goals**, not missing features: named so nothing over-trusts the tool. - **Not an integration engine.** No channels, listeners, routing, scheduling, or running state: that is **`pathways`**. The CLI runs one operation on one input and exits. (The MCP server is a long-lived stdio subprocess, but it is **stateless per call**: a request/response tool host, not an integration.) - **No GUI and no web playground.** Terminal + local stdio MCP only. An online inspector/validator/de-id is a separate, later, differently-stacked concern with its own hosted-PHI threat model. - **No data storage, no persistence, no PHI cache.** The CLI reads the file you point at, operates in memory, writes the output you asked for, and exits. It **never writes a temp file** and **never logs to a file**. No telemetry, no crash-report upload. - **No hosted network service.** The MCP server is a **local subprocess over stdio**, implicitly trusted by the user who launched it, not a remote HTTP endpoint accepting untrusted PHI. A remote/Streamable-HTTP MCP deployment is a future, separately-security-reviewed concern. - **No capability its ground layer lacks, and never a faked one.** Where a wrapped library is not yet built, the command is **unavailable** (a distinct, value-free exit `69`), never stubbed to a fake success: - **`redact` / `deid`** delegates every de-identification decision to `@cosyte/deid` and covers the formats that library covers and this CLI can serialize: `ccda`, `fhir`, `hl7`, `x12`. `astm`, `mllp` and `ncpdp` have no adapter there (`CLI_NOT_IMPLEMENTED`, exit `69`); `dicom` is covered there but its de-identified form is a binary stream this text stdout cannot carry (`CLI_FORMAT_UNSUPPORTED`, exit `65`). If the library reports a locus it could not handle, the run exits `1` with **no output at all**. Under the library's default policy that is every HL7 message carrying a visit number (`PV1-19`) or a placer or filler order number: those are blocked (`DEID_LOCUS_BLOCKED`), and the CLI offers no setting that turns a block into a removal. Medical record, account and member numbers are removed (`DEID_CATEGORY_REMOVED`), not replaced by a surrogate. The CLI ships **no** built-in partial scrub that could give a false sense of safety, and asserts no de-identification standard of its own: it reports the library's own published label and version. - **`validate --profile`** is reserved but gated (`CLI_NOT_IMPLEMENTED`) until the CLI can load a profile; **no profiles are bundled**. - **`convert`** covers **HL7 v2 → FHIR R4** only (via `@cosyte/transform`); **`map-codes`** requires a **bring-your-own** FHIR ConceptMap (via `@cosyte/terminology`), no terminology content is bundled. - **Per-(format, operation) support is honest, not uniform.** All eight Cosyte formats are wired, but a parser only advertises the operations it can actually back. An unsupported (format, op) is a value-free `CLI_FORMAT_UNSUPPORTED`, never a fake: | Format | parse | inspect | fmt | validate | | ---------------------- | ----------------------------------------- | ------- | ------------- | ------------- | | `hl7`, `fhir` | ✓ | ✓ | ✓ | ✓ | | `x12`, `astm`, `ncpdp` | ✓ | ✓ | ✓ | ✓ | | `ccda` | not supported (XML is the canonical form) | ✓ | ✓ (XML) | ✓ | | `dicom` | not supported (binary model) | ✓ | not supported | ✓ | | `mllp` | ✓ (de-framed to HL7) | ✓ | not supported | not supported | **The matrix says what the CLI implements. It is not a promise about what your install can reach**: - **Each cell needs its library installed.** The six breadth formats and `@cosyte/transform` are optional dependencies, and `@cosyte/fhir` is the peer dependency of `@cosyte/transform`, so a default install has all of them. A library that is still missing (a peer your package manager did not install, or a package removed afterwards) turns its cells into a value-free `CLI_PARSER_UNAVAILABLE` with exit `69`, and **`convert`** reports the same when `@cosyte/fhir` or `@cosyte/transform` is missing. The commands do not guess, and they do not report your input as bad. - **Do not install with `--omit=optional`.** The install succeeds, but the `cosyte` command then does not run at all: it fails on a missing `@modelcontextprotocol/sdk` before reaching any command, even `--version`. That is a known defect and is tracked separately; it is recorded here rather than left for you to hit. - **No clinical interpretation, no unit conversion, no terminology content.** The CLI inherits these non-goals from the libraries it wraps. It never decides criticality, rescales a magnitude, or bundles SNOMED/LOINC/CPT. ## The agent surface (MCP) The `cosyte-mcp` server exposes `parse`/`validate`/`inspect`/`convert` as agent-callable tools over the **same core** the terminal drives, so `cosyte parse` and the MCP `parse` tool agree by construction. It runs **value-free with no `--unsafe-show-values` door**: a tool _result_ carries the requested data, a tool _error_ carries only the value-free diagnostic, and a parsed-but-invalid `validate` verdict is a **successful** call reporting the verdict. `redact` and `map-codes` are deliberately **not** exposed as tools yet. ## PHI handling Fixtures are **synthetic-only** (`# synthetic` enforced); diagnostics carry **positional context only, never a value**; and a format-specific PHI scanner gates every change, including the golden stdout/stderr snapshots (the highest-risk artifact). The one deliberate value channel is **stdout** on `parse`/`convert`/`fmt`: the data you explicitly asked for, sent to the sink you chose. Never pipe that channel somewhere you would not put PHI. --- # @cosyte/deid > De-identification toolkit for healthcare data (HIPAA Safe Harbor). These are the 17 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/deid/intro Markdown: https://docs.cosyte.com/deid/intro.md # @cosyte/deid Apply a HIPAA-grounded **de-identification policy** to a healthcare document's structurally-located model and get back a transformed model plus a **value-free manifest** of everything acted on, without reading 45 CFR §164.514 or hand-writing a scrubber. `@cosyte/deid` is a consumer-tier library, **not a parser**. It borrows the Cosyte parser archetype's disciplines (typed diagnostics, immutable output, a policy/profile system) but **inverts the parser's reflex**: where a parser is liberal on input (Postel's Law), a de-identifier is conservative: an unrecognized structure or an un-locatable identifier is **blocked**, never passed through as safe. > **The honesty line that governs the whole library.** Results are **"Safe-Harbor-transformed per the > configured policy"**, never "de-identified" and never "HIPAA-compliant". Safe Harbor is implemented > mechanically; the §164.514(b)(2)(ii) actual-knowledge condition is the consumer's; Expert > Determination (§164.514(b)(1)) is *supported*, never *rendered* or certified. The > certification is always the consumer's. > **Status:** `0.1`, published to npm. Below 1.0, a breaking change raises the minor version and > the changelog says what broke. This release ships the **format-agnostic core** (policy engine, > five transforms, 18-category Safe Harbor model, fail-closed rule, value-free manifest) **plus six > format bindings**: HL7 v2 (`@cosyte/deid/hl7`), C-CDA (`@cosyte/deid/ccda`), > FHIR R4 (`@cosyte/deid/fhir`), X12 EDI (`@cosyte/deid/x12`), NCPDP Telecom (`@cosyte/deid/ncpdp`) and > DICOM (`@cosyte/deid/dicom`). NCPDP SCRIPT is **not** supported, and an entry point handed one > refuses it outright rather than returning a partial pass. ## Install ```bash npm install @cosyte/deid ``` ## The 30-second version ```ts import { deidentify, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const { document, manifest } = deidentify( { loci: [{ path: "PID-19", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.SSN, value: ssn }] }, {}, ); // document.loci[0].value === null (removed); manifest records the category + locus, never the value. ``` ## Next - [Quickstart](https://docs.cosyte.com/deid/quickstart.md): de-identify a model and read the manifest. - [De-identifying HL7 v2](https://docs.cosyte.com/deid/guides-hl7.md): the first end-to-end format adapter (`@cosyte/deid/hl7`). - [Core Concepts](https://docs.cosyte.com/deid/concepts-archetype.md): the policy engine, the transforms, and fail-closed. --- URL: https://docs.cosyte.com/deid/installation Markdown: https://docs.cosyte.com/deid/installation.md # Installation `@cosyte/deid` is a TypeScript library for Node.js with **zero third-party runtime dependencies** (every cryptographic primitive comes from Node's built-in `node:crypto`). It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration. > **Status:** `0.1`, published to npm. The command below installs the current release. > `npm view @cosyte/deid version` is the authority on which one that is. ## Prerequisites - **Node.js >= 22** (the whole `@cosyte/*` suite targets ES2023 / Node 22+). - A package manager: `pnpm`, `npm`, or `yarn`. ## Install ```bash npm install @cosyte/deid ``` ## Smoke test Confirm the package resolves and its output label is the honest one: ```ts import { OUTPUT_LABEL } from "@cosyte/deid"; console.log(OUTPUT_LABEL); // "Safe-Harbor-transformed per the configured policy" ``` Head to the [Quickstart](https://docs.cosyte.com/deid/quickstart.md) to de-identify a model. --- URL: https://docs.cosyte.com/deid/quickstart Markdown: https://docs.cosyte.com/deid/quickstart.md # Quickstart De-identify a structurally-located model under a policy and read the value-free manifest. In `@cosyte/deid`, an unrecognized structure or an un-locatable identifier is **blocked**, never passed through as safe. The result is **"Safe-Harbor-transformed per the configured policy"**, never "de-identified". ## De-identify a model The core operates on a **generic locus model**: a flat list of structurally-located candidate values. The per-format adapters for HL7 v2, C-CDA, FHIR, X12, NCPDP and DICOM live behind their own subpath exports. ```ts runnable import { deidentify, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const { document, manifest } = deidentify( { loci: [ { path: "PID-5", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.NAMES, value: "SENTINEL_NAME" }, { path: "PID-7", kind: "date", category: SAFE_HARBOR_CATEGORIES.DATES, value: "1985-07-02" }, ], }, {}, ); document.loci[0].value; // => null document.loci[1].value; // => "1985" manifest[0].disposition; // => "removed" ``` Each **manifest** entry records the category acted on, the transform applied, the **locus** (a path, never a value), a count, a disposition, a stable code, and a boolean `reidentificationCode` that is `true` only where the pass emitted a **keyed surrogate**: the auditable record of *what* was acted on, never *what the value was*. ## Keyed transforms Pseudonymization and keyed hashing use a **keyed HMAC**: the key is the consumer's and never leaves the process. The **built-in Safe Harbor policy uses no keyed transform at all** - medical record, health plan beneficiary and account numbers are **removed** - so it needs no key. A keyed surrogate is derived from the individual's own value, which is why it belongs to a preset that does not claim Safe Harbor, such as `LIMITED_DATA_SET_PROFILE`. Supply the key through a context: ```ts import { deidentify, createDeidContext, profileOptions, LIMITED_DATA_SET_PROFILE } from "@cosyte/deid"; const context = createDeidContext({ key: process.env.DEID_KEY!, patientId: "patient-1" }); const result = deidentify(model, profileOptions(LIMITED_DATA_SET_PROFILE, context)); // Under THAT preset the MRN becomes a consistent, non-reversible surrogate; the key is never // emitted, and the locus is flagged `reidentificationCode` so the support report inventories it. ``` > **About runnable examples.** The first block above is tagged ```` ```ts runnable ````: the docs > build extracts it, runs it against the package, and asserts the `// =>` result, so a documented > example can never silently drift from the code. ## Next - [Core Concepts](https://docs.cosyte.com/deid/concepts-archetype.md): the policy engine, the transforms, and fail-closed. - **API Reference**: every export, generated from source. --- URL: https://docs.cosyte.com/deid/concepts-archetype Markdown: https://docs.cosyte.com/deid/concepts-archetype.md # Core Concepts `@cosyte/deid` is built from five pieces: the **policy engine**, the **five transforms**, the **18-category Safe Harbor model**, the **fail-closed rule**, and the **value-free manifest**. ## Fail closed (the inverted reflex) A parser is liberal in what it accepts. A de-identifier is the opposite: on each of these ambiguities (an unrecognized structure, an un-locatable identifier, a field it cannot classify, a free-text blob, a date/ZIP it cannot generalize), it **blocks** the value (withholds it), and records the decision. It never passes a value through as "probably safe". Liberality here leaks patients. The mirror guard is **over-scrub**: a locus marked `clinical` (a lab value, a dose, a code, a status) is **retained untouched**. The engine never degenerates into a blanket-blanking "safe but useless" scrubber. ## The five transforms - **redact**: remove the value (the fail-safe floor; the default for SSN, phone, email, URL, IP, …). - **generalize**: reduce precision to **date → year**, **ZIP → initial 3 digits or `000`** (the cited ≤20,000-population rule), **age → `90+`** for ages over 89. - **date-shift**: shift every date for a patient by a single deterministic per-patient offset, preserving intervals. An **Expert-Determination** technique (a shifted real date is still a date), so it is **not** used under the Safe Harbor policy: the offset never leaks. - **pseudonymize**: replace an identifier (MRN, beneficiary, account) with a consistent **keyed HMAC-SHA-256** surrogate so records still link, without being reversible without the key. Like date-shift, an **Expert-Determination** technique and **not** used under the Safe Harbor policy: a surrogate derived from the individual's own value is not a §164.514(c)(1) code, so the Safe Harbor default **removes** those three categories instead. It is available under a preset that does not claim Safe Harbor, such as `LIMITED_DATA_SET_PROFILE`. - **hash**: a keyed one-way digest. An **unsalted** hash of an identifier is re-identifiable, so the library keys it; the unkeyed path is off by default and documented non-conforming to §164.514(c). It is derived output too, so it may not wear the Safe Harbor label either. A policy that carries the `safe-harbor` label and assigns a category a transform whose output is **derived from that category's own value** is **refused** with a typed `DEID_POLICY_INVALID` fatal naming the category and the transform. `generalize` is permitted only on **(B)** geography and **(C)** dates and ages over 89, the two sub-paragraphs that state a permitted coarsening. A policy that does not claim the label keeps its keyed surrogate: the library never silently substitutes a stronger transform, and never rewrites a caller's policy name. ## The 18 Safe Harbor categories The `SAFE_HARBOR_CATEGORIES` registry models §164.514(b)(2)(i)(A) to (R), including the open-ended catch-all **(R)** ("any other unique identifying number, characteristic, or code"), which is exactly why fail-closed is not optional: a closed allow-list of 17 concrete types can never satisfy (R). ## The value-free manifest Every `DeidManifestEntry` records the **category** acted on, the **transform** applied, the **locus** (a path: segment/field index, XPath, FHIRPath, DICOM tag), a **count**, a **disposition**, a stable **code**, and a boolean **`reidentificationCode`**. That last field is `true` exactly where the pass emitted a **keyed surrogate** (`pseudonymize`, `hash` or `date-shift`) and `false` everywhere else: it is a flag, never a key or an offset, and it is what the support report's keyed-surrogate residual inventory is built from. It **never** records the value that was removed, generalized, or pseudonymized: a manifest that logged a value would be a PHI leak in the audit trail. The date-shift offset and the HMAC key never appear anywhere. The **locus** is the one field a per-format adapter builds out of the document, by naming the position with the identifier that sits there (a segment id, an element name, a tag). Each of those is checked against the shape its position promises before it is used, because a malformed document can put something at that position that is not an identifier; one that does not match is refused and the locus reads `WITHHELD_LOCUS_TOKEN` (``) plus a structural index. Content that happens to match the shape is still echoed, so this is a bound rather than an impossibility claim. ## Immutability The input model is never mutated; `deidentify` returns a deeply frozen `DeidResult`. --- URL: https://docs.cosyte.com/deid/guides-overview Markdown: https://docs.cosyte.com/deid/guides-overview.md # Guides Task-oriented recipes for `@cosyte/deid`. These guides cover the policy engine and the transforms, plus a per-format recipe for each shipped adapter: HL7 v2, C-CDA, FHIR, X12 EDI, NCPDP Telecom, and DICOM. ## Choose a policy `safe-harbor` is the built-in default. Derive a custom policy with `defineDeidPolicy`: you can only ever *deviate* from the safe default, never forget a category: ```ts import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; // A research policy that date-shifts instead of generalizing (Expert-Determination-supporting). const research = defineDeidPolicy({ name: "research", transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" }, }); ``` ## Keep records linkable without leaking Pseudonymize identifiers with a consumer-held key so the same MRN maps to the same surrogate everywhere, without the surrogate being reversible. **This is not Safe Harbor**: a surrogate derived from the individual's own value is a re-identification code §164.514(c)(1) does not permit, so the built-in Safe Harbor policy removes those identifiers instead, and keyed surrogates live behind a preset that does not claim the label: ```ts import { createDeidContext, profileOptions, LIMITED_DATA_SET_PROFILE } from "@cosyte/deid"; const context = createDeidContext({ key: process.env.DEID_KEY!, patientId: "patient-1" }); // Pass profileOptions(LIMITED_DATA_SET_PROFILE, context) to deidentify; the key never leaves your // process, and every surrogate is flagged in the manifest as a re-identification code. ``` ## Read the manifest Every action is recorded value-free. Watch for `DEID_LOCUS_BLOCKED` / `DEID_FREETEXT_BLOCKED` (the fail-closed decisions), `DEID_RESIDUAL_RETAINED` (a kept year or safe 3-digit ZIP), and `reidentificationCode: true` (a keyed surrogate): the residuals you surface to a human for the §164.514(b)(2)(ii) actual-knowledge test and for an Expert Determination. ## Per-format guides - [De-identifying HL7 v2](https://docs.cosyte.com/deid/guides-hl7.md), [C-CDA](https://docs.cosyte.com/deid/guides-ccda.md), [FHIR](https://docs.cosyte.com/deid/guides-fhir.md), [X12 EDI](https://docs.cosyte.com/deid/guides-x12.md), [NCPDP Telecom](https://docs.cosyte.com/deid/guides-ncpdp.md), and [DICOM](https://docs.cosyte.com/deid/guides-dicom.md). ## Planned guides - NCPDP SCRIPT (ePrescribing) once a full-fidelity parser surface exists. - Cross-document longitudinal consistency with date-shift + pseudonymization. --- URL: https://docs.cosyte.com/deid/guides-hl7 Markdown: https://docs.cosyte.com/deid/guides-hl7.md # De-identifying HL7 v2 The `@cosyte/deid/hl7` adapter is the first end-to-end format binding of the de-identification core. It locates PHI **structurally** in the parsed [`@cosyte/hl7`](https://github.com/cosyte/hl7) model (a name is at PID-5 because the HL7 v2 standard says PID-5 is the patient name, never because a string "looked like" a name), applies the configured policy, and returns a transformed `Hl7Message` plus the core's **value-free manifest**. > **`@cosyte/hl7` is an optional peer dependency.** Install it alongside `@cosyte/deid` to use this > subpath; a consumer who only de-identifies HL7 v2 pays for nothing else, and the core stays > dependency-free. ```bash npm install @cosyte/deid @cosyte/hl7 ``` ## Quickstart ```ts import { parseHL7 } from "@cosyte/hl7"; import { deidentifyHl7 } from "@cosyte/deid/hl7"; import { createDeidContext } from "@cosyte/deid"; const context = createDeidContext({ key: process.env.DEID_KEY! }); const { document, manifest } = deidentifyHl7(parseHL7(rawMessage), { context }); document.toString(); // spec-clean, de-identified HL7 wire manifest; // value-free audit: category + locus + disposition, never a value ``` The built-in Safe Harbor policy uses **no keyed transform**: MRN, account and beneficiary numbers are **removed**, so a Safe Harbor pass over a v2 message needs no `context` at all. Under a preset that keeps consistent keyed surrogates instead, such as `LIMITED_DATA_SET_PROFILE`, a `context` is required and calling without one when the message needs it is a fatal `DEID_NO_KEY`: the engine never falls back to an unkeyed surrogate. ## What is located, and how it is transformed | Segment | Loci | Transform | | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **PID** | name (5/6/9), DOB (7/29), address (11), SSN (19), phone (13/14), driver's licence (20), MRN/account/mother-id (2/3/4/18/21), county (12), birth place (23) | names/phone/SSN/licence/MRN/account **removed** (a consistent keyed surrogate for MRN and account only under a preset that does not claim the label); DOB → **year**; ZIP → safe **3-digit** (or `000`); county/birth place fail closed | | **NK1 / GT1 / IN1 / IN2** | relatives / guarantor / insured names, addresses, phones, SSNs, DOBs, member/policy/Medicare/Medicaid ids | same category transforms: Safe Harbor removes identifiers of **relatives, employers, and household members**, not only the patient | | **The employer positions** (GT1-16/17/18/19/29, IN1-10/11, IN2-3/49/50/64) | the employer's name, address, phone, contact person, and the employee / employer identification numbers | same category transforms: an employer is inside the removal list, so a name is removed, an address reduced to the safe 3-digit ZIP, a phone removed, an identifier blocked as (R) | | **IN2-70** | the insured employer ORGANISATION (name at XON.1, identifier at XON.10) | decided by the same party-role test the X12 map applies to an `NM1` / `N1` party, and **fails closed**: an employer is never outside the scope clause, so name and identifier both go | | **GT1-20, IN1-3/4/5/7** | the coded employment status, and the INSURER's own company id, name, address and phone | **retained untouched** (the over-scrub guard): an insurer is not the individual, a relative, an employer or a household member | | **OBX-5, NTE-3** | narrative / ambiguous free text (OBX-5 unless OBX-2 types it structured) | **fail closed**: blocked, never regex-scrubbed | | **MRG / ACC / FAM / PEO / PDA** | known patient-identity / relative / geographic segments absent from the map | **fail closed**: blocked (e.g. a merge message's prior name + MRN) | | **Z-segments / unknown structure** | every populated field | **fail closed**: blocked | | **PV1-19, OBR-2/3, ORC-2/3** | visit number, placer + filler order numbers, inside retained segments | **removed**. PV1-19 is routed by its CX-5 type code like PID-3: `VN`/untyped is the encounter identifier, removed as (R) and retainable under a profile that names the class; `MR`/`AN`/`SS` is transformed as that identifier under **both** profiles, never retained | | **PV1-44/45, OBR-7, DG1-5** | admit, discharge, observation and diagnosis dates, inside retained segments | → **year** (§164.514(b)(2)(i)(C) names admission and discharge); kept whole, and recorded, only under a profile that names the class | | **Every other date position of a segment the pass hands through** (EVN-2/3/6, PV2-8/9/…, ORC-9/15, OBR-6/8/14/22, SPM-17/18/19, RXA-3/4, FT1-4/5, TXA-4/6/7/8, MSH-7, …) | every field HL7 v2.5.1 types `DT` or `TS`, and every date/time **component** of a date range or other composite it defines | → **year** under a Safe-Harbor-named policy, **shifted** under a date-shift policy, **blocked** when the transform cannot read the value; **recorded** either way, one manifest entry per repetition and per component | | **OBX-12, OBX-14, OBX-19** | the OBX segment's OWN reference-range, observation and analysis timestamps, whatever OBX-2 types the value at OBX-5 | same as the row above. The segment is handed through by the OBX-2 branch rather than by the retain-list, and a date position inside it is treated no differently for that | | **OBX-5 typed as a date by OBX-2** (`DT`, `DTM`, `TS`, `DR`) | the observation value the message itself types as a date | same as the row above: acted on and recorded, per repetition, and per range component for a `DR` | | Retained clinical/administrative segments (an explicit allow-list: OBR, ORC, AL1, DG1, PV1, RX\*, …) | every field except the rows above | **retained untouched** (the over-scrub guard) | A recognized segment is retained **only** if it is on the explicit retain-list; anything else fails closed. OBX-5 is retained only when OBX-2 positively types it as a structured clinical value (numeric, coded, or a time of day): narrative (`TX`/`FT`), ambiguous String (`ST`), and any empty/unknown OBX-2 block, and a **date/time** value type makes OBX-5 a date the pass acts on rather than a value it keeps. ### How a date position is decided, and at which version A date is located from the **HL7 v2.5.1 segment definitions** (chapters 2 to 15), never from the shape of the value: an eight-digit numeric lab result is not a date, and free text mentioning a year is not one either. The classification is committed in the library as an auditable table of positions, each carrying the chapter, the field number, the component number where the date sits inside a composite, and the name the standard gives it, so a reviewer can re-derive a row rather than trust it. The table covers **every segment whose bytes the pass can hand through**, which is what makes a date position inside one reachable at all: every segment on the retain-list, plus `OBX`, which is handed through by the OBX-2 value-type branch instead of by that list. A segment that fails closed is blocked field by field, so it can carry nothing forward and contributes no position. The version is **fixed at 2.5.1** and is never re-read from `MSH-12`: identical wire bytes yield an identical set of date positions whatever version a sender declares. The price is stated in the limitations below. Each position is acted on **at its own unit**. A `DT`/`TS` field is acted on as a field; a date inside a composite (a specimen collection range, an order's quantity/timing, a discharged-to location's effective date) is acted on as **that component only**, so its siblings keep their bytes and their component positions. A repeating field gets one locus **per repetition**, so an unreadable repetition is emptied without disturbing the one beside it. The manifest path carries all three: `ORC-9[0]` is a field, `SPM-17[0].2` is a component, and `OBX[1]-5[0]` names the repetition. The identifier type inside a PID-3 list is read from the CX-5 type code (`SS` → SSN, `MR` → MRN, `AN` → account, `MA`/`MC`/`PN` → beneficiary), so the parser's typing, not a guess, decides which category each entry lands in and therefore which transform the configured policy applies to it. ## The two guarantees - **No leak.** Every seeded PHI sentinel across PID/NK1/GT1/IN1/IN2, the encounter dates and order identifiers, every other date inside a retained segment, the free-text loci, and Z-segments is gone from the serialized output under the Safe Harbor profile. An unmapped locus that could carry PHI is blocked, never passed through in the clear. - **No over-scrub.** Structured clinical OBX values, units, LOINC/coded observation identifiers, reference ranges, result statuses, times of day (`TM`), and every component of a composite that is not itself a date are retained byte-identical: the de-identifier never degenerates into a blanket-blanking "safe but useless" scrubber. The narrowings of that guarantee are stated rather than implied, and there are two, both of them dates: an OBX-5 whose OBX-2 types it as a **date** is a date, and is acted on and recorded like any other, so a structured OBX value is retained byte-identical **except** for that date-typed subset; and the OBX segment's own date/time fields (OBX-12, OBX-14, OBX-19) are acted on whatever OBX-2 says, while the result, its units and its reference range beside them are untouched. ## Known limitations (this release) - Free text is **block-only**: there is no built-in NLP scrub. - Dates inside retained segments **are** acted on and recorded, and the classification is fixed at HL7 **v2.5.1**. A position that only some other version of the standard types as a date is therefore a **residual**: it is not classified and not acted on. The same applies to a segment the retain-list keeps that v2.5.1 does not define, and to the file and batch envelope headers (`FHS`, `BHS`), whose creation timestamps are left untouched because those headers number their fields from a leading delimiter. - Within retained segments, the **non-date** positions the maps do not name are still passed through untouched. That includes the **provider and other non-patient person names** (PV1-7/8, OBR-16 and their siblings), and the date/time components that live **inside** a person-name or address composite (an effective, expiration or action-performed date carried by a provider name, an authenticator's timestamp, a licence expiry). Retaining a segment is not auditing every field in it. If your threat model includes these, filter them yourself. - Every one of those positions **is counted and located**: it appears on `result.unexaminedResiduals` with its structural locus (`PV1-8.2`, `OBR-32.1`, one component of one repetition), a count, and the fact that nothing examined it, never a value. Counting is not removal: the value is still there, and a position no rule examined has no established Safe Harbor category, so it joins none of the 18. `OBX-5` kept by the over-scrub guard is **not** one of these: a structured clinical value survives on purpose, which is a decision the engine reached, not a silence. - The address generalization keeps only the Safe Harbor 3-digit ZIP (the permitted state is also dropped, conservative, never a leak). - Under a **date-shift** policy, a date whose encoding that transform does not accept, including a legitimately reduced-precision value (year only, or year and month), is **blocked** rather than shifted: a recorded utility loss, never a value passed through unshifted. The honesty line is unchanged: the output is **"Safe-Harbor-transformed per the configured policy,"** never "de-identified" and never "HIPAA-compliant." --- URL: https://docs.cosyte.com/deid/guides-ccda Markdown: https://docs.cosyte.com/deid/guides-ccda.md # De-identifying C-CDA The `@cosyte/deid/ccda` adapter is the C-CDA binding of the de-identification core. It locates PHI **structurally** in an HL7 CDA R2.1 document (a `` under `recordTarget/patientRole/patient` is the patient's name because the CDA standard says so, never because a string "looked like" a name), applies the configured policy, and returns a transformed `CcdaDocument` plus the core's **value-free manifest**. > **`@cosyte/ccda` is an optional peer dependency.** Install it alongside `@cosyte/deid` to use this > subpath; a consumer who only de-identifies C-CDA pays for nothing else, and the core stays > third-party-dependency-free. The adapter reaches the CDA DOM only through `@cosyte/ccda`'s hardened > `parseSecureXml` and re-serializes the node the parser hands back: it never imports the XML substrate > directly. ```bash npm install @cosyte/deid @cosyte/ccda ``` ## Quickstart ```ts import { parseCcda } from "@cosyte/ccda"; import { deidentifyCcda } from "@cosyte/deid/ccda"; import { createDeidContext } from "@cosyte/deid"; const context = createDeidContext({ key: process.env.DEID_KEY! }); const { document, manifest } = deidentifyCcda(parseCcda(xml), { context }); document.toString(); // spec-clean, de-identified C-CDA XML manifest; // value-free audit: category + locus + disposition, never a value ``` The built-in Safe Harbor policy uses **no keyed transform**, so a Safe Harbor pass over a C-CDA needs no `context` at all. Under a preset that keeps consistent keyed surrogates instead, a `context` is required and calling without one when the document needs it is a fatal `DEID_NO_KEY`: the engine never falls back to an unkeyed surrogate. ## What is located, and how it is transformed PHI is located at the CDA **header participations**: the patient and every party the standard attaches to the document. A `` there is always a person or organization; a `` in the clinical body can be a drug or material name, so the body is deliberately never swept. | Participation | Loci | Transform | |---|---|---| | **recordTarget / patientRole** (+ nested `guardian`) | `id` (MRN / SSN), `addr`, `telecom`, `patient/name`, `birthTime` | name/telecom **removed**; `id` **removed** under Safe Harbor, assigning root retained (a consistent keyed surrogate only under a preset that does not claim the label); `birthTime` → **year**; `addr` → safe **3-digit ZIP** (or `000`), finer geography dropped | | **author / dataEnterer / informant / authenticator / legalAuthenticator / participant / custodian** | person `name`, `id`, `addr`, `telecom`, participation `time` | same category transforms: Safe Harbor removes identifiers of **relatives, employers, and household members**, not only the patient | | **componentOf / documentationOf** | encounter / service-event `id`, `effectiveTime` | ids acted on under the configured transform; dates → **year** | | **section `` narrative, `nonXMLBody`** | every narrative block | **fail closed**: blocked, never regex-scrubbed | | **unknown / `sdtc` / foreign elements carrying a value** | any value-bearing element that is neither mapped PHI nor recognized coded structure | **fail closed**: blocked | | **structuredBody clinical entries** (codes, values, units, statuses, dosing periods) | n/a | **retained untouched** (the over-scrub guard) | An `id`'s Safe Harbor category is read from its assigning-authority `root` OID: the SSN OID (`2.16.840.1.113883.4.1`) routes to the SSN category, every other person/organization id to the medical record number category, so the parser's typing, not a guess, decides which transform the configured policy applies to each. A dosing-period `effectiveTime` (`PIVL_TS` / `EIVL_TS`) is a duration, not a calendar date, and is never generalized. ## How to read a C-CDA locus path A locus is a `/`-joined path of element names. Entries that agree on **all five** of locus, category, transform, disposition and code are aggregated into one entry with a running `count`, and two narratives blocked the same way agree on the other four, so two narrative positions printing the same path would arrive as **one row with `count: 2`**. That is why the index in a path matters, and it has one meaning: > `[n]` is the position's index **among its document siblings that print the same segment name**. It > appears when more than one sibling prints that name, and always when the name was refused. ``` component/structuredBody/component[0]/section/text ← the first section's narrative component/structuredBody/component[1]/section/text ← the second section's, its own row recordTarget/patientRole/id[0] ← two siblings, one row each recordTarget/patientRole/patient/name ← the only , so no index is needed recordTarget/patientRole/patient/[0] ← name refused; the index is the only "where" left ``` A refused segment (``) always carries an index, since the token itself names nothing. Three scoping notes, because each is easy to over-read: - **The index is a document position, not a row number, so the indices in a manifest can be gapped.** A sibling that produced nothing to record produces no entry: an empty ``, an `` whose narrative is a `` into the section rather than character data, a `nullFlavor`-only ``. A manifest showing `component[2]` and no `component[0]` or `component[1]` means those two siblings had nothing to record, **not** that rows are missing. Count the rows, not the indices. - This covers the segments built from an **element name**: the header sweep and the body narrative descent. Some segments are fixed strings instead: `structuredBody`, `nonXMLBody/text`, and the `low` / `high` / `center` bounds of an interval. The CDA schema allows each at most once at its position; a document that repeats one anyway still prints one path for both. - An index says two positions were **distinct**, not what stood at either. The path is still value-free: no narrative, no identifier, no value. ## The two guarantees - **No leak.** Every seeded PHI sentinel across the header participations and the section narrative is gone from the serialized output. An unmapped element that could carry PHI is blocked, never passed through in the clear. - **No over-scrub.** Coded clinical entries (observation and medication codes, values, units, result statuses, and dosing periods) are retained byte-identical. The de-identifier never degenerates into a blanket-blanking "safe but useless" scrubber. ## Known limitations (this release) - Narrative is **block-only**: there is no built-in NLP scrub. - Within the **retained** clinical body, entry-level service **dates** (`effectiveTime`), entry **ids**, in-entry **performer** names, and **family-history** relative demographics are **not** de-identified: exactly mirroring the HL7 v2 adapter's retained-clinical-segment boundary. Forgetting one fails **safe** (retained in a coded entry), never leaked, because the leak surface for this release is the header and the narrative. - The document `id` / `code` / `title` envelope is retained (like HL7's MSH). The address generalization keeps state and country (permitted) and the safe 3-digit ZIP, dropping every finer component. The honesty line is unchanged: the output is **"Safe-Harbor-transformed per the configured policy,"** never "de-identified" and never "HIPAA-compliant." --- URL: https://docs.cosyte.com/deid/guides-fhir Markdown: https://docs.cosyte.com/deid/guides-fhir.md # De-identifying FHIR The `@cosyte/deid/fhir` adapter is the FHIR R4 binding of the de-identification core. It locates PHI **structurally** in a parsed FHIR resource (a `name` under a `Patient` is the patient's name because the FHIR spec says so, never because a string "looked like" a name), applies the configured policy, and returns a transformed resource model plus the core's **value-free manifest**. > **`@cosyte/fhir` is an optional peer dependency.** Install it alongside `@cosyte/deid` to use this > subpath; a consumer who only de-identifies FHIR pays for nothing else, and the core stays > third-party-dependency-free. The adapter reaches FHIR data only through `@cosyte/fhir`'s exported model > (`FhirComplex` / `FhirList` / `FhirPrimitive`, `getProperty`, `resourceType`, the node constructors) > and its `parseResource` / `serializeResource` codec: it never imports a third-party JSON substrate. ```bash npm install @cosyte/deid @cosyte/fhir ``` ## Quickstart ```ts import { parseResource, serializeResource } from "@cosyte/fhir"; import { deidentifyFhir } from "@cosyte/deid/fhir"; import { createDeidContext } from "@cosyte/deid"; const context = createDeidContext({ key: process.env.DEID_KEY! }); const { resource } = parseResource(json); const { document, manifest } = deidentifyFhir(resource, { context }); serializeResource(document); // spec-clean, de-identified FHIR JSON manifest; // value-free audit: category + locus + disposition, never a value ``` A convenience wrapper parses, de-identifies, and re-serializes in one call: ```ts import { deidentifyFhirJson } from "@cosyte/deid/fhir"; import { createDeidContext } from "@cosyte/deid"; const { json, manifest } = deidentifyFhirJson(input, { context: createDeidContext({ key: process.env.DEID_KEY! }), }); ``` The built-in Safe Harbor policy uses **no keyed transform**, so a Safe Harbor pass over a resource needs no `context` at all. Under a preset that keeps consistent keyed surrogates instead, a `context` is required and calling without one when the resource needs it is a fatal `DEID_NO_KEY`: the engine never falls back to an unkeyed surrogate. ## What is located, and how it is transformed FHIR is a **graph of typed resources**, so the locus map splits by resource role for the elements a resource's type really does decide, and by the **element's own datatype** for the two it does not. A person's name and a postal address are acted on wherever the graph puts them outside a person resource, because which resource carries one is a choice the producing system made: the same home address arrives at `Patient.address` from one sender and at `Location.address` from a home-health sender, and coverage that depended on that choice would not be coverage at all. Inside a person resource the map decides instead, which reaches the elements it lists and no others; that is the last of the residuals below. | Scope | Loci | Transform | |---|---|---| | **Person resources**: `Patient` / `RelatedPerson` / `Practitioner` / `Person` (+ nested `Patient.contact`, a relative) | `name`, `telecom`, `photo`, `address`, `birthDate`, `deceasedDateTime` | name/telecom/photo **removed**; `address` → safe **3-digit ZIP** (or `000` for a restricted prefix), finer geography dropped; dates → **year** | | **Every resource, by datatype** | any `HumanName`, any `Address`: `Organization.contact.name`, `Location.address`, `Organization.address`, a choice-type `locationAddress` | name **removed**; address → the same safe **3-digit ZIP** (or `000`), finer geography dropped, that a person resource's address gets | | **Every resource (the universal PHI vectors)** | `identifier`, PHI-bearing dates, narrative `text.div`, `extension` / `modifierExtension` values, `Reference.display` | identifier **removed** under Safe Harbor, `system` retained (a consistent keyed surrogate by `system` only under a preset that does not claim the label); dates → **year**; narrative div / extension values / reference labels **blocked** | | **Contained resources & `Bundle` entries** | each nested resource | **walked**: the resource role is re-derived at every `resourceType`, so a contained `RelatedPerson` or a Bundled `Patient` is de-identified too | | **Clinical resources**: `Observation`, `Condition`, `Encounter`, … | codes, values, units, statuses, reference ranges, reference wiring | **retained untouched** (the over-scrub guard) | Positive classification is **closed**, and that is the over-scrub guard: an element is a `HumanName` only when every property it carries is one FHIR R4 defines on `HumanName`, **and** at least one of them is a marker property (`family` / `given` / `prefix` / `suffix`), **and** that marker holds **the exact value shape R4 gives it** - `family` a single string, the other three repeating. An `Address` the same way, with `line` repeating and `city` / `district` / `state` / `postalCode` / `country` single. So an **organisation's own `name`** is untouched (it is a plain string, never a `HumanName`), a `CodeableConcept` carrying only its `text` is untouched (that shape is an `Observation` code, a dose unit or an order status far more often than it is a name), and every clinical code, value, unit and status survives byte-identical. Reading the shape **per marker** is what separates a marker from an R4 element that merely shares its name. The several elements called `country` are coded concepts where `Address.country` is a string, and `Questionnaire.item`, `PlanDefinition.action` and `RequestGroup.action` each carry a `prefix` that is a single string where a name's `prefix` repeats. The latter two backbones make **every** child optional, so `{ "prefix": "1." }` alone is a conformant numbered workflow step, and the shape is the only thing that tells it from a person's name. The rule leans on no sibling those other elements happen to require: that would be an assumption about the document, and this pass validates no conformance. An identifier's Safe Harbor category is read from its `system` URI: the US-SSN system (`http://hl7.org/fhir/sid/us-ssn` or its OID form) routes to the SSN category, every other identifier to the medical record number category, and the configured policy's transform for that category is what runs, with the `system` retained either way. A `Reference.display` (usually a person's name) is blocked, while a `Coding.display` (a coded term such as `Sodium`) is retained: the two are distinguished structurally, not by the property name. Dates are detected by **value shape**: any primitive whose whole value is a real calendar date (`YYYY-MM`, `YYYY-MM-DD`, or a full instant) with a valid month/day is generalized to its year, wherever it sits, so a date in an unexpected element is caught too. A bare four-digit year is already Safe-Harbor-safe and is left as-is; a clinical code that merely looks date-ish (`2951-2`, `1234-56`) is not mistaken for a date, so it survives. ## Fail closed - A bare **unrecognized string** at a person resource's top level is **blocked**: a positive allow-list of recognized coded/administrative elements governs the person sweep, so a vendor ``-level field cannot ride through in the clear (an open-ended allow-list can never satisfy Safe Harbor's open-ended category (R)). - A **`display` that is not on a `Coding`** is a Reference person-label and is **blocked**, including a display-only (`{ display }`) or type+display reference that names no `reference`/`identifier` target. A Coding is identified positively (a `code`/`system` sibling), so `Coding.display` (a coded term) is retained; every other `display` fails closed. - Every **extension value** is dropped: a complex `valueAddress` / `valueHumanName` / `valueIdentifier`, a `modifierExtension`, a deeply nested extension, and a primitive-level `_`-sibling extension alike. Extensions are the FHIR leak frontier; the `url` skeleton is kept, the payload is not. - A **swept `Address` the pass cannot read faithfully** is removed **whole**, never partly retained: a `postalCode` that is not a whole zip code (a four-digit `0110` still has three leading digits, and keeping them would retain a fragment of something that was never a ZIP), or an unexpected JSON shape where R4 types a string at a part the reduction would re-emit verbatim. The street, the city, the state, the country and the ZIP all go, and the disposition is recorded. - At an element name R4 **types** as one of the two datatypes (`name`, `address`, the choice-type `locationAddress`, an open `valueAddress` / `valueHumanName`), **any** complex the classifier cannot pin down is **blocked whole** rather than descended into: a `{ text }` or `{ use, text }` representation with no part it can key on, a `{ family, given, … }` or `{ line, city, … }` carrying some property R4 does not define, and equally a `{ streetAddress, town, zip }` whose every property is foreign to both datatypes. The standard promised a name or an address there and the pass could not read the one it was handed, so the question is readability and not which keys arrived: adding an unrecognized sibling to an already unreadable element never unblocks it. Exactly two conformant R4 backbones share one of those element names, `MedicinalProduct.name` and `SubstanceSpecification.name`, and each is excluded positively by the property R4 makes `1..1` on it plus its own closed property set; a plain string there (`Organization.name`, `Endpoint.address`) is never a candidate. The disposition is recorded either way. That block is scoped to those element names deliberately, and the residual it leaves is stated below. - **Free-text prose** is blocked by default: the `note` element (`Annotation.text` + author), a `Communication`/message `contentString`, and an **uncoded `valueString`** (the FHIR analogue of an HL7 OBX-5 typed `ST`, which the sibling HL7 adapter also fails closed on; a structured `valueQuantity` / `valueCodeableConcept` / `valueDateTime` result is retained). - The narrative **`text.div`** is blocked at any depth (resource-, section-, entry-level). ## The two guarantees - **No leak.** Every seeded PHI sentinel across the person resources, the universal vectors, the nested `contact` relative, and a contained resource is gone from the serialized output. An unmapped element or extension that could carry PHI is blocked, never passed through in the clear. - **No over-scrub.** Clinical resources (observation and medication codes, values, units, result statuses, reference ranges, coded displays) are retained, and reference **wiring** (`Reference.reference` pointers) is preserved, so intra-document linkage survives whichever transform the configured policy applies to an identifier. ## Known limitations (this release) - Extension values are **block-only**: there is no profile-aware retention, so a `us-core-*` demographic extension is dropped rather than kept. - Reference **wiring** and resource logical `id`s are preserved structurally; coordinated pseudonymization of resource ids across a corpus (so the same patient links across documents) is **not** performed. - Free-text **prose** loci (`note`, `contentString`, uncoded `valueString`) fail closed by default; a semantic (NLP) narrative scrub and `contentAttachment` binary content remain out of scope. - Three surfaces are still not reached because FHIR types no person at the position: a **`ContactPoint` outside a person resource** (a phone or an email on an `Organization`, a `Location` or an `Endpoint`), an **organisation's own `name`** (a plain string, never a `HumanName`), and the **individual's employer carried as a separate `Organization` resource**, which would need a cross-resource role derivation rather than a typed read. - A fourth is the stated cost of the closed, shape-read classification: **a name or an address carrying a property R4 does not define, or carrying its only marker at a value shape R4 does not give that marker, at an element name R4 does not type as one of the two datatypes.** The unrecognized sibling or the wrong shape stops the classifier, and the fail-closed block above is scoped to the typed element names, because at any other name the same evidence is routinely something else: `{ "prefix": "1." }` is a conformant `RequestGroup.action` and not a person. Blocking it there would destroy conformant clinical and structural content, the mirror defect and the one no re-run restores. At a **typed** element name neither half is a residual: whatever the classifier declines is blocked whole. - A fifth is the scope of the datatype sweep: **a name or an address inside a person resource, at a property the demographic map does not list.** The map has already decided `name`, `telecom`, `photo` and `address` there, so the sweep does not run inside a person resource, and a vendor `Patient.alias` carrying `{ family, given }` is passed through where the same bytes on an `Organization` are removed. The honesty line is unchanged: the output is **"Safe-Harbor-transformed per the configured policy,"** never "de-identified" and never "HIPAA-compliant." --- URL: https://docs.cosyte.com/deid/guides-x12 Markdown: https://docs.cosyte.com/deid/guides-x12.md # De-identifying X12 EDI The `@cosyte/deid/x12` adapter is the HIPAA 005010 X12 binding of the de-identification core. It locates PHI **structurally** in a parsed X12 interchange (a name is at `NM1-03..07` because the X12 TR3 says so, never because a string "looked like" a name), applies the configured policy, and returns the de-identified X12 byte stream plus the core's **value-free manifest**. > **`@cosyte/x12` is an optional peer dependency.** Install it alongside `@cosyte/deid` to use this > subpath; the core stays third-party-dependency-free. The adapter reaches EDI data only through > `@cosyte/x12`'s exported model (`X12Interchange` / `X12Segment`, the 1-indexed `elements`, the detected > `delimiters`) and re-emits with its **byte-faithful** `serializeX12`, so every segment the map does not > touch keeps its verbatim bytes. ```bash npm install @cosyte/deid @cosyte/x12 ``` ## Quickstart ```ts import { parseX12 } from "@cosyte/x12"; import { deidentifyX12, deidentifyX12String } from "@cosyte/deid/x12"; import { createDeidContext } from "@cosyte/deid"; const context = createDeidContext({ key: process.env.DEID_KEY! }); const { x12, manifest } = deidentifyX12(parseX12(raw), { context }); // or, parse + de-identify in one call: const out = deidentifyX12String(raw, { context }); x12; // the de-identified interchange (byte-faithful for every untouched segment) manifest; // value-free audit: category + locus + disposition, never a value ``` The built-in Safe Harbor policy uses **no keyed transform**, so a Safe Harbor pass over an interchange needs no `context` at all. Under a preset that keeps consistent keyed surrogates instead, a `context` is required and calling without one when the interchange needs it is a fatal `DEID_NO_KEY`: the engine never falls back to an unkeyed surrogate. ## What is located, and how it is transformed X12 is a flat, ordered segment stream; the subscriber (2000B/2010BA) and patient (2000C/2010CA) loops are implicit, so the map keys off each **segment id** plus two **qualifier classifiers**. | Locus | Handling | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | **`NM1`**: subscriber / patient / dependent (`IL` / `QC` / `03`) | name (`NM1-03..07`) **removed**; id (`NM1-09`) routed by the `NM1-08` qualifier: SSN **removed**, member **removed** under Safe Harbor | | **`NM1`** / **`N1`**: the individual's employer (`36`) | name and id **removed**, on the same footing as a patient-side party: §164.514(b)(2)(i) removes the identifiers of the individual's **employers**, so an employer is not an unrelated organisation | | **`NM1`**: recognized provider / organization (`85` / `82` / …) | **retained**: provider identity is not the individual's PHI (mirrors the HL7 adapter's provider retention), and the **entity-identifier code** it was classified on is recorded, value-free, at the party's locus | | **`NM1`**: unknown entity code | **fails closed**: name and id blocked (an unrecognized entity could be the patient) | | **`N1`** (payer / provider org) | **retained**; a patient-side or **unknown** party's name + id scrubbed / **fail closed** (same classification as `NM1`) | | **`SBR`** (subscriber) | `SBR-03` group / policy number **removed** under Safe Harbor, `SBR-04` group name **removed**; relationship codes retained | | **`N3` / `N4`** | street + city **removed**; ZIP → safe 3-digit; state retained; an unmapped element (`N4-06` location id) **fails closed** | | **`DMG-02`**, **`DTP-03`**, **`DTM-02`** | dates → **year** | | **`PER`** | contact name + communication numbers **removed** | | **`REF`** | patient / member / group / SSN identifier **removed** under Safe Harbor; recognized admin/provider reference retained; **unknown qualifier fails closed** | | **`CLM-01` / `CLP-01`** | patient account number **removed** under Safe Harbor (a consistent surrogate only under a preset that does not claim the label) | | **Free text** (`MSG-01`, `III-04`, `K3-01`, `NTE-02`) | **fails closed**: blocked (coded siblings retained), never scrubbed by a naive pass | | **Clinical / financial** (`HI`, `SV*`, `SVC`, `AMT`, `CAS`, …) | **retained untouched**: diagnosis / procedure / revenue codes, amounts, quantities survive byte-identical | | Any **unmapped / unknown** segment | **fails closed**: every element blocked | ## The `REF` frontier Distinguishing a patient identifier from an administrative reference is the hardest call. The adapter routes `REF` by its `REF-01` qualifier: patient identifiers (`SY` SSN, `1W` member, `0F` subscriber, `1L` group, `EA` medical-record) are acted on under the configured policy; recognized non-patient references (`F8` payer claim control, `EI` EIN, `TJ` tax id, `G1` prior authorization, provider ids) are retained; and an **unrecognized qualifier fails closed**: the direct implementation of Safe Harbor category (R) for the "unusual REF qualifier" that a shape guess would miss. ## Known limitations Provider / organization identity is **retained** as non-patient identity, and there is **no** option to suppress it: the retention is structural, in the extractor, not a per-category policy choice. It is not silent, though: every retained party emits a value-free manifest row naming the entity-identifier code that placed it outside the scope clause, so a reader can check the classification rather than infer it from an absence. A retained clinical segment may carry a residual patient-related date the map does not surface as `DTP` / `DTM`: a documented limitation mirroring the HL7 adapter; forgetting one fails **safe** (retained, not leaked, but conversely not generalized). --- URL: https://docs.cosyte.com/deid/guides-ncpdp Markdown: https://docs.cosyte.com/deid/guides-ncpdp.md # De-identifying NCPDP The `@cosyte/deid/ncpdp` adapter is the NCPDP **Telecommunication (vD.0)** binding of the de-identification core. It locates PHI **structurally** in a parsed Telecom transaction (a value is the patient's last name because it sits in field `311-CB`, never because a string "looked like" a name), applies the configured policy, and returns the de-identified Telecom byte stream plus the core's **value-free manifest**. > **`@cosyte/ncpdp` is an optional peer dependency.** Install it alongside `@cosyte/deid` to use this > subpath. The adapter reaches NCPDP data only through `@cosyte/ncpdp`'s exported Telecom model > (`TelecomTransaction` / `TelecomSegment` / `TelecomField`) and its `parseTelecom` / `serializeTelecom` > codec. ```bash npm install @cosyte/deid @cosyte/ncpdp ``` ## Quickstart ```ts import { parseTelecom } from "@cosyte/ncpdp/telecom"; import { deidentifyTelecom, deidentifyTelecomString } from "@cosyte/deid/ncpdp"; import { createDeidContext } from "@cosyte/deid"; const context = createDeidContext({ key: process.env.DEID_KEY! }); const { telecom, manifest } = deidentifyTelecom(parseTelecom(raw), { context }); // or, parse + de-identify in one call: const out = deidentifyTelecomString(raw, { context }); telecom; // the de-identified Telecom transaction manifest; // value-free audit: category + locus + disposition, never a value ``` The built-in Safe Harbor policy uses **no keyed transform**, so a Safe Harbor pass over a transaction needs no `context` at all. Under a preset that keeps consistent keyed surrogates instead, a `context` is required and calling without one when the transaction needs it is a fatal `DEID_NO_KEY`. ## What is located, and how it is transformed Telecom is a flat sequence of segments of `{ id, value }` fields. Field ids are globally unique in the standard, so the map keys off the field id directly. | Segment / locus | Handling | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | **Patient (`01`)** | name (`CA`/`CB`) + phone (`CQ`) removed; street (`CM`) + city (`CN`) removed; ZIP (`CP`) → 3-digit; DOB (`C4`) → year; patient id (`CY`) **removed** under Safe Harbor; gender + state retained | | **Insurance (`04`)** | cardholder id (`C2`) + group id (`C1`) **removed** under Safe Harbor; cardholder name (`CC`/`CD`) removed; person code retained | | **Prescriber (`03`)** | prescriber id (`DB`) **removed** | | **Coordination of Benefits (`05`)** | other-payer cardholder (`NU`) + group (`MJ`) ids **removed** under Safe Harbor; other-payer date (`E8`) → year | | **Header** | Date of Service → year | | **Free text** (`544-FY`, `504-F4`, `526-FQ`) | **fails closed**: blocked, never scrubbed by a naive pass | | **Clinical / financial** (`07`/`08`/`10`/`11`/`12`/`13`) | **retained untouched**: NDC drug codes, quantities, days-supply, pricing, DUR reason codes | | Any **unmapped / unknown** segment | **fails closed**: every field blocked | The X12 adapter **retains** provider identity, while this adapter **removes** the prescriber id: a deliberate asymmetry. ## NCPDP SCRIPT is refused NCPDP **SCRIPT** (ePrescribing XML) de-identification is **not** shipped in this release. The `@cosyte/ncpdp` SCRIPT surface cannot be de-identified faithfully through its public API: `serializeScript` emits only the **modeled** fields (a `parse → serialize` round-trip drops any unmodeled XML element), and the SCRIPT `Patient` model has **no address, phone, or patient-id** field. A partial pass would silently drop content and leave unmodeled patient identifiers unhandled: a false-safety hazard the fail-closed posture forbids. SCRIPT support waits for a parser surface that preserves the full document. **That is a behaviour, not a note.** Hand a SCRIPT document to either entry point and it is **refused**, before anything is parsed: ```ts import { deidentifyTelecomString } from "@cosyte/deid/ncpdp"; import { DeidError, FATAL_CODES } from "@cosyte/deid"; try { // An XML document on an NCPDP surface is SCRIPT, and it is declined outright. deidentifyTelecomString(''); } catch (err) { if (err instanceof DeidError && err.code === FATAL_CODES.DEID_FORMAT_UNSUPPORTED) { // err.message names SCRIPT and the parser-surface reason, and carries no document content. } } ``` The error is a typed `DeidError` carrying `DEID_FORMAT_UNSUPPORTED`, and its message names the format and states the parser-surface reason. It carries no byte of the document, and the call returns **no** transformed document, **no** manifest and no partial output of any kind. A **Telecom** caller sees no change: a parsed Telecom transaction is never a candidate for the refusal. ## Known limitation A retained clinical segment may carry a residual patient-related date the map does not surface (e.g. a previous date of fill): a documented limitation mirroring the HL7 and X12 adapters; forgetting one fails **safe** (retained, not leaked). --- URL: https://docs.cosyte.com/deid/guides-dicom Markdown: https://docs.cosyte.com/deid/guides-dicom.md # De-identifying DICOM The `@cosyte/deid/dicom` adapter is the DICOM binding of the de-identification core, and the one adapter that **delegates rather than reimplements**. `@cosyte/dicom` already ships the **PS3.15 Annex E** de-identification (the Basic Application Level Confidentiality Profile), so this adapter **orchestrates** that pass under the unified policy and **folds its value-free report into the unified manifest**. It never re-does Annex E. > **`@cosyte/dicom` is an optional peer dependency.** Install it alongside `@cosyte/deid` to use this > subpath. The adapter reaches DICOM data only through `@cosyte/dicom`'s own `parseDicom` / `deidentify` / > `serializeDicom` surface. ```bash npm install @cosyte/deid @cosyte/dicom ``` ## Quickstart ```ts import { parseDicom } from "@cosyte/dicom"; import { deidentifyDicom, deidentifyDicomBuffer } from "@cosyte/deid/dicom"; const { dataset, manifest, burnedInAnnotationHazard, metadataOnly } = deidentifyDicom( parseDicom(part10Bytes), ); // or, parse → de-identify → re-serialize in one call: const { bytes } = deidentifyDicomBuffer(part10Bytes); manifest; // value-free audit: category + "(gggg,eeee) Keyword" + disposition, never a value metadataOnly; // => true: always (see the pixel hazard below) ``` No key context is required: Annex E dummying and content-derived UID remapping do not use the pseudonymization key. Pass a `context` for API uniformity if you like; the DICOM adapter ignores it. ## What is located, and how it is transformed The **Basic Application Level Confidentiality Profile** is authoritative for what happens to each tag. The default `safe-harbor` policy applies it in full, with no Retain/Clean deviations: | Locus | Handling | | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | | Patient Name/ID/Birth Date, Other Patient IDs | removed / emptied per Annex E | | Institution, Referring/Performing physician, operators | removed | | Dates and times directly related to the individual | removed / dummied per Annex E | | Accession, device serial, and other identifiers | removed | | **Study / Series / SOP Instance UIDs** | **consistently remapped** (`U`) so the study/series/image relationships survive | | **Private tags** | **removed** (fail-closed, kept only when a known-safe retain list names them; empty by default) | | Modality, image geometry, coded technique, pixel bytes | **retained untouched**: the clinical/technical payload survives | The output carries the mandated `Patient Identity Removed = YES` marker and a De-identification Method naming the profile and the policy. ## The coded declaration: what a machine reads instead of a sentence PS3.15 E.1.1 asks for `Patient Identity Removed (0012,0062) = YES` and then, additionally, for "one or more codes from CID 7050 'De-identification Method' corresponding to the Profile and Options used" in **De-identification Method Code Sequence `(0012,0064)`**, "and/or a text string describing the method used" in **De-identification Method `(0012,0063)`**. This adapter writes **both**: the English sentence stays exactly as it was, and the coded terms are added beside it, so a receiving archive can branch on a code instead of parsing prose. ```ts import { parseDicom } from "@cosyte/dicom"; import { deidentifyDicom } from "@cosyte/deid/dicom"; const { dataset, deidentificationMethodCodes, optionDeclarations } = deidentifyDicom( parseDicom(part10Bytes), ); // What went INTO (0012,0064): the profile, then every option the run applied. deidentificationMethodCodes; // => [{ codeValue: "113100", codingSchemeDesignator: "DCM", // codeMeaning: "Basic Application Confidentiality Profile" }] // Every option, applied or withheld, by its coded term. Under the default policy all twelve are // withheld, because the full Basic Profile applies with no Retain/Clean deviations. optionDeclarations.filter((d) => d.status === "withheld").length; // => 12 dataset.get("00120064"); // the same terms, on the dataset and in the re-serialized bytes ``` Three rules govern it, and the second is the one to read twice: - **Only the published terms are ever emitted.** Every Code Value, Code Meaning and Coding Scheme Designator comes from the thirteen rows of CID 7050 (context group `1.2.840.10008.6.1.925`), reproduced verbatim. Nothing is composed, abbreviated or paraphrased, and no term outside that table is emitted into the dataset or onto the result. - **A withheld option is declared on the result and NEVER in `(0012,0064)`.** The standard's own words are "corresponding to the Profile and Options **used**", so a term in that sequence means the option was applied. Writing a withheld term there would tell an archive the opposite of the truth. Read `optionDeclarations` for what was withheld; read the sequence only for what was used. - **It refuses to declare rather than declare wrongly.** A profile or an option CID 7050 cannot name aborts the pass with `DEID_DECLARATION_UNNAMEABLE` before any output exists, and a declaration this run cannot read back out of its own serialized bytes aborts it with `DEID_OUTPUT_INVALID`. A coded term is acted on by downstream systems without a human, and a study released on a false coded claim cannot be un-released, so no output is the safe answer. A De-identification Method Code Sequence the **input** already carried is **dropped**, not merged with: its items are unaudited bytes from an untrusted file, and no de-identification rule inspects what is inside that sequence, so keeping any of it would put unexamined input text inside output stamped `Patient Identity Removed = YES`. The loss is disclosed rather than made silently, by a value-free `DICOM_INPUT_DEIDENTIFICATION_METHOD_CODES_DROPPED` warning that carries no Code Value, no Code Meaning and no other text read from the input. ## UID remapping: keeping relationships Study/Series/SOP Instance UIDs are replaced with internally-consistent surrogates: the **same** source UID always maps to the **same** replacement, so images still group into series and series into studies. The mapping is content-derived, so it is consistent across runs even without shared state; pass one `Map` as `uidMap` across a whole archive to make it consistent by construction (and O(1) on repeats): ```ts import { parseDicom } from "@cosyte/dicom"; import { deidentifyDicom } from "@cosyte/deid/dicom"; const uidMap = new Map(); for (const file of studyFiles) { const { dataset } = deidentifyDicom(parseDicom(file), { uidMap }); // every object in the study now shares consistently-remapped Study/Series UIDs } ``` The source→replacement map is **never** surfaced in the value-free result (a source UID is a re-linking vector). If you need it for your own re-identification key store, you own the `uidMap` you pass in. **How far that guarantee reaches is on the result, not only in this paragraph.** With **no** shared cache, referential integrity of the replacement UIDs is guaranteed **only within the single call**: the replacement UIDs of one call are not promised to agree with those of any other, because that would depend on inputs you control (a differing `uidRoot`, most obviously). Supply a shared cache and the guarantee reaches every call that shares it, and you own its lifetime and extent. The two cases are told apart by reading a value rather than by remembering which arguments were passed: ```ts import { deidentifyDicom } from "@cosyte/deid/dicom"; deidentifyDicom(dataset).uidReferentialIntegrity.scope; // => "single-call" const uidMap = new Map(); deidentifyDicom(dataset, { uidMap }).uidReferentialIntegrity.scope; // => "caller-supplied-cache" // The same fact in one PHI-free sentence, safe to log or to file in a disclosure record: deidentifyDicom(dataset).uidReferentialIntegrity.statement; ``` ## The burned-in-pixel hazard: flagged, never cleaned This is a **metadata-only** de-identifier: `metadataOnly` is always `true`. It cannot inspect or clean pixels, so recognizable text **burned into the image** (Safe Harbor category Q, full-face photographs and comparable images) is **not** removed. When Pixel Data is present and not affirmatively marked free of burned-in annotation, the result flags it: ```ts import { deidentifyDicom, BURNED_IN_ANNOTATION_CODE } from "@cosyte/deid/dicom"; const { burnedInAnnotationHazard, warnings } = deidentifyDicom(dataset); if (burnedInAnnotationHazard) { // do NOT release the image on metadata alone: pixels may carry burned-in PHI. warnings.some((w) => w.code === BURNED_IN_ANNOTATION_CODE); // true } ``` Pixel-level cleaning needs pixel decode and is out of scope (a future `@cosyte/dicom-pixel`). The adapter warns rather than giving a false sense of safety. ## Known limitations - **Metadata only**: pixels are never inspected; a burned-in-annotation hazard is *flagged*, never cleaned. The two pixel options of CID 7050 (`113101` Clean Pixel Data, `113102` Clean Recognizable Visual Features) are therefore always declared **withheld** and are never written to `(0012,0064)`. - **No Retain/Clean deviations**: the full Basic Profile always applies (maximal removal). Expert-Determination retain options are **not** offered, so every Annex E option is declared withheld. - **Replacement-UID referential integrity is guaranteed within one call** unless you supply a shared `uidMap`, in which case it reaches every call that shares it. The declared scope is on the result. --- URL: https://docs.cosyte.com/deid/guides-longitudinal Markdown: https://docs.cosyte.com/deid/guides-longitudinal.md # Longitudinal de-identification Research and analytics need a de-identified record that stays **linkable**: the same patient across a whole corpus of documents must map to the same pseudonyms and the same shifted dates, so a longitudinal history holds together, while absolute calendar positions and real identifiers are gone. This is the job of the **registry** (`createDeidRegistry`). > **Honesty note.** Date-shifting **retains dates in shifted form**, so a shifted-but-real date is still > "an element of a date" under 45 CFR §164.514(b)(2)(i)(C). A keyed surrogate of an identifier is > likewise **derived from information about the individual**, so it is not a §164.514(c)(1) code and > the (R) exception does not reach it. Both are therefore **Expert-Determination-supporting** > techniques, **not** Safe Harbor. The library enforces this: a policy carrying the `safe-harbor` > label may carry **neither** (see below), and it is refused with a typed fatal naming the offending > category and transform. Under Safe Harbor, dates are generalized to year and the medical record, > health plan beneficiary and account numbers are removed instead. Every keyed surrogate a > non-Safe-Harbor preset does emit is flagged `reidentificationCode` in the manifest and inventoried > in the Expert-Determination support report. ## Cross-document consistency A `DeidRegistry` holds the consumer's key and mints a per-patient context on demand. The same patient key always yields the same deterministic date-shift offset, so a patient's dates shift identically across every document: intervals (`3 days later` stays 3 days later) are preserved exactly. The same identifier maps to the same pseudonym corpus-wide. ```ts runnable import { createDeidRegistry, deidentify, defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const registry = createDeidRegistry({ key: "consumer-held-secret" }); const research = defineDeidPolicy({ name: "research", transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" }, }); // One patient scope, reused across every document belonging to that patient. const ctx = registry.forPatient("patient-1"); const shift = (date: string) => deidentify( { loci: [{ path: "PID-7", kind: "date", category: SAFE_HARBOR_CATEGORIES.DATES, value: date }] }, { policy: research, context: ctx }, ).document.loci[0].value; // The same input date de-identifies to the same shifted value in every document (linkage preserved). shift("2020-03-01") === shift("2020-03-01"); // => true // The same MRN maps to the same pseudonym across the whole corpus. registry.pseudonym("MRN-1") === registry.pseudonym("MRN-1"); // => true ``` Use `registry.forPatient(patientKey)` once per patient and pass its context to `deidentify` (or any per-format adapter) for that patient's documents. Use `registry.pseudonym(id)` for a corpus-wide consistent surrogate of a standalone identifier, and `registry.remapUid(uid)` for opaque unique identifiers (study/series/instance UIDs, GUIDs) you thread across files. ## The key contract - **You supply the key.** The HMAC key (and an optional distinct date-shift seed) is the consumer's. It is held only inside the library and **never** appears in an output document, a manifest, or a thrown error: the registry redacts itself through every stringify channel. - **Fail closed, no weak default.** There is no built-in or default key. An absent or empty key is a fatal `DEID_NO_KEY`, never a silent fallback that would produce a re-identifiable surrogate. - **Rotation is intentional linkage breakage.** A new key deterministically produces *different* offsets and *different* pseudonyms, so a corpus de-identified under a rotated key **no longer links** to records made under the old key. Rotate to sever linkage; keep the key to preserve it. The library holds **no persistent key store**: key custody and lifetime are yours. ```ts runnable throws import { createDeidRegistry } from "@cosyte/deid"; // No key → DEID_NO_KEY. The library never falls back to a weak default. createDeidRegistry({ key: "" }); ``` A date-shifting policy may not claim the Safe Harbor label: the library rejects the mislabel rather than emit shifted real dates under a Safe Harbor claim: ```ts runnable throws import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; // A shifted real date is still a date element: this is Expert-Determination, not Safe Harbor. defineDeidPolicy({ name: "safe-harbor", transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" } }); ``` The same guard covers a keyed surrogate, which is derived from the individual's own value in exactly the same way. The fatal names the offending category and the offending transform: ```ts runnable throws import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; // A keyed surrogate of a medical record number is a re-identification code §164.514(c)(1) does not // permit, so it may not wear the label either. defineDeidPolicy({ name: "safe-harbor", transforms: { [SAFE_HARBOR_CATEGORIES.MRN]: "pseudonymize" } }); ``` ## What the limited-data-set preset keeps, and where it is stricter than the regulation The preset carries the three retention classes §164.514(e)(2) permits: the encounter dates, the encounter and order identifiers, and the postal address parts (e)(2)(ii) names. On an address that is the **town or city, the State and the WHOLE zip code**; the street address and every other geographic component are removed, and every kept part is recorded as a residual at its own component. The three-digit / `000` ZIP rule is §164.514(b)(2)(i)(B), Safe Harbor's, so a restricted-prefix ZIP is kept in full here and is still suppressed under Safe Harbor. The geographic allowance is honoured by the **HL7 v2 pass alone**: the C-CDA, FHIR, X12, NCPDP and DICOM adapters reduce an address exactly as Safe Harbor does. **On dates the preset is deliberately stricter than the regulation it is named for.** §164.514(e)(2) enumerates sixteen direct identifiers and **names no date at all**, so a limited data set may lawfully carry dates at full precision. This preset date-shifts them anyway: removing more than the regulation requires is always lawful, and the alternative would hand every existing consumer real patient dates on an upgrade. The preset says so in its own machine-readable description, so the choice is readable without reading this page: ```ts runnable import { LIMITED_DATA_SET_PROFILE, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; // Dates are STILL date-shifted, not carried at full precision. LIMITED_DATA_SET_PROFILE.policy.transforms[SAFE_HARBOR_CATEGORIES.DATES]; // => "date-shift" LIMITED_DATA_SET_PROFILE.requiresContext; // => true // And the preset's own description states the choice, and whose the data use agreement is. const described = LIMITED_DATA_SET_PROFILE.description; described.includes("deliberately STRICTER than"); // => true described.includes("which names no date and so permits full precision"); // => true described.includes("removing more by choice"); // => true described.includes("DATA USE AGREEMENT"); // => true described.includes("CONSUMER'S responsibility"); // => true described.includes("neither holds nor checks one"); // => true // The geographic allowance, and its one-adapter scope, are stated there too. described.includes("WHOLE zip code"); // => true described.includes("HL7 v2 pass ALONE"); // => true ``` --- URL: https://docs.cosyte.com/deid/guides-freetext Markdown: https://docs.cosyte.com/deid/guides-freetext.md # Free text (block-by-default + BYO redaction) Structured PHI is tractable: the parser tells the engine exactly where a name, a date, or an MRN lives, and the adapter removes or transforms it. **Free text is different.** Narrative loci (C-CDA narrative ``, HL7 `OBX-5` / `NTE`, FHIR `note` / `div`, X12 `MSG` / `NTE`, NCPDP free text) can carry any of the 18 Safe Harbor categories in prose, and there is no structural handle on where the PHI is. `@cosyte/deid` treats free text as an honestly-hard problem and does **two** things with it. ## 1. The default is fail-closed (block) With no redactor supplied, every free-text locus is **blocked**: its value is withheld, never emitted. This is the safe baseline: a de-identifier that guessed at prose would create the _impression_ of safety while missing PHI it did not recognize. ```ts runnable import { deidentify, DEID_DISPOSITION_CODES, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const result = deidentify( { loci: [ { path: "OBX-5", kind: "freetext", category: SAFE_HARBOR_CATEGORIES.OTHER_UNIQUE_ID, value: "narrative with a name in it", }, ], }, {}, ); result.document.loci[0].value; // => null result.manifest[0].code === DEID_DISPOSITION_CODES.DEID_FREETEXT_BLOCKED; // => true ``` > **The library bundles no PHI detector.** It ships **no** NLP model and **no** built-in regex scrub: a > naive regex pass over clinical prose is a false-safety hazard. If you want free text redacted rather > than blocked, you bring the detector (below). ## 2. Bring your own redactor (BYO) Supply a `redactor`, a function wrapping your own regex/pattern engine or clinical-NER de-id model, and the engine invokes it at each free-text locus, writing its output back **in place** instead of blocking. This mirrors the parsers' BYO posture (the terminology adapter, the profile system): the library provides the interface and the orchestration; you provide the detector. ```ts runnable import { deidentify, DEID_DISPOSITION_CODES, type FreeTextRedactor } from "@cosyte/deid"; // Your detector. This toy example uses a pattern; a real one would be a clinical-NER pipeline. const redactor: FreeTextRedactor = ({ text }) => ({ text: text.replace(/\bJohn Doe\b/g, "[NAME]"), }); const result = deidentify( { loci: [{ path: "NTE-3", kind: "freetext", value: "seen by John Doe today" }] }, { redactor }, ); result.document.loci[0].value; // => "seen by [NAME] today" result.manifest[0].code === DEID_DISPOSITION_CODES.DEID_FREETEXT_CONSUMER_REDACTED; // => true ``` The manifest records a BYO-redacted locus with the `byo-redact` transform and the `DEID_FREETEXT_CONSUMER_REDACTED` disposition code, so the audit shows plainly that the free text was cleared by a **consumer-supplied** redactor, not by the library. ## The fail-closed contract (what the library guarantees) Whatever redactor you plug in, the engine still fails **closed** on any failure: a redactor is never allowed to leak free text through: - **No redactor** → the locus is blocked. - **The redactor throws** → the locus is blocked (the free text is not passed through). - **The redactor returns nothing** (`null` / `undefined`, or a result without a string `text`) → blocked. - **The redactor returns `{ text }`** → that prose is written back in place. ```ts runnable import { deidentify, DEID_DISPOSITION_CODES, type FreeTextRedactor } from "@cosyte/deid"; const flaky: FreeTextRedactor = () => { throw new Error("detector unavailable"); }; const result = deidentify( { loci: [{ path: "OBX-5", kind: "freetext", value: "prose that must not leak" }] }, { redactor: flaky }, ); result.document.loci[0].value; // => null result.manifest[0].code === DEID_DISPOSITION_CODES.DEID_FREETEXT_BLOCKED; // => true ``` ## The honesty boundary (what the library does NOT guarantee) When your redactor returns `{ text }`, the engine trusts it as **consumer-asserted**. It does **not** re-scan the returned prose for residual PHI, and it does **not** treat a redactor's "no findings" (returning the text unchanged) as an attestation. **A BYO redactor's completeness is your responsibility**: this is Expert-Determination territory, not a mechanical Safe Harbor guarantee. Two things are unchanged no matter what the redactor does: - **The structural PHI removal the format adapters perform is untouched.** The redactor handles the free _prose_ only; a name in `PID-5`, an MRN in a FHIR `Identifier`, a DOB in `DMG` are still located and transformed by the adapter exactly as before. - **The clinical over-scrub guard is untouched.** A structured clinical value (a lab result, a coded observation, a status) is still retained byte-identical: the redactor never sees it. The library's promise stays narrow and honest: a **fail-closed** free-text default, an **orchestrated** BYO redaction path whose completeness is the consumer's, and a **value-free manifest** that records which path each locus took, never the value. --- URL: https://docs.cosyte.com/deid/guides-expert-determination Markdown: https://docs.cosyte.com/deid/guides-expert-determination.md # Expert-Determination support (never certification) HIPAA gives two routes to de-identification (45 CFR §164.514(b)). `@cosyte/deid` **implements** Safe Harbor (the mechanical 18-category method) and **supports**, but can **never render**, the other one: **Expert Determination** (§164.514(b)(1)), a qualified statistician's judgment that the re-identification risk for a specific dataset and recipient is "very small". The support report structures the value-free manifest into the facts an expert reasons about (**what was done, and what remains**), and hands them over. It is descriptive input, never a verdict. ## The hard boundary: the library makes NO determination This is the load-bearing discipline. The report: - **never** says the output "is de-identified" or "meets Expert Determination"; - **never** computes or fabricates a re-identification **risk score**: it reaches no conclusion; - carries `determination: null` and leads with a prominent non-certification disclaimer. ```ts runnable import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport( [ { category: SAFE_HARBOR_CATEGORIES.NAMES, transform: "redact", locus: "PID-5", count: 1, disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false, }, ], { policy: "safe-harbor" }, ); report.determination; // => null report.disclaimer.includes("NOT a determination"); // => true ``` "The risk is very small" is a judgment about a dataset _and_ its recipient _and_ the other data reasonably available to that recipient. This library sees none of those three. Over-claiming here would be a real compliance harm, so the report is deliberately descriptive. ## What the report contains Feed it the manifest from any adapter (or an array of manifests for a corpus). You get: per-locus dispositions, coverage across all 18 Safe Harbor categories, a disposition roll-up, and **three residual inventories** - the retained-quasi-identifier one, the keyed-surrogate one, and the unexamined-position one. Pass `unexaminedResiduals` from the same pass to fill the third. ```ts runnable import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([ { category: SAFE_HARBOR_CATEGORIES.NAMES, transform: "redact", locus: "PID-5", count: 1, disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false, }, { category: SAFE_HARBOR_CATEGORIES.DATES, transform: "generalize", locus: "PID-7", count: 1, disposition: "transformed", code: "DEID_RESIDUAL_RETAINED", reidentificationCode: false, }, ]); report.categoryCoverage.length; // => 18 report.totals.categoriesActedOn; // => 2 report.retainedQuasiIdentifiers[0].locus; // => "PID-7" ``` The **retained-quasi-identifier inventory** is the residual an expert cares about most: the identifying elements the pass kept for utility and **recorded** as `DEID_RESIDUAL_RETAINED`. Two kinds land there: a coarse residual left by a generalization (a year-only date, a safe 3-digit ZIP prefix, an exact age ≤ 89), and a **whole unreduced value** a profile's retention set kept, such as the admission, discharge and service dates and the encounter and order numbers a limited-data-set preset carries. The second kind is the stronger residual, and it is inventoried here rather than left to a footnote. These are the §164.514(b)(2)(ii) actual-knowledge considerations. Clinical values retained untouched by the over-scrub guard are not identifiers and are not enumerated here. Neither is a position inside a retained structure that no locus rule reached: the pass examined no value there, so it left no residual of one. Those have their own inventory, below. ## The unexamined-position inventory: what the pass never looked at A **value-bearing position inside a structure the pass handed through that no locus rule names** is a third fact, and it is neither of the two above: the pass did not act on it, did not block it, and did not decide to keep it. Nothing reached it. Every adapter counts and locates those positions and returns them on `result.unexaminedResiduals`; hand that list to the report and it becomes an inventory beside the other two. Each record carries the **structural locus**, a **count** and the fact of being unexamined, and never a value, a key or an offset. It is deliberately not folded into the retained-quasi-identifier inventory: that one holds residuals of values the pass **examined**, and admitting a position nothing looked at would make a kept year and an unexamined field read as the same thing. **A measured zero is not silence, and the report says which it is.** Supplying the list, even empty, is what makes the inventory _measured_: the section then reads "measured, and empty". Omitting it leaves the count `null` and the section says **NOT MEASURED** in terms, because an empty inventory a determiner cannot tell apart from an unmeasured one is exactly the emptiness they would otherwise act on. ```ts runnable import { buildExpertDeterminationSupportReport } from "@cosyte/deid"; // Measured, and empty: the pass enumerated what it handed through and a rule reached every position. const measured = buildExpertDeterminationSupportReport([], { unexaminedResiduals: [] }); measured.unexaminedResidualsMeasured; // => true measured.dispositionSummary.unexaminedResidualPositions; // => 0 // Not measured at all: the report refuses to print a zero a reader would take for a clearance. const unmeasured = buildExpertDeterminationSupportReport([]); unmeasured.unexaminedResidualsMeasured; // => false unmeasured.dispositionSummary.unexaminedResidualPositions; // => null ``` **Counting is not removal.** Nothing is scrubbed or generalized because it was counted, and a position no rule examined has **no established Safe Harbor category**: it is credited to none of the 18 and moves no category total. A clinical code, a dose unit and an order status all sit at positions like these, so read the number as the size of what went unexamined, never as a count of PHI. **What the number is a count of.** The set of positions is derived from what each parser's model can actually carry, not from the places a value usually sits, so it includes carriers a reader might not think to ask about: XML character data that arrived as a CDATA section rather than as text, the comments and processing instructions a document is re-serialized with, a FHIR primitive's `_`-sibling element id, and whatever a partly rewritten structure keeps (a generalized address re-emits its state and country exactly as they arrived, along with anything riding inside them). The mirror also holds and matters as much for reading the number: a position the pass **removed** is not counted, because the inventory measures what left the pass untouched. The two edges of the count are stated in [Limitations](https://docs.cosyte.com/deid/limitations.md). ## The keyed-surrogate residual inventory: a different residual, kept apart A **keyed surrogate** is not a retained quasi-identifier and never joins that list. A retained quasi-identifier is a piece of the original value that survived; a keyed surrogate is a _replacement_ **derived** from the value under your key (`pseudonymize`, `hash`, `date-shift`). No plaintext survives, but the **linkage** does: anyone holding the key can re-link the records, which is why §164.514(c)(1) does not permit such a code under Safe Harbor and why the built-in Safe Harbor policy emits none. Folding the two together would tell a determiner that a re-identification code is the same kind of residual as a kept year, so each has its own section. Every locus the pass surrogated carries `reidentificationCode: true` in the manifest, and `report.keyedSurrogateResiduals` is built from that flag, carrying the locus, the category, the count and the transform that produced it - never a value, never the key, never the shift offset. An empty inventory means the pass emitted no keyed surrogate, never that one went unmeasured. ```ts runnable import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([ { category: SAFE_HARBOR_CATEGORIES.MRN, transform: "pseudonymize", locus: "PID-3", count: 1, disposition: "transformed", code: "DEID_CATEGORY_PSEUDONYMIZED", reidentificationCode: true, }, ]); report.keyedSurrogateResiduals[0].transform; // => "pseudonymize" report.keyedSurrogateResiduals[0].locus; // => "PID-3" report.retainedQuasiIdentifiers.length; // => 0 ``` ## The optional k-anonymity indicator: caller-supplied, descriptive only An expert often documents a k-anonymity indicator (the smallest equivalence-class size over a chosen quasi-identifier set). The library has no view of quasi-identifier **values**, so it never derives this itself. If _you_ group your records and supply the class sizes, the report echoes the arithmetic, and stamps it plainly as a descriptive input, **not** a risk score, **not** a determination, **not** a threshold the library evaluates. ```ts runnable import { buildExpertDeterminationSupportReport } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([], { quasiIdentifiers: { quasiIdentifierSet: "3-digit ZIP × birth year × sex", equivalenceClassSizes: [40, 33, 20, 5, 1, 1], }, }); report.quasiIdentifierStatistics?.minimumEquivalenceClassSize; // => 1 report.quasiIdentifierStatistics?.uniqueRecords; // => 2 report.quasiIdentifierStatistics?.note.includes("NOT a re-identification risk score"); // => true ``` With no class sizes supplied, there is no statistic: the library never invents a risk number from the manifest alone: ```ts runnable import { buildExpertDeterminationSupportReport } from "@cosyte/deid"; buildExpertDeterminationSupportReport([]).quasiIdentifierStatistics; // => null ``` ## Human-readable rendering For a document to hand a statistician, `formatExpertDeterminationSupportReport` renders the same value-free facts as Markdown, led by the non-certification banner. ```ts runnable import { buildExpertDeterminationSupportReport, formatExpertDeterminationSupportReport, } from "@cosyte/deid"; const md = formatExpertDeterminationSupportReport(buildExpertDeterminationSupportReport([])); md.startsWith("# Expert-Determination support report"); // => true md.includes("NOT A DETERMINATION"); // => true ``` The library's promise stays narrow and honest: it **transforms per a policy** and **evidences what it did and what remains** in a value-free report, so a qualified expert can make the determination the library cannot. --- URL: https://docs.cosyte.com/deid/api Markdown: https://docs.cosyte.com/deid/api.md # @cosyte/deid Public entry point for `@cosyte/deid`: a healthcare **de-identification** engine. `@cosyte/deid` is **not** a parser. It is a consumer-tier library: it applies a HIPAA-grounded de-identification **policy** (Safe Harbor by default) to a structurally-located model of a healthcare document and returns a transformed model plus a **value-free manifest** of what it acted on. It borrows the parser archetype's disciplines (typed diagnostics, immutable output, a policy/profile system) but **inverts the parser's reflex**: it **fails closed**, an unrecognized structure or an un-locatable identifier is blocked, never passed through as safe. **Honesty line (governs the whole library).** Results are **"Safe-Harbor-transformed per the configured policy"**, never "de-identified" and never "HIPAA-compliant". Safe Harbor is implemented mechanically; the §164.514(b)(2)(ii) actual-knowledge condition is the consumer's; Expert Determination (§164.514(b)(1)) is *supported*, never *rendered* or certified here. The root entry ships the **format-agnostic core**: the policy engine, the five transforms, the 18-category Safe Harbor model, the fail-closed rule, and the value-free manifest: tested against a generic locus model. The per-format locus maps (HL7 v2, C-CDA, FHIR, X12, NCPDP, DICOM) live behind the matching subpath exports. ## Classes ### DeidContext An opaque handle to the consumer's key material. Construct it with [createDeidContext](#createdeidcontext); it exposes **no** secret field and redacts itself through every stringify channel. #### Example ```ts import { createDeidContext } from "@cosyte/deid"; const ctx = createDeidContext({ key: "secret" }); JSON.stringify({ ctx }); // => '{"ctx":"[DeidContext:redacted]"}' ``` #### Constructors ##### Constructor > **new DeidContext**(): [`DeidContext`](#deidcontext) **`Internal`** Use [createDeidContext](#createdeidcontext). ###### Returns [`DeidContext`](#deidcontext) #### Methods ##### forPatient() > **forPatient**(`patientId`): [`DeidContext`](#deidcontext) Derive a sibling context scoped to a different patient, reusing the same key and seed. Used to de-identify a multi-patient corpus with one key while keeping each patient's date-shift offset distinct. ###### Parameters ###### patientId `string` The patient scope for the derived context. ###### Returns [`DeidContext`](#deidcontext) A new [DeidContext](#deidcontext) sharing this one's key/seed, scoped to `patientId`. ###### Example ```ts import { createDeidContext } from "@cosyte/deid"; const base = createDeidContext({ key: "secret" }); const forPatient = base.forPatient("patient-2"); ``` ##### toJSON() > **toJSON**(): `string` Redacts through `JSON.stringify`. ###### Returns `string` ##### toString() > **toString**(): `string` Redacts through `String(...)` and template interpolation. ###### Returns `string` *** ### DeidError The error thrown on a [FATAL\_CODES](#fatal_codes) condition. Carries a stable `code`; its `message` is safe to log: it **never** contains PHI (no value, no key, no offset). #### Example ```ts import { deidentify, DeidError, FATAL_CODES } from "@cosyte/deid"; try { deidentify(null as never, {}); } catch (err) { if (err instanceof DeidError && err.code === FATAL_CODES.EMPTY_INPUT) { // handle empty input } } ``` #### Extends - `Error` #### Constructors ##### Constructor > **new DeidError**(`code`, `message`): [`DeidError`](#deiderror) ###### Parameters ###### code [`FatalCode`](#fatalcode) The [FatalCode](#fatalcode) classifying this fatal. ###### message `string` A PHI-free explanation safe to log. ###### Returns [`DeidError`](#deiderror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`FatalCode`](#fatalcode) The stable fatal code. *** ### DeidRegistry The corpus-level consistency handle. Construct it with [createDeidRegistry](#createdeidregistry). It holds the consumer's key in a module-private registry: the handle exposes **no** secret field and redacts itself through every stringify channel. #### Example ```ts import { createDeidRegistry } from "@cosyte/deid"; const registry = createDeidRegistry({ key: "secret" }); // Same patient across documents shifts by the same offset: const a = registry.forPatient("patient-1"); const b = registry.forPatient("patient-1"); a === b; // => true (memoized, same handle, same offset) ``` #### Constructors ##### Constructor > **new DeidRegistry**(): [`DeidRegistry`](#deidregistry) **`Internal`** Use [createDeidRegistry](#createdeidregistry). ###### Returns [`DeidRegistry`](#deidregistry) #### Methods ##### forPatient() > **forPatient**(`patientKey`): [`DeidContext`](#deidcontext) Return the [DeidContext](#deidcontext) scoped to `patientKey`, minting and memoizing it on first use. The same key always yields the same context, hence the same deterministic date-shift offset, so a patient's dates shift consistently across every document in the corpus. Pass this context as `DeidOptions.context` to [deidentify](#deidentify) (or a per-format adapter) for that patient's documents. ###### Parameters ###### patientKey `string` A stable per-patient key (an MRN, an enterprise patient id, the consumer's choice, provided it is the same across that patient's documents). ###### Returns [`DeidContext`](#deidcontext) The patient-scoped, self-redacting context. ###### Example ```ts import { createDeidRegistry } from "@cosyte/deid"; const registry = createDeidRegistry({ key: "secret" }); const ctx = registry.forPatient("patient-1"); ``` ##### pseudonym() > **pseudonym**(`id`): `string` Map an identifier to its corpus-wide **consistent pseudonym**: a keyed-HMAC surrogate that is the same for the same input everywhere and not reversible without the key. Patient-independent: the same MRN maps to the same token regardless of which patient scope processes it, so records link. ###### Parameters ###### id `string` The identifier to pseudonymize (MRN, beneficiary number, account number). ###### Returns `string` The consistent lowercase-hex surrogate. ###### Example ```ts import { createDeidRegistry } from "@cosyte/deid"; const registry = createDeidRegistry({ key: "secret" }); registry.pseudonym("MRN-1") === registry.pseudonym("MRN-1"); // => true (consistent) ``` ##### remapUid() > **remapUid**(`uid`): `string` Map an opaque unique identifier (a DICOM study/series/instance UID, a GUID) to its corpus-wide **consistent surrogate**, so cross-document UID linkage survives de-identification. Domain-separated from [pseudonym](#pseudonym), so a UID and an identifier with the same text never share a surrogate. This is the format-agnostic linkage primitive; a format that owns UID *validity* (DICOM UIDs must be a valid `0.…` OID) handles that in its own adapter. ###### Parameters ###### uid `string` The unique identifier to remap. ###### Returns `string` The consistent lowercase-hex surrogate. ###### Example ```ts import { createDeidRegistry } from "@cosyte/deid"; const registry = createDeidRegistry({ key: "secret" }); registry.remapUid("1.2.840.113619.2.55") === registry.remapUid("1.2.840.113619.2.55"); // => true ``` ##### toJSON() > **toJSON**(): `string` Redacts through `JSON.stringify`. ###### Returns `string` ##### toString() > **toString**(): `string` Redacts through `String(...)` and template interpolation. ###### Returns `string` ## Interfaces ### CategoryCoverage Per-category coverage, for one of the 18 Safe Harbor categories (45 CFR §164.514(b)(2)(i)(A) to (R)), whether the pass acted on it and how. Present for **all 18** categories in the report (in regulatory order A→R), so a reader sees the categories **not** acted on as plainly as those that were. #### Example ```ts import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([ { category: SAFE_HARBOR_CATEGORIES.SSN, transform: "redact", locus: "PID-19", count: 1, disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false }, ]); const ssn = report.categoryCoverage.find((c) => c.category === SAFE_HARBOR_CATEGORIES.SSN); ssn?.actedOn; // => true ``` #### Properties ##### actedOn > `readonly` **actedOn**: `boolean` `true` when at least one manifest entry acted on this category. ##### category > `readonly` **category**: [`SafeHarborCategory`](#safeharborcategory) The Safe Harbor category. ##### codes > `readonly` **codes**: readonly [`DeidDispositionCode`](#deiddispositioncode)[] The distinct disposition codes recorded for this category, sorted. ##### dispositions > `readonly` **dispositions**: `Readonly`\<`Record`\<[`ReportDisposition`](#reportdisposition), `number`\>\> Count of acted-on values by disposition. ##### letter > `readonly` **letter**: `string` The §164.514(b)(2)(i) sub-paragraph letter (A to R). ##### number > `readonly` **number**: `number` The category ordinal (1 to 18). ##### residualRetained > `readonly` **residualRetained**: `boolean` `true` when a coarse identifying **residual** was retained for this category. ##### title > `readonly` **title**: `string` A short human title (no PHI). ##### totalCount > `readonly` **totalCount**: `number` Total count of values acted on across every locus of this category. ##### transforms > `readonly` **transforms**: readonly [`TransformName`](#transformname)[] The distinct transforms applied to this category, sorted. *** ### DeidContextSpec Specification for [createDeidContext](#createdeidcontext). #### Example ```ts import { createDeidContext } from "@cosyte/deid"; const ctx = createDeidContext({ key: "consumer-held-secret", patientId: "patient-1" }); ``` #### Properties ##### dateShiftSeed? > `readonly` `optional` **dateShiftSeed?**: `string` \| `Uint8Array`\<`ArrayBufferLike`\> A separate seed for deriving per-patient date-shift offsets. Defaults to the `key` when omitted, so date-shifting works out of the box; supply a distinct seed to decouple the two secrets. ##### key > `readonly` **key**: `string` \| `Uint8Array`\<`ArrayBufferLike`\> The HMAC key for keyed transforms. Consumer-held; never emitted. Must be non-empty. ##### maxShiftDays? > `readonly` `optional` **maxShiftDays?**: `number` Absolute bound (days) on the per-patient date-shift offset. Defaults to 365. ##### patientId? > `readonly` `optional` **patientId?**: `string` The patient scope this context de-identifies. Required for the deterministic date-shift offset. *** ### DeidDocument The transformed document: the same loci with de-identified values. Format-specific documents replace this `unknown`-shaped placeholder; the core returns this generic shape. #### Example ```ts import { type DeidDocument } from "@cosyte/deid"; const doc: DeidDocument = { loci: [] }; ``` #### Properties ##### loci > `readonly` **loci**: readonly [`TransformedLocus`](#transformedlocus)[] The transformed loci. *** ### DeidManifestEntry A single value-free manifest entry. #### Example ```ts import { type DeidManifestEntry, SAFE_HARBOR_CATEGORIES, DEID_DISPOSITION_CODES } from "@cosyte/deid"; const entry: DeidManifestEntry = { category: SAFE_HARBOR_CATEGORIES.SSN, transform: "redact", locus: "PID-19", count: 1, disposition: "removed", code: DEID_DISPOSITION_CODES.DEID_CATEGORY_REMOVED, reidentificationCode: false, }; ``` #### Properties ##### category > `readonly` **category**: [`SafeHarborCategory`](#safeharborcategory) The Safe Harbor category acted on. ##### code > `readonly` **code**: [`DeidDispositionCode`](#deiddispositioncode) The stable disposition code. ##### count > `readonly` **count**: `number` How many values at this locus/category/disposition were acted on. ##### disposition > `readonly` **disposition**: `"transformed"` \| `"removed"` \| `"blocked"` \| `"retained"` The disposition. `retained` is the honest fourth outcome, and it covers two different kept things, told apart by the entry's `code`: - an **identifying** locus the configured profile's retention set deliberately kept unchanged, paired with `DEID_RESIDUAL_RETAINED`, so a kept identifier reaches the determiner's residual inventory rather than being invisible; - a **party** whose role places it outside §164.514(b)(2)(i)'s scope clause, paired with `DEID_PARTY_ROLE_RETAINED` and the [partyRole](#partyrole) code that placed it there. That is not a residual of the individual's identity and does not join the residual inventory. (Clinical values kept by the over-scrub guard are **not** identifiers and are not manifest entries at all.) ##### locus > `readonly` **locus**: `string` The format-neutral locus (segment/field index · path · tag), **never** a value. ##### partyRole? > `readonly` `optional` **partyRole?**: `string` The **role code** that placed a party outside §164.514(b)(2)(i)'s scope clause, present **only** on a `DEID_PARTY_ROLE_RETAINED` entry. It answers, at the party's own locus, the question a silent retention could not: *on what did the pass decide this party is not the individual, a relative, an employer or a household member?* Value-free and **additive**, like [reidentificationCode](#reidentificationcode): it is a code from a role table this library ships (an X12 element 98 entity-identifier code; the role an HL7 v2.5.1 field definition types at an organisation-typed position), never a name, an identifier, an address or any other value read off the document, and no other field of the entry changes on account of it. ##### reidentificationCode > `readonly` **reidentificationCode**: `boolean` `true` exactly at a locus where the pass emitted a **keyed surrogate**: the output of `pseudonymize`, `hash` or `date-shift`, which is computed from the value it replaced and is therefore a **re-identification code** anyone holding the key can reverse the linkage of. `false` at every other locus. It is value-free (a boolean; never a value, a key or a shift offset) and **additive**: no other field of this entry changes on account of it, so a consumer branching on `transform`, `disposition` or `code` reads exactly what it read before. It is what the Expert-Determination support report's **keyed-surrogate residual inventory** is built from, so an expert can reason about the residual a surrogate leaves rather than discover it from the transform name. ##### transform > `readonly` **transform**: [`TransformName`](#transformname) The transform applied. *** ### DeidOptions Options for [deidentify](#deidentify). #### Example ```ts import { type DeidOptions, createDeidContext } from "@cosyte/deid"; const opts: DeidOptions = { policy: "safe-harbor", context: createDeidContext({ key: "secret" }) }; ``` #### Properties ##### context? > `readonly` `optional` **context?**: [`DeidContext`](#deidcontext) The context carrying the consumer's key, required only when the policy uses a keyed transform. ##### policy? > `readonly` `optional` **policy?**: [`DeidPolicy`](#deidpolicy) \| `"safe-harbor"` The policy to apply. Defaults to the built-in Safe Harbor policy. ##### redactor? > `readonly` `optional` **redactor?**: [`FreeTextRedactor`](#freetextredactor) A **consumer-supplied** free-text redactor. When present, the engine invokes it at each free-text locus instead of blocking, and records its output as **consumer-asserted** (`DEID_FREETEXT_CONSUMER_REDACTED`). The library bundles **no** redactor. **Fail-closed contract:** when this is omitted, or when the redactor throws or returns nothing, the free-text locus is **blocked** (the safe default), never emitted un-redacted. A returned redaction is trusted as the consumer's; the engine does not re-verify it and does not touch the structural PHI the adapters remove. See [FreeTextRedactor](#freetextredactor). ##### retainedLoci? > `readonly` `optional` **retainedLoci?**: readonly [`RetainedLocusClass`](#retainedlocusclass)[] The **retention classes** the configured profile permits: the named groups of identifying loci a format adapter may pass through unchanged (each one still recorded as a residual). Build this with [profileOptions](#profileoptions) rather than by hand; the widen-never-narrow contract on a derived profile is what keeps a site preset from adding one. **Absent or empty retains nothing**, so a bare options bag gets the strict treatment. *** ### DeidPolicy A de-identification policy: a name plus a per-category transform assignment covering all 18 categories. #### Example ```ts import { SAFE_HARBOR_POLICY } from "@cosyte/deid"; SAFE_HARBOR_POLICY.name; // => "safe-harbor" ``` #### Properties ##### name > `readonly` **name**: `string` The policy name: surfaced in output labelling ("Safe-Harbor-transformed per the configured policy"). ##### transforms > `readonly` **transforms**: `Readonly`\<`Record`\<[`SafeHarborCategory`](#safeharborcategory), [`TransformName`](#transformname)\>\> The transform applied to each Safe Harbor category. *** ### DeidPolicySpec The spec accepted by [defineDeidPolicy](#definedeidpolicy): a name and a **partial** transform map that overrides the Safe Harbor defaults for the categories it names. #### Example ```ts import { type DeidPolicySpec, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const spec: DeidPolicySpec = { name: "research", transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" }, }; ``` #### Properties ##### name > `readonly` **name**: `string` The policy name. ##### transforms? > `readonly` `optional` **transforms?**: `Partial`\<`Readonly`\<`Record`\<[`SafeHarborCategory`](#safeharborcategory), [`TransformName`](#transformname)\>\>\> Per-category transform overrides; unlisted categories keep their Safe Harbor default. *** ### DeidProfile A reusable, named de-identification preset: a policy plus its honest usage posture and an optional default free-text redactor. #### Example ```ts import { SAFE_HARBOR_PROFILE } from "@cosyte/deid"; SAFE_HARBOR_PROFILE.name; // => "safe-harbor" ``` #### Properties ##### description > `readonly` **description**: `string` A one-line honest description of what the profile does and does **not** guarantee: surfaced in docs and tooling so a preset is never adopted without its caveats. ##### name > `readonly` **name**: `string` The profile name (also the policy name). ##### policy > `readonly` **policy**: [`DeidPolicy`](#deidpolicy) The concrete policy the engine applies. ##### redactor? > `readonly` `optional` **redactor?**: [`FreeTextRedactor`](#freetextredactor) An optional default free-text redactor the profile carries into [profileOptions](#profileoptions). ##### requiresContext > `readonly` **requiresContext**: `boolean` Whether the profile **requires** a keyed per-patient [DeidContext](#deidcontext) to run at all (true when any category uses a keyed transform such as `date-shift` on a category that is always present). ##### retainedLoci > `readonly` **retainedLoci**: readonly [`RetainedLocusClass`](#retainedlocusclass)[] The **retention classes** this profile permits: named groups of *identifying* loci a format adapter passes through unchanged, each one still recorded as a `DEID_RESIDUAL_RETAINED` residual. Empty on [SAFE\_HARBOR\_PROFILE](#safe_harbor_profile); [LIMITED\_DATA\_SET\_PROFILE](#limited_data_set_profile) carries the three classes §164.514(e)(2) permits a limited data set to keep. A profile that DECLARES the `limited-data-set` standard is checked against that regulation's own list, so a class beyond it is a fatal [FATAL\_CODES.DEID\_PROFILE\_INVALID](#deid_profile_invalid) rather than an unsupported claim. ##### standard > `readonly` **standard**: [`DeidStandard`](#deidstandard) The standard this profile targets: governs how its output may honestly be described. *** ### DeidProfileSpec The spec accepted by [defineDeidProfile](#definedeidprofile): a name, an optional base profile (default [SAFE\_HARBOR\_PROFILE](#safe_harbor_profile)), per-category transform overrides, and an optional default redactor. #### Example ```ts import { type DeidProfileSpec, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const spec: DeidProfileSpec = { name: "site-a", transforms: { [SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]: "redact" }, // tighten: generalize -> redact }; ``` #### Properties ##### base? > `readonly` `optional` **base?**: [`DeidProfile`](#deidprofile) The base profile to derive from. Defaults to [SAFE\_HARBOR\_PROFILE](#safe_harbor_profile). ##### description? > `readonly` `optional` **description?**: `string` An optional human description; a default is synthesized from the base when omitted. ##### name > `readonly` **name**: `string` The profile name. Must not be a reserved standard label unless it genuinely matches it. ##### redactor? > `readonly` `optional` **redactor?**: [`FreeTextRedactor`](#freetextredactor) An optional default free-text redactor the derived profile carries. ##### retainedLoci? > `readonly` `optional` **retainedLoci?**: readonly [`RetainedLocusClass`](#retainedlocusclass)[] The retention classes the derived profile permits. Must be a **subset** of the base's: dropping a class removes more (a widening, allowed), adding one keeps more (a narrowing, rejected). Omit to inherit the base's set unchanged. ##### transforms? > `readonly` `optional` **transforms?**: `Partial`\<`Readonly`\<`Record`\<[`SafeHarborCategory`](#safeharborcategory), [`TransformName`](#transformname)\>\>\> Per-category transform overrides. Each may only move a category to an **equal-or-stronger** transform than the base (widen-never-narrow); a weakening override is rejected. *** ### DeidRegistrySpec Specification for [createDeidRegistry](#createdeidregistry). Mirrors [DeidContextSpec](#deidcontextspec) minus the per-patient scope: the registry mints patient scopes itself via [DeidRegistry.forPatient](#forpatient-1). #### Example ```ts import { createDeidRegistry } from "@cosyte/deid"; const registry = createDeidRegistry({ key: process.env.DEID_KEY! }); ``` #### Properties ##### dateShiftSeed? > `readonly` `optional` **dateShiftSeed?**: `string` \| `Uint8Array`\<`ArrayBufferLike`\> A separate seed for deriving per-patient date-shift offsets. Defaults to the `key` when omitted; supply a distinct seed to decouple pseudonymization from date-shifting. ##### key > `readonly` **key**: `string` \| `Uint8Array`\<`ArrayBufferLike`\> The HMAC key for keyed transforms. Consumer-held; never emitted. Must be non-empty. ##### maxShiftDays? > `readonly` `optional` **maxShiftDays?**: `number` Absolute bound (days) on each derived per-patient date-shift offset. Defaults to 365. *** ### DeidResult The immutable result of a de-id pass: the transformed document plus the value-free manifest. #### Example ```ts import { deidentify } from "@cosyte/deid"; const result = deidentify({ loci: [] }, {}); result.manifest; // => [] (nothing acted on) ``` #### Properties ##### document > `readonly` **document**: [`DeidDocument`](#deiddocument) The transformed document (the generic locus document from the core). ##### manifest > `readonly` **manifest**: readonly [`DeidManifestEntry`](#deidmanifestentry)[] The value-free audit of every action, in locus order. *** ### DispositionSummary A roll-up of how many values landed in each disposition across the whole report: the one-glance posture of the pass. Every field is a count; none is a value. #### Example ```ts import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([ { category: SAFE_HARBOR_CATEGORIES.SSN, transform: "redact", locus: "PID-19", count: 2, disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false }, ]); report.dispositionSummary.removed; // => 2 ``` #### Properties ##### blocked > `readonly` **blocked**: `number` Loci failed closed (blocked; value withheld). ##### freeTextBlocked > `readonly` **freeTextBlocked**: `number` Free-text loci blocked by default (`DEID_FREETEXT_BLOCKED`). ##### freeTextConsumerRedacted > `readonly` **freeTextConsumerRedacted**: `number` Free-text loci redacted by a **consumer-supplied** BYO redactor (`DEID_FREETEXT_CONSUMER_REDACTED`). ##### removed > `readonly` **removed**: `number` Values removed outright. ##### residualRetained > `readonly` **residualRetained**: `number` Of the transformed values, how many retained a coarse residual (`DEID_RESIDUAL_RETAINED`). ##### retained > `readonly` **retained**: `number` Loci the pass kept unchanged **and recorded**: identifying loci the profile's retention set kept (each one a residual below), plus parties whose role placed them outside §164.514(b)(2)(i)'s scope clause (each one naming the role code it was classified on, and none of them a residual). ##### transformed > `readonly` **transformed**: `number` Values replaced with a surrogate / generalized / shifted / hashed / BYO-redacted. ##### unexaminedResidualPositions > `readonly` **unexaminedResidualPositions**: `number` \| `null` **Value-bearing positions the pass handed through that no locus rule named** (`DEID_POSITION_UNEXAMINED`), or `null` when the pass reported **no measurement at all**. The `null` is the point: a bare `0` in a roll-up is read as "nothing was handed through unexamined", which is exactly the claim an unmeasured pass cannot make. A number here is a measurement; `null` says there is none to report. These positions are **not** counted in [transformed](#transformed), [removed](#removed), [blocked](#blocked) or [retained](#retained): no value was acted on, blocked or kept by a decision, so folding them into any of those four would overstate what the pass did. *** ### ExpertDeterminationReportOptions Options for [buildExpertDeterminationSupportReport](#buildexpertdeterminationsupportreport). #### Properties ##### policy? > `readonly` `optional` **policy?**: `string` \| [`DeidPolicy`](#deidpolicy) The policy applied (or its name): surfaced as the report's policy label. ##### quasiIdentifiers? > `readonly` `optional` **quasiIdentifiers?**: [`QuasiIdentifierClassInput`](#quasiidentifierclassinput) Consumer-supplied quasi-identifier equivalence-class sizes for the descriptive k-indicator. ##### unexaminedResiduals? > `readonly` `optional` **unexaminedResiduals?**: readonly [`UnexaminedResidual`](#unexaminedresidual)[] \| readonly readonly [`UnexaminedResidual`](#unexaminedresidual)[][] The **unexamined residual positions** a pass measured, as returned alongside its manifest by every format adapter. A single pass's list, or an array of them for a corpus, matching the shape of the `manifests` argument. **Supplying it, even empty, is what makes the inventory _measured_.** Omitting it leaves [ExpertDeterminationSupportReport.unexaminedResidualsMeasured](#unexaminedresidualsmeasured) `false` and the roll-up count `null`, so the report says plainly that nothing was counted rather than printing a zero a reader would take for a clearance. An empty array is the opposite claim, and a real one: the pass enumerated the positions it handed through and none went unexamined. *** ### ExpertDeterminationSupportReport The structured, value-free Expert-Determination **support** report. Machine-readable (this object) and human-readable (via [formatExpertDeterminationSupportReport](#formatexpertdeterminationsupportreport)). It **supports** a determination and is **never** one: `determination` is always `null` and [disclaimer](#disclaimer) leads. #### Example ```ts import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([ { category: SAFE_HARBOR_CATEGORIES.NAMES, transform: "redact", locus: "PID-5", count: 1, disposition: "removed", code: "DEID_CATEGORY_REMOVED", reidentificationCode: false }, ]); report.determination; // => null (the library never renders one) report.totals.categoriesActedOn; // => 1 ``` #### Properties ##### categoryCoverage > `readonly` **categoryCoverage**: readonly [`CategoryCoverage`](#categorycoverage)[] Coverage for all 18 Safe Harbor categories, in regulatory order (A→R). ##### determination > `readonly` **determination**: `null` Always `null`: the library renders **no** determination. ##### disclaimer > `readonly` **disclaimer**: `string` The prominent non-certification statement ([EXPERT\_DETERMINATION\_DISCLAIMER](#expert_determination_disclaimer)). ##### dispositionSummary > `readonly` **dispositionSummary**: [`DispositionSummary`](#dispositionsummary) The disposition roll-up. ##### documentCount > `readonly` **documentCount**: `number` How many documents' manifests this report summarizes (1 for a single manifest). ##### keyedSurrogateResiduals > `readonly` **keyedSurrogateResiduals**: readonly [`KeyedSurrogateResidual`](#keyedsurrogateresidual)[] The **keyed-surrogate residual inventory**: every locus whose manifest entry carries `reidentificationCode`, so a determiner can reason about the linkage a keyed surrogate preserves. A sibling of [retainedQuasiIdentifiers](#retainedquasiidentifiers), never folded into it. ##### kind > `readonly` **kind**: `"expert-determination-support"` A stable discriminant for the report shape. ##### outputLabel > `readonly` **outputLabel**: `string` The output label the pass applied: "Safe-Harbor-transformed per the configured policy". ##### perLocus > `readonly` **perLocus**: readonly [`DeidManifestEntry`](#deidmanifestentry)[] Every acted-on locus, aggregated and in a deterministic order: the value-free manifest, structured. ##### policy > `readonly` **policy**: `string` \| `null` The policy name applied, or `null` if not supplied. ##### quasiIdentifierStatistics > `readonly` **quasiIdentifierStatistics**: [`QuasiIdentifierStatistics`](#quasiidentifierstatistics-1) \| `null` Descriptive quasi-identifier statistics, **only** when the consumer supplied class sizes; else `null`. ##### retainedQuasiIdentifiers > `readonly` **retainedQuasiIdentifiers**: readonly [`RetainedQuasiIdentifier`](#retainedquasiidentifier)[] The retained-quasi-identifier residual inventory (coarse residuals the pass recorded as retained). ##### totals > `readonly` **totals**: `object` Headline totals. ###### categoriesActedOn > `readonly` **categoriesActedOn**: `number` How many of the 18 Safe Harbor categories were acted on. ###### loci > `readonly` **loci**: `number` Count of **distinct acted-on loci** (distinct locus paths across the whole report). ###### rows > `readonly` **rows**: `number` Count of aggregated `perLocus` rows (distinct category·transform·locus·disposition·code tuples). ##### unexaminedResiduals > `readonly` **unexaminedResiduals**: readonly [`UnexaminedResidual`](#unexaminedresidual)[] The **unexamined-residual inventory**: every value-bearing position the pass handed through that no locus rule named, with its structural locus and a count. A **sibling** of [retainedQuasiIdentifiers](#retainedquasiidentifiers), never folded into it: that inventory holds residuals of values the pass **examined**, and admitting an unexamined position would make a kept year and a position nothing looked at indistinguishable. Empty when the pass measured none **and** when nothing was measured at all: read [unexaminedResidualsMeasured](#unexaminedresidualsmeasured) to tell those apart. ##### unexaminedResidualsMeasured > `readonly` **unexaminedResidualsMeasured**: `boolean` `true` when the pass **reported a measurement** of its unexamined residual positions, whatever the count. `false` says no measurement was supplied, so an empty [unexaminedResiduals](#unexaminedresiduals-1) is silence rather than a measured zero, and the determiner is told which it is instead of inferring it. *** ### FreeTextRedactionRequest The value-bearing request the engine hands a [FreeTextRedactor](#freetextredactor) at each free-text locus. This is the **one** place a value crosses into consumer code by design: it never reaches the manifest. #### Example ```ts import { type FreeTextRedactionRequest } from "@cosyte/deid"; const request: FreeTextRedactionRequest = { text: "patient prose…", locus: "OBX-5" }; ``` #### Properties ##### category? > `readonly` `optional` **category?**: [`SafeHarborCategory`](#safeharborcategory) The Safe Harbor category associated with the locus, when the adapter could classify it. ##### locus > `readonly` **locus**: `string` The format-neutral locus path (e.g. `"OBX-5"`, `"section/text"`): value-free, for routing/logging. ##### text > `readonly` **text**: `string` The free-text prose at the locus, for the consumer's redactor to de-identify. *** ### FreeTextRedactionResult The successful result of a [FreeTextRedactor](#freetextredactor): the redacted prose to write back in place. To **decline** a locus (so the engine fails closed and blocks it), a redactor returns `null` / `undefined` or throws: it never returns un-redacted text expecting the engine to catch a miss. #### Example ```ts import { type FreeTextRedactionResult } from "@cosyte/deid"; const result: FreeTextRedactionResult = { text: "patient [REDACTED]…" }; ``` #### Properties ##### text > `readonly` **text**: `string` The redacted prose. May be an empty string (all prose removed); that is a valid redaction. *** ### GeneralizeOutcome The outcome of a generalization: the reduced value plus whether it retains a coarse **residual** (a kept year, a retained safe 3-digit ZIP prefix) that the manifest must surface for the actual-knowledge test, versus a fully non-identifying result (`000`, `90+`). #### Properties ##### residual > `readonly` **residual**: `boolean` `true` when a coarse identifying residual was retained; `false` when fully suppressed. ##### value > `readonly` **value**: `string` The generalized value. *** ### GenericLocus A single structurally-located candidate value. `category` is omitted when the caller cannot classify the locus: an unclassified PHI-bearing locus is treated as catch-all (R) and **fails closed**. #### Example ```ts import { type GenericLocus, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const locus: GenericLocus = { path: "PID-5", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.NAMES, value: "SENTINEL_NAME_01", }; ``` #### Properties ##### category? > `readonly` `optional` **category?**: [`SafeHarborCategory`](#safeharborcategory) The Safe Harbor category, when known. Omit to force fail-closed handling as category (R). ##### kind > `readonly` **kind**: [`LocusKind`](#locuskind) The kind of value at this locus. ##### partyRole? > `readonly` `optional` **partyRole?**: `string` The **role code** an adapter classified a *party* on, present **only** at a locus that records a party whose role places it **outside** §164.514(b)(2)(i)'s scope clause and whose name and identifiers the pass therefore left in place. The engine records the code in the manifest at this locus (`DEID_PARTY_ROLE_RETAINED`) and writes nothing back, so a retention that used to be silent can be audited. It is a **code, never a value**: an adapter sets it from its own committed role table (see `classifyPartyRole`), and a locus carrying it must carry an empty [value](#value-1): a party-role record has no value to hand anywhere. A non-empty one **fails closed** (blocked) rather than being retained, so this marker can never become a route for passing an identifier through. ##### path > `readonly` **path**: `string` The format-neutral path to the value. Recorded in the manifest; **never** a value. ##### retainedPart? > `readonly` `optional` **retainedPart?**: [`RetainedLocusPart`](#retainedlocuspart) The **named part** of an otherwise-excluded category this locus carries, present only where the regulation's exclusion is **partial**. §164.514(e)(2)(ii) is the one such clause: it removes postal address information "other than town or city, State, and zip code", so a locus addressing one of those three parts is marked here and the rest of the address is not marked at all. Like [retention](#retention) it is a *proposal*: the engine keeps the value only if the options list the class **and** [isRetainablePart](#isretainablepart) accepts the class, the resolved category and this part together. It is the **only** route past the never-retainable category guard, and it does not widen that guard: `GEOGRAPHIC` remains a category no profile may retain whole. **An adapter that sets this must address ONE part**, at its own structural locus (a component, not the whole address), because the engine keeps exactly what the locus carries. Marking a whole address with a part name would keep the whole address, which is the failure this field exists to make impossible to reach by accident rather than one it can detect. ##### retention? > `readonly` `optional` **retention?**: [`RetainedLocusClass`](#retainedlocusclass) The **retention class** this locus belongs to, when an adapter can name one. It is a *proposal*, never a decision: the engine keeps the value only if the configured options **also** list this class, **and** the resolved category is one a limited data set may carry at all. Both keys are required, so an adapter cannot retain anything on its own and a stale marker cannot leak a value. Absent (the default) the locus takes its normal policy transform. This is **not** the over-scrub guard: a `clinical` locus is not an identifier and is retained without a manifest row, whereas a locus marked here **is** identifying and is always recorded when it is kept. ##### value > `readonly` **value**: `string` The value at the locus. Consumed by the engine, never copied into the manifest. *** ### KeyedSurrogateResidual One entry in the **keyed-surrogate residual inventory**: a locus where the pass replaced the value with a surrogate **derived from that value under the consumer's key** (`pseudonymize`, `hash` or `date-shift`), which the manifest marks with `reidentificationCode`. Anyone holding the key can re-link those records, so §164.514(c)(1) does not permit such a code under Safe Harbor and the residual is an **Expert-Determination** consideration. This inventory is a **sibling** of [RetainedQuasiIdentifier](#retainedquasiidentifier), never a member of it: a retained quasi-identifier is a piece of the original value that survived (a year, a safe 3-digit ZIP prefix, a whole kept date), while this is a computed replacement that carries no plaintext but preserves linkage. Carrying `transform` on every row is what keeps the two distinguishable at a glance. Value-free, like everything else here: the locus, the category, the count and the transform, never a value, a key or a shift offset. #### Example ```ts import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([ { category: SAFE_HARBOR_CATEGORIES.MRN, transform: "pseudonymize", locus: "PID-3", count: 1, disposition: "transformed", code: "DEID_CATEGORY_PSEUDONYMIZED", reidentificationCode: true }, ]); report.keyedSurrogateResiduals[0]?.transform; // => "pseudonymize" ``` #### Properties ##### category > `readonly` **category**: [`SafeHarborCategory`](#safeharborcategory) The Safe Harbor category the surrogate stands in for. ##### count > `readonly` **count**: `number` How many values at this locus were replaced by a keyed surrogate. ##### locus > `readonly` **locus**: `string` The format-neutral locus (segment/field index · path · tag), **never** a value. ##### transform > `readonly` **transform**: [`TransformName`](#transformname) Which keyed transform produced it: `pseudonymize`, `hash` or `date-shift`. *** ### LocusModel The format-agnostic input model: a flat list of located candidate values. A per-format adapter produces this from a parsed HL7 / C-CDA / FHIR / X12 / NCPDP / DICOM model. #### Example ```ts import { type LocusModel } from "@cosyte/deid"; const model: LocusModel = { loci: [{ path: "PID-3", kind: "identifier", value: "X" }] }; ``` #### Properties ##### loci > `readonly` **loci**: readonly [`GenericLocus`](#genericlocus)[] The located candidate values to de-identify. *** ### PartyRoleTable One format's two role-code lists. Both are the format's own committed vocabulary (X12 element 98 entity-identifier codes; the role an HL7 v2.5.1 field definition types at a party), never a value read off a document. #### Example ```ts import { type PartyRoleTable } from "@cosyte/deid"; const table: PartyRoleTable = { subject: new Set(["36"]), outside: new Set(["85"]) }; ``` #### Properties ##### outside > `readonly` **outside**: `ReadonlySet`\<`string`\> Role codes that place the party outside the clause: provider / facility / payer / payee / submitter. ##### subject > `readonly` **subject**: `ReadonlySet`\<`string`\> Role codes the scope clause reaches: the individual, a relative, an **employer**, a household member. *** ### QuasiIdentifierClassInput **Consumer-supplied** quasi-identifier equivalence-class data. The library never derives this: it has no view of the quasi-identifier **values**. The consumer, who holds the values, groups their records by the chosen quasi-identifier set (e.g. 3-digit ZIP × birth year × sex) and supplies the size of each distinct group; the report echoes descriptive counts over those sizes (see [QuasiIdentifierStatistics](#quasiidentifierstatistics-1)). #### Example ```ts import { type QuasiIdentifierClassInput } from "@cosyte/deid"; // Consumer grouped 100 records into classes of these sizes over their chosen quasi-identifier set: const qi: QuasiIdentifierClassInput = { quasiIdentifierSet: "3-digit ZIP × birth year × sex", equivalenceClassSizes: [40, 33, 20, 5, 1, 1], }; ``` #### Properties ##### equivalenceClassSizes > `readonly` **equivalenceClassSizes**: readonly `number`[] One size per distinct quasi-identifier combination: the record count in that equivalence class. ##### quasiIdentifierSet? > `readonly` `optional` **quasiIdentifierSet?**: `string` A human label for the quasi-identifier set the sizes were computed over. No PHI. *** ### QuasiIdentifierStatistics Descriptive statistics over **consumer-supplied** equivalence-class sizes, including the smallest class size, the widely-used **k-anonymity indicator**. **This is not a risk score and not a determination.** It is arithmetic over sizes the consumer supplied: the library counts what it is given, applies no threshold, draws no `k ≥ n ⇒ safe` conclusion, and emits no verdict. A statistician documents an indicator like this *as one input* to a §164.514(b)(1) determination: the determination remains theirs. See [note](#note). #### Example ```ts import { buildExpertDeterminationSupportReport } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([], { quasiIdentifiers: { equivalenceClassSizes: [40, 20, 5, 1, 1] }, }); report.quasiIdentifierStatistics?.minimumEquivalenceClassSize; // => 1 report.quasiIdentifierStatistics?.uniqueRecords; // => 2 ``` #### Properties ##### distinctCombinations > `readonly` **distinctCombinations**: `number` Number of distinct quasi-identifier combinations = the number of equivalence classes supplied. ##### minimumEquivalenceClassSize > `readonly` **minimumEquivalenceClassSize**: `number` The smallest equivalence-class size: the **k-anonymity indicator**. Descriptive only. ##### note > `readonly` **note**: `string` The honesty note: this is a descriptive input, never a risk score or determination. ##### quasiIdentifierSet > `readonly` **quasiIdentifierSet**: `string` \| `null` The label the consumer gave the quasi-identifier set, or `null` if unlabelled. ##### totalRecords > `readonly` **totalRecords**: `number` Total records across all classes = the sum of the supplied sizes. ##### uniqueRecords > `readonly` **uniqueRecords**: `number` How many records fall in a class of size 1 (sample-uniques on the chosen set). Descriptive only. *** ### RetainedQuasiIdentifier One entry in the **retained-quasi-identifier residual inventory**: an identifying element the pass **deliberately kept** for analytic utility and **recorded** as `DEID_RESIDUAL_RETAINED`. Two things land here: - a **coarse residual** left by a generalization: a year-only date, a retained safe 3-digit ZIP prefix, an exact age ≤ 89; - a **whole value kept by the profile's retention set**: under a limited-data-set preset, an admission / discharge / service date or an encounter or order number, which a Safe-Harbor-labelled policy removes instead. These are exactly the residuals an expert reasons about under the §164.514(b)(2)(ii) actual-knowledge test, and the second kind is the stronger one: it is a full, unreduced value. **A keyed surrogate is NOT one of these and never joins this list.** It is a *replacement* computed from the value rather than a piece of the value that survived, so it is inventoried separately as a [KeyedSurrogateResidual](#keyedsurrogateresidual): the two kinds are different residuals and a determiner reasons about them differently. #### Example ```ts import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([ { category: SAFE_HARBOR_CATEGORIES.DATES, transform: "generalize", locus: "PID-7", count: 1, disposition: "transformed", code: "DEID_RESIDUAL_RETAINED", reidentificationCode: false }, ]); report.retainedQuasiIdentifiers[0]?.locus; // => "PID-7" ``` #### Properties ##### category > `readonly` **category**: [`SafeHarborCategory`](#safeharborcategory) The Safe Harbor category of the retained residual. ##### count > `readonly` **count**: `number` How many values at this locus retained a residual. ##### locus > `readonly` **locus**: `string` The format-neutral locus (segment/field index · path · tag), **never** a value. ##### transform > `readonly` **transform**: [`TransformName`](#transformname) How the residual arose, so the two kinds are not indistinguishable in the inventory: `generalize` left a coarse residual (a year, a safe 3-digit ZIP prefix, an age ≤ 89), while `retain` kept the **whole unreduced value**. A determiner reasons about those very differently. *** ### TransformedLocus The transformed value of a locus after a de-id pass: the reduced/surrogate `value`, or `null` when the value was removed or blocked (fail-closed). Carries no secret and no original value. #### Example ```ts import { type TransformedLocus } from "@cosyte/deid"; const t: TransformedLocus = { path: "PID-3", kind: "identifier", value: null, disposition: "removed" }; ``` #### Properties ##### disposition > `readonly` **disposition**: `"transformed"` \| `"removed"` \| `"blocked"` \| `"retained"` What happened to the value. ##### kind > `readonly` **kind**: [`LocusKind`](#locuskind) The kind of value at this locus (unchanged from input). ##### path > `readonly` **path**: `string` The format-neutral path (unchanged from input). ##### value > `readonly` **value**: `string` \| `null` The transformed value, or `null` when removed / blocked. *** ### UnexaminedResidual One **value-bearing position a pass handed through that no locus rule named**, counted and located in the value-free record. #### Example ```ts import { type UnexaminedResidual } from "@cosyte/deid"; const residual: UnexaminedResidual = { locus: "PV1-7", count: 1, examined: false, locusWithheld: false, code: "DEID_POSITION_UNEXAMINED", }; ``` #### Properties ##### code > `readonly` **code**: `"DEID_POSITION_UNEXAMINED"` The stable disposition code: always `DEID_POSITION_UNEXAMINED`. ##### count > `readonly` **count**: `number` How many value-bearing positions at this locus were handed through unexamined. ##### examined > `readonly` **examined**: `false` Always `false`: the fact this record exists to state. No locus rule reached the position, so the pass neither acted on it, nor blocked it, nor decided to keep it. A literal rather than a convention, so a consumer merging inventories cannot mistake one of these for an acted-on entry. ##### locus > `readonly` **locus**: `string` The format-neutral **structural locus** of the position (segment/field index · path · tag), or [WITHHELD\_LOCUS\_TOKEN](#withheld_locus_token) when it could not be expressed. **Never** a value, a key or an offset. ##### locusWithheld > `readonly` **locusWithheld**: `boolean` `true` when the position's structural locus **could not be expressed** and [locus](#locus-4) is therefore [WITHHELD\_LOCUS\_TOKEN](#withheld_locus_token). The position is still counted: an inexpressible "where" is never a reason to drop a "how many". Value-free (a boolean). ## Type Aliases ### DeidDispositionCode > **DeidDispositionCode** = *typeof* [`DEID_DISPOSITION_CODES`](#deid_disposition_codes)\[keyof *typeof* [`DEID_DISPOSITION_CODES`](#deid_disposition_codes)\] A value from [DEID\_DISPOSITION\_CODES](#deid_disposition_codes): the code every manifest entry carries. #### Example ```ts import { DEID_DISPOSITION_CODES, type DeidDispositionCode } from "@cosyte/deid"; const code: DeidDispositionCode = DEID_DISPOSITION_CODES.DEID_CATEGORY_REMOVED; ``` *** ### DeidStandard > **DeidStandard** = `"safe-harbor"` \| `"limited-data-set"` \| `"custom"` The named standard a profile targets, surfaced so output labelling can never overclaim. *** ### FatalCode > **FatalCode** = *typeof* [`FATAL_CODES`](#fatal_codes)\[keyof *typeof* [`FATAL_CODES`](#fatal_codes)\] A value from [FATAL\_CODES](#fatal_codes): the code carried by a thrown [DeidError](#deiderror). #### Example ```ts import { FATAL_CODES, type FatalCode } from "@cosyte/deid"; const code: FatalCode = FATAL_CODES.EMPTY_INPUT; ``` *** ### FreeTextRedactor > **FreeTextRedactor** = (`request`) => [`FreeTextRedactionResult`](#freetextredactionresult) \| `null` \| `undefined` A consumer-supplied free-text redactor. The engine invokes it at each free-text locus and treats a returned [FreeTextRedactionResult](#freetextredactionresult) as **consumer-asserted** (recorded as `DEID_FREETEXT_CONSUMER_REDACTED`). Returning `null` / `undefined`, or throwing, makes the engine **fail closed** and block the locus. The library never inspects the returned text for residual PHI; completeness is the consumer's responsibility. #### Parameters ##### request [`FreeTextRedactionRequest`](#freetextredactionrequest) The free-text value, its value-free locus path, and its category when known. #### Returns [`FreeTextRedactionResult`](#freetextredactionresult) \| `null` \| `undefined` The redacted prose as `{ text }`, or `null` / `undefined` to decline (engine blocks). #### Example ```ts import { deidentify, type FreeTextRedactor } from "@cosyte/deid"; // A consumer plugs in their own detector; the library bundles none. const redactor: FreeTextRedactor = ({ text }) => ({ text: myNerModel.scrub(text) }); const result = deidentify(model, { redactor }); ``` *** ### LocusKind > **LocusKind** = `"identifier"` \| `"date"` \| `"age"` \| `"zip"` \| `"freetext"` \| `"clinical"` \| `"unknown"` The kind of value at a locus: drives which generalization applies and whether the engine must fail closed. `clinical` is the over-scrub guard: a clinical value (a lab result, a dose, a code, a status) is **not** an identifier and must survive untouched. #### Example ```ts import { type LocusKind } from "@cosyte/deid"; const kind: LocusKind = "identifier"; ``` *** ### PartyRoleClassification > **PartyRoleClassification** = \{ `roleCode`: `string`; `scope`: `"safe-harbor-subject"` \| `"outside-scope"`; \} \| \{ `roleCode?`: `undefined`; `scope`: `"unknown"`; \} The outcome of [classifyPartyRole](#classifypartyrole). `roleCode` is present only when the role was **recognized**, and is then the matching member of the caller's own table: it carries no name, no identifier and no other value, so it is safe to record in the value-free manifest. #### Union Members ##### Type Literal \{ `roleCode`: `string`; `scope`: `"safe-harbor-subject"` \| `"outside-scope"`; \} ###### roleCode > `readonly` **roleCode**: `string` The matching member of the caller's table: a code, never a value. ###### scope > `readonly` **scope**: `"safe-harbor-subject"` \| `"outside-scope"` The role was recognized on one of the two lists. *** ##### Type Literal \{ `roleCode?`: `undefined`; `scope`: `"unknown"`; \} ###### roleCode? > `readonly` `optional` **roleCode?**: `undefined` Never carried for an unrecognized role: an unknown code is not this library's to echo. ###### scope > `readonly` **scope**: `"unknown"` The role is absent, empty, or on neither list: fail closed. #### Example ```ts import { classifyPartyRole, type PartyRoleClassification } from "@cosyte/deid"; const c: PartyRoleClassification = classifyPartyRole("85", { subject: new Set(["36"]), outside: new Set(["85"]), }); c.scope; // => "outside-scope" ``` *** ### PartyRoleScope > **PartyRoleScope** = `"safe-harbor-subject"` \| `"outside-scope"` \| `"unknown"` Where a party's role places it relative to §164.514(b)(2)(i): - `safe-harbor-subject`: the individual, a relative, an employer or a household member. Name and identifiers are transformed under the Safe Harbor categories. - `outside-scope`: a party the clause does not reach (provider / facility / payer / payee / submitter / receiver / clearinghouse). Name and identifiers are left in place. - `unknown`: the role could not be established. **Fails closed**, handled exactly like a subject. #### Example ```ts import { type PartyRoleScope } from "@cosyte/deid"; const scope: PartyRoleScope = "outside-scope"; ``` *** ### ReportDisposition > **ReportDisposition** = `"transformed"` \| `"removed"` \| `"blocked"` \| `"retained"` A manifest disposition: the four outcomes an acted-on locus can have. `retained` covers two kept things, told apart by the entry's code: an **identifying** locus the configured profile's retention set deliberately kept unchanged (`DEID_RESIDUAL_RETAINED`, which also appears in the residual inventory below), and a **party** whose role places it outside §164.514(b)(2)(i)'s scope clause (`DEID_PARTY_ROLE_RETAINED`, which does not: it is not a residual of the individual's identity). *** ### RetainedLocusClass > **RetainedLocusClass** = *typeof* [`RETAINED_LOCUS_CLASSES`](#retained_locus_classes)\[keyof *typeof* [`RETAINED_LOCUS_CLASSES`](#retained_locus_classes)\] A value from [RETAINED\_LOCUS\_CLASSES](#retained_locus_classes): the class a profile lists to keep, and an adapter checks before it passes a locus through. #### Example ```ts import { RETAINED_LOCUS_CLASSES, type RetainedLocusClass } from "@cosyte/deid"; const cls: RetainedLocusClass = RETAINED_LOCUS_CLASSES.ENCOUNTER_IDENTIFIERS; ``` *** ### RetainedLocusPart > **RetainedLocusPart** = *typeof* [`LIMITED_DATA_SET_ADDRESS_PARTS`](#limited_data_set_address_parts)\[keyof *typeof* [`LIMITED_DATA_SET_ADDRESS_PARTS`](#limited_data_set_address_parts)\] A value from [LIMITED\_DATA\_SET\_ADDRESS\_PARTS](#limited_data_set_address_parts): the **named part** of an otherwise-excluded category that an adapter proposes keeping, and that [isRetainablePart](#isretainablepart) checks. #### Example ```ts import { LIMITED_DATA_SET_ADDRESS_PARTS, type RetainedLocusPart } from "@cosyte/deid"; const part: RetainedLocusPart = LIMITED_DATA_SET_ADDRESS_PARTS.STATE; ``` *** ### SafeHarborCategory > **SafeHarborCategory** = *typeof* [`SAFE_HARBOR_CATEGORIES`](#safe_harbor_categories)\[keyof *typeof* [`SAFE_HARBOR_CATEGORIES`](#safe_harbor_categories)\] A value from [SAFE\_HARBOR\_CATEGORIES](#safe_harbor_categories): the type a policy and a manifest entry carry. #### Example ```ts import { SAFE_HARBOR_CATEGORIES, type SafeHarborCategory } from "@cosyte/deid"; const c: SafeHarborCategory = SAFE_HARBOR_CATEGORIES.SSN; ``` *** ### TransformName > **TransformName** = `"redact"` \| `"generalize"` \| `"date-shift"` \| `"pseudonymize"` \| `"hash"` \| `"block"` \| `"byo-redact"` \| `"retain"` The name of a transform a policy can assign to a category. `block` is the fail-closed action (withhold the value); `generalize` selects the correct generalization from the locus kind (date / ZIP / age). `byo-redact` is **not** a policy-assignable Safe Harbor transform and **not** something the library performs: it is the manifest marker the engine records when a **consumer-supplied free-text redactor** redacts a free-text locus. The library bundles no redactor; the consumer brings the detector. Assigning `byo-redact` to a category in a policy has no effect beyond the fail-closed default (the engine blocks it), because free-text redaction is driven by the `redactor` option, not by the per-category policy map. `retain` is likewise **not** policy-assignable: it is the manifest marker for a locus the profile's **retention set** deliberately kept unchanged, which is driven by that set and not by the per-category map. Assigning it to a category fails closed to a block, exactly like `byo-redact`. #### Example ```ts import { type TransformName } from "@cosyte/deid"; const t: TransformName = "pseudonymize"; ``` ## Variables ### DEID\_DISPOSITION\_CODES > `const` **DEID\_DISPOSITION\_CODES**: `object` **Disposition codes**: the value-free record of what the engine did at a locus. Every manifest entry carries exactly one. They describe the *action and its residual*, never the value acted on. #### Type Declaration ##### DEID\_CATEGORY\_DATE\_SHIFTED > `readonly` **DEID\_CATEGORY\_DATE\_SHIFTED**: `"DEID_CATEGORY_DATE_SHIFTED"` = `"DEID_CATEGORY_DATE_SHIFTED"` A date was shifted by a deterministic per-patient offset (interval-preserving). ##### DEID\_CATEGORY\_GENERALIZED > `readonly` **DEID\_CATEGORY\_GENERALIZED**: `"DEID_CATEGORY_GENERALIZED"` = `"DEID_CATEGORY_GENERALIZED"` A category was generalized to a fully non-identifying form (ZIP → `000`, age → `90+`). ##### DEID\_CATEGORY\_HASHED > `readonly` **DEID\_CATEGORY\_HASHED**: `"DEID_CATEGORY_HASHED"` = `"DEID_CATEGORY_HASHED"` A value was replaced by a keyed one-way digest (keyed hash). ##### DEID\_CATEGORY\_PSEUDONYMIZED > `readonly` **DEID\_CATEGORY\_PSEUDONYMIZED**: `"DEID_CATEGORY_PSEUDONYMIZED"` = `"DEID_CATEGORY_PSEUDONYMIZED"` A category was replaced by a consistent keyed-HMAC surrogate (pseudonymization). ##### DEID\_CATEGORY\_REMOVED > `readonly` **DEID\_CATEGORY\_REMOVED**: `"DEID_CATEGORY_REMOVED"` = `"DEID_CATEGORY_REMOVED"` A category was removed outright (redaction). ##### DEID\_FREETEXT\_BLOCKED > `readonly` **DEID\_FREETEXT\_BLOCKED**: `"DEID_FREETEXT_BLOCKED"` = `"DEID_FREETEXT_BLOCKED"` Fail-closed: a free-text locus was blocked by default (no naive regex scrub). ##### DEID\_FREETEXT\_CONSUMER\_REDACTED > `readonly` **DEID\_FREETEXT\_CONSUMER\_REDACTED**: `"DEID_FREETEXT_CONSUMER_REDACTED"` = `"DEID_FREETEXT_CONSUMER_REDACTED"` A free-text locus was redacted **by a consumer-supplied BYO redactor**, not by the library. The library ships **no** NLP/PHI-detection engine; it orchestrates the consumer's redactor at free-text loci and records the outcome here. This code is **consumer-asserted, never a library guarantee**: "no findings" from a BYO redactor is not an attestation, and a redactor's completeness is the consumer's responsibility (Expert-Determination territory). The structural PHI removal the format adapters perform is unaffected: this covers only the free *prose*. ##### DEID\_LOCUS\_BLOCKED > `readonly` **DEID\_LOCUS\_BLOCKED**: `"DEID_LOCUS_BLOCKED"` = `"DEID_LOCUS_BLOCKED"` Fail-closed: an unrecognized / un-locatable / uncertain locus was blocked (value withheld). ##### DEID\_PARTY\_ROLE\_RETAINED > `readonly` **DEID\_PARTY\_ROLE\_RETAINED**: `"DEID_PARTY_ROLE_RETAINED"` = `"DEID_PARTY_ROLE_RETAINED"` A **party** was left in place because the role its format types at that party puts it **outside** §164.514(b)(2)(i)'s scope clause (a treating clinician, a facility, a payer, a payee, a submitter, a receiver, a clearinghouse: not the individual, a relative, an employer or a household member). The entry names the party's structural locus and, in `partyRole`, the **role code the pass classified on**, so a retention decision that used to be silent can be audited. It carries no name, no identifier and no other value. It is deliberately **not** `DEID_RESIDUAL_RETAINED`: that code is a residual of the *individual's* own identity (a kept year, a safe ZIP prefix, a whole value a limited-data-set retention set kept) and feeds the determiner's retained-quasi-identifier inventory. A party outside the scope clause is a different fact and stays out of that inventory. The disposition-code set is additions-only. ##### DEID\_POSITION\_UNEXAMINED > `readonly` **DEID\_POSITION\_UNEXAMINED**: `"DEID_POSITION_UNEXAMINED"` = `"DEID_POSITION_UNEXAMINED"` A **value-bearing position inside a structure the pass handed through that no locus rule names**. The pass reached **no decision** there: it neither acted on the position, nor blocked it, nor decided to keep it. The record exists so that such a position is counted and located rather than passing through in silence. It is deliberately **not** `DEID_RESIDUAL_RETAINED`, and the difference is the whole point of the code: that one is a residual of a value the pass **examined** (a kept year, a safe 3-digit ZIP prefix, a whole value a retention class kept) and it feeds the determiner's retained-quasi-identifier inventory. This one is the opposite fact, an **unexamined** position, and it has its own inventory so a kept year and a position nothing looked at can never be read as the same thing. **It is a measurement, not an allegation.** An unexamined position is not thereby an identifier: a clinical code, a unit and a status all sit at positions no locus rule names. Nothing is scrubbed, removed or generalized on account of this record, and the position has **no established Safe Harbor category**, because no rule established one. The record carries the structural locus, a count and the fact of being unexamined: never a value, never a key, never an offset. The disposition-code set is additions-only. ##### DEID\_RESIDUAL\_RETAINED > `readonly` **DEID\_RESIDUAL\_RETAINED**: `"DEID_RESIDUAL_RETAINED"` = `"DEID_RESIDUAL_RETAINED"` A generalization retained a coarse residual (a kept year, a retained safe 3-digit ZIP prefix). Surfaced so a human can apply the §164.514(b)(2)(ii) actual-knowledge test with the facts present. #### Example ```ts import { DEID_DISPOSITION_CODES } from "@cosyte/deid"; DEID_DISPOSITION_CODES.DEID_LOCUS_BLOCKED; // => "DEID_LOCUS_BLOCKED" ``` *** ### EXPERT\_DETERMINATION\_DISCLAIMER > `const` **EXPERT\_DETERMINATION\_DISCLAIMER**: `string` The prominent non-certification statement. It is the first field a reader sees and is repeated at the head of the human-readable rendering. Its job is to make over-claiming impossible to do by accident: the report supports a determination, it is never the determination. #### Example ```ts import { EXPERT_DETERMINATION_DISCLAIMER } from "@cosyte/deid"; EXPERT_DETERMINATION_DISCLAIMER.includes("NOT a determination"); // => true ``` *** ### FATAL\_CODES > `const` **FATAL\_CODES**: `object` **Fatal codes**: conditions that abort a de-identification pass by throwing a [DeidError](#deiderror). The engine fails **closed**: it never silently degrades a fatal into a pass-through of PHI. #### Type Declaration ##### DEID\_CONTEXT\_INVALID > `readonly` **DEID\_CONTEXT\_INVALID**: `"DEID_CONTEXT_INVALID"` = `"DEID_CONTEXT_INVALID"` A [DeidContext](#deidcontext) was configured with an invalid parameter that would silently weaken de-identification: most importantly a `maxShiftDays` that floors to **0**, which pins **every** per-patient date-shift offset to zero, so a `date-shift` policy would emit the **original real dates** under a research label. A no-op shift is a leak, so the engine rejects the degenerate configuration at construction rather than silently shipping unshifted dates. The fatal set is additions-only. ##### DEID\_DECLARATION\_UNNAMEABLE > `readonly` **DEID\_DECLARATION\_UNNAMEABLE**: `"DEID_DECLARATION_UNNAMEABLE"` = `"DEID_DECLARATION_UNNAMEABLE"` A pass would apply a **profile or option that its published coding vocabulary cannot name**, so the machine-readable declaration it is required to write could only be an approximation. Fail closed on the declaration itself. A coded term is read by a downstream system as a property of the document and is acted on **without a human**, so an approximate code is worse than no output at all: it is a claim about what was removed that nobody re-checks, and a document released on a false coded claim cannot be un-released. A pass that cannot name what it did therefore returns nothing rather than a document stamped with a declaration that is not true of it. The message names the **profile or option** (a bounded structural token from the pass's own closed option set, never a value read from the document) and carries no value, no key and no offset. The fatal set is additions-only. ##### DEID\_FORMAT\_UNSUPPORTED > `readonly` **DEID\_FORMAT\_UNSUPPORTED**: `"DEID_FORMAT_UNSUPPORTED"` = `"DEID_FORMAT_UNSUPPORTED"` A caller handed an adapter a **document in a format that adapter refuses outright**, rather than the format its entry point de-identifies. Refusal, not best effort. An adapter reaches a document only through the parser surface its peer package publishes, and where that surface cannot express a faithful structural pass, a partial pass is a **false-safety hazard**: it would return a document a consumer reads as Safe-Harbor-transformed while positions the surface never modelled rode straight through it. Refusing is the fail-closed answer and it is the one a test can pin; a documented non-goal in prose is not, because prose is not a behaviour. The message names the **format** and the parser-surface reason, both fixed text this library owns. It carries no value read from the document, no key and no offset, and the pass returns no transformed document, no manifest and no partial output of any kind. The fatal set is additions-only. ##### DEID\_NO\_KEY > `readonly` **DEID\_NO\_KEY**: `"DEID_NO_KEY"` = `"DEID_NO_KEY"` A **keyed** transform (pseudonymize / keyed-hash / date-shift) was required for a category present in the model, but no key (or, for date-shift, no per-patient scope) was supplied. The engine **never** falls back to an unkeyed transform: an unkeyed hash of an identifier is re-identifiable. ##### DEID\_OUTPUT\_INVALID > `readonly` **DEID\_OUTPUT\_INVALID**: `"DEID_OUTPUT_INVALID"` = `"DEID_OUTPUT_INVALID"` The transformed document could not be re-serialized, or no longer round-trips through its own parser: what came back out of the writer is not what the reader reads. A de-identification pass that cannot vouch for the shape of its own output cannot vouch for what a downstream reader will make of the values inside it, so the pass fails rather than hand back a partially transformed document. The fatal set is additions-only. ##### DEID\_POLICY\_INVALID > `readonly` **DEID\_POLICY\_INVALID**: `"DEID_POLICY_INVALID"` = `"DEID_POLICY_INVALID"` A policy violates the key/label contract: whatever claims the reserved **`safe-harbor`** label, either as a policy **name** or as a profile's declared **standard**, assigns a category a transform whose output for that category is **derived from that category's own value**. A shifted-but-real date is still "an element of a date" under §164.514(b)(2)(i)(C), and a keyed surrogate of a medical record, beneficiary or account number is a code "derived from ... information about the individual", which §164.514(c)(1) does not permit and the (R) exception therefore does not reach. Both are Expert-Determination techniques, **not** Safe Harbor ones, and labelling either `safe-harbor` would misrepresent the residual risk. The refusal names the offending category and transform, carries no value / key / offset, and fires both at mint time and at point of use, so a hand-built policy object cannot slip past. It also covers an assignment that is not a published transform name at all: a pair whose derivation cannot be established is refused, never permitted. A policy that does **not** claim the label is entitled to its keyed surrogate and is applied unchanged. The fatal set is additions-only. ##### DEID\_POSITIONS\_UNENUMERABLE > `readonly` **DEID\_POSITIONS\_UNENUMERABLE**: `"DEID_POSITIONS_UNENUMERABLE"` = `"DEID_POSITIONS_UNENUMERABLE"` The **value-bearing positions of a structure the pass would hand through could not be enumerated**, so the pass cannot say how much of that structure it never examined. Fail closed on the measurement itself. The alternative outcomes are both worse than a failure: a count of **zero** would tell a determiner the structure held nothing unexamined, and a **partial** count would understate it, and either is read as a measurement rather than as a gap. A pass that cannot enumerate a structure therefore returns nothing at all rather than a number nobody can qualify. The message names the **structure** (a bounded structural token: a segment identifier, an element name, a tag) and carries no value, no key and no offset. The fatal set is additions-only. ##### DEID\_PROFILE\_INVALID > `readonly` **DEID\_PROFILE\_INVALID**: `"DEID_PROFILE_INVALID"` = `"DEID_PROFILE_INVALID"` A [DeidProfile](#deidprofile) spec violates the **widen-never-narrow** contract: a per-site profile derived from a base profile may only move a category to an **equal-or-stronger** transform (more removal, never less), and may never re-weaken a category the base scrubs. A profile that would *reduce* the de-identification strength of any category is rejected, so a site preset can only ever tighten, not quietly loosen, the base standard's protection. The fatal set is additions-only. ##### EMPTY\_INPUT > `readonly` **EMPTY\_INPUT**: `"EMPTY_INPUT"` = `"EMPTY_INPUT"` The input model was null/undefined or carried no locus list, nothing to de-identify. #### Example ```ts import { FATAL_CODES } from "@cosyte/deid"; FATAL_CODES.DEID_NO_KEY; // => "DEID_NO_KEY" ``` *** ### KEYED\_TRANSFORMS > `const` **KEYED\_TRANSFORMS**: `ReadonlySet`\<[`TransformName`](#transformname)\> The transforms that require the consumer's key (and, for `date-shift`, a per-patient scope). *** ### LIMITED\_DATA\_SET\_ADDRESS\_PARTS > `const` **LIMITED\_DATA\_SET\_ADDRESS\_PARTS**: `object` The **three parts of a postal address §164.514(e)(2)(ii) names as surviving** a limited data set, in the regulation's own words: "other than **town or city, State, and zip code**". Nothing else in an address is named, so nothing else survives: not the street, not a second address line, not the county or parish, not the census tract, not the country, not a birth place. #### Type Declaration ##### STATE > `readonly` **STATE**: `"state"` = `"state"` "State": the state or province the address sits in. ##### TOWN\_OR\_CITY > `readonly` **TOWN\_OR\_CITY**: `"town-or-city"` = `"town-or-city"` "town or city": the populated place, and nothing finer. ##### ZIP\_CODE > `readonly` **ZIP\_CODE**: `"zip-code"` = `"zip-code"` "zip code": the WHOLE zip code. The three-digit rule is Safe Harbor's, not (e)(2)'s. #### Example ```ts import { LIMITED_DATA_SET_ADDRESS_PARTS } from "@cosyte/deid"; LIMITED_DATA_SET_ADDRESS_PARTS.ZIP_CODE; // => "zip-code" ``` *** ### LIMITED\_DATA\_SET\_DIRECT\_IDENTIFIERS > `const` **LIMITED\_DATA\_SET\_DIRECT\_IDENTIFIERS**: `ReadonlySet`\<[`SafeHarborCategory`](#safeharborcategory)\> The **sixteen direct identifiers §164.514(e)(2) excludes from a limited data set**, mapped onto this library's category model: (i) names, (ii) postal address other than town/city/State/ZIP, (iii) telephone, (iv) fax, (v) email, (vi) social security, (vii) medical record, (viii) health plan beneficiary, (ix) account, (x) certificate/licence, (xi) vehicle, (xii) device, (xiii) URL, (xiv) IP, (xv) biometric, (xvi) full-face image. **This is the guard that makes the retention citation true rather than merely asserted.** The argument for keeping an encounter or order number is that this list has no catch-all; the argument for keeping a service date is that it has no date. **Neither argument survives if the value at the locus turns out to be one of the sixteen** -- and a visit number field routinely carries a medical record or account number, typed as such by the standard's own identifier-type code. So retention is **refused** whenever the resolved category is on this list, whatever an adapter asked for, in both the adapter and the engine. Exactly two of the eighteen Safe Harbor categories are absent from it: `DATES` and the (R) catch-all `OTHER_UNIQUE_ID`. Those two, and only those two, are retainable. #### Example ```ts import { LIMITED_DATA_SET_DIRECT_IDENTIFIERS, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; LIMITED_DATA_SET_DIRECT_IDENTIFIERS.has(SAFE_HARBOR_CATEGORIES.MRN); // => true (never retainable) LIMITED_DATA_SET_DIRECT_IDENTIFIERS.has(SAFE_HARBOR_CATEGORIES.DATES); // => false (retainable) ``` *** ### LIMITED\_DATA\_SET\_PROFILE > `const` **LIMITED\_DATA\_SET\_PROFILE**: [`DeidProfile`](#deidprofile) The **Limited Data Set / longitudinal research** profile. Identical to Safe Harbor **except** that dates are **date-shifted** (a single consistent per-patient offset, intervals preserved) rather than generalized to year, and that the medical record, health plan beneficiary and account numbers are replaced by a **consistent keyed surrogate** rather than removed, so cross-document linkage survives. Both are keyed transforms, and both are why this preset does not, and may not, claim Safe Harbor: a surrogate derived from the individual's own value is a re-identification code §164.514(c)(1) does not permit. Every such locus is flagged `reidentificationCode` in the manifest and listed in the support report's keyed-surrogate residual inventory. It also **keeps**, unchanged, the three classes of identifying locus §164.514(e)(2) permits a limited data set to carry and Safe Harbor does not: **encounter dates** (admission / discharge / service / diagnosis), **encounter and order identifiers** (visit number, placer and filler order numbers), and the **postal address parts (e)(2)(ii) names**: town or city, State and the **whole** zip code, with the street address and every other geographic component removed. That list of sixteen direct identifiers names no date and has **no catch-all**, which is precisely why the first two survive here and are removed under Safe Harbor; (e)(2)(ii) is its only **partial** exclusion, which is why the third does. The three-digit / `000` ZIP rule is §164.514(b)(2)(i)(B), **Safe Harbor's**, and has no (e)(2) counterpart, so a restricted-prefix ZIP is kept in full here too. Each kept locus is still **recorded** as a `DEID_RESIDUAL_RETAINED` residual, located to its own component, so it reaches the determiner's residual inventory. **The geographic allowance is honoured by the HL7 v2 pass alone.** The C-CDA, FHIR, X12, NCPDP and DICOM adapters do not read retention classes, so under those five an address is reduced exactly as Safe Harbor reduces it. The direction is the safe one and it is stated rather than left to be discovered. **On dates the preset is deliberately STRICTER than §164.514(e)(2).** The list of sixteen names no date, so a limited data set may carry dates at full precision; this preset date-shifts them anyway, by choice. Removing more than the regulation requires is always lawful, and the alternative would hand every existing consumer real patient dates on an upgrade, which is the direction no re-run undoes. **This is deliberately less protective than Safe Harbor and is NOT Safe Harbor.** A shifted-but-real date is still "an element of a date" (§164.514(b)(2)(i)(C)), so this profile: - is **not** labelled `safe-harbor` (the reserved-label guard would reject it); - **requires** a keyed per-patient [DeidContext](#deidcontext) (an absent key is a fatal `DEID_NO_KEY`); - produces an **Expert-Determination-supporting** dataset, **not** a certified de-identification, and **not**, on its own, a HIPAA §164.514(e) Limited Data Set: disclosing an actual Limited Data Set additionally requires a Data Use Agreement, which is the consumer's responsibility. #### Example ```ts import { LIMITED_DATA_SET_PROFILE } from "@cosyte/deid"; LIMITED_DATA_SET_PROFILE.requiresContext; // => true (date-shift needs a per-patient key) ``` *** ### LIMITED\_DATA\_SET\_RETENTION\_CLASSES > `const` **LIMITED\_DATA\_SET\_RETENTION\_CLASSES**: `ReadonlySet`\<[`RetainedLocusClass`](#retainedlocusclass)\> The retention classes a profile **declaring** the `limited-data-set` standard may carry: exactly those §164.514(e)(2) leaves out of its sixteen direct identifiers, plus the parts (e)(2)(ii) names. The list has **no date** and **no catch-all**, so encounter dates and encounter / order identifiers survive it; (e)(2)(ii) is a **partial** exclusion, so the three named address parts survive it. A class outside this set is a claim the regulation does not support, and [assertLimitedDataSetRetention](#assertlimiteddatasetretention) refuses it rather than letting the profile wear the name. #### Example ```ts import { LIMITED_DATA_SET_RETENTION_CLASSES, RETAINED_LOCUS_CLASSES } from "@cosyte/deid"; LIMITED_DATA_SET_RETENTION_CLASSES.has(RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES); // => true ``` *** ### NO\_RETAINED\_LOCI > `const` **NO\_RETAINED\_LOCI**: readonly [`RetainedLocusClass`](#retainedlocusclass)[] The empty retention set: the fail-closed default every entry point uses. *** ### OUTPUT\_LABEL > `const` **OUTPUT\_LABEL**: `"Safe-Harbor-transformed per the configured policy"` = `"Safe-Harbor-transformed per the configured policy"` The label the library applies to its output. Deliberately **not** "de-identified" / "HIPAA-compliant": the certification is always the consumer's. #### Example ```ts import { OUTPUT_LABEL } from "@cosyte/deid"; OUTPUT_LABEL; // => "Safe-Harbor-transformed per the configured policy" ``` *** ### RESTRICTED\_ZIP3 > `const` **RESTRICTED\_ZIP3**: `ReadonlySet`\<`string`\> The 17 restricted three-digit ZIP prefixes (population ≤ 20,000 per 2000 Census). A ZIP whose first three digits are in this set is generalized to `000`; any other prefix retains its three digits. #### Example ```ts import { RESTRICTED_ZIP3 } from "@cosyte/deid"; RESTRICTED_ZIP3.has("036"); // => true RESTRICTED_ZIP3.has("902"); // => false ``` *** ### RESTRICTED\_ZIP3\_SOURCE > `const` **RESTRICTED\_ZIP3\_SOURCE**: `Readonly`\<\{ `census`: `"2000"`; `citation`: `"HHS OCR, Guidance Regarding Methods for De-identification of PHI (2012); 45 CFR §164.514(b)(2)(i)(B)"`; \}\> The Census vintage this list is grounded in, surfaced so a consumer knows exactly which published artifact the `000` rule is applied from. #### Example ```ts import { RESTRICTED_ZIP3_SOURCE } from "@cosyte/deid"; RESTRICTED_ZIP3_SOURCE.census; // => "2000" ``` *** ### RETAINED\_LOCUS\_CLASSES > `const` **RETAINED\_LOCUS\_CLASSES**: `object` The stable registry of retention classes. `key === value` so the full set survives an `Object.values(...)` snapshot into a stability tripwire. These are part of the public contract: renaming or removing one is a **breaking change**; new classes may be **added** in a later release. #### Type Declaration ##### ENCOUNTER\_DATES > `readonly` **ENCOUNTER\_DATES**: `"encounter-dates"` = `"encounter-dates"` Patient-related **dates** carried by retained clinical / visit structures: admission, discharge, observation / service, and diagnosis dates. These are elements of dates directly related to the individual, so **Safe Harbor removes them** (only the year may remain); a limited data set **may keep them**, because §164.514(e)(2)'s direct-identifier list names no date. ##### ENCOUNTER\_IDENTIFIERS > `readonly` **ENCOUNTER\_IDENTIFIERS**: `"encounter-identifiers"` = `"encounter-identifiers"` **Encounter and order identifiers**: the visit / encounter number, and the placer and filler order numbers. These are not one of the seventeen concrete Safe Harbor identifier types, so Safe Harbor reaches them through the **(R)** catch-all and they are blocked; §164.514(e)(2) has **no** catch-all, so a limited data set **may keep them**. ##### LIMITED\_DATA\_SET\_GEOGRAPHY > `readonly` **LIMITED\_DATA\_SET\_GEOGRAPHY**: `"limited-data-set-geography"` = `"limited-data-set-geography"` The **named parts of a postal address** §164.514(e)(2)(ii) permits a limited data set to carry. The clause excludes "Postal address information, **other than town or city, State, and zip code**", which makes it the **only PARTIAL exclusion** in the list of sixteen: three named parts survive and everything else in the address does not. **What survives**: town or city, State, and the **whole** zip code. The digit limit and the `000` substitution are §164.514(b)(2)(i)(B), which is **Safe Harbor's** rule; (e)(2) states no digit limit and no population condition, so the ZIP is kept in full here. **What does not**: the street address, any second address line, the county or parish, the census tract or other geographic designation, the country, and a birth place. None of those is named by the clause, so none of them is widened by this class. **It permits PARTS, never the category.** `GEOGRAPHIC` stays on [LIMITED\_DATA\_SET\_DIRECT\_IDENTIFIERS](#limited_data_set_direct_identifiers) and [isRetainableCategory](#isretainablecategory) still returns `false` for it, because a partial exclusion is not a whole-category one. The only route past that guard is [isRetainablePart](#isretainablepart), which is keyed on this class, that category and one of the three names above. **Scope, stated rather than implied**: only the **HL7 v2** adapter reads this class today, like every other. Under the C-CDA, FHIR, X12, NCPDP and DICOM adapters an address is reduced exactly as it is without it (the Safe Harbor generalization), which is the stricter direction. #### Example ```ts import { RETAINED_LOCUS_CLASSES } from "@cosyte/deid"; RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES; // => "encounter-dates" ``` *** ### SAFE\_HARBOR\_CATEGORIES > `const` **SAFE\_HARBOR\_CATEGORIES**: `object` The stable registry of the 18 Safe Harbor identifier categories. `key === value` so the set survives an `Object.values(...)` snapshot into a stability tripwire. Renaming a category is a **breaking change**: consumers branch on these in policies and manifests. #### Type Declaration ##### ACCOUNT > `readonly` **ACCOUNT**: `"ACCOUNT"` = `"ACCOUNT"` (J) Account numbers. ##### BIOMETRIC > `readonly` **BIOMETRIC**: `"BIOMETRIC"` = `"BIOMETRIC"` (P) Biometric identifiers, including finger and voice prints. ##### CERTIFICATE\_LICENSE > `readonly` **CERTIFICATE\_LICENSE**: `"CERTIFICATE_LICENSE"` = `"CERTIFICATE_LICENSE"` (K) Certificate / license numbers. ##### DATES > `readonly` **DATES**: `"DATES"` = `"DATES"` (C) All elements of dates (except year) directly related to the individual, and all ages > 89. ##### DEVICE > `readonly` **DEVICE**: `"DEVICE"` = `"DEVICE"` (M) Device identifiers and serial numbers. ##### EMAIL > `readonly` **EMAIL**: `"EMAIL"` = `"EMAIL"` (F) Email addresses. ##### FAX > `readonly` **FAX**: `"FAX"` = `"FAX"` (E) Fax numbers. ##### FULL\_FACE\_PHOTO > `readonly` **FULL\_FACE\_PHOTO**: `"FULL_FACE_PHOTO"` = `"FULL_FACE_PHOTO"` (Q) Full-face photographs and any comparable images. ##### GEOGRAPHIC > `readonly` **GEOGRAPHIC**: `"GEOGRAPHIC"` = `"GEOGRAPHIC"` (B) All geographic subdivisions smaller than a state: street, city, county, precinct, ZIP, geocodes. ##### HEALTH\_PLAN\_BENEFICIARY > `readonly` **HEALTH\_PLAN\_BENEFICIARY**: `"HEALTH_PLAN_BENEFICIARY"` = `"HEALTH_PLAN_BENEFICIARY"` (I) Health plan beneficiary numbers. ##### IP\_ADDRESS > `readonly` **IP\_ADDRESS**: `"IP_ADDRESS"` = `"IP_ADDRESS"` (O) IP addresses. ##### MRN > `readonly` **MRN**: `"MRN"` = `"MRN"` (H) Medical record numbers. ##### NAMES > `readonly` **NAMES**: `"NAMES"` = `"NAMES"` (A) Names: patient, relatives, employers, household members. ##### OTHER\_UNIQUE\_ID > `readonly` **OTHER\_UNIQUE\_ID**: `"OTHER_UNIQUE_ID"` = `"OTHER_UNIQUE_ID"` (R) Any other unique identifying number, characteristic, or code (the open-ended catch-all). ##### PHONE > `readonly` **PHONE**: `"PHONE"` = `"PHONE"` (D) Telephone numbers. ##### SSN > `readonly` **SSN**: `"SSN"` = `"SSN"` (G) Social Security numbers. ##### URL > `readonly` **URL**: `"URL"` = `"URL"` (N) Web URLs. ##### VEHICLE > `readonly` **VEHICLE**: `"VEHICLE"` = `"VEHICLE"` (L) Vehicle identifiers and serial numbers, including license plates. #### Example ```ts import { SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; SAFE_HARBOR_CATEGORIES.MRN; // => "MRN" ``` *** ### SAFE\_HARBOR\_CATEGORY\_META > `const` **SAFE\_HARBOR\_CATEGORY\_META**: `Readonly`\<`Record`\<[`SafeHarborCategory`](#safeharborcategory), \{ `letter`: `string`; `number`: `number`; `title`: `string`; \}\>\> Per-category regulatory metadata: the §164.514(b)(2)(i) sub-paragraph letter (A to R), the ordinal number (1 to 18), and a short human title. Grounded firsthand in the regulation text; contains no PHI. #### Example ```ts import { SAFE_HARBOR_CATEGORY_META, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; SAFE_HARBOR_CATEGORY_META[SAFE_HARBOR_CATEGORIES.GEOGRAPHIC].letter; // => "B" ``` *** ### SAFE\_HARBOR\_POLICY > `const` **SAFE\_HARBOR\_POLICY**: [`DeidPolicy`](#deidpolicy) The built-in **Safe Harbor** policy. Direct identifiers with no analytic value are redacted, and that includes the medical record, health plan beneficiary and account numbers: their value is **removed**, never replaced by a keyed surrogate. Geography and dates are generalized; the open-ended catch-all (R) is **blocked** (fail-closed). Dates generalize to year, and a keyed surrogate of an identifier is derived from information about the individual, so both date-shift and pseudonymization are Expert-Determination techniques rather than Safe Harbor ones. A consumer who needs a consistent keyed surrogate for those three categories uses `LIMITED_DATA_SET_PROFILE`, which does not claim Safe Harbor. #### Example ```ts import { SAFE_HARBOR_POLICY, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; SAFE_HARBOR_POLICY.transforms[SAFE_HARBOR_CATEGORIES.MRN]; // => "redact" ``` *** ### SAFE\_HARBOR\_PROFILE > `const` **SAFE\_HARBOR\_PROFILE**: [`DeidProfile`](#deidprofile) The **Safe Harbor** profile: the fail-closed default. Wraps [SAFE\_HARBOR\_POLICY](#safe_harbor_policy): direct identifiers removed, medical record / beneficiary / account numbers **removed** rather than replaced by a keyed surrogate, geography and dates generalized, the catch-all (R) blocked. It therefore needs **no key at all**. Output is **"Safe-Harbor-transformed per the configured policy"**, never "de-identified". #### Example ```ts import { SAFE_HARBOR_PROFILE } from "@cosyte/deid"; SAFE_HARBOR_PROFILE.standard; // => "safe-harbor" ``` *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, synced with `package.json#version` at release time. #### Example ```ts import { VERSION } from "@cosyte/deid"; typeof VERSION; // => "string" ``` *** ### WITHHELD\_LOCUS\_TOKEN > `const` **WITHHELD\_LOCUS\_TOKEN**: `""` = `""` What a manifest locus prints in place of an identifier it may not echo, i.e. an identifier the adapter read out of the document that does not match the shape its position promises. Deliberately carries **no length and no prefix of the refused token**: the length of a refused identifier is itself derived from the content that was refused. #### Example ```ts import { WITHHELD_LOCUS_TOKEN } from "@cosyte/deid"; // A manifest entry whose position could not be trusted reads, e.g., "-1". WITHHELD_LOCUS_TOKEN; // => "" ``` ## Functions ### assertLimitedDataSetRetention() > **assertLimitedDataSetRetention**(`subject`, `retainedLoci`): `void` Enforce §164.514(e)(2) on a profile that **declares** the `limited-data-set` standard: every class it retains must be one [LIMITED\_DATA\_SET\_RETENTION\_CLASSES](#limited_data_set_retention_classes) permits. A profile carrying the regulation's name while keeping something the regulation excludes is refused, naming the class, before any locus is transformed. #### Parameters ##### subject `string` What is being checked, for the diagnostic (a profile, a derivation base). ##### retainedLoci readonly [`RetainedLocusClass`](#retainedlocusclass)[] \| `undefined` The retention classes the profile carries, if any. #### Returns `void` #### Throws [DeidError](#deiderror) `DEID_PROFILE_INVALID` naming every class outside the permitted set. #### Example ```ts import { assertLimitedDataSetRetention } from "@cosyte/deid"; assertLimitedDataSetRetention("a profile", ["encounter-dates"]); // ok ``` *** ### assertRetentionContract() > **assertRetentionContract**(`policyName`, `retainedLoci`): `void` Enforce the **label contract on retention**, failing closed: a policy carrying the reserved `safe-harbor` label may not run with a non-empty retention set. Retaining an admission date or an encounter number is strictly weaker than the transform the label promises, so allowing it would let an options bag emit a Safe-Harbor-labelled result that is not Safe Harbor. This is the retention analogue of the guard that stops a date-shifting policy wearing the same label, and it closes the hand-built-options route that no profile check can see. #### Parameters ##### policyName `string` The name of the resolved policy. ##### retainedLoci readonly [`RetainedLocusClass`](#retainedlocusclass)[] \| `undefined` The retention classes the options bag carries, if any. #### Returns `void` #### Throws [DeidError](#deiderror) `DEID_POLICY_INVALID` when a `safe-harbor`-labelled policy is asked to retain. #### Example ```ts import { assertRetentionContract } from "@cosyte/deid"; assertRetentionContract("limited-data-set", ["encounter-dates"]); // ok assertRetentionContract("safe-harbor", []); // ok (retains nothing) ``` *** ### buildExpertDeterminationSupportReport() > **buildExpertDeterminationSupportReport**(`manifests`, `options?`): [`ExpertDeterminationSupportReport`](#expertdeterminationsupportreport) Build the [ExpertDeterminationSupportReport](#expertdeterminationsupportreport) from the value-free manifest(s) of one or more de-identification passes. Deterministic; the input is never mutated; the result is deeply frozen. Accepts either a **single** manifest (`readonly DeidManifestEntry[]`) or a **corpus** (an array of manifests). Counts for identical loci are summed across the corpus. The report is **value-free**: it carries categories, dispositions, loci, and counts, **never a PHI value**, and it renders **no determination**: `determination` is `null` and the disclaimer leads. #### Parameters ##### manifests readonly [`DeidManifestEntry`](#deidmanifestentry)[] \| readonly readonly [`DeidManifestEntry`](#deidmanifestentry)[][] A single manifest, or an array of manifests (a corpus). ##### options? [`ExpertDeterminationReportOptions`](#expertdeterminationreportoptions) = `{}` The policy label and optional consumer-supplied quasi-identifier class sizes. #### Returns [`ExpertDeterminationSupportReport`](#expertdeterminationsupportreport) The frozen, value-free support report. #### Example ```ts import { deidentify, buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const { manifest } = deidentify( { loci: [{ path: "PID-5", kind: "name", category: SAFE_HARBOR_CATEGORIES.NAMES, value: "X" }] }, {}, ); const report = buildExpertDeterminationSupportReport(manifest, { policy: "safe-harbor" }); report.determination; // => null report.policy; // => "safe-harbor" ``` *** ### classifyPartyRole() > **classifyPartyRole**(`roleCode`, `table`): [`PartyRoleClassification`](#partyroleclassification) Classify a party from the role code its format types at the party, against that format's own [PartyRoleTable](#partyroletable). Comparison is trimmed and upper-cased, so a lower-case wire code resolves to the same committed member. An absent, empty or unlisted code is `unknown` and **fails closed**. #### Parameters ##### roleCode `string` The role code the format types at the party (`NM1-01` / `N1-01`; the role an HL7 v2.5.1 field definition names at an organisation-typed position). ##### table [`PartyRoleTable`](#partyroletable) The format's committed subject / outside-scope role lists. #### Returns [`PartyRoleClassification`](#partyroleclassification) The classification, carrying the table's own code when the role was recognized. #### Example ```ts import { classifyPartyRole } from "@cosyte/deid"; const table = { subject: new Set(["36"]), outside: new Set(["85"]) }; classifyPartyRole("36", table); // => { scope: "safe-harbor-subject", roleCode: "36" } classifyPartyRole("zq", table); // => { scope: "unknown" } (fails closed) ``` *** ### createDeidContext() > **createDeidContext**(`spec`): [`DeidContext`](#deidcontext) Create a [DeidContext](#deidcontext) from consumer-held key material. The key is coerced to bytes and stored only in the module-private registry, never on the returned handle. #### Parameters ##### spec [`DeidContextSpec`](#deidcontextspec) The key, optional date-shift seed, optional patient scope, and offset bound. #### Returns [`DeidContext`](#deidcontext) An opaque, self-redacting context. #### Throws [DeidError](#deiderror) with code `DEID_NO_KEY` if the key (or an explicit seed) is empty. #### Example ```ts import { createDeidContext } from "@cosyte/deid"; const ctx = createDeidContext({ key: "consumer-secret", patientId: "patient-1" }); ``` *** ### createDeidRegistry() > **createDeidRegistry**(`spec`): [`DeidRegistry`](#deidregistry) Create a [DeidRegistry](#deidregistry) from consumer-held key material. **Fails closed** on an absent/empty key: there is no default or weak key (the underlying [createDeidContext](#createdeidcontext) throws `DEID_NO_KEY`). The key is stored only in the module-private state, never on the returned handle. #### Parameters ##### spec [`DeidRegistrySpec`](#deidregistryspec) The key, optional date-shift seed, and optional offset bound. #### Returns [`DeidRegistry`](#deidregistry) An opaque, self-redacting corpus consistency handle. #### Throws [DeidError](#deiderror) with code `DEID_NO_KEY` if the key (or an explicit seed) is empty. #### Example ```ts import { createDeidRegistry } from "@cosyte/deid"; const registry = createDeidRegistry({ key: "consumer-secret" }); ``` *** ### dateShift() > **dateShift**(`value`, `ctx`): `string` \| `null` Shift a date by the context's deterministic per-patient offset, preserving intervals. Supports ISO `YYYY-MM-DD`, HL7 `YYYYMMDD`, and an ISO datetime (`YYYY-MM-DDThh:mm…`). Fails closed (`null`) on an unparseable or invalid date. **Timezone-independent.** Only the calendar-date portion is shifted (via UTC calendar math); a datetime's time-of-day and zone designator are preserved **verbatim**, so the same input yields the same output on every host regardless of the machine's `TZ`. Because the offset is a whole number of days and the clock/zone are untouched, intervals are preserved exactly. The **offset is not returned**: only the shifted value is. Two dates for the same patient move by the same amount, so the number of days between them is unchanged. #### Parameters ##### value `string` The date value to shift. ##### ctx [`DeidContext`](#deidcontext) The de-identification context (must carry a `patientId` scope for the offset). #### Returns `string` \| `null` The shifted date at the original precision, or `null` if it could not be parsed. #### Throws [DeidError](#deiderror) with code `DEID_NO_KEY` if the context has no per-patient scope. #### Example ```ts import { dateShift, createDeidContext } from "@cosyte/deid"; const ctx = createDeidContext({ key: "secret", patientId: "patient-1" }); const a = dateShift("2020-01-01", ctx); const b = dateShift("2020-01-11", ctx); // a and b are shifted, but remain exactly 10 days apart. ``` *** ### defineDeidPolicy() > **defineDeidPolicy**(`spec`): [`DeidPolicy`](#deidpolicy) Derive a custom policy from the Safe Harbor defaults. Unlisted categories keep the safe default, so a custom policy can only ever be built by *deviating* from Safe Harbor deliberately, never by forgetting a category. The result is frozen. #### Parameters ##### spec [`DeidPolicySpec`](#deidpolicyspec) The policy name and per-category overrides. #### Returns [`DeidPolicy`](#deidpolicy) A frozen [DeidPolicy](#deidpolicy) covering all 18 categories. #### Example ```ts import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const research = defineDeidPolicy({ name: "research", transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" }, }); research.transforms[SAFE_HARBOR_CATEGORIES.NAMES]; // => "redact" (kept from Safe Harbor) ``` *** ### defineDeidProfile() > **defineDeidProfile**(`spec`): [`DeidProfile`](#deidprofile) Derive a per-site [DeidProfile](#deidprofile) from a base profile (Safe Harbor by default), enforcing the **widen-never-narrow** contract: every override must move its category to an equal-or-**stronger** transform than the base's. A weakening override, or reclaiming a reserved standard label, is **rejected** ([FATAL\_CODES.DEID\_PROFILE\_INVALID](#deid_profile_invalid)), so a site preset can only tighten, never loosen, the base standard's protection. #### Parameters ##### spec [`DeidProfileSpec`](#deidprofilespec) The profile name, base, per-category overrides, and optional redactor. #### Returns [`DeidProfile`](#deidprofile) A frozen [DeidProfile](#deidprofile). #### Throws [DeidError](#deiderror) `DEID_PROFILE_INVALID` if an override weakens a category, if `retainedLoci` adds a retention class the base does not retain, or if the name reclaims a reserved standard label; `DEID_POLICY_INVALID` if the base declares the `safe-harbor` standard while carrying a derived-output pair, or if the derived policy violates the key/label contract (e.g. a `safe-harbor`-labelled policy that date-shifts). A base refused at derive time is refused with `DEID_PROFILE_INVALID` first: a profile that is refused never mints a policy to label. #### Example ```ts import { defineDeidProfile, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const strict = defineDeidProfile({ name: "site-strict", transforms: { [SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]: "redact" }, // generalize -> redact: OK }); strict.policy.transforms[SAFE_HARBOR_CATEGORIES.GEOGRAPHIC]; // => "redact" ``` *** ### deidentify() > **deidentify**(`model`, `options`): [`DeidResult`](#deidresult) De-identify a format-agnostic [LocusModel](#locusmodel) under a policy. Returns the transformed document and a value-free manifest. The input model is never mutated; the result is deeply frozen. The output is **"Safe-Harbor-transformed per the configured policy"**: it is not certified de-identified, and Expert Determination is not rendered. #### Parameters ##### model The located candidate values to de-identify. ###### loci? readonly [`GenericLocus`](#genericlocus)[] ##### options [`DeidOptions`](#deidoptions) The policy and (for keyed transforms) the key context. #### Returns [`DeidResult`](#deidresult) The frozen [DeidResult](#deidresult): transformed document + value-free manifest. #### Throws [DeidError](#deiderror) `EMPTY_INPUT` if the model is null or carries no locus list; `DEID_NO_KEY` if a keyed transform is required but no key context was supplied; `DEID_POLICY_INVALID` if a `safe-harbor`-labelled policy is asked to retain an identifying locus. #### Example ```ts import { deidentify, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; const result = deidentify( { loci: [{ path: "PID-19", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.SSN, value: "SENTINEL" }] }, {}, ); result.document.loci[0]?.value; // => null (removed) result.manifest[0]?.disposition; // => "removed" ``` *** ### formatExpertDeterminationSupportReport() > **formatExpertDeterminationSupportReport**(`report`): `string` Render an [ExpertDeterminationSupportReport](#expertdeterminationsupportreport) as a human-readable Markdown document: the same value-free facts as the structured object, led by the non-certification disclaimer. Suitable to hand to a statistician alongside the machine-readable report. #### Parameters ##### report [`ExpertDeterminationSupportReport`](#expertdeterminationsupportreport) A report from [buildExpertDeterminationSupportReport](#buildexpertdeterminationsupportreport). #### Returns `string` A Markdown string (value-free: categories, dispositions, loci, counts, never a value). #### Example ```ts import { buildExpertDeterminationSupportReport, formatExpertDeterminationSupportReport } from "@cosyte/deid"; const md = formatExpertDeterminationSupportReport(buildExpertDeterminationSupportReport([])); md.startsWith("# Expert-Determination support report"); // => true ``` *** ### generalizeAge() > **generalizeAge**(`age`): [`GeneralizeOutcome`](#generalizeoutcome) \| `null` Generalize an age (§164.514(b)(2)(i)(C)): any age **over 89** aggregates to `"90+"`; ages 0 to 89 are retained (a residual). Fails closed (`null`) for a non-finite or negative age. #### Parameters ##### age `number` The age in years. #### Returns [`GeneralizeOutcome`](#generalizeoutcome) \| `null` The `{ value, residual }` outcome (`residual: false` for `"90+"`, `true` for a kept age), or `null` if the age is not a finite non-negative number. #### Example ```ts import { generalizeAge } from "@cosyte/deid"; generalizeAge(92)?.value; // => "90+" generalizeAge(89)?.value; // => "89" ``` *** ### generalizeDate() > **generalizeDate**(`value`): [`GeneralizeOutcome`](#generalizeoutcome) \| `null` Generalize a date to its **year** (§164.514(b)(2)(i)(C)). Accepts common encodings: ISO `YYYY-MM-DD` / `YYYY-MM-DDThh:mm:ss`, HL7 `YYYYMMDD`, anything beginning with a plausible four-digit year. Fails closed (`null`) when no year can be extracted; the retained year is a residual. #### Parameters ##### value `string` The date value to generalize. #### Returns [`GeneralizeOutcome`](#generalizeoutcome) \| `null` The `{ value: year, residual: true }` outcome, or `null` if no year is present. #### Example ```ts import { generalizeDate } from "@cosyte/deid"; generalizeDate("2019-03-14")?.value; // => "2019" generalizeDate("not-a-date"); // => null ``` *** ### generalizeZip() > **generalizeZip**(`zip`): [`GeneralizeOutcome`](#generalizeoutcome) \| `null` Generalize a ZIP code to its safe form (§164.514(b)(2)(i)(B)): the initial three digits, or `000` when those three digits name an area with ≤ 20,000 people (the cited restricted list). Fails closed (`null`) when three leading digits cannot be read. #### Parameters ##### zip `string` The ZIP code (5-digit, ZIP+4, or any form beginning with digits). #### Returns [`GeneralizeOutcome`](#generalizeoutcome) \| `null` The `{ value, residual }` outcome (`residual: false` for `000`, `true` for a kept prefix), or `null` if fewer than three leading digits are present. #### Example ```ts import { generalizeZip } from "@cosyte/deid"; generalizeZip("90210")?.value; // => "902" generalizeZip("03601")?.value; // => "000" (036 is a restricted prefix) ``` *** ### isRetainableCategory() > **isRetainableCategory**(`category`): `boolean` Whether a locus of this category may be retained at all. `false` for every one of the sixteen direct identifiers [LIMITED\_DATA\_SET\_DIRECT\_IDENTIFIERS](#limited_data_set_direct_identifiers) names, whatever retention class an adapter attached to it. #### Parameters ##### category [`SafeHarborCategory`](#safeharborcategory) The resolved Safe Harbor category of the locus. #### Returns `boolean` `true` only for `DATES` and the (R) catch-all. #### Example ```ts import { isRetainableCategory, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid"; isRetainableCategory(SAFE_HARBOR_CATEGORIES.MRN); // => false isRetainableCategory(SAFE_HARBOR_CATEGORIES.DATES); // => true ``` *** ### isRetainablePart() > **isRetainablePart**(`cls`, `category`, `part`): `boolean` Whether a **named part** of a locus whose category is otherwise excluded may be retained. This is the **only** route past [isRetainableCategory](#isretainablecategory), and it is narrow by construction: all three of the class, the resolved category and the part name must line up, and each is an allow-list membership rather than a negation. It exists because §164.514(e)(2)(ii) is the list's only **partial** exclusion. Making `GEOGRAPHIC` retainable as a category would keep a county code, a birth place and a street address along with the three parts the clause names, so the category stays excluded and the parts are named here. #### Parameters ##### cls [`RetainedLocusClass`](#retainedlocusclass) The retention class the adapter proposed for the locus. ##### category [`SafeHarborCategory`](#safeharborcategory) The resolved Safe Harbor category of the locus. ##### part [`RetainedLocusPart`](#retainedlocuspart) The named part the adapter proposed keeping. #### Returns `boolean` `true` only for the geographic class, the `GEOGRAPHIC` category, and a named address part. #### Example ```ts import { isRetainablePart, LIMITED_DATA_SET_ADDRESS_PARTS, RETAINED_LOCUS_CLASSES, SAFE_HARBOR_CATEGORIES, } from "@cosyte/deid"; const geo = RETAINED_LOCUS_CLASSES.LIMITED_DATA_SET_GEOGRAPHY; isRetainablePart(geo, SAFE_HARBOR_CATEGORIES.GEOGRAPHIC, LIMITED_DATA_SET_ADDRESS_PARTS.STATE); // => true isRetainablePart(geo, SAFE_HARBOR_CATEGORIES.NAMES, LIMITED_DATA_SET_ADDRESS_PARTS.STATE); // => false ``` *** ### isRetainableZipCode() > **isRetainableZipCode**(`zip`): `boolean` Whether a value is a **whole zip code** this library will carry through unreduced under [RETAINED\_LOCUS\_CLASSES.LIMITED\_DATA\_SET\_GEOGRAPHY](#limited_data_set_geography): five digits, optionally followed by the ZIP+4 add-on with or without its hyphen. Nothing else, and no surrounding whitespace. **It is deliberately stricter than the generalization's input rule, and the asymmetry is the point**: generalization reduces whatever it is given until it is safe, so reading three leading digits off a malformed value is safe. Retention emits the value **unreduced**, so a value whose shape this library cannot vouch for is not retained at all: the locus falls back to the Safe Harbor generalization, which fails closed to dropping the whole address. #### Parameters ##### zip `string` The candidate zip code, exactly as it sits at the locus. #### Returns `boolean` `true` only for `12345`, `12345-6789` or `123456789`. #### Example ```ts import { isRetainableZipCode } from "@cosyte/deid"; isRetainableZipCode("62704"); // => true isRetainableZipCode("627"); // => false (a partial ZIP is not a zip code) ``` *** ### keyedHash() > **keyedHash**(`value`, `ctx`): `string` Replace a value with a **keyed one-way digest** (HMAC-SHA-256). Like [pseudonymize](#pseudonymize) it is consistent and non-reversible without the key, but domain-separated so it is a distinct surrogate space from pseudonyms. #### Parameters ##### value `string` The value to hash. ##### ctx [`DeidContext`](#deidcontext) The de-identification context holding the consumer's key. #### Returns `string` A lowercase-hex keyed digest, consistent per (key, value). #### Example ```ts import { keyedHash, createDeidContext } from "@cosyte/deid"; const ctx = createDeidContext({ key: "secret" }); typeof keyedHash("value", ctx); // => "string" ``` *** ### profileOptions() > **profileOptions**(`profile`, `context?`, `overrides?`): [`DeidOptions`](#deidoptions) Build the [DeidOptions](#deidoptions) to pass to any adapter (`deidentifyHl7`, `deidentifyFhir`, …) from a profile: its policy, its retention set, the supplied key context, and the profile's default redactor (unless overridden). **Going through here is what carries the retention set**: an options bag built by hand from `profile.policy` alone retains nothing, which is the fail-closed direction. It is also where the **label contract** is enforced on a profile that DECLARES the `safe-harbor` standard: a profile claiming that standard while assigning a category a transform whose output is derived from that category's own value is refused here, before any locus is transformed, because the engine downstream sees only the policy and not the standard the profile claimed. #### Parameters ##### profile [`DeidProfile`](#deidprofile) The profile to apply. ##### context? [`DeidContext`](#deidcontext) The keyed per-patient context (required by profiles whose `requiresContext` is true). ##### overrides? Optional `context`/`redactor` overrides merged over the profile's defaults. ###### context? [`DeidContext`](#deidcontext) ###### redactor? [`FreeTextRedactor`](#freetextredactor) #### Returns [`DeidOptions`](#deidoptions) The [DeidOptions](#deidoptions) for an adapter call. #### Throws [DeidError](#deiderror) `DEID_POLICY_INVALID` when the profile claims the `safe-harbor` label, on either its policy name or its declared standard, while carrying a derived-output pair. #### Example ```ts import { SAFE_HARBOR_PROFILE, profileOptions, createDeidContext } from "@cosyte/deid"; const opts = profileOptions(SAFE_HARBOR_PROFILE, createDeidContext({ key: "k" })); opts.policy === SAFE_HARBOR_PROFILE.policy; // => true ``` *** ### pseudonymize() > **pseudonymize**(`id`, `ctx`): `string` Replace an identifier with a **consistent keyed-HMAC surrogate**. The same input under the same key always yields the same surrogate (linkage preserved); the surrogate is **not reversible without the key** (collision-resistant, key-dependent). Domain-separated from [keyedHash](#keyedhash). #### Parameters ##### id `string` The identifier to pseudonymize (MRN, beneficiary number, account number). ##### ctx [`DeidContext`](#deidcontext) The de-identification context holding the consumer's key. #### Returns `string` A lowercase-hex surrogate, consistent per (key, id). #### Example ```ts import { pseudonymize, createDeidContext } from "@cosyte/deid"; const ctx = createDeidContext({ key: "secret" }); pseudonymize("MRN-123", ctx) === pseudonymize("MRN-123", ctx); // => true (consistent) ``` *** ### redact() > **redact**(): `null` Redact a value. Returns `null`: the removed-value sentinel used throughout the transformed document. Pure and keyless. #### Returns `null` `null`, signalling the value has been removed. #### Example ```ts import { redact } from "@cosyte/deid"; redact(); // => null ``` *** ### resolvePolicy() > **resolvePolicy**(`policy`): [`DeidPolicy`](#deidpolicy) Resolve the policy argument accepted by the engine: the string `"safe-harbor"` (or `undefined`) yields the built-in policy; a [DeidPolicy](#deidpolicy) object is returned as-is. #### Parameters ##### policy [`DeidPolicy`](#deidpolicy) \| `"safe-harbor"` \| `undefined` `"safe-harbor"`, a [DeidPolicy](#deidpolicy), or `undefined`. #### Returns [`DeidPolicy`](#deidpolicy) The concrete policy to apply. #### Example ```ts import { resolvePolicy, SAFE_HARBOR_POLICY } from "@cosyte/deid"; resolvePolicy("safe-harbor") === SAFE_HARBOR_POLICY; // => true ``` *** ### retains() > **retains**(`classes`, `cls`): `boolean` Test whether a retention class is enabled by a (possibly absent) retention set. An absent or empty set retains **nothing**, so a caller that never mentions retention gets the strict treatment. #### Parameters ##### classes readonly [`RetainedLocusClass`](#retainedlocusclass)[] \| `undefined` The retention classes a profile or options bag enabled, if any. ##### cls [`RetainedLocusClass`](#retainedlocusclass) The class to test. #### Returns `boolean` `true` only when `classes` is present and lists `cls`. #### Example ```ts import { retains, RETAINED_LOCUS_CLASSES } from "@cosyte/deid"; retains(undefined, RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES); // => false (fail closed) retains(["encounter-dates"], RETAINED_LOCUS_CLASSES.ENCOUNTER_DATES); // => true ``` *** ### unkeyedHash() > **unkeyedHash**(`value`): `string` A **plain, unsalted SHA-256** digest: **NON-CONFORMING** to §164.514(c) and never used by the engine. It is re-identifiable for a small, enumerable identifier space (an attacker hashes every candidate and matches). Exported solely so the test suite can *prove* the reversibility hazard the keyed path avoids. **Do not use this to de-identify anything.** #### Parameters ##### value `string` The value to hash. #### Returns `string` The lowercase-hex SHA-256 of `value`: reversible for a small input space. #### Example ```ts import { unkeyedHash } from "@cosyte/deid"; // Deterministic and unsalted: this is the footgun, shown so tests can assert against it. unkeyedHash("a") === unkeyedHash("a"); // => true ``` --- URL: https://docs.cosyte.com/deid/troubleshooting Markdown: https://docs.cosyte.com/deid/troubleshooting.md # Troubleshooting & Known Limitations ## A value I expected to be transformed was `blocked` That is the **fail-closed rule** working. The engine blocks (withholds the value, disposition `blocked`) when it cannot confidently handle a locus: an unclassified PHI-bearing locus, an `unknown`-kind locus, a free-text locus, a date/ZIP whose value it cannot generalize, or a policy transform it cannot apply to that locus. The block is recorded in the manifest with `DEID_LOCUS_BLOCKED` or `DEID_FREETEXT_BLOCKED`. It is never a silent pass-through of the value. ## A keyed transform threw `DEID_NO_KEY` Pseudonymize, keyed-hash, and date-shift are **keyed**. Supply a `context` (`createDeidContext({ key })`): the engine **never** falls back to an unkeyed transform, because an unkeyed hash of an identifier is re-identifiable. Date-shift additionally needs a `patientId` scope. The **built-in Safe Harbor policy uses none of the three**, so it never raises this: if a Safe Harbor pass asks you for a key, a custom policy or profile put a keyed transform in it. ## A policy threw `DEID_POLICY_INVALID` A policy claiming the `safe-harbor` label, either by its own name or through a profile that declares the `safe-harbor` standard, assigned a category a transform whose output is **derived from that category's own value**: `pseudonymize`, `hash` or `date-shift` on any category, or `generalize` on any category other than (B) geography and (C) dates and ages over 89. The message names the offending category and transform. Those are Expert-Determination techniques rather than Safe Harbor ones, so either withhold the value (`redact` or `block`) or name the policy distinctly - the library never silently strengthens a transform, and never renames a policy, behind your back. An assignment that is not one of the eight published transform names draws the same refusal: a pair whose derivation cannot be established is refused, never permitted. ## `deidentify` threw `EMPTY_INPUT` The model was null/undefined or carried no `loci` array. Pass `{ loci: [...] }`. ## What the manifest is safe to log, and the one thing to know about the locus Error messages carry **no PHI**: every message this library raises is a fixed sentence, and no value, key, or date-shift offset is ever interpolated into one. A manifest entry never carries the value that was removed, generalized, or pseudonymized, and never the key or the offset. Its `reidentificationCode` field is a **boolean** flag saying whether a keyed surrogate was emitted there, never the surrogate, the key or the offset itself. The **locus** needs one more sentence, because it is the one field built out of the document rather than chosen by the library. A per-format adapter names a locus using the identifier at that position: an HL7 v2 segment id, a C-CDA element name, a FHIR element name, an X12 segment or transaction-set id, an NCPDP segment code. When the input is malformed, an upstream parser may report something at one of those positions that is not an identifier at all, and on an unrecognized narrative line that content can be clinical prose. Each identifier is therefore checked against the shape its position promises before it is used; one that does not match is refused outright and the locus reads `WITHHELD_LOCUS_TOKEN` (``) plus a structural index, so two refused positions stay distinguishable and nothing is silently dropped. That is a bound, not a guarantee of impossibility, and how much it leaves behind depends on the position. An HL7 v2 segment id, an X12 segment or transaction-set id and an NCPDP code are at most three characters with no separator and no whitespace, so the residue there is a narrative line whose entire content is three letters, which is indistinguishable from a real segment id. A C-CDA element name and a FHIR element name are bounded at 64 and 65 characters, so an unspaced token that long is still echoed at those positions. No whitespace passes any of the six, which is what keeps prose out, but "no whitespace" is not "no content". So treat a manifest as safe to log, and treat a run producing `` loci as a signal that the input was not what it claimed to be. Never log the input model or the raw document; it carries protected health information. > **If you have `0.0.2` installed, upgrade.** In `0.0.2` and earlier those identifiers were interpolated > unbounded, so a malformed document could put document content into the manifest and into > `buildExpertDeterminationSupportReport`'s output. See the changelog entry for the full detail. ## Known Limitations (this release, the format-agnostic core) The library's promise is **narrow and honest**. Do **not** over-trust it: - **Not a certification.** Output is **"Safe-Harbor-transformed per the configured policy,"** never "de-identified" / "HIPAA-compliant". The actual-knowledge condition (§164.514(b)(2)(ii)) is the consumer's; Expert Determination is not rendered. - **The root entry is the generic core.** `@cosyte/deid` itself is the transform/policy/manifest core over a **generic locus model**, so the caller supplies each locus's `path`, `kind`, and `category`. The per-format locus maps (HL7 v2, C-CDA, FHIR, X12, NCPDP, DICOM), which is where "the parser knows where the name is" becomes automatic, live behind the matching subpath exports. - **DOB vs. age is not linked in the core.** Under the default policy a date generalizes to its **year**, and a year is retained as a `DEID_RESIDUAL_RETAINED` residual. A birth-date *indicative of an age over 89* is **not** aggregated to `90+` by the generic core, because resolving DOB→age needs a reference date the core does not have. Surface the residual and apply the age-90 rule at the format layer (or via a profile) when a DOB is known to indicate age > 89. - **`GEOGRAPHIC` is generalized as a ZIP.** A locus of category `GEOGRAPHIC` is treated as a ZIP code (initial-3-digit or `000`). Street/city/county elements should be **removed**: mark them for redaction (or a non-ZIP kind) rather than relying on ZIP generalization, which would keep a leading digit fragment (recorded as a residual, never silently). The per-format locus maps classify these precisely. - **Free text is block-only.** A `freetext` locus is blocked by default, no naive regex scrub (a false-safety hazard). A bring-your-own redaction interface is available via the `redactor` option. - **Date-shift is not Safe Harbor.** It is an Expert-Determination-supporting mode; a shifted real date is still a date. The `safe-harbor` policy generalizes dates to year instead. The **API Reference** always reflects exactly what this release ships: treat it as the source of truth. --- URL: https://docs.cosyte.com/deid/limitations Markdown: https://docs.cosyte.com/deid/limitations.md # Known Limitations & Honesty **`@cosyte/deid` transforms a healthcare document per a configured de-identification policy (Safe Harbor by default) and emits a value-free manifest of what it acted on. It never labels output "de-identified," and it never certifies HIPAA de-identification.** Safe Harbor is implemented mechanically; **Expert Determination is _supported_, never _rendered_.** The structured-field core is the guarantee; **free text is blocked by default**; **DICOM is metadata-only** (burned-in pixels are flagged, not cleaned). On an unrecognized structure, an un-locatable identifier, an unknown segment or extension, or a free-text blob, the library blocks or removes, never passing a value through as "probably safe." Read this page before you rely on the library for anything that leaves your control. ## What it does - Locates PHI **structurally** at each parser's loci (never by regex over raw bytes) and applies the configured policy transform per Safe Harbor category. - Returns an immutable transformed document plus a **value-free manifest** (category + transform + locus + count + disposition + a boolean `reidentificationCode`, **never the value removed**, never the key, never the date-shift offset). - **Fails closed:** an unrecognized structure, an un-locatable identifier, an unknown segment/extension, or a free-text blob is **blocked or removed**, never emitted as safe. - Preserves clinical/financial values (codes, units, results, statuses, amounts), the **over-scrub guard**, so it does not degenerate into a "safe but useless" blanket scrubber. - Ships two named [policy profiles](#policy-profiles) and a `defineDeidProfile()` that can only ever **tighten** (widen safety), never loosen, a base standard. ## What it does NOT do | It does **not**… | Because… | | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Certify HIPAA de-identification | The library **transforms and evidences**; it never certifies. Output is _"Safe-Harbor-transformed per the configured policy,"_ never _"de-identified."_ | | Discharge the §164.514(b)(2)(ii) **actual-knowledge** clause | That is an organizational judgment about what a recipient knows: the library surfaces the residual (kept year, safe-3-digit ZIP) so a human can apply it, but cannot make it. | | Render or certify **Expert Determination** (§164.514(b)(1)) | _"The risk is very small"_ is a qualified statistician's contextual judgment about a dataset **and its recipient**. The [ED support report](#expert-determination) emits value-free facts as **input**; `determination` is always `null` and it computes no risk score. | | De-identify **free text / narrative** | Free-text loci (HL7 OBX-5/NTE, C-CDA narrative ``, FHIR notes/`div`, X12 MSG/NTE, NCPDP free text) are **blocked by default**. A [BYO redactor](#free-text) is **consumer-asserted**, never the library's guarantee; a naive built-in regex scrub is deliberately **refused** as a false-safety hazard. | | Clean **DICOM burned-in pixels** or full-face images (category Q) | v1 is **metadata-only** (delegated PS3.15 Annex E). Burned-in annotation raises `DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` and `burnedInAnnotationHazard`; pixel decode is a future `@cosyte/dicom-pixel`. The two pixel options of CID 7050 are declared **withheld** on the result and never written to `(0012,0064)`. **Do not release an image on metadata alone.** | | Guarantee **DICOM replacement-UID consistency across calls** by default | Referential integrity of replacement UIDs is guaranteed **only within a single call** unless the caller supplies a shared `uidMap`, which then extends it to every call sharing that cache. The declared scope is on the result (`uidReferentialIntegrity.scope`), so it is read rather than inferred. | | Handle **NCPDP SCRIPT** ePrescribing | **Refused, not deferred in prose.** The current parser surface (lossy serialize + an address-less `Patient` model) cannot support a faithful structural de-id, so an NCPDP entry point handed a SCRIPT document raises a typed `DEID_FORMAT_UNSUPPORTED` naming the format and the parser-surface reason and returns **no** document, manifest or partial output at all. NCPDP **Telecom** is supported and is unaffected. | | Guarantee against a determined re-identification attack | De-identification reduces risk to the regulatory bar; it is not a cryptographic guarantee. **Key custody is the consumer's**: a leaked HMAC key or date-shift offset re-identifies. | | Do anything the manifest does not record | If it is not in the manifest, the library did not do it. The manifest is the complete, value-free audit. | ## Fail-closed posture Uniquely, harm here runs in **two** directions and both are guarded: - **Under-scrub (a leak):** a missed PHI element leaves a patient identifiable. The reflex is the inverse of a parser's Postel's-Law liberality: **when in doubt, block/remove.** - **Over-scrub (destroyed meaning):** a clinical value wrongly treated as an identifier destroys data a clinician relies on. Clinical-kind loci are **retained untouched**. A keyed transform with no key is a **fatal** `DEID_NO_KEY`, never a silent unkeyed fallback (an unkeyed hash of an identifier is re-identifiable). A context configured with `maxShiftDays: 0` is a fatal `DEID_CONTEXT_INVALID`, a zero-bound shift is a guaranteed no-op, i.e. the original real dates. ## What is retained, and what "fails closed" does NOT cover **Fail-closed is a rule about _structures_, not about every field.** An unrecognized segment, resource, loop, or extension is blocked. But a segment on a format's **retain-list** is passed through, and retaining the structure is not the same as auditing every field inside it. Read this before you assume an unmapped position was removed. Two things follow, and the second is the one that surprises people: - **The identifying loci inside retained HL7 v2 structures are carved back out and acted on.** Under a Safe-Harbor-labelled policy the admit (PV1-44), discharge (PV1-45), observation (OBR-7) and diagnosis (DG1-5) dates are reduced to their **year**, and the visit number (PV1-19) with the placer and filler order numbers (OBR-2/3, ORC-2/3) are **removed**. §164.514(b)(2)(i)(C) names admission and discharge dates in the regulation text itself. - **PV1-19 is routed by its CX-5 identifier-type code**, like PID-3. A `VN`-typed or untyped visit number is the encounter identifier and is removed as the (R) catch-all; an `MR`/`AN`/`SS`-typed one is handled as the medical record / account / social security number it actually is, so it is **transformed under both profiles and is never retained** (§164.514(e)(2) names all three). A kept visit number is therefore only ever one the wire did not type as something stronger. - **Dates inside retained HL7 v2 segments are acted on and recorded.** Every position the **HL7 v2.5.1** segment definitions type as a date or date/time, in any segment whose bytes the pass can hand through, is reduced to its year under a Safe-Harbor-named policy, shifted under a date-shift policy, or blocked when the configured transform cannot read the value, and **every one of those outcomes is in the manifest** and therefore in the support report. That covers the timestamps in EVN, PV2, PR1, RXA, RXD, FT1, TXA and SPM, the order dates in ORC and OBR that no earlier limitation named, the date components of a date range or other composite (a specimen collection range, an order's quantity/timing, a discharged-to location's effective date), an OBX-5 the message itself types as a date, and the **OBX segment's own** reference-range, observation and analysis timestamps (OBX-12, OBX-14, OBX-19). The classification is **structural, never a guess from the value**: an eight-digit numeric result is not a date. - **The set of segments that reaches is what the pass hands through, not a list of names.** It is the HL7 v2 retain-list plus `OBX`, which is handed through by its OBX-2 value-type branch rather than by that list. A segment that fails closed is blocked field by field and carries nothing forward, so a date inside one cannot survive to be recorded. - **The version that classification is fixed at is a residual of its own.** It is HL7 v2.5.1 and it is never re-read from the message, so identical bytes always yield the same set of positions. A position only some other version types as a date is therefore **not** classified and **not acted on** (it is counted as an unexamined position, per the next bullets, which is not the same as being classified); the same holds for a retained segment v2.5.1 does not define, and for the file and batch envelope headers (`FHS`, `BHS`), which number their fields from a leading delimiter. - **Every NON-DATE field of a retained structure is still passed through untouched.** The carve-outs above narrow this class; they do not close it. The attending / referring **provider** names survive in PV1-7/8 and OBR-16, among others, and so do the date/time components carried **inside** a person-name or address composite (a provider name's effective or expiration date, an authenticator's timestamp, a licence expiry), because acting on those dates while leaving the names they qualify would record half a position. **Do not read the named loci above as the complete set of what a retained structure can carry.** If your threat model includes these, filter them yourself. - **Every one of those positions IS counted and located, as an unexamined residual.** A value-bearing position inside a structure the pass hands through that no locus rule names is recorded on `result.unexaminedResiduals` with its **structural locus**, a **count** and the fact that nothing examined it. It carries no value, no key and no offset, exactly like the manifest, and it is a separate list precisely so that a position nothing looked at can never be mistaken for a residual of a value the pass did examine. All six adapters produce it: the fields of a retained HL7 v2 segment, the entry dates / entry ids / in-entry performers and family-history demographics inside a retained C-CDA clinical body, the codes and statuses a FHIR walk descends past, the elements of a retained X12 segment, the fields of a retained NCPDP segment and its fixed header, and every DICOM attribute the delegated Annex E report does not account for, nested sequence items included. - **The envelope around a document is counted too, in every format that has one.** Retaining a _structure_ names no position inside it, so the positions of an HL7 v2 `MSH`, of a CDA document envelope, of the X12 interchange and functional-group envelope (`ISA` / `TA1` / `GS` / `GE` / `IEA`) and of the DICOM Part 10 File Meta group `(0002,xxxx)` are all enumerated and reported. Read those counts as the measurement they are: control numbers, trading-partner ids, timestamps and transfer syntaxes sit there, and the number says only that no rule examined them. - **The unit is the position, never the element it sits on.** A rule that reaches one position says nothing about the ones beside it: a C-CDA `` has its `@value` removed and its `@use` handed through, and the `@use` is counted. That is why the inventory can list a coordinate on an element the manifest also names. - **A position is wherever the format lets a value sit, not only where a value usually sits.** The enumeration is derived from what each parser's model can carry, so it reaches the places a value is easy to overlook: XML character data delivered as a **CDATA section** rather than as text, and the **comments** and **processing instructions** an XML document is re-serialized with, each counted at its own carrier's coordinate; a FHIR primitive's **`_`-sibling element id**, which travels beside the value it annotates; and whatever a **partly** rewritten structure keeps, such as the state and country of a generalized address, which are re-emitted exactly as they arrived along with anything riding inside them. - **What is removed is not counted, and that is the point of the number.** The inventory measures what the pass _hands through_, so a position it deletes (an address line, a blocked narrative and every carrier inside one, a FHIR primitive's extension metadata) does not appear: counting one would report an exposure that does not exist. The count is a measurement of what left the pass untouched. - **Counting is not removal, and it is not an allegation.** Nothing is scrubbed, generalized, blocked or otherwise transformed on account of the count: what to do about a measured residual is a separate decision, and the mirror risk of acting on it blindly is over-removal, which destroys clinical meaning. A position no rule examined also has **no established Safe Harbor category**, so it is attributed to none of the 18 and moves no category total: a clinical code, a dose unit and an order status all sit at positions like these. - **Two fail-safes, because a count nobody can qualify is worse than no count.** A position whose structural locus cannot be expressed is **still counted**, recorded under a withheld locus token and flagged, so losing the "where" never also loses the "how many". And a structure whose value-bearing positions cannot be enumerated **fails the pass** with a typed `DEID_POSITIONS_UNENUMERABLE` error naming the structure, rather than emitting a zero or a partial count that a reader would take for a clearance. - **Two stated edges of the count itself, so it is not read as finer than it is.** An **X12 element is one position even when it carries a composite**: `HI-01` holding a qualifier and a diagnosis code is counted once, not twice, which is the unit every X12 locus already uses. And **undecoded bytes after an X12 interchange's `IEA` terminator contribute no position at all**: they are neither a segment nor a loop, they commonly hold a whole second interchange, and any number reported for them would measure nothing. Those bytes are re-emitted, and the count does not speak for them. - **The employer is a Safe Harbor subject, and two employer surfaces still are not reached.** §164.514(b)(2)(i) removes the identifiers of the individual "or of relatives, **employers**, or household members", so an X12 party whose entity-identifier code is `36` and the employer positions the HL7 v2.5.1 financial segments type (GT1-16/17/18/19/29, IN1-10/11, IN2-3/49/50/64, and IN2-70 as an organisation) are acted on and recorded. What that does **not** reach: **an employer named only in free text**, which is the consumer's redactor's business like any other prose; and **an employer carried as a separate `Organization` resource in a FHIR graph**, where no role is typed at the position and the reference `display` that names it is blocked rather than classified. The guarantor's employer organisation name at **GT1-51** is likewise unreached: it is not among the positions this pass names. - **In FHIR, the element's DATATYPE decides, not the resource carrying it.** Which resource a person's name or address ends up on is a choice the producing system made, and the same home address arrives at `Patient.address` from one sender and at `Location.address` from a home-health sender. So a `HumanName` is removed and an `Address` is reduced to the Safe Harbor granularity **wherever the graph puts them outside a person resource**, `Organization.contact.name` and `Location.address` included, in contained resources and `Bundle` entries as well. Neither is stated as out of scope any longer. Inside a person resource the demographic map decides instead, which is a narrower reach and the last residual below. Positive classification is closed and marker-bound, which is what keeps the wider sweep off a clinical value: an element is a `HumanName` only when every property it carries is one FHIR R4 defines on `HumanName`, at least one of them is a marker property (`family`, `given`, `prefix`, `suffix`), and that marker holds **the exact value shape R4 gives it**: `family` a single string, the other three repeating. An `Address` the same way, with `line` repeating and `city`, `district`, `state`, `postalCode` and `country` single. Reading the shape per marker is what separates a marker from an R4 element that merely shares its name, and R4 supplies two such collisions: `country`, which every colliding element types as a `CodeableConcept` where an address types a string, and `prefix`, which `Questionnaire.item`, `PlanDefinition.action` and `RequestGroup.action` each type as a single string where a name types it repeating. Both of the latter two backbones make **every** child optional, so a conformant instance can carry nothing but `prefix`, and the shape is the only thing that tells it from a person's name. A newly reached element the pass cannot read faithfully, an `Address` whose `postalCode` is not a whole zip code or an unexpected shape at a part the reduction would keep, is **removed whole** and recorded, never partly retained. So is **any** complex the classifier cannot pin down at an element name R4 **types** as one of the two datatypes (`name`, `address`, a choice-type `locationAddress`, an open `valueAddress` / `valueHumanName`) - whether nothing is left to key on, a property R4 does not define sits beside a `family` or a `line`, or every property it carries is foreign to both datatypes. The test is whether the pass can read what the standard promised at that position, not which keys are present, so an unrecognized sibling never unblocks an element that was already unreadable. Two conformant R4 backbones share one of those element names, `MedicinalProduct.name` and `SubstanceSpecification.name`, and both are excluded positively: the property R4 makes `1..1` on each, plus that backbone's own closed property set. A plain string at one of those names is never a candidate at all, which is what leaves `Organization.name` and `Endpoint.address` alone. - **Five FHIR surfaces this release still does not reach.** Three because no person is typed at the position. A **`ContactPoint` outside a person resource**: a phone or an email on an `Organization`, a `Location` or an `Endpoint` is passed through, because widening to telecom would put a payer's or a facility's own switchboard number in scope, which the HL7 v2 pass deliberately keeps (`IN1-7`, the insurer's own phone, is untouched there). An **organisation's own `name`**: a plain string, never a `HumanName`, and administrative content rather than a person's identity. And the **individual's employer carried as a separate `Organization` resource**, per the bullet above. The fourth is the stated cost of the closed, shape-read classification: **a name or an address carrying a property R4 does not define, or carrying its only marker at a value shape R4 does not give that marker, at an element name R4 does not type as one of the two datatypes.** The unrecognized sibling or the wrong shape stops the classifier, and the fail-closed block is scoped to the typed element names, because at any other name that same evidence is routinely something else entirely: `{ prefix }` alone is a conformant `RequestGroup.action` and not a person, and blocking it would destroy conformant clinical and structural content, which is the mirror defect and the one no re-run restores. At a typed element name neither half is a residual: the standard promised a name or an address there, so whatever the classifier declines is blocked whole. The fifth is the scope of the sweep itself: **a name or an address inside a person resource, at a property the demographic map does not list.** The map already decides `name`, `telecom`, `photo` and `address` there, so the datatype sweep does not run, and a vendor `Patient.alias` carrying `{ family, given }` is passed through where the same bytes on an `Organization` are removed. All five are passed through, and all five are counted as unexamined residual positions like anything else no rule names. Vendor-proprietary loci absent from public specs are deferred, **not invented**: a quirk is encoded only when a real de-identified document grounds it. ## Policy profiles, and the Limited Data Set caveat - **`SAFE_HARBOR_PROFILE`**: the fail-closed default, dates generalized to year, the (R) catch-all blocked. It retains **no** identifying locus: an admission, discharge or service date keeps only its year, and an encounter or order number is removed. It emits **no keyed surrogate**: the medical record, health plan beneficiary and account numbers are **removed**, because a surrogate derived from the individual's own value is not a §164.514(c)(1) code and the (R) exception does not reach it. It therefore **needs no key at all**. - **`LIMITED_DATA_SET_PROFILE`**: a **research / longitudinal** preset that **date-shifts** dates (interval-preserving) rather than generalizing them, and that keeps a **consistent keyed surrogate** for the medical record, health plan beneficiary and account numbers so cross-document linkage survives. It is deliberately **less protective than Safe Harbor** on both counts: a shifted-but-real date is still "an element of a date," and a keyed surrogate is a re-identification code anyone holding the key can reverse the linkage of. Therefore it is **not** labelled `safe-harbor`, it **requires** a keyed per-patient context, and it produces an **Expert-Determination-supporting** dataset, **not** a certified de-identification, and **not**, on its own, a HIPAA §164.514(e) Limited Data Set. Disclosing an actual Limited Data Set additionally requires a **Data Use Agreement**, which is the consumer's responsibility. Every keyed surrogate it emits is flagged `reidentificationCode` in the manifest and listed in the support report's keyed-surrogate residual inventory. It also **keeps unchanged** the three classes §164.514(e)(2) permits and Safe Harbor does not: the **encounter dates** (admission / discharge / service / diagnosis), the **encounter and order identifiers** (a `VN`-typed or untyped visit number, and the placer and filler order numbers), and the **named parts of a postal address**. That list of sixteen direct identifiers names no date and has no catch-all, which is exactly why the first two survive here and are removed under Safe Harbor. It **does** name medical record, account and social security numbers, so a PV1-19 typed as one of those is transformed here too. Every one is still **recorded** as a `DEID_RESIDUAL_RETAINED` residual and appears in the support report's inventory, so nothing is kept silently. ### What a limited data set keeps of an address, and what it still removes §164.514(e)(2)(ii) is the **only partial exclusion** in the list of sixteen. It removes "postal address information, **other than town or city, State, and zip code**", so under the `limited-data-set-geography` retention class those three named parts survive and **everything else in the address does not**: the street address, any second address line, the county or parish, the census tract or other geographic designation, and the country. A county-code field and a birth place are removed under every profile, because the clause names neither. - **The zip code is kept WHOLE.** The initial-three-digits rule, and the `000` substitution for a prefix whose area has 20,000 people or fewer, are §164.514(b)(2)(i)(B): **Safe Harbor's** rule, with no (e)(2) counterpart. A restricted-prefix ZIP is therefore carried in full under this class, and is still reduced to `000` under Safe Harbor, which this class does not touch. - **The allowance follows the party list.** (e)(2) opens on "the individual or of relatives, employers, or household members", so the patient, the next of kin, the guarantor, the guarantor's employer and the insured are all treated alike. - **Every kept part is recorded**, as a `DEID_RESIDUAL_RETAINED` residual located to its own field, repetition and component, so it reaches the determiner's residual inventory. - **Nothing widens by omission.** A profile or an options bag that does not name the class reduces every address exactly as it always did. Options built by hand from a profile's `policy` alone keep nothing, and a profile derived from the Safe Harbor base may not add the class at all. - **It fails closed.** An address whose zip code is not a whole zip code is not partially kept: the locus falls back to the Safe Harbor generalization, which drops the whole address when it cannot read a prefix. No residual is recorded for anything that was not actually retained. - **The geographic allowance is honoured by the HL7 v2 pass alone.** The **C-CDA, FHIR, X12, NCPDP and DICOM** adapters do not read retention classes at all, so under those five an address is reduced exactly as Safe Harbor reduces it. They remain stricter, and that is the safe direction, but one profile therefore means something narrower for those five formats than it does for HL7 v2. - **A profile that DECLARES the `limited-data-set` standard is checked against the regulation.** A retention class beyond those (e)(2) permits is a fatal `DEID_PROFILE_INVALID` naming the class, both where the profile becomes engine options and where it is used as a derivation base. **On dates the preset is deliberately STRICTER than the regulation.** §164.514(e)(2) names no date, so a limited data set may carry dates at full precision. This preset date-shifts them anyway. Removing more than the regulation requires is always lawful; the alternative would hand every existing consumer real patient dates on an upgrade, and no re-run undoes a disclosure. `defineDeidProfile()`'s widen-never-narrow contract covers retention too, and it reads the opposite way round from a transform override: a derived profile may **drop** a retained class (keep less, remove more) but may never **add** one. Retention is also opt-in at the call: options built by hand from a profile's `policy` alone keep nothing. `defineDeidProfile()` derives a per-site profile under a **widen-never-narrow** contract: a site may move a category to an equal-or-stronger transform (more removal), but **never** re-weaken a category, a weakening override is a fatal `DEID_PROFILE_INVALID`. A site preset can only tighten the base. ## Free text is the consumer's responsibility The library bundles **no** NLP/PHI detector. With no redactor, free-text loci are **blocked**. With a `FreeTextRedactor`, its output is recorded as **consumer-asserted** (`DEID_FREETEXT_CONSUMER_REDACTED`) and is **not re-verified** by the library: "no findings" from a redactor is not an attestation. The structural PHI the adapters remove is unaffected either way. ## The Expert-Determination report makes no determination The report is descriptive input a determiner consumes and documents; it reaches no conclusion: ```ts runnable import { OUTPUT_LABEL, SAFE_HARBOR_PROFILE, LIMITED_DATA_SET_PROFILE } from "@cosyte/deid"; // The output is never labelled "de-identified". OUTPUT_LABEL; // => "Safe-Harbor-transformed per the configured policy" OUTPUT_LABEL.includes("de-identified"); // => false // Safe Harbor is the fail-closed default; the LDS preset is honestly not Safe Harbor. SAFE_HARBOR_PROFILE.standard; // => "safe-harbor" LIMITED_DATA_SET_PROFILE.standard; // => "limited-data-set" LIMITED_DATA_SET_PROFILE.requiresContext; // => true ``` ```ts runnable import { buildExpertDeterminationSupportReport } from "@cosyte/deid"; const report = buildExpertDeterminationSupportReport([], { policy: "safe-harbor" }); report.determination; // => null report.disclaimer.includes("NOT a determination"); // => true ``` The library's promise is narrow and honest: **structured-field, policy-driven Safe-Harbor transformation with a value-free manifest, never a leaked patient, and never a claim of "de-identified."** ## NCPDP Telecom: one transaction per message `deidentifyTelecom` handles a Telecom message that carries one transaction. A message whose header declares more than one is not handled in this release: with `@cosyte/ncpdp` 0.1.x installed the call throws that parser's multi-transaction build error and returns nothing, and an older `@cosyte/ncpdp` returns only the first transaction with the header count unchanged. Split such a message first. A later release de-identifies every transaction or refuses the message with a typed `DEID_*` error. --- # @cosyte/synth > Deterministic synthetic healthcare fixture generator for testing and conformance. These are the 15 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/synth/intro Markdown: https://docs.cosyte.com/synth/intro.md # @cosyte/synth Generate **deterministic, seedable synthetic healthcare fixtures** (HL7 v2, FHIR R4 / US Core, C-CDA, X12, NCPDP and ASTM) without hand-writing a byte of the wire format, and **without any chance the "patient" you just generated is a real person**. `@cosyte/synth` is a **consumer** of the Cosyte parsers, not a parser. It builds each artifact **through the parser's own builder/serializer** (so each artifact re-parses through that parser with zero warnings) and draws every identifier, name, date, phone, and address from a **guaranteed-non-colliding synthetic source** (SSA never-issued SSNs, NANP `555-01xx` phones, `example.*` domains, TEST-NET IPs, a synthetic assigning authority for MRNs, and a shipped clearly-fake name pool). It is a **format/conformance generator, not a clinical simulator**: it does not model disease progression (that is Synthea). > **Status:** `0.1`, published to npm. The version shown on the npm package page is the one that is > live; this page never repeats it. ## Install ```bash npm install @cosyte/synth @cosyte/hl7 ``` `@cosyte/hl7` is an **optional peer dependency**: install it only if you use the `@cosyte/synth/hl7` subpath. The package core (PRNG + safe providers) has **zero third-party runtime dependencies**. ## Generate a spec-clean message ```ts runnable import { generateAdt, roundTrip } from "@cosyte/synth/hl7"; // Same seed → byte-identical message, on any machine, any run. const message = generateAdt({ seed: 12345, trigger: "A01" }); // It round-trips through @cosyte/hl7 with zero warnings: spec-clean by construction. roundTrip(message).specClean; // => true ``` Every value in that message is provably synthetic: the SSN is never-issued (`900` to `999` area), the phone is in the reserved `555-01xx` block, the name is from a clearly-fake pool, and the MRN lives under a synthetic assigning authority. ## Next - [Quickstart](https://docs.cosyte.com/synth/quickstart.md): the core primitives and the HL7 corpus. - [Core Concepts](https://docs.cosyte.com/synth/concepts-archetype.md): synthetic-by-construction, determinism, the round-trip gate. --- URL: https://docs.cosyte.com/synth/installation Markdown: https://docs.cosyte.com/synth/installation.md # Installation `@cosyte/synth` is a TypeScript package for Node.js with **zero third-party runtime dependencies** in its core. It ships dual **ESM + CJS** builds with per-condition type declarations, so it works from either module system without configuration. > **Status:** `0.1`, published to npm. The version shown on the npm package page is the one that is > live; this page never repeats it. ## Prerequisites - **Node.js >= 22** (the whole `@cosyte/*` suite targets ES2023 / Node 22+). - A package manager: `pnpm`, `npm`, or `yarn`. ## Install ```bash npm install @cosyte/synth @cosyte/hl7 ``` `@cosyte/hl7` is an **optional peer dependency**, needed only for the `@cosyte/synth/hl7` subpath (HL7 v2 generation). Each format is a lazily-loaded subpath, so importing the package **core** (`@cosyte/synth`) never pulls a parser: a consumer who only needs the seeded PRNG and the safe providers installs nothing extra. ## Smoke test Confirm the package resolves and its version symbol is present: ```ts runnable import { VERSION } from "@cosyte/synth"; typeof VERSION; // => "string" ``` `VERSION` is the version of the package you installed. It is written from the manifest at release time, so it always names the release npm actually served you rather than a number left behind by an earlier one. This check asserts the value's **type** rather than the value itself, deliberately, so that this page never has to name a version and go stale on the next publish. If that resolves, the install is good. Head to the [Quickstart](https://docs.cosyte.com/synth/quickstart.md). --- URL: https://docs.cosyte.com/synth/quickstart Markdown: https://docs.cosyte.com/synth/quickstart.md # Quickstart Generate reproducible, synthetic-by-construction fixtures in a few lines. A **seed** determines the output entirely, so a fixture set regenerates identically anywhere. ## Generate a spec-clean HL7 v2 message This is the program the README opens with. The `@cosyte/synth/hl7` subpath builds each message **through `@cosyte/hl7`'s own `buildMessage`**, so it is spec-clean by construction, and `roundTrip` feeds it straight back to the parser. `@cosyte/hl7` is the optional peer this subpath needs; see [Installation](https://docs.cosyte.com/synth/installation.md): ```ts runnable import { generateAdt, generateOru, generateHl7, hl7Corpus, roundTrip } from "@cosyte/synth/hl7"; // Same seed → byte-identical message, on any machine, any run. const adt = generateAdt({ seed: 12345, trigger: "A01" }); const oru = generateOru({ seed: 12345 }); // Spec-clean by construction: it round-trips through @cosyte/hl7 with zero warnings. console.log(roundTrip(adt).specClean); // true console.log(roundTrip(oru).specClean); // true // Or generate a reproducible mixed corpus across every family: const corpus = hl7Corpus({ seed: 42, count: 7 }); // one of each family, cycled console.log(corpus.artifacts.every((a) => a.warnings.length === 0)); // true, all spec-clean // Dispatch by kind when the message type is data: console.log(roundTrip(generateHl7("VXU^V04", 12345)).content.split("\r")[0]); // its MSH segment ``` The first three lines print `true`, and the last prints the `MSH` segment of a generated `VXU^V04`. Every identifier, name and date in these messages is drawn from a reserved range or the shipped fake-name pool. ## Draw a synthetic value The `safe` providers each draw from a reserved, never-collide source. Everything is a pure function of an explicit seeded generator (`createRng`): ```ts runnable import { createRng, safe, isSyntheticSsn } from "@cosyte/synth"; const rng = createRng(42); const nationalId = safe.ssn(rng); // The SSN is always drawn from the SSA never-issued space: it cannot be a real SSN. isSyntheticSsn(nationalId); // => true ``` ## Check the round trip yourself `roundTrip` returns more than the verdict: the warnings the parser raised on the way back, which for a spec-clean artifact is none at all: ```ts runnable import { generateAdt, roundTrip } from "@cosyte/synth/hl7"; const result = roundTrip(generateAdt({ seed: 7, trigger: "A08" })); result.warnings; // => [] ``` ## Build a reproducible corpus `hl7Corpus` returns a deep-frozen, self-describing `Corpus`: a seed plus a manifest of what was generated. Pin the seed and every downstream test gets a stable fixture set: ```ts runnable import { hl7Corpus } from "@cosyte/synth/hl7"; const corpus = hl7Corpus({ seed: 2026, count: 3 }); corpus.artifacts.length; // => 3 ``` ## Determinism is the contract The same seed yields byte-identical output. This is a tested invariant, not a nicety: ```ts runnable import { generateAdt } from "@cosyte/synth/hl7"; const a = generateAdt({ seed: 99 }).toString(); const b = generateAdt({ seed: 99 }).toString(); a === b; // => true ``` ## Next - [Core Concepts](https://docs.cosyte.com/synth/concepts-archetype.md): synthetic-by-construction, determinism, the round-trip gate. - **API Reference**: every export, generated from source. --- URL: https://docs.cosyte.com/synth/concepts-archetype Markdown: https://docs.cosyte.com/synth/concepts-archetype.md # Core Concepts `@cosyte/synth` borrows the Cosyte parser archetype's _disciplines_ (immutability, stable typed codes, the profile system) but its central reflex is neither the parser's liberal parse nor a fail-closed de-identifier: it is **synthetic-by-construction**. ## Synthetic-by-construction There is **no code path** that emits a value not drawn from a guaranteed-non-colliding synthetic source. This inverts a parser's liberality: a parser is liberal on _input_; `synth` is **closed-world on its data sources**. The hazard is asymmetric and specific: a generated value that looks real enough to be mistaken for, or collide with, a real person's PHI. So every identifier comes from a reserved range (SSA never-issued SSNs, NANP `555-01xx` phones, `example.*` domains, RFC 5737 TEST-NET IPs) or a shipped clearly-fake pool, and a CI gate proves it: ```ts runnable import { createRng, safe, isSyntheticSsn, isSyntheticPhone } from "@cosyte/synth"; const rng = createRng(1); [isSyntheticSsn(safe.ssn(rng)), isSyntheticPhone(safe.phone(rng))]; // => [true, true] ``` ## Checked by the parser's own judgment `synth` never hand-writes bytes. It builds **through the parser's own conservative serializer**, so an artifact is checkable by feeding it straight back into that parser: it re-parses with **zero warnings** and re-serializes byte-identically: ```ts runnable import { generateAdt, roundTrip } from "@cosyte/synth/hl7"; roundTrip(generateAdt({ seed: 3 })).specClean; // => true ``` ## Determinism: a seed → the same bytes Reproducibility is a first-class guarantee, not a nicety. The PRNG is a hand-rolled, seeded, zero-dep generator (`sfc32` seeded by `splitmix32`); `Math.random` is **lint-banned** in source because it is not seedable. The seeded state is threaded explicitly, never global, so the same seed yields byte-identical output on any machine, any run. ## Immutability Generated artifacts and the `Corpus` result are **deep-frozen**: a fixture is safe to share across a pipeline without defensive copying. ## Stable fatal codes A generator has no input to tolerate, so its diagnostics are **fatal**: a request it cannot honor (an unsupported format or quirk) throws a typed `SynthError` carrying a stable `SYNTH_FATAL_CODES` value, never a silent fabrication. Codes are `key === value`, so the full set survives an `Object.values(...)` snapshot into a stability tripwire. `SynthError` takes a **code and nothing else**. Its message is looked up in the frozen `SYNTH_FATAL_MESSAGES` table, so a caller-supplied value has no parameter to enter through and cannot reach `message`, `stack`, or any field on the thrown object. The trade is deliberate: a fatal names the rule that refused rather than the value that tripped it, and the caller reads the value back off the arguments it passed. --- URL: https://docs.cosyte.com/synth/limitations Markdown: https://docs.cosyte.com/synth/limitations.md # What `@cosyte/synth` does and does not do `@cosyte/synth` is a **format/conformance generator, not a clinical simulator (not Synthea); every value is synthetic-by-construction; output is deterministic per seed within a version window; no terminology is bundled; and there is no DICOM in v1.** That one sentence governs the whole library. This page is the honest, blunt shape of the promise and its edges, read it before you rely on `synth`. The **API Reference** is always the exact truth of what a given release ships; this page is the shape of the whole. ## The promise (narrow, on purpose) `@cosyte/synth` emits **deterministic, seedable synthetic fixtures (and, in quirk mode, deliberately off-spec ones)** across the six Cosyte formats, and **every value it emits is drawn from a guaranteed-non-colliding synthetic source.** - **Built through the parser.** Each artifact is built **through the parser's own builder/serializer** (`@cosyte/hl7`'s `buildMessage`, `@cosyte/fhir`'s model + serializer, `@cosyte/ccda`'s `buildCcda`, the X12/NCPDP/ASTM domain builders), and it is checked by feeding the output straight back into that parser and asserting **zero warnings**. `synth` never hand-writes wire bytes around a builder. **Who did the proving matters, and it is not the same answer for every format: see [Who checks the output, and who does not](#who-checks-the-output-and-who-does-not) below.** - **Synthetic-by-construction.** There is no code path that can emit a name, identifier, date, phone, email, address, or IP not sourced from a reserved range or a shipped clearly-fake pool (see the posture below). This is the inverse of a de-identifier: `deid` proves real PHI is _gone_; `synth` proves plausibly-real PHI was _never generated_. - **Deterministic.** The same seed yields **byte-identical** output on any machine, any run, the property every downstream regression and golden-file suite depends on. Determinism is threaded through a hand-rolled seeded PRNG (`splitmix32`/`sfc32`); `Math.random` is lint-banned in `src/`. ## Who checks the output, and who does not **A round-trip is not an independent verdict.** Every artifact is built through a sibling parser's own builder and then read straight back by that same parser, which reports zero warnings. That is a real structural property and it is the one every format here carries. It is also **the same code grading its own work**: where the builder and the reader share a misreading of a standard, the round-trip cannot see it, because the misreading is on both sides of the check. So the round-trip is the floor, not the ceiling, and it is worth exactly what it says and no more. **One format is additionally graded by somebody who is not us.** On every change, the HL7-maintained FHIR validator (the `validator_cli` tool published with FHIR itself, pinned by version and by content digest so a verdict is attributable to a build) validates a seeded corpus of generated FHIR R4 resources against **US Core 6.1.0**. **Any issue it reports at severity `error` or `fatal` fails the build.** It is never recorded as a warning, never downgraded, and there is no suppression list. If an artifact ever has to come out of the graded set, it is named, with its seed and its reason, in the coverage declaration the run publishes alongside the verdict. | Format | Independent external verdict? | Who grades it, or why nobody does | | -------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **FHIR R4** | **Yes**, on every change | The HL7-maintained FHIR validator, against US Core 6.1.0 on FHIR R4 4.0.1 | | **HL7 v2** | No: round-trip only | No public, file-based, offline HL7 v2 validator was found. The available conformance tooling is a hosted service with no documented command-line or file entry point | | **C-CDA R2.1** | No: round-trip only | The public reference validator is server-shaped (a Java 8 web archive needing several gigabytes of memory, driven over an HTTP API) and ships none of the vocabulary it needs | | **X12 005010** | No: round-trip only | No public, file-based validator was found | | **NCPDP** | No: round-trip only | No public, file-based validator was found | | **ASTM E1394/E1381** | No: round-trip only | No public, file-based validator was found | The table is **generated against the formats the library actually emits**, not maintained by hand: a format that ships without an entry saying who grades it, or why nobody does, fails the run. **What the FHIR verdict does not include.** The external run is configured with **no terminology server**, deliberately: a verdict that depends on a live public service is reproducible by rerunning a job rather than from a seed, and nothing generated here is sent to a third party for terminology resolution. So the independent verdict covers structure and profile conformance against the pinned package, and **terminology-bound checks (code membership in a value set) are not part of it**. Its own record says so, alongside the exact list of endpoints the run contacted. ## What it does **not** do These are **non-goals**, not missing features: named so nothing over-trusts the generator. - **Not a clinical-simulation engine: this is the load-bearing boundary vs Synthea.** **Synthea** (MITRE) models each patient's disease progression and lifetime medical history and emits **clinically-coherent** records. `synth` is the opposite kind of tool: its randomness is **structural** (field shapes, delimiters, quirks, edge cases), not epidemiological. A `synth`-generated `ORU` may pair a diagnosis code and a result value that make no clinical sense, **and that is correct**, because its job is to exercise the _parser_, not to be a plausible patient. `synth` does **not** reimplement Synthea (optional Synthea-content ingestion is a documented future concern, below). - **Not statistically-representative populations.** No claim is made that a generated cohort matches any real-world distribution of age, sex, condition prevalence, or geography. `synth` optimizes for **structural coverage** (every message type, every quirk, every edge case), not demographic fidelity. - **Not a load / performance-test harness.** A seeded corpus is a fine throughput input, but `synth` ships no benchmark runner. - **No bundled terminology.** Codes (LOINC/SNOMED/ICD/RxNorm/CVX/NDC) come from a small **curated, license-clean** example pool (spec-example codes, invented `00000`-labeler NDCs, `X`-prefixed local codes) or a consumer-supplied pool. `synth` **never** bundles SNOMED, CPT, or a full LOINC table. - **No real-data ingestion as a value source.** `synth` never reads a real patient record to "learn" values. The only external content it may consume (later, optionally) is Synthea output, itself synthetic and PHI-free by construction. - **No DICOM (or any format outside the six).** DICOM Part 10 is binary with a distinct pixel-data hazard surface; v1 scopes the six text/EDI formats (HL7 v2, FHIR, C-CDA, X12, NCPDP, ASTM). A format `synth` does not support is a typed `SYNTH_UNSUPPORTED_FORMAT`, never a hand-written byte fallback. ## The synthetic-safety posture (the floors) Every PHI-bearing locus is filled from a source chosen so that it cannot denote a real person or resource. Each row below names the authority that reserves the range or defines the check digit, **by that authority's own published identifier**, so you can open the text and check the claim rather than taking this page's word for it. Where a row draws an inference from its source instead of quoting one, it says so. Two loci have **no** reserving authority at all and say that too, and one rests on a source that is **not** the issuing agency and says what it is not. | Locus | Source | Why it cannot be real, and the authority that says so | | --- | --- | --- | | **SSN** | area `900-999` with a group outside every published ITIN band, plus the fixed `987-00-432x` block | [SSA POMS RM 10201.035](https://secure.ssa.gov/poms.nsf/lnx/0110201035), Invalid Social Security Numbers (SSNs), defines an invalid SSN as "one that we never assigned" and identifies one by a first three digits (former area number) of `000`, `666`, or "in the 900 series", or a second group of two digits (former group number) of `00`. Both blocks above are invalid under that section. [IRS Internal Revenue Manual 3.21.263](https://www.irs.gov/irm/part3/irm_03-021-263r) adds the second authority sharing the number space: "An ITIN begins with a `9` and the 4th and 5th digits are 50-65, 70-88, 90-92 and 94-99", so a group outside those bands is not ITIN-formatted either. This replaces the earlier, uncited claim that SSA "never issues" these areas: what the manual states is invalidity, and that is what is cited. | | **NPI** | 10 digits with a **deliberately-invalid Luhn** check digit | [69 FR 3434](https://www.federalregister.gov/documents/full_text/text/2004/01/23/04-1149.txt), the final rule adopting the NPI (FR Doc 04-1149, docket CMS-0045-F), requires that "the NPI check digit calculation must always be performed as though the NPI is preceded by" `80840`, and that the check digit is "calculated using the ISO standard Luhn check digit algorithm", a modulus 10 double-add-double algorithm. A number whose check digit fails that calculation cannot be a validly issued NPI, and `isSyntheticNpi` proves the failure. This corrects the earlier, uncited attribution to a "CMS NPI check-digit rule, ISO 7812", and only the ISO half of it is retracted. **ISO 7812 is replaced by** the rule's own name for the algorithm, "the ISO standard Luhn check digit algorithm", quoted above: the rule names no ISO document number anywhere in its text. **The CMS attribution stands**, and is now cited instead of asserted: the same document records its issuing agency as "Centers for Medicare & Medicaid Services, HHS", carries the docket number above, and adds that "The specification for calculation of the NPI check digit will be made available on the CMS Web site". | | **DEA** | registrant letter + last-name initial + 7 digits with a **deliberately-invalid** check digit | **Non-normatively sourced.** The formula this floor inverts is stated in a pharmacy journal article, [Gabay, Federal Controlled Substances Act: Controlled Substances Prescriptions, Hospital Pharmacy, PMC3847977](https://pmc.ncbi.nlm.nih.gov/articles/PMC3847977/): "add the sum of the first, third, and fifth digits to twice the sum of the second, fourth, and sixth digits. The total should be a number whose last digit is the same as the last digit of the DEA number". **That article is not the DEA**, and no DEA-published statement of the algorithm is cited here. `isSyntheticDea` proves the failure against that formula and nothing stronger, so if the formula is wrong this floor can emit a checksum-**valid** DEA number while asserting the opposite. | | **Phone** | NANP `555-0100` through `555-0199` only | [NANPA, 555 Line Numbers](https://nanpa.com/numbering/555-line-numbers): "The fictitious, non-working numbers, 555-0100 through 555-0199, will remain reserved for entertainment/advertising." Not any 555 number: the same page lists `555-1212` and `555-4334` as still in use. | | **Email / domain** | `example.com/.net/.org`, `.test/.example/.invalid/.localhost` | [RFC 2606](https://datatracker.ietf.org/doc/html/rfc2606), Reserved Top Level DNS Names: "To safely satisfy these needs, four domain names are reserved" (the four top-level names above), plus the second-level names `example.com`, `example.net` and `example.org`. [RFC 6761](https://datatracker.ietf.org/doc/html/rfc6761) section 6.5 carries them into the special-use registry: "The domains example., example.com., example.net., example.org., and any names falling within those domains, are special". | | **IP** | `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`, `2001:db8::/32` | [RFC 5737](https://datatracker.ietf.org/doc/html/rfc5737): "The blocks 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), and 203.0.113.0/24 (TEST-NET-3) are provided for use in documentation." [RFC 3849](https://datatracker.ietf.org/doc/html/rfc3849): "The prefix allocated for documentation purposes is 2001:DB8::/32". | | **MRN / member / account** | a clearly-synthetic **assigning-authority** namespace (`COSYTE-SYNTH`) | **No authority reserves this locus.** An MRN is unique only inside its assigning authority and no registry reserves a range of them, so there is nothing external to cite and nothing is claimed. What the floor rests on instead is the _namespace_: every value is minted under an assigning authority this project invented and no real facility uses, so it cannot collide with a real record whatever its digits are. The OID `2.16.840.1.113883.19.999` is likewise a value this project chose, and no authority is cited for the root it sits under: no published text designating that root for example use could be shown, so no such designation is claimed here. | | **ZIP** | `00000` | [USPS Postal Facts, Lowest ZIP Code number](https://facts.usps.com/lowest-zip-code/): "The lowest ZIP Code number is 00501, unique for the Internal Revenue Service in Holtsville, NY." `00000` is below `00501`, from which **this project infers** that it is not an assigned ZIP Code. The inference is ours, not the Postal Service's: USPS states the lowest assigned number and says nothing about `00000`. | | **Names / streets / cities** | a shipped, curated **clearly-fake pool** | **No authority reserves this locus.** No registry reserves personal, street or city names, so there is nothing external to cite and nothing is claimed. What the floor rests on instead is the pool itself: every token is an invented, fixture-flavoured word (`Testina Testerson`, `Example Boulevard`, `Faketon`) that a reader can see at a glance names nobody, and no code path draws a name, street or city from anywhere else. | The floors are enforced two ways: a **union `phi-scan`** (the parsers' own PHI scanners) sweeps a representative generated corpus in CI and must report **zero** real-data hits, and a **property suite** asserts, for arbitrary seeds and every format, that no emitted value escapes these sources. A quirk fixture deviates _structure_, never _provenance_: the safety floor holds over quirk output too. **The diagnostics have their own floor, and it is a different one.** The table above is about the values `synth` emits. A separate guarantee covers what `synth` says about your call: - A fatal message is a **fixed string from a frozen registry**. `SynthError` takes a code and no value parameter, so there is no position through which a value could reach `message`, `stack`, or a field on the thrown object. - Caller-supplied **selectors** (a message kind, a document type, a corpus mix entry, a claim variant, a Bundle type, a resource profile, a quirk name) are resolved against their own closed set before anything is generated. An unrecognised one is a fatal `SYNTH_UNSUPPORTED_KIND` (or `SYNTH_UNSUPPORTED_QUIRK`), so it does not reach a peer builder that might quote it back and does not become an artifact `kind` or a manifest key. - A round-trip result keeps only the sibling parser's warning **codes**, never the parser's message and never a snippet of the document. Each of those is asserted per position, against a marker planted in each one, rather than argued from the fact that a fixture generator's values are synthetic anyway. **The list of positions is an enumeration, not a proof of exhaustiveness.** What holds generally is the mechanism: the error type has no value parameter, and a selector is resolved against its closed set in one place, everywhere except the `@cosyte/synth/deid` pairing loops. Those are the stated exception: `x12DeidLoop({ variant })` and `ncpdpTelecomDeidLoop({ transaction })` still surface an uncoded `TypeError` for an unrecognised value, and `ccdaDeidLoop({ documentType })` still generates a Referral Note for anything but `"ccd"`. None echoes your value into a message, and all three are tracked separately. **Three things it does not cover, stated rather than implied.** The **artifact** is not a diagnostic: `content` is what you asked to be built and carries what you asked for, and the same goes for a name pool or a profile name you supply and get handed back. A **document or model you pass to a round-trip harness** is parsed by the sibling parser, and a fatal it raises on input it could not read is that parser's diagnostic, not this one's. And a **caller-authored label** (a `SynthProfile` name, an `Artifact` you construct yourself and hand to `makeCorpus`) is stored and returned verbatim, because that is what you asked for; no `synth` code path derives anything from it. ```ts runnable import { createRng, safe, isSyntheticSsn, isItinFormatted, isSyntheticNpi, isSyntheticDea, } from "@cosyte/synth"; const rng = createRng(1); // Every provider draws from a never-collide source: the checks below can never be false. const nationalId = safe.ssn(rng); // The SSN locus clears BOTH authorities that share its number space, not just SSA's. const ssnOk = isSyntheticSsn(nationalId) && !isItinFormatted(nationalId); ssnOk && isSyntheticNpi(safe.npi(rng)) && isSyntheticDea(safe.dea(rng)); // => true ``` ## Determinism holds within a version window A seed maps to the same bytes **within a documented compatibility window**, not across _major_ `synth` versions. A version bump may change a value list or the algorithm and thus the seed→bytes mapping; that is a **documented breaking change**. For a long-lived golden fixture, **pin the `synth` version, and the versions of the parsers you generate with, alongside the seed.** Each format's bytes come from that parser's own builder and serializer, so upgrading a parser can change them even when `synth` has not moved: measured, the same C-CDA seeds serialize differently under two releases of `@cosyte/ccda`. **Which engines that is verified across, and how.** The seed→bytes mapping is verified **byte-identical across Node 22 and Node 24**, on every change. It is verified by **comparing digests across separate runs**, not by generating twice in one process: a declared seed corpus covering all six formats is generated in its own job on each of those Node majors, each job carries out one digest per `(format, seed)` pair and nothing else, and a third job fails the build if two engines disagree. A mismatch is reported by seed, format and engine, never by content. Each per-engine run is also compared against a **committed baseline** for the current window, so a toolchain or dependency change that moves every engine together is caught as well. **A change to that mapping is released as a breaking change.** The baseline is committed, and it cannot change without a release declaring a major version bump for the package, so a golden fixture pinned to a version inside the window keeps matching. Node majors outside the verified pair are not promised: `engines.node` says which ones are supported, and this check says which ones are measured. ## Coverage, and what is deferred The generation core is **feature-complete across all six formats**. Quirk mode and the `deid` pairing loop ship for a subset; the honest gaps: - **Vendor-quirk mode** ships for the three richest profile systems: **HL7 v2, C-CDA, ASTM**. Quirk recipes for **FHIR / X12 / NCPDP are deferred**, as is any quirk needing a private vendor corpus (built-in quirks are grounded only on **public** vendor profiles). - **The `deid` pairing loop** ships for **HL7 v2, FHIR, C-CDA, X12, and NCPDP Telecom**. NCPDP **SCRIPT**, **ASTM**, and **DICOM** pairing are deferred (no adapter, or not generated: `DEID_LOOP_SKIPPED` names each). - **Format-specific gaps flagged, never faked:** the X12 **270** request (`@cosyte/x12` ships `build270` from 0.1.0; `synth` does not use it yet) and NCPDP **SCRIPT lifecycle responses** are not generated, a gap is surfaced, never hand-written. - **Built-in `synth` profiles.** `defineSynthProfile()` is the public growth-loop hook, and ready-made quirk profiles ship for the three quirk formats; broader **named site/vendor** recipes stay **consumer-authored** until a public spec grounds a built-in one (the same public-only discipline the parsers hold). - **Optional Synthea clinical-content ingestion** (re-serialize Synthea's coherent records through the Cosyte parsers) is a **documented future concern**, not a v1 promise. ### US Core 6.1.0 profile coverage You can address `synth` by **US Core 6.1.0 profile name**, and there are exactly two answers: a fixture that claims that profile, or a refusal raised **before any artifact exists**. There is no third outcome, and no default profile to fall through to. The adopted implementation guide publishes **49 resource profiles**. This library generates **11** of them: `us-core-allergyintolerance`, `us-core-condition-problems-health-concerns`, `us-core-diagnosticreport-lab`, `us-core-encounter`, `us-core-immunization`, `us-core-medicationrequest`, `us-core-observation-lab`, `us-core-patient`, `us-core-procedure`, `us-core-provenance`, `us-core-vital-signs`. **Every other adopted profile refuses**, and it refuses with a code distinct from the one an unrecognized name gets, because the two are different facts about your request: - `SYNTH_PROFILE_NOT_GENERATED`: the guide publishes this profile and this build does not generate it yet. The other 38 adopted profiles are here. - `SYNTH_UNSUPPORTED_KIND`: the name is not in the adopted set at all: a typo, a blank, a value that is not a string, or one of the guide's **extension definitions** (`us-core-race` and its nine siblings), which are not standalone artifacts and so are not addressable. Neither returns a mislabelled artifact, and neither quotes your value back (see the diagnostics floor above). **Read the split as data, not off this page.** `usCoreCoverage()` reports one entry per adopted profile, carrying its canonical URL and whether this build generates it. This page can go stale between releases; that function is derived from the generators themselves and cannot. ```ts runnable import { usCoreCoverage, generateUsCoreProfile, roundTrip } from "@cosyte/synth/fhir"; const coverage = usCoreCoverage(); coverage.length; // => 49 coverage.filter((entry) => entry.generated).length; // => 11 // A covered profile: an artifact claiming that profile's canonical URL. const provenance = generateUsCoreProfile({ profile: "us-core-provenance", seed: 42 }); roundTrip(provenance).specClean; // => true // An adopted profile this build does not generate: a refusal, never an artifact. let refusedCode = ""; try { generateUsCoreProfile({ profile: "us-core-careteam", seed: 42 }); } catch (err) { refusedCode = (err as { code: string }).code; } refusedCode; // => "SYNTH_PROFILE_NOT_GENERATED" ``` ## Licensing & PHI posture - **The library is MIT.** Third-party **runtime** dependencies are **zero**; the parser and `deid` peers are first-party, optional, and lazily loaded per format. - **No real PHI.** `synth`'s entire output _looks like_ PHI and contains none, by construction. Every value it emits is drawn from the sources in the floor table above, and a `phi-scan` gate sweeps this project's own sources, fixtures and tooling on every change. You can commit and log a generated corpus without a PHI review of its contents: that is the whole point. --- URL: https://docs.cosyte.com/synth/guides-overview Markdown: https://docs.cosyte.com/synth/guides-overview.md # Guides Task-oriented recipes ("how do I X?") for `@cosyte/synth`. Each guide is a short, copy-pasteable answer to one real fixture-generation question. > **Status:** the seeded-PRNG core, the synthetic-safety providers and the round-trip harness ship, > alongside the HL7 v2 message set (`ADT`, `ORU^R01`, `ORM^O01`, `SIU^S12`, `VXU^V04`); FHIR R4 / > US Core (see the [FHIR guide](https://docs.cosyte.com/synth/guides-fhir.md)); C-CDA generation (CCD + Referral Note via > `@cosyte/ccda`'s `buildCcda`, the [C-CDA guide](https://docs.cosyte.com/synth/guides-ccda.md)); X12 005010 (837P/I/D, 835, 271 via > `@cosyte/x12`, the [X12 guide](https://docs.cosyte.com/synth/guides-x12.md)); NCPDP (SCRIPT + Telecom via `@cosyte/ncpdp`, the > [NCPDP guide](https://docs.cosyte.com/synth/guides-ncpdp.md)); and ASTM (E1394 records + E1381 framing via `@cosyte/astm`, the > [ASTM guide](https://docs.cosyte.com/synth/guides-astm.md)), the generation core across all six formats. On top of it, > **vendor-quirk generation** for HL7 v2, C-CDA, and ASTM (see the [quirk guide](https://docs.cosyte.com/synth/guides-quirks.md)). A > guide is only written once the behavior it documents is shipped and its runnable example passes the > doc/code-agreement check. ## Pin a seed for a reusable golden fixture A `Corpus` is a seed plus a manifest of what was generated: deep-frozen and self-describing. Pin the seed (and the versions of `synth` and the parsers you generate with) and every downstream run regenerates the identical set: ```ts runnable import { hl7Corpus } from "@cosyte/synth/hl7"; const corpus = hl7Corpus({ seed: 1867, count: 4 }); corpus.seed; // => 1867 ``` ## Verify an artifact round-trips before you trust it The round-trip harness checks an artifact by the parser's own judgment: it re-parses with zero warnings and re-serializes byte-identically: ```ts runnable import { generateAdt, roundTrip } from "@cosyte/synth/hl7"; const { warnings, byteStable } = roundTrip(generateAdt({ seed: 21 })); [warnings.length, byteStable]; // => [0, true] ``` ## Planned guides Shipped: **[vendor-quirk generation](https://docs.cosyte.com/synth/guides-quirks.md)**, inject the deviations a parser tolerates, each round-tripping to exactly the intended warning code (HL7 v2, C-CDA, and ASTM). Expect recipes such as: - **Feed a parser's three-tier conformance corpus**: tiers 1 and 2, mechanically and seedably. - **Pair with `@cosyte/deid`**: generate clean, plant tagged synthetic sentinels, de-identify, verify. - **Quirk recipes for FHIR, X12, and NCPDP**: available as those parsers' profile/quirk surface lands. Until then, the [Quickstart](https://docs.cosyte.com/synth/quickstart.md) covers the core primitives and the HL7 corpus. --- URL: https://docs.cosyte.com/synth/guides-fhir Markdown: https://docs.cosyte.com/synth/guides-fhir.md # Generate FHIR R4 / US Core resources The `@cosyte/synth/fhir` subpath builds FHIR resources **through `@cosyte/fhir`'s own model constructors and serializer**, so every resource is **spec-clean by construction**: it validates under `@cosyte/fhir.validateResource` and re-serializes byte-for-byte. `@cosyte/fhir` is an **optional peer dependency**, needed only for this subpath. The FHIR surface covers the **US Core clinical set**: `Patient` (base + US Core), `Condition` (problem-list item), `Observation` (US Core Laboratory Result + Vital Signs), `MedicationRequest`, `Encounter`, `DiagnosticReport` (Laboratory), `Immunization`, `AllergyIntolerance`, `Procedure`, and `Provenance`, assembled into a `collection`, `transaction`, or `document` `Bundle`. ## A US Core Patient, spec-clean by construction ```ts runnable import { generatePatient, roundTrip } from "@cosyte/synth/fhir"; // Same seed → byte-identical resource, on any machine, any run. const patient = generatePatient({ seed: 12345, profile: "us-core" }); // Fed straight back into @cosyte/fhir: validates with zero errors and re-serializes byte-identically. const result = roundTrip(patient); result.specClean; // => true ``` Every identity field (name, MRN, birth date, phone, email, address) is drawn from a guaranteed-non-colliding synthetic source, so **no generated value can be real or plausibly-real PHI**. ## The clinical spine, wired to a patient Each clinical generator takes a `subject` reference (and, for `MedicationRequest`, a `requester`): ```ts runnable import { generateCondition, generateObservationLab, roundTrip } from "@cosyte/synth/fhir"; const condition = generateCondition({ seed: 7, subject: "Patient/syn-patient-1" }); const lab = generateObservationLab({ seed: 8, subject: "Patient/syn-patient-1" }); roundTrip(condition).errors; // => [] roundTrip(lab).errors; // => [] ``` ## Validating against US Core (bring your own profile) `@cosyte/synth` bundles **no** US Core IG: matching `@cosyte/fhir`'s content-free posture. To assert US Core conformance, load the published US Core `StructureDefinition`s yourself and pass them to `roundTrip`: ```ts import { generatePatient, roundTrip } from "@cosyte/synth/fhir"; import { loadStructureDefinition, parseResource } from "@cosyte/fhir"; import { readFileSync } from "node:fs"; const { resource } = parseResource(readFileSync("us-core-patient.json", "utf8")); const usCorePatient = loadStructureDefinition(resource); const result = roundTrip(generatePatient({ seed: 1, profile: "us-core" }), { profiles: usCorePatient ? [usCorePatient] : [], }); result.errors; // [], conformant to the real US Core 6.1.0 Patient profile ``` ## The rest of the clinical set `Encounter`, `DiagnosticReport`, `Immunization`, `AllergyIntolerance`, and `Procedure` follow the same shape, a `subject`/`patient` reference and a US Core `meta.profile` by default: ```ts runnable import { generateEncounter, generateImmunization, generateAllergyIntolerance, generateProcedure, generateDiagnosticReport, roundTrip, } from "@cosyte/synth/fhir"; const p = "Patient/syn-patient-1"; roundTrip(generateEncounter({ seed: 11, subject: p })).errors; // => [] roundTrip(generateImmunization({ seed: 12, patient: p })).errors; // => [] roundTrip(generateAllergyIntolerance({ seed: 13, patient: p })).errors; // => [] roundTrip(generateProcedure({ seed: 14, subject: p })).errors; // => [] roundTrip(generateDiagnosticReport({ seed: 15, subject: p })).errors; // => [] ``` `Provenance` records who authored and transmitted a resource. It follows the same shape, with a `target` reference instead of a subject: ```ts runnable import { generateProvenance, roundTrip } from "@cosyte/synth/fhir"; const provenance = generateProvenance({ seed: 16, target: "Patient/syn-patient-1" }); roundTrip(provenance).errors; // => [] ``` ## Asking by profile name A generator name is this library's vocabulary; a **US Core 6.1.0 profile name** is the regulation's. `generateUsCoreProfile` takes the second and resolves it against the profile set the adopted guide publishes **before** generating anything, so a name it cannot honor is a refusal rather than a mislabelled artifact. `usCoreCoverage` is the readable form of that set: one entry per adopted profile, with its canonical URL and whether this build generates it. ```ts runnable import { usCoreCoverage, generateUsCoreProfile, roundTrip } from "@cosyte/synth/fhir"; usCoreCoverage().length; // => 49 usCoreCoverage().filter((entry) => entry.generated).length; // => 11 const patient = generateUsCoreProfile({ profile: "us-core-patient", seed: 12345 }); roundTrip(patient).specClean; // => true ``` Which profiles are covered, which refuse, and the two distinct refusal codes: [what it does and does not do](https://docs.cosyte.com/synth/limitations.md). ## A self-contained Bundle `generateBundle` assembles a `Patient` and its full clinical spine into a `collection`, `transaction`, or `document` Bundle, wired by `urn:uuid:` references so **every reference resolves in-bundle**. A `document` Bundle leads with the FHIR-mandated `Composition` and carries the required `identifier` and `timestamp`: ```ts runnable import { generateBundle, fhirCorpus } from "@cosyte/synth/fhir"; const doc = generateBundle({ seed: 42, type: "document" }); doc.properties.some((p) => p.name === "entry"); // => true // Or a reproducible mixed corpus across the whole clinical set: const corpus = fhirCorpus({ seed: 2026, count: 11 }); corpus.artifacts.length; // => 11 ``` ## Next - [Core Concepts](https://docs.cosyte.com/synth/concepts-archetype.md): synthetic-by-construction, determinism, the round-trip gate. - **API Reference**: every export, generated from source. --- URL: https://docs.cosyte.com/synth/guides-ccda Markdown: https://docs.cosyte.com/synth/guides-ccda.md # Generate C-CDA documents The `@cosyte/synth/ccda` subpath builds Consolidated CDA R2.1 documents **through `@cosyte/ccda`'s `buildCcda`**, so template IDs, LOINC section codes, and structured/narrative agreement are the builder's own: every document is **spec-clean by construction** and round-trips through `parseCcda` with **zero warnings**. `@cosyte/ccda` is an **optional peer dependency**, needed only for this subpath. The C-CDA surface emits a **Continuity of Care Document (CCD)** (`generateCcd`) or a **Referral Note** (`generateReferralNote`), each populating the CCD sections: Problems, Allergies, Medications, Results, Vital Signs, Immunizations, Procedures, and Social History (Smoking Status), from the reused, license-clean example-code pools. Every `recordTarget` identity is drawn from the synthetic-safety providers: the patient name from the shipped fake-name pool, the MRN under a synthetic assigning-authority OID, and every date from the seeded generator. ## A CCD, spec-clean by construction The round-trip harness proves spec-cleanliness by `@cosyte/ccda`'s own judgment: a spec-clean document re-parses with zero warnings and re-serializes byte-identically: ```ts runnable import { generateCcd, roundTrip } from "@cosyte/synth/ccda"; // Same seed → byte-identical document, on any machine, any run. const { warnings, byteStable } = roundTrip(generateCcd({ seed: 12345 })); [warnings.length, byteStable]; // => [0, true] ``` ## A Referral Note `generateReferralNote` emits the second document type `buildCcda` supports: its own US Realm Header specialization plus the Reason-for-Referral and Assessment narrative sections: ```ts runnable import { generateReferralNote } from "@cosyte/synth/ccda"; const note = generateReferralNote({ seed: 7 }); note.documentType; // => "referralNote" ``` ## A reproducible mixed corpus `ccdaCorpus` builds a deep-frozen, self-describing corpus (one CCD and one Referral Note by default), each document round-tripped through the parser so its `warnings` record the verdict: ```ts runnable import { ccdaCorpus } from "@cosyte/synth/ccda"; const corpus = ccdaCorpus({ seed: 1867, count: 4 }); corpus.artifacts.every((a) => a.warnings.length === 0); // => true ``` --- URL: https://docs.cosyte.com/synth/guides-x12 Markdown: https://docs.cosyte.com/synth/guides-x12.md # Generate X12 EDI transactions The `@cosyte/synth/x12` subpath builds HIPAA **005010** transactions **through `@cosyte/x12`'s domain builders** (`build837P/I/D`, `build835`, `build271`), so the ISA/GS/ST…SE/GE/IEA envelope, the computed HL spine, the control numbers, and every segment are the builder's own: each transaction round-trips through `@cosyte/x12` with **zero warnings**. `@cosyte/x12` is an **optional peer dependency**, needed only for this subpath. The X12 surface emits: - **837** claims: Professional (`generate837P`), Institutional (`generate837I`), and Dental (`generate837D`); - the **835** remittance (`generate835`): **balance-checked by construction** (the line, claim, and remit balance identities are satisfied before the builder is called, so `build835` never refuses it); - the **271** eligibility response (`generate271`). ## A claim, spec-clean by construction The round-trip harness checks an interchange by `@cosyte/x12`'s own judgment: it re-parses with zero warnings and re-serializes byte-identically: ```ts runnable import { generate837P, roundTrip } from "@cosyte/synth/x12"; // Same seed → byte-identical EDI, on any machine, any run. const result = roundTrip(generate837P({ seed: 12345 })); result.warnings; // => [] result.byteStable; // => true result.specClean; // => true ``` ## A reproducible corpus `x12Corpus` builds a self-describing, deterministic corpus: one of each of 837P/I/D + 835 + 271 by default: ```ts runnable import { x12Corpus } from "@cosyte/synth/x12"; const corpus = x12Corpus({ seed: 42 }); corpus.artifacts.map((a) => a.kind); // => ["837P", "837I", "837D", "835", "271"] corpus.artifacts.every((a) => a.warnings.length === 0); // => true ``` ## Synthetic-by-construction, hardest-attacked here An 837/271 is identity-dense: it carries subscriber and patient names, member ids, provider NPIs, a provider tax id, dates of birth, and addresses across two HL loops. Every value has a construction-level guarantee, not a heuristic: - **NPI** (`NM1*XX`): a 10-digit National Provider Identifier with a **deliberately-invalid Luhn check digit**. A real NPI must satisfy the CMS `80840`-prefixed Luhn check, so a `synth` NPI can **never** be a NPPES-issued provider. (`safe.npi`, `isSyntheticNpi`.) - **Provider tax id** (`REF*SY`): an SSA **never-issued** (900-range) SSN, so it can never be a real SSN or a sole-proprietor's real tax id. - **Member id** (`NM1*MI`): scoped to a synthetic assigning authority (there is no reserved member-id range, so the _namespace_ is the guarantee). - **Names**: the shipped clearly-fake pool; **DOBs / dates**: the seeded generator (no real event implied); **addresses**: a synthetic street + reserved ZIP. The repository's `phi-scan` gate runs X12-aware structured detection over every generated corpus: a Luhn-**valid** `NM1*XX` NPI, a real-area `REF*SY` SSN, or an `NM1*34` raw-SSN qualifier is a hard failure. ## Deferred - The **270** eligibility _request_: `@cosyte/x12` ships `build270` from 0.1.0, but `synth` does not generate a 270 yet. `synth` builds every artifact through the parser's own builder or serializer and never hand-writes the bytes. - **Vendor-quirk mode** (deliberately off-spec fixtures) is deferred for X12. --- URL: https://docs.cosyte.com/synth/guides-ncpdp Markdown: https://docs.cosyte.com/synth/guides-ncpdp.md # Generate NCPDP messages The `@cosyte/synth/ncpdp` subpath builds both NCPDP standards **through `@cosyte/ncpdp`'s own emit surface**, so every message round-trips through the parser with **zero warnings**. `@cosyte/ncpdp` is an **optional peer dependency**, needed only for this subpath. NCPDP is two structurally unrelated standards under one brand, and the surface covers both: - **SCRIPT** (XML ePrescribing): `generateNewRx` (built through the validated `buildNewRx` builder), `generateRxRenewalRequest`, and `generateRxChangeRequest` (built as `@cosyte/ncpdp`'s public typed `ScriptMessage` model, then serialized by `serializeScript`: the same typed-model→serializer path the X12 surface uses, never a hand-written byte). - **Telecom** (vD.0 pharmacy claims): `generateB1` (billing), `generateB2` (reversal), and `generateB3` (rebill), built through `buildTelecomRequest` + `serializeTelecom`. ## A NewRx, spec-clean by construction The round-trip harness checks a message by `@cosyte/ncpdp`'s own judgment: it re-parses with zero warnings and re-serializes byte-identically: ```ts runnable import { generateNewRx, scriptRoundTrip } from "@cosyte/synth/ncpdp"; // Same seed → byte-identical SCRIPT XML, on any machine, any run. const result = scriptRoundTrip(generateNewRx({ seed: 12345 })); result.warnings; // => [] result.byteStable; // => true result.specClean; // => true ``` ## A Telecom claim ```ts runnable import { generateB1, telecomRoundTrip } from "@cosyte/synth/ncpdp"; const result = telecomRoundTrip(generateB1({ seed: 777 })); result.specClean; // => true ``` ## A reproducible corpus `ncpdpCorpus` builds a self-describing, deterministic corpus: one of each shipped transaction by default: ```ts runnable import { ncpdpCorpus } from "@cosyte/synth/ncpdp"; const corpus = ncpdpCorpus({ seed: 42 }); corpus.artifacts.map((a) => a.kind); // => ["NewRx", "RxRenewalRequest", "RxChangeRequest", "B1", "B2", "B3"] corpus.artifacts.every((a) => a.warnings.length === 0); // => true ``` ## Synthetic-by-construction: patient _and_ prescriber NCPDP carries patient identity (name, DOB) **and** prescriber identity, including the **DEA** number, an identity locus the other formats do not have. Every value has a construction-level guarantee: - **Prescriber NPI**: a 10-digit NPI with a **deliberately-invalid Luhn check digit**, so it can never be a NPPES-issued provider (`safe.npi`, `isSyntheticNpi`). - **Prescriber DEA**: a `XX`+7-digit DEA number with a **deliberately-invalid checksum**. A real DEA number's 7th digit satisfies the published `(d1+d3+d5) + 2·(d2+d4+d6)` checksum, so a `synth` DEA can **never** be a validly-issued registration (`safe.dea`, `isSyntheticDea`). - **Patient / cardholder / member ids**: scoped to a synthetic assigning authority (`MBR`-prefixed; there is no reserved id range, so the _namespace_ is the guarantee). - **Names**: the shipped clearly-fake pool; **phone**: the reserved `555-01xx` block; **DOBs / dates** (including SCRIPT `SentTime`): the seeded generator (never wall-clock); **drug content**: a small license-clean example pool with invented `00000`-labeler NDCs (no NCPDP-copyrighted text is bundled). ```ts runnable import { isSyntheticNpi, isSyntheticDea, createRng } from "@cosyte/synth"; import { ncpdpPrescriber } from "@cosyte/synth/ncpdp"; const dr = ncpdpPrescriber(createRng(1)); isSyntheticNpi(dr.npi); // => true isSyntheticDea(dr.dea); // => true ``` The repository's `phi-scan` gate runs NCPDP-aware structured detection over every generated corpus: a Luhn-**valid** `` / `DB` or a checksum-**valid** `` is a hard failure: it could denote a real provider. ## Deferred - SCRIPT coverage **tracks the parser's builder surface**: the renewal/change _responses_ and other lifecycle transactions land as `@cosyte/ncpdp` grows its builders. `synth` never hand-writes bytes around a missing builder. - **Vendor-quirk mode** (deliberately off-spec fixtures) is deferred for NCPDP. --- URL: https://docs.cosyte.com/synth/guides-astm Markdown: https://docs.cosyte.com/synth/guides-astm.md # Generate ASTM messages The `@cosyte/synth/astm` subpath builds ASTM laboratory messages **through `@cosyte/astm`'s own emit surface**: `buildAstmMessage` for the E1394 record layer and `composeAstmFrames` for the E1381 frame layer, so every message round-trips through the parser with **zero warnings**. `@cosyte/astm` is an **optional peer dependency**, needed only for this subpath. The surface covers both layers: - **Records (E1394)**: `generateAstmResult` emits a full `H`/`P`/`O`/`R`…/`C`/`L` result report; `generateAstmOrder` emits the `H`/`P`/`O`/`L` order side. Both are built through `buildAstmMessage`, so the `H|\^&` delimiter declaration, the record type letters, the per-type sequence counters, and the `L` terminator are the parser's own conservative emit, never a hand-written byte. - **Framing (E1381)**: `generateAstmResultFramed` wraps the same records in the ` FN text CS ` frame envelope, with the **modulo-256 checksum and the `0` to `7` frame number computed by `@cosyte/astm`**, never faked. Every value at the `P` (patient) record (the name, birthdate, and the practice- and laboratory-assigned patient IDs) is drawn from the synthetic-safety providers, so no output can be real or plausibly-real PHI. The practice- and lab-assigned IDs are minted independently, so they stay **distinct** (exactly as `@cosyte/astm` keeps them distinct on parse). `synth` is a **format/conformance generator, not a clinical simulator**: a generated result pairs a code and a value with no claim of clinical coherence. ## A result report The round-trip harness checks a message by `@cosyte/astm`'s own judgment: it re-parses with zero warnings and re-serializes byte-identically: ```ts runnable import { generateAstmResult, astmRoundTrip } from "@cosyte/synth/astm"; // Same seed → byte-identical ASTM record stream, on any machine, any run. const result = astmRoundTrip(generateAstmResult({ seed: 12345 })); result.warnings; // => [] result.byteStable; // => true result.specClean; // => true ``` ## A framed (E1381) message The framed twin carries the same records with a real frame envelope: the checksum and frame numbers are computed, so it decodes with zero frame **and** record warnings: ```ts runnable import { generateAstmResultFramed, astmFramedRoundTrip } from "@cosyte/synth/astm"; const framed = astmFramedRoundTrip(generateAstmResultFramed({ seed: 12345 })); framed.warnings; // => [] framed.specClean; // => true ``` ## A reproducible corpus `astmCorpus` builds a deep-frozen, seed-tagged set: a downstream repo pins the seed and gets a stable fixture set that regenerates identically: ```ts runnable import { astmCorpus } from "@cosyte/synth/astm"; const corpus = astmCorpus({ seed: 42 }); corpus.artifacts.length; // => 2 corpus.artifacts.every((a) => a.warnings.length === 0); // => true ``` ## Vendor-quirk mode **Quirk mode ships for ASTM**: a non-standard `&Z&` escape round-trips to exactly `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, re-badged by `@cosyte/astm`'s public `referenceCorpus` profile, and a site-defined record type round-trips to `ASTM_RECORD_UNKNOWN_TYPE`. See [Vendor-quirk generation](https://docs.cosyte.com/synth/guides-quirks.md). The frame-layer transport quirks (a deliberately-wrong checksum, framing dropped over TCP) target non-profile diagnostics and are not generated. --- URL: https://docs.cosyte.com/synth/guides-quirks Markdown: https://docs.cosyte.com/synth/guides-quirks.md # Generate vendor-quirk fixtures Spec-clean fixtures prove a parser reads a _correct_ message. The harder, more valuable test is whether a parser tolerates the **realistic vendor deviations** real-world traffic carries, and surfaces exactly the right diagnostic when it does. That is what quirk mode generates. The load-bearing idea: **the quirk vocabulary _is_ the parsers' own profile systems.** A `synth` quirk deviates the _structure_ of a message that otherwise re-parses with zero warnings so it round-trips through the parser to **exactly one intended, stable warning code**, the tolerance the corresponding parser profile (`hl7.defineProfile`, `ccda.defineCcdaProfile`, `astm.defineAstmProfile`) encodes. This is the **intended-warning contract**: a quirk fixture is never a fiction, it exercises a documented, coded leniency, and where a built-in **public** parser profile claims the deviation, the quirk round-trips cleanly under it (suppressed, or re-badged to `PROFILE_QUIRK_APPLIED`). Every quirk changes the message _shape_, never the _provenance_ of the data, so a quirk fixture is still **synthetic-by-construction** and passes the same synthetic-safety gate. Three formats ship quirks: **HL7 v2**, **C-CDA**, and **ASTM**, the parsers with the richest profile systems. A quirk a format's profile system does not support fails closed with a stable `SYNTH_UNSUPPORTED_QUIRK` diagnostic, never a silently-wrong fixture. ## HL7 v2: a vendor Z-segment A site-defined `Z`-segment is the canonical HL7 v2 quirk. A receiver with no profile flags it `UNKNOWN_SEGMENT`; a `defineProfile` that declares the segment (here `@cosyte/hl7`'s public `visage` imaging profile, which grounds `ZDS` in the Visage 7 interface spec) **suppresses** it: ```ts runnable import { generateHl7Quirk, hl7QuirkRoundTrip } from "@cosyte/synth/hl7"; const rt = hl7QuirkRoundTrip(generateHl7Quirk({ seed: 12345, quirk: "unknown-zsegment" })); rt.warnings; // => ["UNKNOWN_SEGMENT"] rt.intendedWarningHeld; // => true rt.withProfile?.tolerated; // => true ``` ## C-CDA: a deprecated LOINC code C-CDA profiles **re-badge** a tolerated deviation to the value-free `PROFILE_QUIRK_APPLIED` marker. A deprecated result LOINC round-trips to `DEPRECATED_LOINC`, and `@cosyte/ccda`'s public `smartScorecard` profile (grounded in the SMART C-CDA Scorecard) re-badges it as expected: ```ts runnable import { generateCcdaQuirk, ccdaQuirkRoundTrip } from "@cosyte/synth/ccda"; const rt = ccdaQuirkRoundTrip(generateCcdaQuirk({ seed: 12345, quirk: "deprecated-loinc" })); rt.warnings; // => ["DEPRECATED_LOINC"] rt.withProfile?.warnings; // => ["PROFILE_QUIRK_APPLIED"] rt.withProfile?.tolerated; // => true ``` ## ASTM: a non-standard escape An ASTM `&Z&` escape body round-trips to `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, and `@cosyte/astm`'s public `referenceCorpus` profile (grounded in the redistributable kxepal/python-astm + senaite OSS corpus) re-badges it: ```ts runnable import { generateAstmQuirk, astmQuirkRoundTrip } from "@cosyte/synth/astm"; const rt = astmQuirkRoundTrip(generateAstmQuirk({ seed: 12345, quirk: "unknown-escape" })); rt.warnings; // => ["ASTM_UNKNOWN_ESCAPE_SEQUENCE"] rt.withProfile?.tolerated; // => true ``` ## Unsupported quirks fail closed A quirk the format's profile system does not support is a fatal `SYNTH_UNSUPPORTED_QUIRK`, never a silent no-op and never a fabricated quirk with a made-up warning: ```ts runnable import { generateHl7Quirk } from "@cosyte/synth/hl7"; let code = ""; try { generateHl7Quirk({ seed: 1, quirk: "no-such-quirk" as never }); } catch (err) { code = (err as { code?: string }).code ?? ""; } code; // => "SYNTH_UNSUPPORTED_QUIRK" ``` Branch on `err.code`, never on `err.message`. The message is a fixed entry in the frozen `SYNTH_FATAL_MESSAGES` table and does **not** name the quirk you asked for: `SynthError` takes a code and no value parameter, so nothing you pass can reach the thrown object. To see what a format accepts, read its registry directly: `Object.keys(HL7_QUIRKS)`, `Object.keys(CCDA_QUIRKS)`, `Object.keys(ASTM_QUIRKS)`. ## A reproducible quirk corpus Each format ships a quirk corpus that cycles every built-in quirk, seed-tagged and self-describing: its manifest lists the applied quirks: ```ts runnable import { ccdaQuirkCorpus } from "@cosyte/synth/ccda"; const corpus = ccdaQuirkCorpus({ seed: 42 }); [...corpus.manifest.quirks].sort(); // => ["deprecated-code-system", "deprecated-loinc", "template-extension-absent"] ``` ## Grounding: public-only, never a private corpus Every shipped quirk is grounded in a **publicly-documented** deviation or a parser's **public** profile: a published IG, a vendor interface spec, or a redistributable OSS corpus. A quirk that would need a **private, vendor-attributed corpus** to ground is **not shipped**. Quirk recipes for FHIR, X12, and NCPDP are deferred. --- URL: https://docs.cosyte.com/synth/guides-deid Markdown: https://docs.cosyte.com/synth/guides-deid.md # Co-validate with `@cosyte/deid` A synthetic-fixture generator and a de-identifier are natural partners: `@cosyte/synth` produces data that **looks like** PHI but contains none, and `@cosyte/deid` removes PHI. The `@cosyte/synth/deid` subpath closes the loop between them, a deterministic, seeded **co-validation harness**: 1. **Generate** a synthetic artifact through `synth`'s own generators. 2. **Enumerate** the distinctive synthetic PHI sentinels `synth` planted at the patient loci. 3. **De-identify** it through `@cosyte/deid`. 4. **Verify** every sentinel is gone from the de-identified output: a surviving sentinel is a hard failure, **and** that the clinical payload survives (the over-scrub guard). > **Honest scope.** This proves _the pair works on `synth`'s own output_: it is **not** an independent > audit of `@cosyte/deid` against real-world documents. A sentinel `deid` **blocks** rather than redacts > still passes the removal check (blocked = gone). The loop covers the five formats both packages > support; NCPDP **SCRIPT**, **ASTM**, and **DICOM** are skipped and named. `@cosyte/deid` is an **optional peer dependency**, needed only for this subpath (alongside the parsers for the formats you pair). The package core keeps **zero third-party runtime dependencies**. ## One artifact through the loop ```ts runnable import { hl7DeidLoop } from "@cosyte/synth/deid"; const r = hl7DeidLoop({ seed: 42, kind: "ORU^R01" }); r.pass; // => true r.survivors; // => [] r.clinicalScrubbed; // => [] ``` `r.planted` lists the distinctive synthetic tokens `synth` placed at the patient loci (a fake-name-pool name, a `900`-range SSN, a `555-01xx` phone, a synthetic-assigning-authority MRN); `r.survivors` is the subset still present at those loci after de-identification: **empty** on a clean pass. ## Every covered format, one summary ```ts runnable import { hl7DeidLoop, fhirDeidLoop, x12DeidLoop, ncpdpTelecomDeidLoop, ccdaDeidLoop, summarizeDeidCoverage, } from "@cosyte/synth/deid"; const summary = summarizeDeidCoverage([ hl7DeidLoop({ seed: 1 }), fhirDeidLoop({ seed: 1 }), x12DeidLoop({ seed: 1, variant: "837P" }), ncpdpTelecomDeidLoop({ seed: 1, transaction: "B1" }), ccdaDeidLoop({ seed: 1 }), ]); summary.allPass; // => true summary.totalSurvivors; // => 0 summary.totalClinicalScrubbed; // => 0 ``` ## The removal check is locus-scoped `synth` draws patient **and** provider/organization names, addresses, and phones from the same synthetic pools, and a de-identifier legitimately **retains** provider identity. So the removal check sweeps only the de-identified values that remain **at the former PHI loci**, not the whole document, so a provider name that a de-identifier keeps (and that happens to match a patient token) never reads as a false survivor. The check still reads the de-identifier's actual serialized output, so a de-identifier that locates a locus but fails to strip it is still caught. ## Deterministic and seeded The loop is a pure function of the seed: the same seed yields the byte-identical artifact, the byte-identical de-identified output, and the same sentinel set. ```ts runnable import { fhirDeidLoop } from "@cosyte/synth/deid"; fhirDeidLoop({ seed: 7 }).deidentified === fhirDeidLoop({ seed: 7 }).deidentified; // => true ``` ## Skipped formats, named A coverage gap is always named, never silent: ```ts runnable import { DEID_LOOP_SKIPPED } from "@cosyte/synth/deid"; DEID_LOOP_SKIPPED.map((s) => s.format); // => ["ncpdp-script", "astm", "dicom"] ``` `@cosyte/deid` ships no NCPDP **SCRIPT** or **ASTM** adapter, and `synth` does not generate **DICOM**, so those pairings are deferred until the adapters exist. Optional **Synthea** clinical-content ingestion (re-serializing Synthea's coherent records through the Cosyte parsers) is a documented future concern. --- URL: https://docs.cosyte.com/synth/api Markdown: https://docs.cosyte.com/synth/api.md # @cosyte/synth `@cosyte/synth`: a deterministic, seedable **synthetic-data / test-fixture generator** for the cosyte healthcare formats. A *consumer* of the parsers, not a parser: it builds artifacts **through each parser's own builder/serializer** (so output is spec-clean by construction) and draws every value from a **guaranteed-non-colliding synthetic source** (so no output can be real or plausibly-real PHI). It is a **format/conformance generator, not a clinical simulator**. This root entry point exposes the **format-agnostic core**: the seeded PRNG, the synthetic-safety providers, the `Corpus` abstraction, the profile skeleton, and the fatal codes. Per-format generation lives behind its own subpath (`@cosyte/synth/hl7`) so importing the root never pulls a parser: the lazy per-format boundary. ## Classes ### SynthError The typed error every fatal `@cosyte/synth` condition throws. Carries a stable [SynthFatalCode](#synthfatalcode) so callers branch on `err.code` without matching message text. It takes **no value parameter**. The message is whatever [SYNTH\_FATAL\_MESSAGES](#synth_fatal_messages) holds for the code, so no caller-supplied string can reach a diagnostic surface by any route, not `message`, not `stack`, not a field on the thrown object. #### Example ```ts import { SynthError, SYNTH_FATAL_CODES } from "@cosyte/synth"; throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_FORMAT); ``` #### Extends - `Error` #### Constructors ##### Constructor > **new SynthError**(`code`): [`SynthError`](#syntherror) ###### Parameters ###### code [`SynthFatalCode`](#synthfatalcode) The stable [SynthFatalCode](#synthfatalcode). The message comes from the frozen registry. ###### Returns [`SynthError`](#syntherror) ###### Overrides `Error.constructor` #### Properties ##### code > `readonly` **code**: [`SynthFatalCode`](#synthfatalcode) The stable fatal code. ## Interfaces ### Artifact One generated artifact: the serialized wire text plus the metadata needed to reproduce and check it. `warnings` records what the artifact's own parser reported on the round-trip (zero for a spec-clean artifact). #### Properties ##### content > `readonly` **content**: `string` The serialized wire text, produced by the parser's own conservative serializer. ##### format > `readonly` **format**: [`SynthFormat`](#synthformat) The format this artifact belongs to. ##### kind > `readonly` **kind**: `string` A format-specific kind label (e.g. `"ADT^A01"`). ##### warnings > `readonly` **warnings**: readonly `string`[] The warning codes the parser emitted when the artifact was round-tripped (empty = spec-clean). *** ### Corpus A reproducible, self-describing set of generated artifacts. #### Properties ##### artifacts > `readonly` **artifacts**: readonly [`Artifact`](#artifact)[] The generated artifacts, in generation order. ##### manifest > `readonly` **manifest**: [`CorpusManifest`](#corpusmanifest-1) The manifest describing what was generated. ##### seed > `readonly` **seed**: `number` The seed the corpus was generated from: regenerating from it yields byte-identical artifacts. *** ### CorpusManifest A self-describing manifest of a [Corpus](#corpus). #### Properties ##### counts > `readonly` **counts**: `Readonly`\<`Record`\<`string`, `number`\>\> Per-kind artifact counts (e.g. `{ "ADT^A01": 3 }`). ##### formats > `readonly` **formats**: readonly [`SynthFormat`](#synthformat)[] The formats present in the corpus. ##### quirks > `readonly` **quirks**: readonly `string`[] The quirk names applied. *** ### QuirkArtifact One generated quirk artifact: the off-spec wire text plus the contract it is meant to satisfy. #### Properties ##### content > `readonly` **content**: `string` The **quirked** wire text (deterministic in the seed + quirk). ##### format > `readonly` **format**: [`SynthFormat`](#synthformat) The format this artifact belongs to. ##### intendedWarnings > `readonly` **intendedWarnings**: readonly `string`[] The exact parser warning code(s) this artifact is meant to round-trip to. ##### kind > `readonly` **kind**: `string` The underlying spec-clean message kind the quirk was injected into (e.g. `"ORU^R01"`). ##### quirk > `readonly` **quirk**: `string` The quirk recipe applied. *** ### QuirkDescriptor A public, grounded description of one vendor quirk: the metadata that binds a quirk recipe to a real parser warning code and a **publicly-groundable** deviation (cited-public, never a private vendor corpus). #### Properties ##### disposition > `readonly` **disposition**: [`QuirkProfileDisposition`](#quirkprofiledisposition) How [toleratingProfile](#toleratingprofile) treats the quirk. ##### format > `readonly` **format**: [`SynthFormat`](#synthformat) The format this quirk applies to. ##### grounding > `readonly` **grounding**: `string` The **public** grounding for this quirk, the spec clause or the parser's public profile that documents the tolerance. Never a private vendor-attributed corpus. ##### intendedWarnings > `readonly` **intendedWarnings**: readonly `string`[] The **exact** parser warning code(s) a bare parse (no profile) surfaces for this quirk, the intended-warning contract. A quirk that produces any other code, or none, is a generation bug. ##### name > `readonly` **name**: `string` The quirk recipe name (e.g. `"unknown-zsegment"`). Stable; part of the public contract. ##### toleratingProfile? > `readonly` `optional` **toleratingProfile?**: `string` The parser profile that tolerates this quirk (when a built-in public one exists). *** ### QuirkProfiledVerdict The verdict of a bare parse under the tolerating profile, if any. #### Properties ##### disposition > `readonly` **disposition**: [`QuirkProfileDisposition`](#quirkprofiledisposition) How the profile treats the quirk. ##### profileName > `readonly` **profileName**: `string` The profile applied. ##### tolerated > `readonly` **tolerated**: `boolean` `true` iff the profile handled the quirk as its disposition declares: `"suppressed"` ⇒ the intended code is gone; `"rebadged"` ⇒ the intended code is gone and `PROFILE_QUIRK_APPLIED` is present. ##### warnings > `readonly` **warnings**: readonly `string`[] The warning codes the parser emitted with the profile active. *** ### QuirkRoundTripResult The verdict of round-tripping a quirk artifact through its parser. #### Properties ##### content > `readonly` **content**: `string` The quirked wire text that was parsed. ##### intendedWarningHeld > `readonly` **intendedWarningHeld**: `boolean` `true` iff the bare parse produced **exactly** the intended code(s), the intended-warning contract. ##### intendedWarnings > `readonly` **intendedWarnings**: readonly `string`[] The exact code(s) the quirk is meant to produce. ##### warnings > `readonly` **warnings**: readonly `string`[] The warning codes a **bare** parse (no profile) emitted. ##### withProfile? > `readonly` `optional` **withProfile?**: [`QuirkProfiledVerdict`](#quirkprofiledverdict) The verdict under the tolerating profile, when a built-in public one exists. *** ### Rng A seeded, deterministic random source. Created via [createRng](#createrng); passed explicitly to every provider. All draw methods advance the internal state deterministically. #### Properties ##### seed > `readonly` **seed**: `number` The integer seed this generator was created from (part of the `Corpus` manifest). #### Methods ##### bool() > **bool**(`p?`): `boolean` `true` with probability `p` (default `0.5`). ###### Parameters ###### p? `number` ###### Returns `boolean` ##### digits() > **digits**(`n`): `string` A string of `n` decimal digits (`0` to `9`), each drawn uniformly. ###### Parameters ###### n `number` The number of digits (`>= 0`). ###### Returns `string` ##### float() > **float**(): `number` The next float in `[0, 1)`. ###### Returns `number` ##### int() > **int**(`min`, `max`): `number` A uniformly-distributed integer in the inclusive range `[min, max]`. ###### Parameters ###### min `number` Inclusive lower bound (integer). ###### max `number` Inclusive upper bound (integer, `>= min`). ###### Returns `number` ##### nextUint32() > **nextUint32**(): `number` The next unsigned 32-bit integer. ###### Returns `number` ##### pick() > **pick**\<`T`\>(`items`): `T` Pick one element from a non-empty array. ###### Type Parameters ###### T `T` ###### Parameters ###### items readonly `T`[] A non-empty readonly array. ###### Returns `T` *** ### Sfc32State The mutable four-word `sfc32` state. Threaded explicitly (never global) by ../rng/rng.Rng. #### Properties ##### a > **a**: `number` State word `a`. ##### b > **b**: `number` State word `b`. ##### c > **c**: `number` State word `c`. ##### d > **d**: `number` Counter word `d`. *** ### SyntheticAddress A synthetic postal address: synthetic street + city, a fixed non-real ZIP. #### Properties ##### city > `readonly` **city**: `string` A clearly-fake city. ##### state > `readonly` **state**: `string` A US state abbreviation (structural only; never combined with a real street + name + DOB). ##### street > `readonly` **street**: `string` A clearly-fake street line. ##### zip > `readonly` **zip**: `string` A reserved non-real ZIP (`00000`). *** ### SyntheticIdentifier A synthetic identifier scoped to the synthetic assigning authority. #### Properties ##### assigningAuthority > `readonly` **assigningAuthority**: `string` The synthetic assigning-authority namespace id. ##### assigningAuthorityOid > `readonly` **assigningAuthorityOid**: `string` The synthetic assigning-authority OID (HL7 example arc). ##### typeCode > `readonly` **typeCode**: `"MR"` \| `"AN"` \| `"MB"` HL7 identifier type code (`MR` = medical record, `AN` = account, `MB` = member). ##### value > `readonly` **value**: `string` The identifier value (digits): unique only within the synthetic namespace. *** ### SyntheticName A synthetic person name drawn from the shipped fake-name pool. #### Properties ##### family > `readonly` **family**: `string` A clearly-fake family name. ##### given > `readonly` **given**: `string` A clearly-fake given name. *** ### SynthProfile A frozen, validated fixture recipe produced by [defineSynthProfile](#definesynthprofile). #### Properties ##### familyNames? > `readonly` `optional` **familyNames?**: readonly `string`[] The family-name pool this profile draws from. ##### givenNames? > `readonly` `optional` **givenNames?**: readonly `string`[] The given-name pool this profile draws from (overrides or the built-in default). ##### name > `readonly` **name**: `string` The profile name. ##### quirks > `readonly` **quirks**: readonly `string`[] The requested quirk recipe names. *** ### SynthProfileSpec The user-authored spec passed to [defineSynthProfile](#definesynthprofile). #### Properties ##### familyNames? > `readonly` `optional` **familyNames?**: readonly `string`[] Optional family-name pool override (clearly-synthetic names only). ##### givenNames? > `readonly` `optional` **givenNames?**: readonly `string`[] Optional given-name pool override (clearly-synthetic names only, see the safety invariant). ##### name > `readonly` **name**: `string` A stable, human-readable profile name (e.g. `"acme-hospital"`). Required, non-empty. ##### quirks? > `readonly` `optional` **quirks?**: readonly `string`[] The vendor quirk recipe names this profile requests. Validated against the target format's quirk registry when the profile drives a quirk corpus (an unsupported quirk is a fatal `SYNTH_UNSUPPORTED_QUIRK`, never a silent no-op). ## Type Aliases ### QuirkProfileDisposition > **QuirkProfileDisposition** = `"suppressed"` \| `"rebadged"` \| `"bare"` How the parser's matching profile treats a quirk once it is active: the three shapes the parsers' profile systems actually exhibit (verified firsthand against each parser): - `"suppressed"`, the profile makes the warning **disappear** (HL7 v2: a `defineProfile` `customSegments` claim suppresses `UNKNOWN_SEGMENT` for a declared Z-segment). - `"rebadged"`, the profile **downgrades** the warning to the value-free `PROFILE_QUIRK_APPLIED` marker with `expected: true` (C-CDA `defineCcdaProfile` / ASTM `defineAstmProfile` `profileQuirkApplied`). - `"bare"`, no shipped profile tolerates it; the quirk targets a real coded leniency a consumer can tolerate via their own `defineProfile`/`defineAstmProfile`, but no built-in re-badges it. *** ### SsnBlock > **SsnBlock** = `"never-issued"` \| `"advertising"` Which SSN reserved space to draw from. *** ### SynthFatalCode > **SynthFatalCode** = *typeof* [`SYNTH_FATAL_CODES`](#synth_fatal_codes)\[keyof *typeof* [`SYNTH_FATAL_CODES`](#synth_fatal_codes)\] A value from [SYNTH\_FATAL\_CODES](#synth_fatal_codes): the type carried by a thrown [SynthError](#syntherror). *** ### SynthFormat > **SynthFormat** = `"hl7v2"` \| `"fhir"` \| `"ccda"` \| `"x12"` \| `"ncpdp"` \| `"astm"` The format an artifact was generated for. ## Variables ### DEA\_REGISTRANT\_TYPES > `const` **DEA\_REGISTRANT\_TYPES**: readonly `string`[] The DEA-registration prefix letters `@cosyte/synth` draws a synthetic DEA number's first character from. A real DEA number is `` + 7 digits; the first letter is the registrant type (A/B/F/G/M/P/R/X are the widely-published values; the second letter is the registrant's last-name initial). These letters are a **fact** about the number's shape, not copyrighted prose: they only shape the value; the synthetic guarantee is the deliberately-**invalid checksum** (see [dea](#dea) / [isSyntheticDea](#issyntheticdea)). *** ### DOC\_V6\_PREFIX > `const` **DOC\_V6\_PREFIX**: `"2001:db8"` = `"2001:db8"` RFC 3849 IPv6 documentation prefix. *** ### ITIN\_GROUP\_RANGES > `const` **ITIN\_GROUP\_RANGES**: readonly `Readonly`\<\{ `max`: `number`; `min`: `number`; \}\>[] The published **IRS ITIN group ranges**, inclusive `[min, max]` bands over the two group digits (positions 4 and 5) of a `9NN-GG-NNNN` value. An Individual Taxpayer Identification Number is an SSN-format number that begins with `9` and carries a group inside one of these bands, so these bands are what separates an SSN the SSA manual calls invalid from a validly formatted ITIN. These are **facts** about the number's shape, not copyrighted prose (IRS Internal Revenue Manual 3.21.263). Group values `89` and `93` sit between the bands on purpose: the IRM records them as reserved for other IRS programs rather than for ITINs, so a value carrying one is **not** ITIN-formatted (see [isItinFormatted](#isitinformatted)). *** ### NPI\_LUHN\_PREFIX > `const` **NPI\_LUHN\_PREFIX**: `"80840"` = `"80840"` The `80840` prefix prepended to a 10-digit NPI before the Luhn check. A real NPI satisfies `luhn("80840" + npi) ≡ 0 (mod 10)`. The rule is **69 FR 3434**, the final rule adopting the NPI (FR Doc 04-1149, docket CMS-0045-F), issued by the agency that document names as "Centers for Medicare & Medicaid Services, HHS": "the NPI check digit calculation must always be performed as though the NPI is preceded by" `80840`. The prefix itself is not that rule's: it credits the NCITS.284 standard health care identification card, which "requires that the first five digits of the card issuer identifier be" `80840`, "where the initial two digits, 80, signify health applications, the next three digits, 840, signify United States". The rule cites no ISO document number for the prefix or for the check digit, and neither does this module. [https://www.federalregister.gov/documents/full_text/text/2004/01/23/04-1149.txt](https://www.federalregister.gov/documents/full_text/text/2004/01/23/04-1149.txt) *** ### PROFILE\_QUIRK\_APPLIED > `const` **PROFILE\_QUIRK\_APPLIED**: `"PROFILE_QUIRK_APPLIED"` = `"PROFILE_QUIRK_APPLIED"` The stable, value-free re-badge code the C-CDA and ASTM parsers emit when a profile tolerates a quirk. HL7 v2 has no equivalent (it suppresses instead: see [QuirkProfileDisposition](#quirkprofiledisposition)). *** ### RESERVED\_EMAIL\_DOMAINS > `const` **RESERVED\_EMAIL\_DOMAINS**: readonly `string`[] RFC 2606 / 6761 reserved email domains `@cosyte/synth` draws from. *** ### safe > `const` **safe**: `Readonly`\<\{ `address`: (`rng`) => [`SyntheticAddress`](#syntheticaddress); `dateYmd`: (`rng`, `minYear`, `maxYear`) => `string`; `dea`: (`rng`, `person?`) => `string`; `email`: (`rng`, `person?`) => `string`; `identifier`: (`rng`, `typeCode`) => [`SyntheticIdentifier`](#syntheticidentifier); `ipv4`: (`rng`) => `string`; `ipv6`: (`rng`) => `string`; `name`: (`rng`) => [`SyntheticName`](#syntheticname); `npi`: (`rng`) => `string`; `phone`: (`rng`) => `string`; `ssn`: (`rng`, `block`) => `string`; `uuid`: (`rng`) => `string`; \}\> The synthetic-safety provider namespace. Every function draws only from a reserved range or the shipped fake-name pool, no value it returns can be real or plausibly-real PHI. #### Example ```ts import { createRng, safe } from "@cosyte/synth"; const rng = createRng(42); safe.ssn(rng); // never-issued SSN safe.phone(rng); // reserved 555-01NN number ``` *** ### SYNTH\_FATAL\_CODES > `const` **SYNTH\_FATAL\_CODES**: `object` The stable **fatal** code registry. Additions-only thereafter. #### Type Declaration ##### SYNTH\_EMPTY\_POOL > `readonly` **SYNTH\_EMPTY\_POOL**: `"SYNTH_EMPTY_POOL"` = `"SYNTH_EMPTY_POOL"` A value was drawn from an empty pool. Fatal, never a fabricated substitute. ##### SYNTH\_INTENDED\_WARNING\_MISMATCH > `readonly` **SYNTH\_INTENDED\_WARNING\_MISMATCH**: `"SYNTH_INTENDED_WARNING_MISMATCH"` = `"SYNTH_INTENDED_WARNING_MISMATCH"` A bare parse of a freshly-generated quirk artifact did not produce exactly the declared intended warning code(s). Fatal, never emit a mislabeled fixture. ##### SYNTH\_INVALID\_DECIMAL > `readonly` **SYNTH\_INVALID\_DECIMAL**: `"SYNTH_INVALID_DECIMAL"` = `"SYNTH_INVALID_DECIMAL"` A money value could not be read as an X12 decimal. Fatal: a generator never rounds to a float. ##### SYNTH\_INVALID\_PROFILE > `readonly` **SYNTH\_INVALID\_PROFILE**: `"SYNTH_INVALID_PROFILE"` = `"SYNTH_INVALID_PROFILE"` A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. ##### SYNTH\_INVALID\_RANGE > `readonly` **SYNTH\_INVALID\_RANGE**: `"SYNTH_INVALID_RANGE"` = `"SYNTH_INVALID_RANGE"` An integer range was requested with its maximum below its minimum. Fatal. ##### SYNTH\_PROFILE\_NOT\_GENERATED > `readonly` **SYNTH\_PROFILE\_NOT\_GENERATED**: `"SYNTH_PROFILE_NOT_GENERATED"` = `"SYNTH_PROFILE_NOT_GENERATED"` A requested profile IS published by the adopted implementation guide, and this build does not generate it. Deliberately distinct from `SYNTH_UNSUPPORTED_KIND`, which says the name is not in the adopted set at all: "the guide does not publish this" and "we do not generate this yet" are different answers, and a caller building to a regulatory profile set has to be able to tell them apart without matching message text. Fatal, and raised **before** anything is generated: a mislabelled artifact is worse than no artifact. ##### SYNTH\_QUIRK\_ANCHOR\_ABSENT > `readonly` **SYNTH\_QUIRK\_ANCHOR\_ABSENT**: `"SYNTH_QUIRK_ANCHOR_ABSENT"` = `"SYNTH_QUIRK_ANCHOR_ABSENT"` A quirk transform found no structural anchor to mutate, so the fixture would not carry the deviation it is labelled with. Fatal: a golden file that lies about its parser verdict is worse than no golden file. ##### SYNTH\_UNMAPPED\_CODE\_SYSTEM > `readonly` **SYNTH\_UNMAPPED\_CODE\_SYSTEM**: `"SYNTH_UNMAPPED_CODE_SYSTEM"` = `"SYNTH_UNMAPPED_CODE_SYSTEM"` A concept's code-system URI has no OID mapping in the C-CDA example-code table. Fatal. ##### SYNTH\_UNSUPPORTED\_FORMAT > `readonly` **SYNTH\_UNSUPPORTED\_FORMAT**: `"SYNTH_UNSUPPORTED_FORMAT"` = `"SYNTH_UNSUPPORTED_FORMAT"` A format was requested that this build cannot generate through a real parser builder/serializer. Fatal, never a hand-written byte fallback. **No code path in this build raises it.** All six formats generate, so it is reserved for a future format that does not, and is kept because removing a published code is a breaking change. An unsupported *kind* within a format that does generate is `SYNTH_UNSUPPORTED_KIND`. ##### SYNTH\_UNSUPPORTED\_KIND > `readonly` **SYNTH\_UNSUPPORTED\_KIND**: `"SYNTH_UNSUPPORTED_KIND"` = `"SYNTH_UNSUPPORTED_KIND"` A caller-supplied selector (a message kind, a document type, a corpus mix entry, a claim variant, a Bundle type, a resource profile) is not in the closed set that governs it. Fatal: see `resolveKind`: a selector union is erased at run time, and a selector that falls through either mislabels the fixture or hands the value to a peer builder that quotes it back. ##### SYNTH\_UNSUPPORTED\_QUIRK > `readonly` **SYNTH\_UNSUPPORTED\_QUIRK**: `"SYNTH_UNSUPPORTED_QUIRK"` = `"SYNTH_UNSUPPORTED_QUIRK"` A vendor quirk was requested that the target format's profile system does not support. Fatal, never a silent no-op and never a fabricated quirk. #### Example ```ts import { SYNTH_FATAL_CODES, SynthError } from "@cosyte/synth"; try { // ...generate... } catch (err) { if (err instanceof SynthError && err.code === SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_FORMAT) { // handle an unsupported format request } } ``` *** ### SYNTH\_FATAL\_MESSAGES > `const` **SYNTH\_FATAL\_MESSAGES**: `Readonly`\<`Record`\<[`SynthFatalCode`](#synthfatalcode), `string`\>\> The **frozen message registry**: the only place a [SynthError](#syntherror) message can come from. A message here is a fixed string. It never quotes the request that produced it, and there is no parameter through which it could: [SynthError](#syntherror) takes a code and nothing else. That is the whole mechanism, and it is deliberately a mechanism rather than a habit. Every one of these messages used to be assembled by interpolating the caller's value into a template, and the reason that was safe was not the design: it was that the caller happened to be passing a quirk name. The trade is real and is accepted: a fatal no longer tells you *which* value it rejected. It tells you which rule refused, on `err.code`, and the stack frame tells you where. The caller already holds the value it passed. #### Example ```ts import { SYNTH_FATAL_CODES, SYNTH_FATAL_MESSAGES } from "@cosyte/synth"; SYNTH_FATAL_MESSAGES[SYNTH_FATAL_CODES.SYNTH_EMPTY_POOL]; // => "A value was drawn from an empty pool." ``` *** ### SYNTHETIC\_ASSIGNING\_AUTHORITY > `const` **SYNTHETIC\_ASSIGNING\_AUTHORITY**: `Readonly`\<\{ `namespaceId`: `"COSYTE-SYNTH"`; `universalId`: `"2.16.840.1.113883.19.999"`; `universalIdType`: `"ISO"`; \}\> The synthetic **assigning authority** `@cosyte/synth` mints MRNs / account / member identifiers under. **No authority reserves this locus.** An MRN is unique only within its assigning-authority / OID namespace, no registry reserves a range of them, and none is cited here. What the floor rests on instead is the *namespace*, as a documented design decision: every synthetic identifier is scoped to a namespace that clearly cannot be a real facility's, a `SYNTH`-labelled authority this package mints and no real facility uses. A value under this AA can never collide with a real record whatever its digits are, because the namespace itself is synthetic. The OID is **uncited for the same reason**. `2.16.840.1.113883.19.999` is a value this package chose; no published text designating the root `2.16.840.1.113883.19` for example use could be shown, so this module claims no such designation and the guarantee above does not rest on one. *** ### SYNTHETIC\_CITY\_NAMES > `const` **SYNTHETIC\_CITY\_NAMES**: readonly `string`[] Obviously-synthetic city names. Combined only ever with a synthetic street + a fake name (the *combination* is what identifies, and the combination is always synthetic). *** ### SYNTHETIC\_FAMILY\_NAMES > `const` **SYNTHETIC\_FAMILY\_NAMES**: readonly `string`[] Obviously-synthetic family names. None is a plausible real surname at a real address. *** ### SYNTHETIC\_GIVEN\_NAMES > `const` **SYNTHETIC\_GIVEN\_NAMES**: readonly `string`[] Obviously-synthetic given names. None is a plausible real person's name. *** ### SYNTHETIC\_STREET\_NAMES > `const` **SYNTHETIC\_STREET\_NAMES**: readonly `string`[] Obviously-synthetic street names for structured address fields. *** ### TEST\_NET\_V4\_PREFIXES > `const` **TEST\_NET\_V4\_PREFIXES**: readonly `string`[] RFC 5737 IPv4 documentation (TEST-NET) `/24` network prefixes. *** ### VERSION > `const` **VERSION**: `string` = `"0.1.1"` Library version string, equal to this package's published `package.json#version`. Changesets owns the bump and rewrites `package.json` only, so the release `version` script runs `scripts/sync-version.mjs` to rewrite this declaration in the same commit, and `test/sanity.test.ts` compares the two so a skipped sync goes red instead of shipping a version string that lies. The `: string` annotation is deliberate: without it the declaration's inferred type is the string literal itself, which bakes the current release into the emitted declarations and narrows every consumer's type on each bump. #### Example ```ts import { VERSION } from "@cosyte/synth"; console.log(VERSION); ``` ## Functions ### address() > **address**(`rng`): [`SyntheticAddress`](#syntheticaddress) A **synthetic address**: a fake street + city, a reserved non-real ZIP (`00000`). A real state abbreviation may appear (structural only) but is never combined with a real street + name + DOB. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. #### Returns [`SyntheticAddress`](#syntheticaddress) A [SyntheticAddress](#syntheticaddress). #### Example ```ts import { createRng, address } from "@cosyte/synth"; address(createRng(1)); // { street, city, state, zip: "00000" } ``` *** ### assertIntendedWarnings() > **assertIntendedWarnings**(`intendedWarnings`, `bareWarnings`): `void` Assert a freshly-generated quirk artifact **actually** round-trips to its intended warning(s), or **fail closed**. This is the generator's self-check on the intended-warning contract: a fixture whose bare parse does not produce exactly the declared code(s) is a *mislabeled* fixture, a golden file that lies about the parser verdict it anchors, and must never be emitted. It is a stronger guard than "the transform changed some bytes": a transform can mutate the wrong element (a template a given document type does not key its warning on) and still change bytes while producing no warning. Every format's `generate*Quirk` calls this after transforming, so the contract is enforced at generation time, not merely at round-trip time. It no longer takes the quirk name. That parameter existed for one reason, to be interpolated into the refusal, and a parameter whose only job is to reach a message is the exact shape this package is removing, so it is gone rather than merely unused. The refusal names neither code list either; both are caller-supplied, and the caller reads the comparison back off the arguments it holds. #### Parameters ##### intendedWarnings readonly `string`[] The declared intended code(s). ##### bareWarnings readonly `string`[] The code(s) a bare parse of the generated artifact actually produced. #### Returns `void` #### Throws SynthError `SYNTH_INTENDED_WARNING_MISMATCH` when the bare parse did not produce exactly the intended code(s). #### Example ```ts import { assertIntendedWarnings } from "@cosyte/synth"; assertIntendedWarnings(["UNKNOWN_SEGMENT"], ["UNKNOWN_SEGMENT"]); // ok ``` *** ### createRng() > **createRng**(`seed`): [`Rng`](#rng) Create a seeded, deterministic [Rng](#rng). The same `seed` yields the same sequence everywhere. #### Parameters ##### seed `number` The integer seed. Coerced to a 32-bit integer. #### Returns [`Rng`](#rng) A fresh, independent [Rng](#rng). #### Example ```ts import { createRng } from "@cosyte/synth"; const rng = createRng(12345); rng.int(1, 6); // deterministic for seed 12345 ``` *** ### dateYmd() > **dateYmd**(`rng`, `minYear?`, `maxYear?`): `string` A **synthetic date** in HL7 `YYYYMMDD` form, drawn uniformly within an inclusive year range. Comes from the seeded generator (never wall-clock), so it is reproducible and implies no real event. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. ##### minYear? `number` = `1930` Inclusive lower year bound (default `1930`). ##### maxYear? `number` = `2010` Inclusive upper year bound (default `2010`). #### Returns `string` An `YYYYMMDD` date string (always a valid calendar day). #### Example ```ts import { createRng, dateYmd } from "@cosyte/synth"; dateYmd(createRng(1), 1970, 2000); // "YYYYMMDD" ``` *** ### dea() > **dea**(`rng`, `person?`): `string` A **synthetic DEA number**: `` + 7 digits with a **deliberately-invalid checksum**, so it can never be a validly-issued DEA registration (a real DEA number's 7th digit satisfies the published DEA checksum). The first letter is a registrant-type letter, the second is derived from `person` (its family initial) when supplied so the number reads plausibly; the 6-digit base is seeded and the check digit is set to `(correct + 1) mod 10`, guaranteeing the value fails validation. NCPDP carries prescriber DEA, and this is the identity locus a refuter attacks hardest, so, like [npi](#npi), non-collision is a construction-level guarantee, not a heuristic. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. ##### person? [`SyntheticName`](#syntheticname) Optional name whose family initial becomes the DEA's second letter. #### Returns `string` A DEA-shaped string that is provably not a real DEA registration. #### Example ```ts import { createRng, dea, isSyntheticDea } from "@cosyte/synth"; isSyntheticDea(dea(createRng(1))); // true: invalid checksum by construction ``` *** ### deaCheckDigit() > **deaCheckDigit**(`base6`): `number` The correct DEA check digit for a 7-digit numeric base. The checksum is `(d1 + d3 + d5) + 2·(d2 + d4 + d6)`, whose **units digit** is the 7th (check) digit. A real DEA number satisfies this; a number whose 7th digit differs cannot be a validly-issued DEA registration. **NON-NORMATIVELY SOURCED, and this is the one locus in this module that is.** The formula above is quoted from a pharmacy journal article, Gabay, "Federal Controlled Substances Act: Controlled Substances Prescriptions", Hospital Pharmacy (PMC3847977): "add the sum of the first, third, and fifth digits to twice the sum of the second, fourth, and sixth digits. The total should be a number whose last digit is the same as the last digit of the DEA number." [https://pmc.ncbi.nlm.nih.gov/articles/PMC3847977/](https://pmc.ncbi.nlm.nih.gov/articles/PMC3847977/) **That article is not the DEA.** It is a secondary description of the agency's algorithm, not the agency's own statement of it, and no DEA-published text stating the algorithm is cited here. The consequence is stated rather than hidden: if the formula is wrong, a value this package builds to fail it may in fact **pass** the real check, and the generator would then emit a checksum-valid DEA number while [isSyntheticDea](#issyntheticdea) asserts the opposite. Every other entry in this module's `Sources:` list names the issuing authority's own text; this one cannot. #### Parameters ##### base6 `string` The first 6 digits of the DEA number (positions 1 to 6). #### Returns `number` The check digit (`0` to `9`) a real DEA number would carry for this base. #### Example ```ts import { deaCheckDigit } from "@cosyte/synth"; deaCheckDigit("123456"); // the units digit of (1+3+5) + 2·(2+4+6) ``` *** ### defineSynthProfile() > **defineSynthProfile**(`spec`): [`SynthProfile`](#synthprofile) Define a reusable, frozen synthetic-fixture profile. #### Parameters ##### spec [`SynthProfileSpec`](#synthprofilespec) The profile spec; `name` is required and non-empty. #### Returns [`SynthProfile`](#synthprofile) A deep-frozen [SynthProfile](#synthprofile). #### Throws SynthError `SYNTH_INVALID_PROFILE` when `name` is missing or blank. #### Example ```ts import { defineSynthProfile } from "@cosyte/synth"; const acme = defineSynthProfile({ name: "acme-hospital", quirks: [] }); ``` *** ### email() > **email**(`rng`, `person?`): `string` A **synthetic email** at an RFC 2606 / 6761 reserved domain: `@example.com`. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. ##### person? [`SyntheticName`](#syntheticname) Optional name to derive the local-part slug from; otherwise a random slug is used. #### Returns `string` A synthetic email address. #### Example ```ts import { createRng, email, name } from "@cosyte/synth"; email(createRng(1), name(createRng(1))); // ".@example.com" ``` *** ### identifier() > **identifier**(`rng`, `typeCode?`): [`SyntheticIdentifier`](#syntheticidentifier) A **synthetic identifier** (MRN / account / member id) scoped to the synthetic assigning authority. There is no reserved MRN range, so non-collision is guaranteed by the *namespace*, not the value: the identifier lives under a `SYNTH` authority no real facility uses. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. ##### typeCode? `"MR"` \| `"AN"` \| `"MB"` The HL7 identifier type: `MR` (default), `AN`, or `MB`. #### Returns [`SyntheticIdentifier`](#syntheticidentifier) A [SyntheticIdentifier](#syntheticidentifier). #### Example ```ts import { createRng, identifier } from "@cosyte/synth"; identifier(createRng(1), "MR"); // { value, typeCode: "MR", assigningAuthority: "COSYTE-SYNTH", ... } ``` *** ### ipv4() > **ipv4**(`rng`): `string` A **synthetic IPv4** in an RFC 5737 TEST-NET block, never routable. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. #### Returns `string` A TEST-NET IPv4 address string. #### Example ```ts import { createRng, ipv4 } from "@cosyte/synth"; ipv4(createRng(1)); // e.g. "192.0.2.NN" ``` *** ### ipv6() > **ipv6**(`rng`): `string` A **synthetic IPv6** in the RFC 3849 documentation prefix `2001:db8::/32`, never routable. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. #### Returns `string` A documentation-prefix IPv6 address string. #### Example ```ts import { createRng, ipv6 } from "@cosyte/synth"; ipv6(createRng(1)); // e.g. "2001:db8::NNNN" ``` *** ### isItinFormatted() > **isItinFormatted**(`value`): `boolean` Whether a `ddd-dd-dddd` (or `ddddddddd`) value is a **validly formatted IRS ITIN**: it begins with `9` and its group digits (positions 4 and 5) fall inside a published ITIN group range ([ITIN\_GROUP\_RANGES](#itin_group_ranges)). This is the second issuing authority sharing the SSN number space, so `isSyntheticSsn(v) && !isItinFormatted(v)` is the full "cannot be a federally issued national id" guarantee, of which the area rule alone is only half. `true` means the value is ITIN-shaped and therefore **must not** be emitted at an SSN locus. A value that is not exactly 9 digits once separators are stripped returns `false` (not an SSN/ITIN shape) rather than throwing, as do the group values `89` and `93`, which the IRM reserves for other IRS programs rather than for ITINs. #### Parameters ##### value `string` The candidate national id (dashes and other separators optional). #### Returns `boolean` `true` when the value is formatted as a valid ITIN. #### Example ```ts import { isItinFormatted } from "@cosyte/synth"; isItinFormatted("912-70-1234"); // true: group 70 is inside a published ITIN range isItinFormatted("912-66-1234"); // false: group 66 is outside every published ITIN range ``` *** ### isSyntheticDea() > **isSyntheticDea**(`value`): `boolean` Whether a DEA number (`XX` + 7 digits, case-insensitive) is **provably synthetic**: its check digit (the 7th digit) does **not** match the checksum [deaCheckDigit](#deacheckdigit) computes, so it cannot be a validly-issued DEA registration. A checksum-valid DEA number (which *could* denote a real prescriber) returns `false`; a value that is not the DEA shape returns `false`. **NON-NORMATIVELY SOURCED.** This predicate is only as strong as the algorithm it inverts, and that algorithm is cited to a pharmacy journal article (PMC3847977), **not to the DEA**: no DEA-published statement of it is cited anywhere in this package. Read `true` as "fails the formula [deaCheckDigit](#deacheckdigit) implements", never as "the DEA could not have issued this". The full citation and the consequence of the formula being wrong are on [deaCheckDigit](#deacheckdigit). #### Parameters ##### value `string` The candidate DEA number (with or without incidental separators). #### Returns `boolean` `true` when the DEA number's checksum is wrong (never a real DEA registration). #### Example ```ts import { isSyntheticDea } from "@cosyte/synth"; isSyntheticDea("AF1234561"); // depends on the base: true when the 7th digit is wrong ``` *** ### isSyntheticEmail() > **isSyntheticEmail**(`value`): `boolean` Whether an email's domain is an RFC 2606 / 6761 reserved / test domain. #### Parameters ##### value `string` The candidate email address. #### Returns `boolean` `true` when the domain is reserved (never real). #### Example ```ts import { isSyntheticEmail } from "@cosyte/synth"; isSyntheticEmail("faux.testerson@example.com"); // true ``` *** ### isSyntheticIp() > **isSyntheticIp**(`value`): `boolean` Whether an IP string is in an RFC 5737 (IPv4 TEST-NET) or RFC 3849 (IPv6 documentation) reserved block. A real routable address returns `false`. #### Parameters ##### value `string` The candidate IPv4 or IPv6 address. #### Returns `boolean` `true` when the address is a reserved documentation address. #### Example ```ts import { isSyntheticIp } from "@cosyte/synth"; isSyntheticIp("192.0.2.44"); // true (TEST-NET-1) isSyntheticIp("8.8.8.8"); // false (real) ``` *** ### isSyntheticNpi() > **isSyntheticNpi**(`value`): `boolean` Whether a 10-digit NPI is **provably synthetic**, i.e. its check digit is invalid, so it cannot be a validly issued NPI. A Luhn-valid 10-digit NPI (which *could* denote a real registered provider) returns `false`; a non-10-digit value returns `false` (not an NPI shape). The check this inverts is the one 69 FR 3434 (FR Doc 04-1149) requires: the Luhn check digit, computed as though the NPI were preceded by [NPI\_LUHN\_PREFIX](#npi_luhn_prefix). Unlike the DEA locus, this one cites the issuing rule itself. [https://www.federalregister.gov/documents/full_text/text/2004/01/23/04-1149.txt](https://www.federalregister.gov/documents/full_text/text/2004/01/23/04-1149.txt) #### Parameters ##### value `string` The candidate NPI (digits only, or with incidental separators). #### Returns `boolean` `true` when the NPI's check digit is wrong (never a real NPI). #### Example ```ts import { isSyntheticNpi } from "@cosyte/synth"; isSyntheticNpi("1234567894"); // true: invalid check digit (valid would be 1234567893) isSyntheticNpi("1234567893"); // false: Luhn-valid, could be a real NPI ``` *** ### isSyntheticPhone() > **isSyntheticPhone**(`value`): `boolean` Whether a phone string contains the NANP `555-0100…555-0199` reserved fictional line range. #### Parameters ##### value `string` The candidate phone (any formatting). #### Returns `boolean` `true` when the number is in the reserved fictional block. #### Example ```ts import { isSyntheticPhone } from "@cosyte/synth"; isSyntheticPhone("(202) 555-0142"); // true ``` *** ### isSyntheticSsn() > **isSyntheticSsn**(`value`): `boolean` Whether a `ddd-dd-dddd` (or `ddddddddd`) SSN string carries an area SSA's own manual identifies as **invalid**: `000`, `666`, or "in the 900 series". A real, issuable SSN returns `false`. The citable claim is SSA POMS RM 10201.035, which defines an invalid SSN as "one that we never assigned" and lists those three areas as identifying one. The wording here says invalid rather than never-issued because invalidity is what the manual states. [https://secure.ssa.gov/poms.nsf/lnx/0110201035](https://secure.ssa.gov/poms.nsf/lnx/0110201035) #### Parameters ##### value `string` The candidate SSN (dashes optional). #### Returns `boolean` `true` when the SSN is provably synthetic. #### Example ```ts import { isSyntheticSsn } from "@cosyte/synth"; isSyntheticSsn("900-12-3456"); // true (the 900 series marks an invalid SSN) isSyntheticSsn("123456789"); // false (issuable area 123) ``` *** ### luhnMod10() > **luhnMod10**(`digits`): `number` **`Internal`** The Luhn sum (mod 10) of a numeric string, doubling every second digit from the right. Used to verify (or deliberately break) an NPI check digit. #### Parameters ##### digits `string` A string of decimal digits. #### Returns `number` The Luhn sum modulo 10 (0 ⇒ the string passes the Luhn check). *** ### makeCorpus() > **makeCorpus**(`seed`, `artifacts`, `quirks?`): [`Corpus`](#corpus) Assemble a deep-frozen [Corpus](#corpus) from a seed and its artifacts, deriving the manifest. #### Parameters ##### seed `number` The seed the artifacts were generated from. ##### artifacts readonly [`Artifact`](#artifact)[] The generated artifacts, in order. ##### quirks? readonly `string`[] = `[]` The quirk names applied (default none). #### Returns [`Corpus`](#corpus) A deep-frozen, self-describing [Corpus](#corpus). #### Example ```ts import { makeCorpus } from "@cosyte/synth"; const corpus = makeCorpus(1, [{ format: "hl7v2", kind: "ADT^A01", content, warnings: [] }]); corpus.manifest.counts["ADT^A01"]; // 1 ``` *** ### name() > **name**(`rng`): [`SyntheticName`](#syntheticname) A **synthetic name** drawn from the shipped clearly-fake pool. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. #### Returns [`SyntheticName`](#syntheticname) A [SyntheticName](#syntheticname). #### Example ```ts import { createRng, name } from "@cosyte/synth"; const { given, family } = name(createRng(1)); ``` *** ### npi() > **npi**(`rng`): `string` A **synthetic NPI**: a 10-digit National Provider Identifier with a **deliberately-invalid Luhn check digit**, so it can never be a NPPES-issued NPI (a real NPI must satisfy the `80840`-prefixed Luhn check). The 9-digit base is drawn from the seeded generator; the check digit is set to `(correct + 1) mod 10`, guaranteeing the full value fails validation. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. #### Returns `string` A 10-digit NPI-shaped string that is provably not a real NPI. #### Example ```ts import { createRng, npi, isSyntheticNpi } from "@cosyte/synth"; isSyntheticNpi(npi(createRng(1))); // true: invalid check digit by construction ``` *** ### npiCheckDigit() > **npiCheckDigit**(`base9`): `number` The correct NPI check digit for a 9-digit base: the value that makes `80840` + base + check pass the Luhn check. The algorithm this inverts is cited: 69 FR 3434 (FR Doc 04-1149) requires the check digit to be "calculated using the ISO standard Luhn check digit algorithm", a modulus 10 double-add-double algorithm, performed as though the NPI were preceded by [NPI\_LUHN\_PREFIX](#npi_luhn_prefix). That rule names the algorithm and its behaviour but no ISO document number, and points onward for the step-by-step form: "The specification for calculation of the NPI check digit will be made available on the CMS Web site". What this function implements is the rule's own description, modulus 10 Luhn over the prefixed digits; no separate specification is cited for it. [https://www.federalregister.gov/documents/full_text/text/2004/01/23/04-1149.txt](https://www.federalregister.gov/documents/full_text/text/2004/01/23/04-1149.txt) #### Parameters ##### base9 `string` The 9-digit NPI base (positions 1 to 9). #### Returns `number` The check digit (`0` to `9`) a real NPI would carry for this base. #### Example ```ts import { npiCheckDigit } from "@cosyte/synth"; npiCheckDigit("123456789"); // 3, so 1234567893 is a Luhn-valid NPI shape ``` *** ### phone() > **phone**(`rng`): `string` A **synthetic phone** in the NANP reserved fictional block: `(AAA) 555-01NN`. The reserved guarantee is the `555-01NN` tail (exchange 555, line 0100 to 0199); the area code is any NANP-valid `NXX`. Can never be a working number. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. #### Returns `string` A formatted synthetic phone string. #### Example ```ts import { createRng, phone } from "@cosyte/synth"; phone(createRng(1)); // e.g. "(2XX) 555-01NN" ``` *** ### profileTolerated() > **profileTolerated**(`disposition`, `intendedWarnings`, `warningsUnderProfile`): `boolean` Evaluate whether a profiled parse tolerated a quirk as its disposition declares. Shared across the formats so the "suppressed vs re-badged" logic lives in exactly one place. #### Parameters ##### disposition [`QuirkProfileDisposition`](#quirkprofiledisposition) The quirk's declared profile disposition. ##### intendedWarnings readonly `string`[] The bare-parse intended code(s). ##### warningsUnderProfile readonly `string`[] The code(s) the parser emitted with the profile active. #### Returns `boolean` `true` iff the profile handled the quirk correctly for its disposition. #### Example ```ts import { profileTolerated } from "@cosyte/synth"; profileTolerated("suppressed", ["UNKNOWN_SEGMENT"], []); // true: the profile suppressed it ``` *** ### resolveKind() > **resolveKind**\<`T`\>(`allowed`, `requested`): `T` Resolve one caller-supplied selector against the closed set that governs it, or **fail closed**. #### Type Parameters ##### T `T` *extends* `string` #### Parameters ##### allowed readonly `T`[] Every value the selector may take. ##### requested `string` The selector the caller supplied. #### Returns `T` `requested`, narrowed to the union. #### Throws SynthError `SYNTH_UNSUPPORTED_KIND` when `requested` is not in `allowed`. #### Example ```ts import { resolveKind } from "@cosyte/synth"; resolveKind(["ccd", "referralNote"] as const, "ccd"); // "ccd" ``` *** ### resolveMix() > **resolveMix**\<`T`\>(`allowed`, `requested`, `fallback`): readonly `T`[] Resolve every entry of a caller-supplied corpus mix, in order, or **fail closed** on the first unrecognised one. It substitutes the default **only** when the caller supplied nothing, which is exactly what the `??` it replaced did. An empty array is a supplied mix and is returned as one. An earlier version of this function also treated `[]` as "nothing supplied", on the stated grounds that it matched the previous behaviour; it did not: `??` fires on `undefined` and never on `[]`, and it changed the result of six published entry points, turning an explicit empty selection into "generate one of everything". A convenience that fails open is not a convenience. #### Type Parameters ##### T `T` *extends* `string` #### Parameters ##### allowed readonly `T`[] Every kind the corpus may generate. ##### requested readonly `string`[] \| `undefined` The mix the caller supplied, or `undefined` for the default. ##### fallback readonly `T`[] The default mix, used only when `requested` is `undefined`. #### Returns readonly `T`[] The resolved mix. #### Throws SynthError `SYNTH_UNSUPPORTED_KIND` on the first unrecognised entry. #### Example ```ts import { resolveMix } from "@cosyte/synth"; resolveMix(["Result", "Order"] as const, ["Order"], ["Result", "Order"]); // ["Order"] ``` *** ### resolveQuirk() > **resolveQuirk**(`registry`, `format`, `name`): [`QuirkDescriptor`](#quirkdescriptor) Resolve a requested quirk name against a format's registry, or **fail closed**. A quirk the format's profile system does not support is a fatal `SYNTH_UNSUPPORTED_QUIRK`, never a silent no-op and never a fabricated quirk with a made-up warning. The refusal names neither the request nor the registry. `registry`, `format` and `name` are all caller-supplied, and a diagnostic that quotes its input is a diagnostic that can be made to carry anything the caller was holding, which for a fixture generator wired into someone else's pipeline is not a hypothetical. Branch on `err.code`; the supported set is the registry you passed (`HL7_QUIRKS`, `CCDA_QUIRKS`, `ASTM_QUIRKS`), which you can enumerate directly. #### Parameters ##### registry `Readonly`\<`Record`\<`string`, [`QuirkDescriptor`](#quirkdescriptor)\>\> The format's quirk descriptors, keyed by name. ##### format [`SynthFormat`](#synthformat) The format being generated. ##### name `string` The requested quirk name. #### Returns [`QuirkDescriptor`](#quirkdescriptor) The matching [QuirkDescriptor](#quirkdescriptor). #### Throws SynthError with code `SYNTH_UNSUPPORTED_QUIRK` when `name` is not a supported quirk. #### Example ```ts import { resolveQuirk } from "@cosyte/synth"; import { HL7_QUIRKS } from "@cosyte/synth/hl7"; resolveQuirk(HL7_QUIRKS, "hl7v2", "unknown-zsegment").intendedWarnings; // ["UNKNOWN_SEGMENT"] ``` *** ### sameCodeSet() > **sameCodeSet**(`a`, `b`): `boolean` Exact multiset (order-independent) equality of two code lists: the intended-warning comparison. #### Parameters ##### a readonly `string`[] The first code list. ##### b readonly `string`[] The second code list. #### Returns `boolean` `true` iff the two lists contain the same codes with the same multiplicities. #### Example ```ts import { sameCodeSet } from "@cosyte/synth"; sameCodeSet(["A", "B"], ["B", "A"]); // true ``` *** ### sfc32Next() > **sfc32Next**(`s`): `number` Advance an [Sfc32State](#sfc32state) in place by one step and return the next unsigned 32-bit integer. This is the canonical `sfc32` step. The state object is mutated (the counter `d` increments and the mixing words rotate); callers that need reproducible independence hold their own state and never share it: ../rng/rng.Rng creates a fresh state per seed so two runs from the same seed are identical. #### Parameters ##### s [`Sfc32State`](#sfc32state) The state to advance. Mutated in place. #### Returns `number` The next `uint32` in the stream. #### Example ```ts import { sfc32Next, type Sfc32State } from "@cosyte/synth"; const s: Sfc32State = { a: 1, b: 2, c: 3, d: 4 }; const x = sfc32Next(s); // uint32 ``` *** ### splitmix32() > **splitmix32**(`seed`): () => `number` A stateful `splitmix32` step function. Each call advances the internal 32-bit state and returns the next unsigned 32-bit integer. Deterministic for a given seed. #### Parameters ##### seed `number` The 32-bit seed. Coerced to a 32-bit integer via `| 0`. #### Returns A nullary function returning the next `uint32` in the stream. () => `number` #### Example ```ts import { splitmix32 } from "@cosyte/synth"; const next = splitmix32(12345); const a = next(); // deterministic uint32 ``` *** ### ssn() > **ssn**(`rng`, `block?`): `string` A **synthetic SSN**: dashed `AAA-GG-SSSS`, drawn so it can be neither an SSA-assignable Social Security number nor a validly formatted IRS ITIN. Two authorities share this number space: SSA never issues area `900-999`, and the IRS issues ITINs *inside* that area, distinguished by the group digits. So the area rule alone is only half the guarantee, and both blocks below also keep the group outside every published ITIN group range (see [isItinFormatted](#isitinformatted)). Default draws the never-issued area space (`900-999`) with a group from SSN\_SYNTHETIC\_GROUPS; `block: "advertising"` returns the fixed display block `987-00-4320` to `987-00-4329`, whose group `00` is one SSA never assigns and one no published ITIN range contains. **`"advertising"` no longer means SSA's own advertising block.** That published block is `987-65-4320` to `987-65-4329`, and group `65` sits inside a published ITIN group range, so every value in it is ITIN-formatted and none of them can be emitted here. The option keeps its name (renaming it would break call sites for a property no test asserts) and keeps its purpose, a fixed ten-value block safe to print on screen, but it is a display block of this package's choosing now, not a citation of SSA's. Do not reintroduce the `65` group to recover the provenance. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. ##### block? [`SsnBlock`](#ssnblock) = `"never-issued"` Which reserved space to draw from. Defaults to `"never-issued"`. #### Returns `string` A dashed synthetic SSN string. #### Example ```ts import { createRng, ssn } from "@cosyte/synth"; ssn(createRng(1)); // e.g. a 900-area, never-issued, never-ITIN-formatted SSN ``` *** ### uuid() > **uuid**(`rng`): `string` A **deterministic UUIDv4-shaped** surrogate key from the seeded generator. Because it is seeded (not from `node:crypto`, which is not reproducible), the cryptographic non-collision argument is weaker, acceptable because the identifier namespace is synthetic anyway, and noted honestly. #### Parameters ##### rng [`Rng`](#rng) The seeded generator. #### Returns `string` A canonical `8-4-4-4-12` lowercase-hex UUID string with version `4` and RFC 4122 variant. #### Example ```ts import { createRng, uuid } from "@cosyte/synth"; uuid(createRng(1)); // "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" ``` *** ### validateProfileQuirks() > **validateProfileQuirks**(`profile`, `registry`, `format`): readonly `string`[] Validate the quirk names carried by a [SynthProfile](#synthprofile) against a format's registry, failing closed on the first unsupported one. Lets a consumer author a fixture recipe with `defineSynthProfile` and have its quirks checked against the *parser's* real tolerance before any fixture is generated. #### Parameters ##### profile [`SynthProfile`](#synthprofile) The synth profile whose `quirks` to validate. ##### registry `Readonly`\<`Record`\<`string`, [`QuirkDescriptor`](#quirkdescriptor)\>\> The format's quirk descriptors. ##### format [`SynthFormat`](#synthformat) The format being generated. #### Returns readonly `string`[] The validated quirk names (the profile's, in order). #### Throws SynthError `SYNTH_UNSUPPORTED_QUIRK` for the first unsupported quirk. #### Example ```ts import { validateProfileQuirks, defineSynthProfile } from "@cosyte/synth"; import { HL7_QUIRKS } from "@cosyte/synth/hl7"; const p = defineSynthProfile({ name: "site", quirks: ["unknown-zsegment"] }); validateProfileQuirks(p, HL7_QUIRKS, "hl7v2"); // ["unknown-zsegment"] ``` --- URL: https://docs.cosyte.com/synth/troubleshooting Markdown: https://docs.cosyte.com/synth/troubleshooting.md # Troubleshooting Common questions when generating fixtures with `@cosyte/synth`. ## "I got a different message than last run" You almost certainly changed the **seed**, or let the format's builder fill a nondeterministic default. `synth` supplies a seeded control id and timestamp to `@cosyte/hl7` precisely so the bytes are reproducible. Pass the same `seed` and you get byte-identical output: ```ts runnable import { generateAdt } from "@cosyte/synth/hl7"; generateAdt({ seed: 5 }).toString() === generateAdt({ seed: 5 }).toString(); // => true ``` Note that a **`synth` version bump may change the seed→bytes mapping**: that is a documented breaking change, so pin the version alongside the seed for a long-lived golden fixture. Pin the parser peers too: each format's bytes come from that parser's own builder, so a parser upgrade can change them. ## "Cannot find module @cosyte/hl7" The `@cosyte/synth/hl7` subpath needs the optional peer `@cosyte/hl7` installed. The package **core** (`@cosyte/synth`) has no such requirement: import from there if you only need the PRNG and the safe providers. ## An unsupported request threw A generator has nothing to tolerate, so it **fails closed**. Asking for a format or quirk this build cannot produce throws a typed `SynthError` with a stable `SYNTH_FATAL_CODES` value, never a hand-written byte workaround: ```ts runnable throws import { defineSynthProfile } from "@cosyte/synth"; // A blank profile name is a programming error, and is rejected up front: this block is expected // to throw (a `SynthError` with code `SYNTH_INVALID_PROFILE`), and the docs gate asserts that it does. defineSynthProfile({ name: "" }); ``` Every fatal message comes from `SYNTH_FATAL_MESSAGES`, a frozen table keyed by code. It says which rule refused; it does **not** quote the request that tripped it, and there is no parameter through which it could: `SynthError` takes a code and nothing else. So a fatal tells you less about the specific call than it used to, on purpose. Branch on `err.code` and read the offending value off the arguments you passed; the stack frame names the call site. ## Is the generated output safe to commit and log? Yes: that is the whole point. **Every** value is drawn from a reserved/synthetic source, proven by the synthetic-safety gate (the inverse of a de-identifier's leak test: `synth` proves plausibly-real PHI was _never generated_). You can commit a generated corpus as a fixture without a PHI review of its contents. The **diagnostics** are safe for a different reason, and the distinction is worth keeping straight. Output is safe because of where its values come from. A `@cosyte/synth` diagnostic is safe because it has no value in it at all: a fatal message is a fixed registry string, a selector you pass is resolved against its closed set before it can travel anywhere, and a round-trip result keeps only the parser's warning **codes**. Neither guarantee rests on the other. What that does not cover: a value you pass to a generator is your value, and it goes into the artifact you asked for. `content` is the fixture, not a diagnostic. And if you hand a **document or a model** to a round-trip harness and the sibling parser cannot read it, what you catch is that parser's fatal, raised and worded by that package: `@cosyte/synth` re-throws it unchanged and adds nothing. ## Known limitations All six formats are wired (HL7 v2, FHIR, C-CDA, X12, NCPDP, ASTM), with vendor-quirk mode for HL7 v2 / C-CDA / ASTM and the `deid` pairing loop for HL7 v2 / FHIR / C-CDA / X12 / NCPDP Telecom. The honest shape of what `synth` does, does **not** do, and defers, plus the full synthetic-safety posture, lives in **[What it does and does not do](https://docs.cosyte.com/synth/limitations.md)**. The headline: `synth` is a **format/conformance generator, not a clinical simulator**; every value is synthetic-by-construction; output is deterministic per seed within a version window; and no terminology is bundled. The **API Reference** always reflects exactly what this release ships. --- # @cosyte/dates > Precision-preserving date and time utilities for the Cosyte parsers that never guess a timezone. These are the 7 pages of the v0.1.1 documentation, the newest release, in sidebar order. Each page opens with a URL line naming its page on the site and a Markdown line naming its plain Markdown copy. --- URL: https://docs.cosyte.com/dates/intro Markdown: https://docs.cosyte.com/dates/intro.md # @cosyte/dates Work with the dates the `@cosyte/*` parsers give you, without losing what the wire actually said. The `@cosyte/*` parsers hand back dates as parts rather than as JavaScript `Date` objects. An HL7 v2 `|1970|` is a year, `|19880507|` is a calendar day, and a timestamp with no offset is the sender's local time. The parsers keep exactly that and invent nothing else. `@cosyte/dates` is the opt-in companion that validates, renders and converts those parts, with the same two promises: - **Precision is preserved.** A year stays a year. Nothing adds a month, a day or a midnight that the value did not state. - **No zone is ever guessed.** A value whose offset is unknown cannot become an absolute instant until you say which zone applies. There is no fallback to the host time zone and none to UTC. ```js import { toISO, toInstant } from '@cosyte/dates'; toISO({ year: 1988 }); // '1988' toISO({ year: 1988, month: 5, day: 7 }); // '1988-05-07' toInstant({ year: 1988, month: 5, day: 7, hour: 13, minute: 45, offsetMinutes: 330 }).toString(); // '1988-05-07T08:15:00Z' ``` ## What it covers - **Validation** of a date-parts value: every fault reported, each one naming the component and the rule it broke (`validateParts`, `isValidParts`, `assertValidParts`). - **Precision**: the least significant component present is the precision (`precisionOf`). - **Rendering** to ISO 8601 at the value's own precision (`toISO`). - **Conversion** to the narrowest Temporal type that carries everything the value states (`toTemporal`), and to a zoned date-time or an absolute instant once a zone is known (`toZonedDateTime`, `toInstant`). ## What it does not do - It parses no vendor date text: it consumes parts a parser already produced. - It never interprets a two-digit year. - It carries no arithmetic, comparison or formatting helpers: convert with `toTemporal()` and use Temporal for the rest. - It handles years 1000 to 9999 in the proleptic Gregorian calendar, and no leap seconds. ## Status `0.1.0` is the first published release. The exported functions, the shape of `DateParts`, the issue codes and the error classes are settled and safe to depend on. Below `1.0.0`, a breaking change to any of them ships in a new minor version and is called out in the changelog. Next: [Installation](https://docs.cosyte.com/dates/installation.md), then the [Quickstart](https://docs.cosyte.com/dates/quickstart.md). --- URL: https://docs.cosyte.com/dates/installation Markdown: https://docs.cosyte.com/dates/installation.md # Installation ```bash npm install @cosyte/dates ``` or `pnpm add @cosyte/dates`. ## Requirements - **Node.js 22 or newer.** - **ESM only.** The package ships no CommonJS build, so load it with `import`; `require()` will not load it. ## Dependencies One runtime dependency, [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill), because `Temporal.PlainDate` is the only widely available type that holds a calendar date with no time of day and no zone. `Temporal` is re-exported from this package, so you work with the same objects rather than installing a second copy: two Temporal implementations do not share `instanceof`. ```js import { Temporal } from '@cosyte/dates'; ``` ## Check the install ```js import { toISO, precisionOf } from '@cosyte/dates'; console.log(toISO({ year: 1988, month: 5 }), precisionOf({ year: 1988, month: 5 })); // 1988-05 month ``` If that prints, the install is good. Head to the [Quickstart](https://docs.cosyte.com/dates/quickstart.md). --- URL: https://docs.cosyte.com/dates/quickstart Markdown: https://docs.cosyte.com/dates/quickstart.md # Quickstart A complete program. Save it as `quickstart.mjs` in a project where `@cosyte/dates` is installed and run it with `node quickstart.mjs`. ```js import { precisionOf, toISO, toInstant, toTemporal, validateParts } from '@cosyte/dates'; // A date of birth that arrived as a year and nothing more. It stays a year. const birthYear = { year: 1988 }; console.log(toISO(birthYear), '| precision:', precisionOf(birthYear)); // A calendar date. No midnight is invented, and no zone is attached. const admitted = { year: 1988, month: 5, day: 7 }; console.log(toISO(admitted), '| Temporal:', toTemporal(admitted).constructor.name); // A timestamp whose sender stated its offset. The offset is kept exactly. const collected = { year: 1988, month: 5, day: 7, hour: 13, minute: 45, offsetMinutes: 330 }; console.log(toISO(collected), '->', toInstant(collected).toString()); // The same timestamp with an unknown offset. Nothing guesses one. const unknownZone = { year: 1988, month: 5, day: 7, hour: 13, minute: 45 }; try { toInstant(unknownZone); } catch (error) { console.log(error.name); } // A date that does not exist is refused, and says which rule it broke. const result = validateParts({ year: 2025, month: 2, day: 29 }); console.log(result.valid ? 'valid' : result.issues[0].message); ``` It prints: ```text 1988 | precision: year 1988-05-07 | Temporal: PlainDate 1988-05-07T13:45+05:30 -> 1988-05-07T08:15:00Z MissingZoneError day 29 does not exist: February 2025 has 28 days, because 2025 is not a leap year in the proleptic Gregorian calendar ``` The output is the same under every host `TZ`: nothing here reads the machine's time zone. ## The shape it accepts A parts value is a plain object: `year` (four digits), then optionally `month` (1 to 12, January is 1), `day`, `hour`, `minute`, `second`, `fraction` (of one second) and `offsetMinutes` (signed minutes from UTC). Components run from `year` downward with no gaps, and whichever is the least significant one present is the value's precision. The [Precision](https://docs.cosyte.com/dates/concepts-precision.md) page has the full rules. ## Next - [Precision](https://docs.cosyte.com/dates/concepts-precision.md): what a parts value is, and why a year stays a year. - [Zones and instants](https://docs.cosyte.com/dates/concepts-zones.md): when an absolute instant is possible, and when it is refused. - [Troubleshooting](https://docs.cosyte.com/dates/troubleshooting.md): every error and issue code, and what to do about it. --- URL: https://docs.cosyte.com/dates/concepts-precision Markdown: https://docs.cosyte.com/dates/concepts-precision.md # Precision A healthcare date is often less than a full timestamp. A date of birth can arrive as a year, an admission as a calendar day, a specimen collection as a time to the minute with or without an offset. `@cosyte/dates` keeps each of those at exactly the precision it arrived with. ## The parts value | component | range | notes | | --------------- | ------------------------- | --------------------------------------------- | | `year` | 1000 to 9999 | required, four digits, never a two-digit year | | `month` | 1 to 12 | spec-native: January is 1, not 0 | | `day` | 1 to 31 | and a day that exists in that month and year | | `hour` | 0 to 23 | | | `minute` | 0 to 59 | | | `second` | 0 to 59 | leap seconds are not representable | | `fraction` | 0 up to, not including, 1 | of one second, down to one nanosecond | | `offsetMinutes` | -1439 to 1439 | absent means UNKNOWN, never UTC | Two rules make it a contract: 1. **Contiguity.** Components are present from `year` downward with no gaps. `{ year, month }` is a value; `{ year, day }` is refused with `missing-component`. 2. **Precision is what is present.** The least significant component present is the precision, so `{ year: 1988, month: 5 }` has precision `month`. ```js import { precisionOf, PRECISION_LADDER } from '@cosyte/dates'; precisionOf({ year: 1988 }); // 'year' precisionOf({ year: 1988, month: 5, day: 7, hour: 13 }); // 'hour' PRECISION_LADDER; // ['year', 'month', 'day', 'hour', 'minute', 'second', 'fraction'] ``` ## Validation `validateParts()` never throws and reports every fault, not only the first: ```js import { validateParts } from '@cosyte/dates'; const result = validateParts({ year: 1988, month: 13, day: 40 }); result.valid; // false result.issues.map((issue) => issue.code); // ['out-of-range', 'out-of-range'] ``` On success it returns the recognised components and the precision: ```js validateParts({ year: 1988, month: 5, day: 7 }); // { valid: true, parts: { year: 1988, month: 5, day: 7 }, precision: 'day' } ``` `isValidParts()` is the same check as a type guard, and `assertValidParts()` returns the recognised components or throws a `DatePartsError` carrying the same `issues`. ## Rendering and converting at the value's own precision `toISO()` renders ISO 8601 at the precision the value has, and nothing finer: ```js import { toISO } from '@cosyte/dates'; toISO({ year: 1988 }); // '1988' toISO({ year: 2026, month: 3 }); // '2026-03' toISO({ year: 2026, month: 3, day: 9, hour: 8 }); // '2026-03-09T08' toISO({ year: 2026, month: 3, day: 9, hour: 8, minute: 5, offsetMinutes: -300 }); // '2026-03-09T08:05-05:00' ``` `toTemporal()` returns the narrowest Temporal type that carries everything the value states: | the value has | `toTemporal()` returns | | --------------------------------- | ------------------------- | | a year and a month | `Temporal.PlainYearMonth` | | a calendar date | `Temporal.PlainDate` | | a time of day, no offset | `Temporal.PlainDateTime` | | a time of day and `offsetMinutes` | `Temporal.ZonedDateTime` | A year on its own throws a `PrecisionError`: Temporal has no year-only type, and this package will not invent a month to reach one. Use `toISO()` to render it, or `precisionOf()` to branch first. ## Refusals instead of guesses - **No two-digit years.** `{ year: 88 }` is refused with `year-not-four-digits`. There is no century window and no setting that adds one. - **No rounding.** A `fraction` finer than one nanosecond is refused with `finer-than-nanosecond` rather than rounded to fit. - **No calendar rollover.** `{ year: 2025, month: 2, day: 29 }` is refused with `no-such-calendar-date`; it never becomes 1 March. - **No offset without a time.** An `offsetMinutes` on a value with no time of day is refused with `offset-without-time-of-day` rather than dropped. --- URL: https://docs.cosyte.com/dates/concepts-zones Markdown: https://docs.cosyte.com/dates/concepts-zones.md # Zones and instants A timestamp with no offset is the sender's local time, and nothing on the wire says which zone that was. `@cosyte/dates` treats an absent `offsetMinutes` as UNKNOWN: not UTC, and not the zone of the machine running your code. ## When an instant is possible `toInstant()` and `toZonedDateTime()` need two things: a time of day, and a zone. - **The value's own offset always wins.** When the sender stated `offsetMinutes`, that is the offset used, and any zone you pass is ignored for that value, so data the sender already qualified is never re-zoned. - **Otherwise you say which zone applies**, with `{ timeZone }` (an IANA identifier) or `{ offsetMinutes }` (a fixed offset). ```js import { toInstant, toZonedDateTime } from '@cosyte/dates'; const local = { year: 2026, month: 3, day: 9, hour: 8, minute: 5 }; toInstant(local, { timeZone: 'America/New_York' }).toString(); // '2026-03-09T12:05:00Z' toInstant(local, { offsetMinutes: 0 }).toString(); // '2026-03-09T08:05:00Z' toZonedDateTime(local, { timeZone: 'America/New_York' }).toString(); // '2026-03-09T08:05:00-04:00[America/New_York]' const stated = { ...local, offsetMinutes: -300 }; toInstant(stated).toString(); // '2026-03-09T13:05:00Z' ``` ## When it is refused | situation | what happens | | -------------------------------------------------- | -------------------------------- | | no `offsetMinutes` on the value and no zone passed | throws `MissingZoneError` | | the value has no time of day | throws `PrecisionError` | | the local time falls in a daylight-saving gap | throws `AmbiguousLocalTimeError` | | the local time happens twice at a transition | throws `AmbiguousLocalTimeError` | | the zone identifier is not one Temporal knows | throws `RangeError` | | both `timeZone` and `offsetMinutes` are passed | throws `TypeError` | The daylight-saving cases are refused rather than resolved, because picking a side silently shifts a clinical timeline by an hour. The error message names both candidate offsets where there are two; pass the one you mean as `{ offsetMinutes }`. ```js toInstant({ year: 2026, month: 11, day: 1, hour: 1, minute: 30 }, { timeZone: 'America/New_York' }); // AmbiguousLocalTimeError: 2026-11-01T01:30:00 happens twice in America/New_York ... toInstant( { year: 2026, month: 11, day: 1, hour: 1, minute: 30 }, { offsetMinutes: -240 }, ).toString(); // '2026-11-01T05:30:00Z' ``` An identifier Temporal does not know, such as the abbreviation `CST`, throws a `RangeError`. An abbreviation can mean different offsets in different regions, so pass an IANA identifier such as `America/Chicago`, or a numeric offset. --- URL: https://docs.cosyte.com/dates/api Markdown: https://docs.cosyte.com/dates/api.md # @cosyte/dates ## Classes ### AmbiguousLocalTimeError A local time was placed in a named zone where it does not exist, or exists twice. Both cases are resolved by REFUSING rather than by picking. A daylight-saving transition is exactly where a silent choice shifts a clinical timeline by an hour, so the caller is told and decides. #### Extends - `Error` #### Constructors ##### Constructor > **new AmbiguousLocalTimeError**(`message`): [`AmbiguousLocalTimeError`](#ambiguouslocaltimeerror) ###### Parameters ###### message `string` ###### Returns [`AmbiguousLocalTimeError`](#ambiguouslocaltimeerror) ###### Overrides `Error.constructor` *** ### DatePartsError A conversion was asked for a value that validation reports invalid. `issues` is the SAME array `validateParts()` returns for that value, so a caller that catches this never has to validate again to learn what was wrong, and a conversion never returns a coerced or clamped stand-in. #### Extends - `Error` #### Constructors ##### Constructor > **new DatePartsError**(`issues`): [`DatePartsError`](#datepartserror) ###### Parameters ###### issues readonly [`ValidationIssue`](#validationissue)[] ###### Returns [`DatePartsError`](#datepartserror) ###### Overrides `Error.constructor` #### Properties ##### issues > `readonly` **issues**: readonly [`ValidationIssue`](#validationissue)[] *** ### MissingZoneError An absolute instant was asked for, and nothing said which zone the local time is in. This package never falls back to the host timezone, to UTC, or to any other default, because a fallback turns an unknown into a plausible wrong answer. #### Extends - `Error` #### Constructors ##### Constructor > **new MissingZoneError**(`message`): [`MissingZoneError`](#missingzoneerror) ###### Parameters ###### message `string` ###### Returns [`MissingZoneError`](#missingzoneerror) ###### Overrides `Error.constructor` *** ### PrecisionError The value does not carry enough precision for what was asked, and supplying the missing components would mean inventing them. #### Extends - `Error` #### Constructors ##### Constructor > **new PrecisionError**(`message`, `precision`): [`PrecisionError`](#precisionerror) ###### Parameters ###### message `string` ###### precision `string` ###### Returns [`PrecisionError`](#precisionerror) ###### Overrides `Error.constructor` #### Properties ##### precision > `readonly` **precision**: `string` The precision the value actually has. ## Interfaces ### DateParts The date-parts value every `@cosyte/*` parser hands back, and the only shape this package accepts. Two rules make it a contract rather than a bag of numbers. 1. CONTIGUITY. Components are present from the most significant downward with no gaps. `{ year, month }` is legal; `{ year, day }` is not. 2. PRECISION IS WHAT IS PRESENT. The least significant present component IS the precision of the value. Nothing here adds a component that was not supplied, and an absent `offsetMinutes` means the offset is UNKNOWN, which is not the same as UTC and not the same as the host zone. `month` is SPEC-NATIVE 1-12, not JavaScript's 0-11. #### Properties ##### day? > `readonly` `optional` **day?**: `number` 1-31, and a day that exists in the given month of the given year. ##### fraction? > `readonly` `optional` **fraction?**: `number` Sub-second precision as a fraction of one second: `0 <= fraction < 1`. ##### hour? > `readonly` `optional` **hour?**: `number` 0-23. ##### minute? > `readonly` `optional` **minute?**: `number` 0-59. ##### month? > `readonly` `optional` **month?**: `number` 1-12, spec-native. January is 1. ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` Signed minutes from UTC, -1439 to 1439. ABSENT means the offset is unknown. It never means zero, and it is never filled in from the host zone. ##### second? > `readonly` `optional` **second?**: `number` 0-59. Leap seconds are not representable. ##### year? > `readonly` `optional` **year?**: `number` Four digits, 1000-9999. Required. Two-digit years are never interpreted. *** ### InvalidPartsResult An invalid value, with every fault found rather than only the first. #### Properties ##### issues > `readonly` **issues**: readonly [`ValidationIssue`](#validationissue)[] ##### valid > `readonly` **valid**: `false` *** ### ValidationIssue One thing wrong with one component, named. #### Properties ##### code > `readonly` **code**: [`IssueCode`](#issuecode) ##### component > `readonly` **component**: [`PartName`](#partname) \| `"value"` The component at fault, or `'value'` when the argument itself is wrong. ##### message > `readonly` **message**: `string` Human-readable, names the component and the range or calendar rule broken. *** ### ValidPartsResult A validated value, carrying only the recognised components that were present. #### Properties ##### parts > `readonly` **parts**: [`DateParts`](#dateparts) ##### precision > `readonly` **precision**: `"year"` \| `"month"` \| `"day"` \| `"hour"` \| `"minute"` \| `"second"` \| `"fraction"` ##### valid > `readonly` **valid**: `true` *** ### ZoneOptions How to place a value that carries no offset of its own onto the timeline. A value that DOES carry `offsetMinutes` is never re-zoned: its own offset is what the sender said, and overriding it here would move the instant. #### Properties ##### offsetMinutes? > `readonly` `optional` **offsetMinutes?**: `number` A fixed offset in signed minutes from UTC, -1439 to 1439. ##### timeZone? > `readonly` `optional` **timeZone?**: `string` An IANA time zone identifier, for example `'America/New_York'`. ## Type Aliases ### IssueCode > **IssueCode** = `"not-a-parts-object"` \| `"missing-component"` \| `"unreadable"` \| `"not-a-number"` \| `"not-finite"` \| `"not-an-integer"` \| `"out-of-range"` \| `"year-not-four-digits"` \| `"finer-than-nanosecond"` \| `"offset-without-time-of-day"` \| `"no-such-calendar-date"` What a diagnostic says went wrong. *** ### PartName > **PartName** = [`Precision`](#precision-2) \| `"offsetMinutes"` Every component name the contract defines. *** ### Precision > **Precision** = *typeof* [`PRECISION_LADDER`](#precision_ladder)\[`number`\] The least significant component present in a value IS its precision. *** ### TemporalValue > **TemporalValue** = `Temporal.PlainYearMonth` \| `Temporal.PlainDate` \| `Temporal.PlainDateTime` \| `Temporal.ZonedDateTime` The narrowest Temporal type that carries everything a value states, and nothing more. *** ### ValidationResult > **ValidationResult** = [`ValidPartsResult`](#validpartsresult) \| [`InvalidPartsResult`](#invalidpartsresult) ## Variables ### PRECISION\_LADDER > `const` **PRECISION\_LADDER**: readonly \[`"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`, `"fraction"`\] The precision ladder, most significant first. *** ### Temporal > **Temporal**: `any` The Temporal implementation this package converts into, re-exported so a consumer works with the same objects rather than installing a second copy. Two Temporal implementations do not share `instanceof`. ## Functions ### assertValidParts() > **assertValidParts**(`value`): [`DateParts`](#dateparts) Validate and return the recognised components, or throw [DatePartsError](#datepartserror) carrying the same diagnostics [validateParts](#validateparts) would have reported. #### Parameters ##### value `unknown` #### Returns [`DateParts`](#dateparts) *** ### isValidParts() > **isValidParts**(`value`): `value is DateParts` A type guard over [validateParts](#validateparts), for callers that want a boolean. #### Parameters ##### value `unknown` #### Returns `value is DateParts` *** ### precisionOf() > **precisionOf**(`value`): `"year"` \| `"month"` \| `"day"` \| `"hour"` \| `"minute"` \| `"second"` \| `"fraction"` The precision of a value: its least significant present component. Throws [DatePartsError](#datepartserror) for a value that does not satisfy the contract, because an invalid value has no precision to report. #### Parameters ##### value `unknown` #### Returns `"year"` \| `"month"` \| `"day"` \| `"hour"` \| `"minute"` \| `"second"` \| `"fraction"` *** ### toInstant() > **toInstant**(`value`, `options?`): `Instant` The absolute instant a value denotes, in a stated zone. Same requirements as [toZonedDateTime](#tozoneddatetime): a time of day, and a zone that something states. Nothing is defaulted. #### Parameters ##### value `unknown` ##### options? [`ZoneOptions`](#zoneoptions) #### Returns `Instant` *** ### toISO() > **toISO**(`value`): `string` Render a value as an ISO 8601 string AT ITS OWN PRECISION. A year-only value renders `1988`; a month-only value renders `1988-05`. No component the value did not state is ever added, so nothing here turns a year of birth into the first of January. The result is a pure function of the parts. It does not read the host clock or the host time zone, so it is identical under every `TZ`. #### Parameters ##### value `unknown` #### Returns `string` #### Throws if the value does not satisfy the contract. *** ### toTemporal() > **toTemporal**(`value`): `any` Convert to the narrowest Temporal type that carries everything the value states, and nothing it does not. | precision of the value | result | |---|---| | `month` | `Temporal.PlainYearMonth` | | `day` | `Temporal.PlainDate`, with no time of day and no zone | | `hour` and finer, no offset | `Temporal.PlainDateTime`, with no zone | | `hour` and finer, with `offsetMinutes` | `Temporal.ZonedDateTime` at that exact offset | A year-only value has no Temporal counterpart and raises [PrecisionError](#precisionerror) rather than acquiring a month. Use [toISO](#toiso) for those, and [precisionOf](#precisionof) to branch. Components below the value's own precision are zero in the Temporal object, because Temporal has no partial type below `PlainYearMonth`. The value's real precision is still available from [precisionOf](#precisionof); it is not lost, it just cannot be carried by the returned object. #### Parameters ##### value `unknown` #### Returns `any` #### Throws if the value does not satisfy the contract. #### Throws for a year-only value. *** ### toZonedDateTime() > **toZonedDateTime**(`value`, `options?`): `ZonedDateTime` Place a value on the timeline in a stated zone. Requires a time of day. A day-precision value is a calendar date, and turning one into a zoned instant means choosing midnight, which is a component the value does not carry. Use [toTemporal](#totemporal) for those. Requires a zone. If the value carries `offsetMinutes` that offset is used and PRESERVED EXACTLY, never normalised to UTC or to the host zone, and `options` are not consulted. Otherwise `options.timeZone` or `options.offsetMinutes` must say which zone the local time is in. A local time that a named zone skips or repeats across a daylight-saving transition raises [AmbiguousLocalTimeError](#ambiguouslocaltimeerror) rather than resolving to one of the two candidates, because a silent pick shifts the value by an hour. #### Parameters ##### value `unknown` ##### options? [`ZoneOptions`](#zoneoptions) #### Returns `ZonedDateTime` #### Throws if the value does not satisfy the contract. #### Throws if the value states no time of day. #### Throws if nothing says which zone applies. #### Throws at a daylight-saving transition. *** ### validateParts() > **validateParts**(`value`): [`ValidationResult`](#validationresult) Check a value against the date-parts contract. Every fault is reported, not only the first, and every diagnostic names the component at fault together with the range or calendar rule it broke. Nothing is coerced, clamped or inferred: a value that does not satisfy the contract comes back invalid rather than corrected. This function never throws, for any argument at all. Hostile input (`null`, a string, a JavaScript `Date`, `NaN`, `Infinity`, a non-integer component, a component that throws when it is read) is reported, not raised. Components are looked up by name, so an object carrying them on its prototype (a class instance with accessors, which is a shape a parser may well hand over) is read the same way a plain object literal is. #### Parameters ##### value `unknown` #### Returns [`ValidationResult`](#validationresult) --- URL: https://docs.cosyte.com/dates/troubleshooting Markdown: https://docs.cosyte.com/dates/troubleshooting.md # Troubleshooting Every refusal in `@cosyte/dates` is named. This page lists them and what to do about each. ## Errors | error | thrown when | what to do | | ------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `DatePartsError` | a conversion or `assertValidParts()` is given a value that fails validation | read `error.issues`: the same array `validateParts()` returns for that value | | `PrecisionError` | the value lacks a component the conversion needs | branch on `precisionOf()` first; `error.precision` is the precision it has | | `MissingZoneError` | an instant is asked for, the value has no offset, and no zone was passed | pass `{ timeZone }` or `{ offsetMinutes }` for the zone the sender meant | | `AmbiguousLocalTimeError` | the local time is skipped or repeated by a daylight-saving transition | pass the offset you mean as `{ offsetMinutes }`; the message names the candidates | | `RangeError` | the `timeZone` is not an identifier Temporal knows | use an IANA identifier such as `America/New_York`, or a numeric offset | | `TypeError` | both `timeZone` and `offsetMinutes` are passed | pass one of them | The four named error classes are exported, so `instanceof` works: ```js import { toInstant, MissingZoneError } from '@cosyte/dates'; try { toInstant({ year: 1988, month: 5, day: 7, hour: 13, minute: 45 }); } catch (error) { if (error instanceof MissingZoneError) { // ask which zone the sender was in; never assume one } } ``` ## Issue codes `validateParts()` returns one issue per fault, each with a `component`, a `code` and a `message`: | code | meaning | | ---------------------------- | -------------------------------------------------------------------------- | | `not-a-parts-object` | the argument is not an object at all | | `missing-component` | a more significant component is absent while a less significant one is set | | `unreadable` | reading the component threw, so its value could not be seen | | `not-a-number` | the component is present but is not a number | | `not-finite` | the component is `NaN` or infinite | | `not-an-integer` | the component must be a whole number and is not | | `out-of-range` | the component is outside the range its row in the contract gives it | | `year-not-four-digits` | the year is not four digits; no century window is ever applied | | `finer-than-nanosecond` | `fraction` carries detail below one nanosecond; it is not rounded | | `offset-without-time-of-day` | `offsetMinutes` is set on a value that states no time of day | | `no-such-calendar-date` | each component is in range, but together they name no real date | ## Logging A validation message quotes the one component at fault and its value (`month must be an integer in 1-12 ...; received the number 13`). It never assembles the full date into the message. A date of birth is PHI, so decide whether to log even one component before you log a message. The package itself writes nothing: no logging, no telemetry, no files and no network, on any code path.