@cosyte/hl7 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -15,18 +15,417 @@ per the cosyte version ladder (`0.0.x` until first alpha).
15
15
 
16
16
  ### Added
17
17
 
18
+ - **The published JSDoc is swept, and `src/` doc comments are gated
19
+ (`pnpm check:no-internal-refs` third pass; `PUBLIC-SURFACE-HYGIENE`).** Doc comments compile
20
+ into `dist/index.d.ts` and `dist/index.d.cts`, `dist` is the first entry in `files`, and every
21
+ `npm i @cosyte/hl7` receives them, so what a consumer's editor renders on hover is public
22
+ surface and the founder directive of 2026-07-27 applies to it. Re-measured with the shipped
23
+ rules before the change: **293 distinct lines** across tracked `src/*.ts` (253 phase-rule, 39
24
+ identifier-rule, 5 ADR, 1 jargon, 1 traceability marker), of which a local build carried
25
+ **164 distinct lines into `dist/index.d.ts`** (137 phase-rule, 22 identifier-rule, 5 ADR,
26
+ 1 jargon, 1 traceability). After: **0 on every rule, in `src/` doc comments and in both built
27
+ declaration files.** A consumer hovering `parseHL7`, `defineProfile`, `Medication` or
28
+ `buildMessage` no longer reads which internal phase built it. Every `Plan N` build-order
29
+ reference was swept in the same pass, by hand, and **the built declaration file now carries
30
+ none**: no rule catches them, and they contradicted the `CLAUDE.md` sentence this change adds.
31
+ For scale, the base commit's build carried 28 such lines, from 61 doc-comment lines in `src/`.
32
+
33
+ **Identifiers were translated, never deleted, and no public export lost its documentation.**
34
+ That is measured, not asserted: the built `dist/index.d.ts` carries **691 doc-comment blocks,
35
+ 633 documented declarations and 237 `@example` blocks before and after, and the sorted list of
36
+ documented declaration names is byte-identical.** Heads left behind by a front-of-line strip
37
+ were repaired: nine composite parsers wrapped `HL7-VALUE-REDECODE)` onto a continuation line,
38
+ where a naive strip leaves `). Absent / empty components are OMITTED`.
39
+
40
+ **Stripping the framing exposed a false claim on a published API, which is the argument for
41
+ doing this at all.** `defineProfile`'s doc block in `dist/index.d.ts` told consumers that
42
+ `opts.extends` was "ACCEPTED but IGNORED, resulting in `lineage === [opts.name]` regardless".
43
+ The implementation merges parents (lineage, `dateFormats`, `customSegments`, `description`,
44
+ composed `onWarning`) and re-validates the merged result. The text now describes what the
45
+ function does. Four sibling claims of the same kind were also stale and are gone: `emitMessage`,
46
+ `emitPrettyPrint`, `emitJson` and `buildMessage` were each documented as an unimplemented stub
47
+ ("Stub throws", "Implementation lives in..."), and all four have been implemented for some time.
48
+ Internal framing is where stale claims hide, which is why this is a correctness change and not
49
+ a tidying one.
50
+
51
+ **The gate now has a third pass** with its own rule array, its own extractor and its own
52
+ positive/negative self-tests written in the language of source rather than markdown. It scans
53
+ `/** */` text only, line by line and again over the block reflowed the way a hover tooltip
54
+ reflows it, because multi-token rules are blind to a violation that straddles a wrap and this
55
+ package wraps its doc comments. `//` and `/* */` comments are deliberately **not** covered and
56
+ keep their identifiers: they do not reach `dist` (checked both directions), and the convention
57
+ names source comments as a place identifiers belong. 61 such lines remain in `src/` by choice.
58
+
59
+ Every claim above was checked RED against a seeded violation rather than assumed: a single-line
60
+ violation in an exported function's JSDoc; a violation split across a wrap that a line-only
61
+ grep scores 0 on; the same bookkeeping in a `//` comment staying green; widening the source
62
+ identifier rule to the `WORD-N` shape failing its own negative self-test naming `MSH-2`; an
63
+ extractor that strips the comment leader before testing the terminator reporting 66 false hits
64
+ on a clean tree; and an extractor that yields nothing refusing to report green.
65
+
66
+ **The ceiling is stated rather than implied shut, and "0 on all six rules" is a statement about
67
+ these rules, not about the founder's rule.** Measured on the built `dist/index.d.ts`, what still
68
+ ships is **144 lines carrying `D-NN` internal decision numbers** and **51 lines carrying item
69
+ identifiers from nine prefixes the list does not hold** (`HELPERS` 14, `BIP` 15, `PROF` 8,
70
+ `SER` 6, `MODEL` 4, `PARSE`, `WR`, `TOL`, `TYPES` 1 each). `D-NN` is the deliberate
71
+ non-catch it has always been, because legacy SNOMED RT codes are axis-prefixed in exactly that
72
+ shape (`D-13000`, `T-32000`, `M-80003`). The item identifiers need new prefixes added to the
73
+ list, which is a **rule** change needing its own negative self-tests (`SER`, `WR`, `TOL` and
74
+ `BIP` are exactly the short generic tokens that made `PKG` unsafe), so it is not smuggled in on
75
+ the back of a prose sweep. Separately, `dist/` is untracked build output, so no checked-in gate
76
+ can read it: this gates dist's _source_, which holds only because the dts build copies doc text
77
+ verbatim, and the `164` figure is a hand-taken snapshot. Two "the per-X slice of Y" false
78
+ positives, where `slice` means portion, were fixed by rewording rather than by narrowing the
79
+ rule.
80
+
81
+ Documentation, tooling and CI only. No change to the published package surface, parser
82
+ behavior, or warning codes.
83
+
84
+ - **Public-surface hygiene gate in CI (`scripts/check-no-internal-refs.sh`,
85
+ `pnpm check:no-internal-refs`, `.github/workflows/no-internal-refs.yml`;
86
+ `PUBLIC-SURFACE-HYGIENE`).** Founder directive of 2026-07-27: no internal project bookkeeping on
87
+ any surface a consumer reads. The remediation half of that directive was a sweep of 149 references
88
+ across 11 repos, and a sweep regresses the first time someone writes `(CCDA-P8)` into a README, so
89
+ the deliverable here is the gate; the sweep rides along. **50 violating lines across 15
90
+ `docs-content/` pages were cleared** (re-measured with the shipped rules against the previous tree;
91
+ the backlog's count of 44 was markdown-only and predates two of the rules): phase identifiers in
92
+ page titles and headings, `HL7-N` / `HL7-J` / `HL7-ESC` / `HL7-R` / `VERIFY-AT-BUILD` item
93
+ identifiers, a citation of `operations/roadmaps/hl7.md`, bracketed spec-trace tags (`[S-NTE]`,
94
+ `[S-DTM-IMPL]`), an "Open-question #12", and review-process commentary ("All confirmed 3-0
95
+ (pass 5)", "all verified 3-0 in the research pass", "Assumption logged"). One more, in `README.md`,
96
+ was fixed by hand and is caught by no rule: a pointer to "the roadmap's known-limitations", a file
97
+ the reader does not have. Identifiers were translated at the boundary, not merely deleted, and
98
+ every head left behind by a front-of-line strip was repaired: a heading cut to `: datetime
99
+ precision` reads worse than the text it replaced. The datetime page needed more than a strip: with
100
+ its phase framing gone it read as documentation of shipped behaviour while still describing, in the
101
+ present tense, a `parseHl7TsDtm` zero-fill defect that no longer exists in the library (the API it
102
+ planned has shipped). It is now written as what the package does, its unexecuted plan and test
103
+ bookkeeping are gone, and a provenance claim that survived the strip in a stronger form than the
104
+ original ("all verified against primary Ch. 2A") is removed rather than reworded. The detection
105
+ rules are lifted
106
+ from `cosyte/.github` `scripts/release-notes.mjs`, which is already tested byte-for-byte against
107
+ the 14 published release bodies, and they are **keyed on known project prefixes, never on the
108
+ `WORD-N` shape**: `MSH-2`, `PID-3`, `OBX-5`, `TQ1-7`, `NM1-03`, `PKG-4`, `ICD-10-CM`, `HL7-V2`,
109
+ `FHIR-R4`, `DICOM-SR`, `NCPDP-SCRIPT`, `FHIR-bridge`, `HL7-defined` and the four `CSP`
110
+ Clinical Study **Phase** field names are the reference material an HL7 parser's docs exist to
111
+ provide, and a shape rule deletes them. That is asserted, not assumed: the script carries a
112
+ **negative** self-test per rule, built from that material, alongside the positive one, so widening
113
+ a rule into the `WORD-N` shape reds the gate instead of silently corrupting a segment reference on
114
+ the next sweep. Where a rule could not be guarded it was **cut** instead: `phase` after a
115
+ determiner ("the non-goals of this phase") is not detected at all, because catching it also caught
116
+ "the phase of the clinical study" and "the phase of illness", and when the collision is
117
+ with clinical reference material the reference material wins. The scan harness is the em-dash gate's,
118
+ in mllp's variant: scan list built and `./`-prefixed in one loop so the scan stays a single command
119
+ with stderr bound to all of it, no `sed -z`, no `-d skip`, no `-I`, `-e`/`--`, `-0 -r`, and a
120
+ refusal to print OK from an empty list, an unreadable input, a non-regular-file entry, or any
121
+ scanner stderr. Every one of those routes was checked RED against a seeded violation rather than
122
+ inherited. **Every rule is also applied a second time to paragraph-joined text**, because the rules
123
+ are multi-token and this repo hard-wraps markdown: a violation straddling a wrap was invisible to a
124
+ line scan and one was live (`A future phase` / `may add opt-in decode`, with the gate printing OK).
125
+ The join squeezes whitespace the way markdown does, which is load-bearing rather than cosmetic: a
126
+ verbatim join leaves the continuation line's indentation in place, and about 600 lines of
127
+ `docs-content/` start indented, so the pass would have missed the dominant wrap shape while
128
+ reporting that it ran.
129
+ It scans the public surface only (`README.md`, `TRADEMARKS.md`, `LICENSE`, `docs-content/`, and the
130
+ npm `description` and `keywords`) because the same identifier is required on the inside:
131
+ `CHANGELOG.md`, `.changeset/`, commits and PRs are where it belongs. A tripwire refuses the run if
132
+ `package.json`'s `files` starts shipping anything the gate does not cover. **Three holes are measured and disclosed rather than implied
133
+ shut:** `dist/` ships the compiled JSDoc, and the tracked `src/*.ts` files carry 253 phase-rule and
134
+ 39 identifier-rule lines, of which a local build puts 137 and 22 into `dist/index.d.ts` for every
135
+ consumer's editor to render, unswept and ungated here because remediating ~290 source doc comments
136
+ is its own reviewable change (**that change is the entry above, which swept them and added the
137
+ third pass**; `dist/` is untracked, so no checked-in gate can re-derive that figure without
138
+ building first, and that part is still true); determiner-plus-`phase` is not detected, by choice;
139
+ and the rules catch identifiers, not English sentences about our process. Documentation and CI only: no change to the
140
+ published package surface, parser behavior, or warning codes.
141
+ - **Em-dash brand gate in CI (`scripts/check-no-emdash.sh`, `pnpm check:no-emdash`,
142
+ `.github/workflows/no-emdash.yml`; `EMDASH-CONFORMANCE` part 1).** The founder directive of
143
+ 2026-07-24 (`knowledgebase/06-brand/voice-and-tone.md`) bans `U+2014` outright across every cosyte
144
+ surface and names commit messages explicitly, and the meta-repo's `documentation/conventions.md`
145
+ has described the rule as CI-gated; it was in fact gated in only 3 repos of 10. This ports
146
+ `knowledgebase`'s scanner (the text-only variant, correct here because hl7 tracks no binaries:
147
+ all 414 tracked files read as us-ascii or utf-8 and none holds a NUL byte) and wires it to a
148
+ dedicated workflow that checks **both** the tracked files and the **PR title, body, and branch
149
+ commit messages**, the last of these on the non-default `edited` activity type so a description
150
+ retitled after the final push is re-checked before a squash merge turns it into the commit
151
+ message. It is a separate workflow rather than a job in `ci.yml` because `ci.yml`'s triggers drive
152
+ the Node 22 + 24 matrix plus the examples and starter-kit `extras` job, which should not re-run on
153
+ every PR-description edit. Within the bytes it is designed to match, the scanner refuses to report
154
+ green from a scan it could not complete: it pins `LC_ALL=C.UTF-8` (without it GNU grep 3.8 aborts
155
+ on `\x{2014}` and a naive port prints OK having matched nothing), self-tests against a known em
156
+ dash before believing a clean result, fails on any scanner stderr, and refuses an empty file list.
157
+ Two limits are documented in the script rather than engineered away: it excludes itself (it has to
158
+ name the encodings it bans), and it matches `U+2014` as UTF-8 plus five textual encodings, so an em
159
+ dash in a legacy-charset fixture (CP1252 `0x97`, UTF-16) scans clean and is a reviewer's job, not
160
+ the gate's. **No content changed**: hl7 was already
161
+ clean (0 of 53 markdown files), so this is regression prevention only. Dev-tooling only: no change
162
+ to the published package surface, parser behavior, or warning codes.
163
+ - **Performance / throughput bar: reproducible benchmark suite + a ratio-based perf-regression guard
164
+ (HL7-W, Phase W, sixth and final phase of the v2.4 capability arc; completes it).** `pnpm bench`
165
+ (`scripts/bench.ts`) measures parse **throughput** (msgs/sec + MB/sec across a small ADT^A01 and a
166
+ larger multi-OBX ORU^R01), **per-message retained memory**, and the **streaming peak memory** that
167
+ substantiates `parseStream`'s O(one-message) claim, over synthetic in-process inputs (no PHI, no
168
+ fixtures). Indicative numbers are published in `docs-content/benchmarks.md`. A CI-gating
169
+ perf-regression guard (`test/perf/perf-regression.test.ts`) fails on a regression while staying
170
+ non-flaky across runners: every assertion is **relative**, not an absolute floor: throughput
171
+ **linearity by message count** (`t(4N)/t(N) ≤ 10`, min-of-runs; catches super-linear-in-count),
172
+ **linearity by message size** (`t(4S)/t(S) ≤ 10`; catches an O(n²)-in-length tokenizer regression),
173
+ and streaming **read-ahead ≤ 2** (a pure count, the O(one-message) memory invariant as a perf gate,
174
+ which trips if `parseStream` ever buffers the file); one coarse absolute liveness floor (~100× below
175
+ real throughput) trips only on a catastrophic hang. Absolute memory numbers are published from the
176
+ benchmark (which forces GC via `--expose-gc`); the gate deliberately avoids un-GC'd `heapUsed` deltas.
177
+ **No behavior change**: measurement + a guard only; parse output is byte-identical, no `src/`
178
+ runtime code touched, no public API added. New: the `bench` package script and the `benchmarks` doc.
179
+ - **FHIR-bridge readiness: a versioned IR-stability contract + coverage proof (HL7-V, Phase V, fifth
180
+ of the v2.4 capability arc).** Documents and **versions** the mapping-source IR that
181
+ `@cosyte/transform` builds against, and proves, firsthand against the HL7 **v2-to-FHIR** IG
182
+ (v1.0.0, FHIR R4), that hl7 surfaces the source paths the IG references. **No v2→FHIR mapping is
183
+ added to hl7.** hl7 constructs no FHIR resource, ships no ConceptMap, translates no terminology;
184
+ that stays in `@cosyte/transform`. This is documentation + a coverage test over **existing** surface;
185
+ no new runtime API.
186
+ - **IR Contract v1.0.0** (`docs-content/spec-notes-fhir-bridge.md`) pins the stable mapping surface:
187
+ dot-path source addressing (`msg.get`/`resolvePath`/`parsePath`, `segment.field[.component]`
188
+ one-to-one with the IG), the twelve typed composites (`Field.asXpn`/`asCx`/`asCwe`/…, the
189
+ datatype-map inputs), code-system provenance, `parseDtm` datetime precision + timezone, the text
190
+ codec (`decodeText`/`renderText`/`Field.render`), and message metadata, with a **semver
191
+ deprecation posture** so `transform` can build once without forcing later hl7 churn.
192
+ - **Coverage proof** (`test/fhir-bridge-coverage.test.ts`) grounds against the IG's vendored raw
193
+ segment-map CSVs + its public sample corpus (provenance in
194
+ `test/fixtures/fhir-bridge/PROVENANCE.md`). Invariants: every IG-referenced field-level source path
195
+ is **addressable** (no hl7-side gap); every path the public sample **populates** resolves through
196
+ the IR (**63/63** exercised paths surfaced); the datatype-map shapes (XPN, CX with assigning
197
+ authority, CWE provenance, DTM precision+tz, rendered narrative) surface from the sample; and the
198
+ package exports carry **no** FHIR-typed symbol (scope-boundary guard).
199
+ - **Gaps recorded honestly.** The IG's public sample corpus is, verified firsthand, **one** v2
200
+ message (`MDM^T02`), not the multi-family `samples/` the roadmap assumed. **233 of 296**
201
+ IG-referenced field paths are un-exercised by that single sample: a **corpus-coverage** gap (the
202
+ thin sample set), **not** a hl7 reachability gap (hl7 addresses all 296 generically).
203
+ - **Conformance-profile tooling: `validateAgainstProfile` (HL7-U, Phase U, fourth of the v2.4
204
+ capability arc).** hl7 could parse and author messages and flag missing spec-Required segment
205
+ groups (`msg.structure`), but could not answer "does this feed meet _our_ interface spec?"
206
+ `validateAgainstProfile(message, profile)` runs a **consumer-authored declarative conformance
207
+ profile** and returns **typed findings**.
208
+ - **BYO profile + value sets: the deliberate scope boundary.** The profile schema is a bounded
209
+ subset of the HL7 v2 Message-Profile / NIST-IGAMT model: per-segment/field usage
210
+ (`R`/`RE`/`C`/`CE`/`O`/`X`), cardinality, length, and value-set binding against a
211
+ **consumer-supplied** code list. hl7 ships **no** vendor/IHE/regulatory profile, **no** bundled
212
+ code set (no LOINC/SNOMED/ICD/RxNorm lookup), and makes **no** network call, the sanctioned
213
+ functionality-plane replacement for the retired vendor-corpus arc.
214
+ - **Four invariants.** The engine **never throws**. A malformed profile yields `PROFILE_MALFORMED`
215
+ findings, not an exception (never a silent pass); the optional `defineConformanceProfile` is a
216
+ fail-fast authoring gate that _does_ throw `ProfileDefinitionError`. A **valid message yields zero
217
+ findings**. **No finding carries PHI.** Each names the structural locus (segment / field /
218
+ component / repetition / occurrence) and the rule, never the offending value. Validation is
219
+ **read-only**. `C`/`CE` presence is not evaluated (no conditional-predicate language, a
220
+ documented boundary), keeping valid⇒zero-findings airtight. Property-tested for never-throws,
221
+ read-only, and well-formed PHI-safe findings across message × profile pairs.
222
+ - **"No findings" is explicitly NOT a conformance attestation.** It means no _declared_ rule was
223
+ violated, nothing about undeclared parts of the message and no conformance certification.
224
+ - New public exports: `validateAgainstProfile`, `defineConformanceProfile`, the `conformance`
225
+ namespace, `FINDING_CODES`, `USAGE_CODES`, and the `ConformanceProfile` / `SegmentRule` /
226
+ `FieldRule` / `Cardinality` / `UsageCode` / `ConformanceFinding` / `FindingLocus` / `FindingCode`
227
+ / `FindingSeverity` / `ConformanceResult` types. Six additive finding codes:
228
+ `PROFILE_REQUIRED_ABSENT`, `PROFILE_NOT_PERMITTED`, `PROFILE_CARDINALITY`, `PROFILE_LENGTH`,
229
+ `PROFILE_VALUE_NOT_IN_SET`, `PROFILE_MALFORMED`. Additive: no change to existing output. Distinct
230
+ from the parse-profile system (`defineProfile`/`profiles`). See
231
+ `docs-content/spec-notes-conformance.md`.
232
+ - **Streaming / incremental parse: `parseStream` (HL7-S, Phase S, third of the v2.4 capability
233
+ arc).** hl7 could only parse a message (`parseHL7`) or a whole in-memory batch (`splitBatch`). A
234
+ large ELR/IIS/lab file or a live feed had to be fully buffered first. `parseStream(source, opts?)`
235
+ parses **incrementally**: it consumes a chunked source (a Node `Readable`, an async-iterable, or an
236
+ iterable of `string`/`Buffer` chunks) and **yields one message per `MSH`-delimited boundary** as it
237
+ completes, with **O(one-message)** memory, the whole stream is never retained.
238
+ - **Chunk-boundary invariant.** A message split across chunk boundaries (mid-segment, mid-field,
239
+ even mid-`MSH|^~\&`) is reassembled; feeding the same bytes in 1-byte chunks vs. one big chunk
240
+ yields **identical** messages. A `\r\n` terminator straddling a chunk boundary is not mistaken for
241
+ a bare `\r`; `\r` / `\r\n` / `\n` are all tolerated. Property-tested across randomized chunk
242
+ boundaries and 1-byte chunks.
243
+ - **No second grammar.** Each demarcated message is parsed by the shipped `parseHL7` (the second
244
+ argument (profile / `strict` / `charset` / `dateFormats`) is forwarded per message), so a
245
+ streamed message is byte-for-byte what `splitBatch` produces for the same bytes.
246
+ - **Isolation, never a dropped tail.** A malformed message mid-stream surfaces as an isolated typed
247
+ failure entry (the `Hl7ParseError`) and the stream **continues**. It never suppresses later
248
+ messages. Batch-envelope segments (`FHS`/`BHS`/`BTS`/`FTS`) act as boundaries and are never
249
+ yielded, so **yielded count == `MSH` count** (envelope count reconciliation stays `splitBatch`'s
250
+ job). A final message with no trailing terminator is still yielded in full, flagged via the new
251
+ stream-level `UNTERMINATED_STREAM_MESSAGE` warning on the entry's `streamWarnings` (never on
252
+ `Hl7Message.warnings`, never a throw).
253
+ - Consumes an **already-de-framed** stream. MLLP `<SB>…<EB><CR>` framing/timing/TLS remain
254
+ `@cosyte/mllp`'s. New public exports: `parseStream`, `unterminatedStreamMessage`, the
255
+ `Hl7StreamSource` / `StreamMessageEntry` types, and the `UNTERMINATED_STREAM_MESSAGE` warning code.
256
+ Additive: no change to `parseHL7` / `splitBatch` output. See `docs-content/spec-notes-stream.md`.
257
+ - **Typed emit symmetry: composite setters + `buildAdt` / `buildOru` (HL7-T, Phase T, second of the
258
+ v2.4 capability arc).** hl7 could read a message into typed objects but could only _write_ one
259
+ through stringly-typed field arrays (`buildMessage(...).addSegment("PID", ["", "", "MRN"])`, the
260
+ consumer hand-encoding `^`/`&`/`~`). Phase T makes hl7 an **authoring** library (the
261
+ conservative-emit mirror of the read helpers) built on Phase R's encode-safe codec:
262
+ - **`encodeComposite(kind, value)`** (and the per-type `encodeXpn`/`encodeXad`/`encodeCx`/`encodeCwe`/
263
+ `encodeCe`/`encodeXtn`/`encodePl`/`encodeHd`/`encodeTs`/`encodeNm`/`encodeXcn`, plus
264
+ `encodeCompositeReps` for repeating fields) turn a typed composite (an XPN name, a CX identifier
265
+ (nested-HD aware), a TS timestamp) into a spec-clean `RawField`. **`Hl7Message.setComposite(path,
266
+ kind, value)`** sets one at a field or `field[rep]` dot-path. Component values are stored
267
+ **decoded** and the serializer re-escapes on emit, so an embedded delimiter (`"Smith^Jr"`) is
268
+ **escaped, never injected**. It re-parses to the exact string, never forging a component boundary.
269
+ The **emit ∘ parse identity** holds on every modelled field (interior empties preserved, trailing
270
+ trimmed); property-tested across all composites over the delimiter-laden alphabet.
271
+ - **`buildAdt(event, init)`** and **`buildOru(init)`** assemble a full message (MSH + EVN + PID + PV1
272
+ for ADT; MSH + PID + OBR + OBX for ORU) from typed inputs, generating the control id / timestamp /
273
+ encoding characters correctly. The emitted message is **spec-clean, zero-warning, and
274
+ structurally complete**. It re-parses with `warnings: []` and `msg.structure.missingGroups` empty
275
+ (verified against the parser's own structure net across `A01`/`A02`/`A03`/`A04`/`A08`).
276
+ - **Never fabricate.** A builder emits only values the caller supplied; an omitted optional field is
277
+ an empty/absent field, never a defaulted clinical value. A required-but-absent structural input
278
+ (`patient`, an empty ADT `event`, or an ORU with no observation) is a typed `TypeError`, not a
279
+ guessed value. Complements the shipped `buildAck` (Phase C); the low-level `buildMessage` remains
280
+ for the long tail. Emit is spec-clean **base standard**, not a vendor dialect.
281
+
282
+ New public exports: `buildAdt`, `buildOru`, `encodeComposite`, `encodeCompositeReps`, the eleven
283
+ per-type `encodeXxx` functions, `Hl7Message.setComposite`, and the `CompositeKind` /
284
+ `CompositeValueByKind` / `BuildAdtInit` / `AdtPatient` / `AdtVisit` / `AdtEvent` / `BuildOruInit` /
285
+ `OruPatient` / `OruOrder` / `OruObservation` types. Ships two synthetic, PHI-scanned goldens
286
+ (`fixtures/build/adt-a01.golden.hl7`, `oru-r01.golden.hl7`) plus a property layer (emit ∘ parse
287
+ identity + no-injection). Additive: no change to parse output or existing emit. Depends on Phase R.
288
+
289
+ - **Formatted-text rendering + a first-class text codec (HL7-R, Phase R, first of the v2.4
290
+ capability arc).** Completes Phase A's explicit defer: the lenient parser preserves the
291
+ presentational escapes (`\H\`/`\N\` highlight, the `\.sp\`/`\.in\`/… formatting commands, charset
292
+ switches, vendor `\Z..\`) as un-rendered sentinels; a note surfaced to a human with those raw
293
+ sentinels in it is **misread**. Two additions close that, as an opt-in **layer** over the unchanged
294
+ raw extraction (Postel's Law, existing parse output is byte-for-byte identical):
295
+ - **`renderText(input, enc?, opts?)`** turns HL7 v2 §2.7 escape/formatting-bearing content into a
296
+ normalized **display model**: a plain-text `text` (formatting commands → whitespace/line breaks
297
+ per a conservative, documented normalization; highlight boundaries dropped) plus highlight-aware
298
+ `runs` (`{ text, highlighted }[]`). It **never fabricates**: a charset switch, a vendor `\Z..\`,
299
+ or a malformed/unterminated escape is preserved as its literal characters and flagged in
300
+ `unrenderedSequences`, never dropped or replaced with a guessed glyph. Grounded firsthand on the
301
+ HL7 v2 Ch. 2 §2.7 escape spec (delimiter §2.7.1, charset §2.7.2/3, hex §2.7.5/6, formatting
302
+ commands §2.7.6/7 [FT], local `\Z..\` §2.7.7/8, truncation §2.5.5.2, confirmed against the
303
+ v2.5.1 and v2.8.2 chapter text). **`Field.render(opts?)`** is the one-call form on a parsed field.
304
+ - a first-class **text codec**: `decodeText` / `encodeText`, also bundled as the `text` namespace
305
+ (`text.decode` / `text.encode` / `text.render`). The load-bearing invariant is **encode-safety**:
306
+ `encodeText` escapes every reserved character (the escape char first, then the
307
+ field/component/subcomponent/repetition separators, the truncation char, and framing-critical
308
+ CR/LF) so an arbitrary string round-trips (`decodeText(encodeText(s)) === s`) and re-parses to
309
+ exactly itself with **no delimiter injection**. A value can never break framing or forge a
310
+ component boundary. This is the safe-encode primitive Phase T (typed emit) will build on.
311
+
312
+ New public exports: `renderText`, `decodeText`, `encodeText`, the `text` namespace,
313
+ `Field.render()`, and the `RenderedText` / `TextRun` / `RenderTextOptions` types. Ships four
314
+ synthetic, PHI-scanned `fixtures/text/*.hl7` fixtures (formatting note, highlight, hex+delimiters,
315
+ inject-on-encode) plus a property layer (encode-safety over arbitrary strings; `renderText` is
316
+ total; a formatting sentinel never survives into the plain-text render). No new warning codes.
317
+
318
+ - **`profiles.va`: eighth built-in vendor profile (VA VistA Radiology/Nuclear Medicine imaging).**
319
+ Declares the `ZDS` Z-segment that carries the DICOM **Study Instance UID** (field 1, `RP` composite,
320
+ the UID sits in the first component, ZDS-1.1 "Pointer") so a VistA radiology feed parses without an
321
+ `UNKNOWN_SEGMENT` warning and `zds.get("studyInstanceUid")` resolves by name. `ZDS` is the **IHE
322
+ Radiology** RIS↔PACS bridge segment, the same cross-vendor extension `profiles.visage` and
323
+ `profiles.philips` declare, **not** a VA-proprietary quirk; the value this profile adds is a distinct
324
+ **named federal source** and coverage of the shape the VA spec documents that the imaging-vendor
325
+ specs do not: `ZDS` on **ORU** (result) messages (VistA sends `ZDS` in both ORM and ORU). Grounded in
326
+ the **publicly published** [Radiology/Nuclear Medicine 5.0 HL7 Interface Specification](https://www.va.gov/vdl/documents/clinical/radiology_nuclear_med/ra5_0hl7is.pdf)
327
+ (Version 3.6, Patch RA\*5.0\*203, June 2024, U.S. Department of Veterans Affairs), which documents
328
+ "ZDS Segment Fields in ORU and ORM", not an invented quirk (ADR 0018). The spec's messaging is
329
+ HL7-native v2.4, so the profile declares no custom date formats. Ships one synthetic, PHI-scanned
330
+ `vendor-shapes/va/oru-r01.hl7` (ORU^R01 + ZDS) fixture whose Study Instance UID uses the Medical
331
+ Connections free UID root, not the reserved DICOM arc. Additive: no change to existing profiles,
332
+ warning codes, or the parse/serialize surface.
333
+
334
+ - **`profiles.philips`: seventh built-in vendor profile (Philips Vue PACS "IS Link" imaging).**
335
+ Declares the six Vue PACS custom Z-segments, one per filler role: `ZDS` (DICOM **Study Instance
336
+ UID**, an RP composite whose first component is the UID), `ZLK` (linked studies / linked orders),
337
+ `ZAO` (order additional details: modality, body part, result-transfer + acquisition status,
338
+ technician and radiologist name/id, and the vendor's custom string/number/date slots), `ZEB`
339
+ (encrypted patient-info blob), `ZAP` (patient additional details), and `ZAV` (visit additional
340
+ details), so a Vue PACS ORM/ADT feed parses without `UNKNOWN_SEGMENT` warnings and each field
341
+ resolves by name (e.g. `zao.get("acquisitionStatus")`). Field positions are transcribed **verbatim**
342
+ from the spec, including two of its own gaps (`ZAO` has no field 7; `ZAP` has no field 2). Grounded
343
+ in the **publicly published** [Vue PACS 12.2.8 HL7 Interface Specifications](https://www.documents.philips.com/assets/Conformance%20Statements/20240409/8941f89d89aa4983aab7b14d00db578c.pdf)
344
+ (Philips, doc HA1669 Rev A, §§5.11–5.16), not an invented quirk (ADR 0018). Vendor `TS` timestamps
345
+ are HL7-native, so the profile declares no custom date formats. Naming the `ZEB` field is precisely
346
+ how a consumer learns that segment is PHI-bearing and must not be logged; the profile only _names_
347
+ fields. It never decodes, decrypts, or rewrites them. Ships two synthetic, PHI-scanned fixtures
348
+ (`vendor-shapes/philips/orm-o01.hl7` order-filler; `vendor-shapes/philips/adt-a08.hl7`
349
+ patient/visit-filler) with documented provenance. Additive: no change to existing profiles, warning
350
+ codes, or the parse/serialize surface.
351
+
352
+ - **`profiles.visage`: sixth built-in vendor profile (Visage 7 imaging/PACS).** Declares the `ZDS`
353
+ Z-segment that RIS/PACS order feeds use to carry the DICOM **Study Instance UID** (field 1, first
354
+ component), the IHE Radiology bridge that correlates an HL7 order to its DICOM study, so an
355
+ imaging ORM parses without an `UNKNOWN_SEGMENT` warning and `zds.get("studyInstanceUid")` resolves
356
+ by name. Grounded in the **publicly published** [Visage 7 HL7 Interface Specification](https://www.visageimaging.com/downloads/Visage7/Visage7_HL7InterfaceSpecification.pdf)
357
+ (V23.00, Jun 2026, Visage Imaging GmbH), its §4.4 ORM^O01 example and ZDS segment table, not an
358
+ invented quirk (ADR 0018: public specs unblock quirk-corpus work; a _specific undocumented_ vendor
359
+ deviation still waits on a private feed). Vendor dates in that spec are HL7-native, so the profile
360
+ declares no custom date formats. Ships with a synthetic `vendor-shapes/visage/orm-o01.hl7` fixture
361
+ (PHI-scanned) and a `test/fixtures/vendor-shapes/README.md` recording per-fixture provenance.
362
+ Additive: no change to existing profiles, warning codes, or the parse/serialize surface.
363
+
364
+ - **PHI commit-scanner (`scripts/phi-scan.ts`, `pnpm phi-scan`).** A zero-dependency, HL7 v2
365
+ shape-aware scanner that refuses fixtures (and a conservative text pass over `src/`) carrying
366
+ real-looking PHI, so a real patient message can never be committed by accident. It parses each
367
+ message's delimiters (`MSH-1` / `MSH-2`) and inspects only the fields that actually carry each
368
+ category: patient / person names (PID-5/-6/-9, NK1-2, GT1-3, IN1-16, MRG-7 as XPN; PV1-7/-8/-9/-17,
369
+ ORC-12, OBR-16, AIP-3, TXA-9, ROL-4 as XCN, reading the family/given from the correct components),
370
+ dates of birth (PID-7 / NK1-16), SSNs (PID-19, CX identifier-type `SS`, dashed patterns anywhere),
371
+ MRN / account numbers (PID-3 / -18), addresses (PID-11 / NK1-4 / GT1-5 / IN1-19), phones
372
+ (PID-13/-14 / NK1 / GT1 without the `555` fake-exchange convention), non-test emails, and a
373
+ site-defined `Z…`-segment name backstop, deliberately NOT a naive `Family^Given` text regex, which
374
+ would trip on coded values like `CBC^Complete Blood Count^LN` (false confidence). Synthetic
375
+ fixtures are positively declared in `scripts/phi-allow-list.txt` (HL7 v2 is byte-strict at the
376
+ front, so an inline `# synthetic: true` header is impossible, same model as `@cosyte/dicom` /
377
+ `@cosyte/x12`); a whole-file bypass requires `--allow-fixture` **and** an audit entry in
378
+ `phi-scan-overrides.md`. All `git` calls use `execFileSync` array-args (never shell-form). Runs at
379
+ pre-commit (`simple-git-hooks --staged`) and in CI (`run-phi-scan: true`); `scripts/verify.sh` now
380
+ reports `phi-scan ✓`. Dev-tooling only: no change to the published package surface or warning codes.
381
+
382
+ - **Trademark notice (`TRADEMARKS.md`).** This package names third-party systems to describe what it
383
+ interoperates with; the notice records that cosyte is not affiliated with, endorsed by, or
384
+ sponsored by any of them, that every reference is descriptive, and that the built-in profiles are
385
+ authored from public sources only. Added to `files` so it ships inside the published tarball, not
386
+ just on GitHub. Documentation only: no runtime or API change.
387
+
18
388
  - **Full documentation Diátaxis spine + doc/code-agreement gate (DOCS-CONTENT-P1).** `docs-content/`
19
389
  gains **Installation**, **Quickstart**, **Guides**, and **Troubleshooting** (with Known
20
390
  Limitations) categories on top of the existing Overview + Core Concepts, authored from the real API
21
- against synthetic messages — the template of record every other cosyte parser's docs copy. A new
391
+ against synthetic messages, the template of record every other cosyte parser's docs copy. A new
22
392
  `test/docs-content.test.ts` runs `@cosyte/vitest-config@^0.0.2`'s `docSnippetSuite()` over the
23
393
  pages, so every ` ```ts runnable ` snippet is compiled, executed against the built package, and its
24
- inline `// =>` assertions checked — a documented example can never silently drift from the code.
394
+ inline `// =>` assertions checked: a documented example can never silently drift from the code.
25
395
  Bumps the `@cosyte/vitest-config` devDependency `^0.0.1 → ^0.0.2`. Documentation + test-tooling
26
- only — no runtime/API change; the published artifact is unchanged.
396
+ only: no runtime/API change; the published artifact is unchanged.
27
397
 
28
398
  ### Changed
29
399
 
400
+ - **Corrected stale "not yet published to npm / unpublished" language in the docs site
401
+ (README-ORG-SWEEP).** `@cosyte/hl7` is now published on npm at `0.0.1` and public, so the
402
+ `docs-content/installation.md` Status blockquote ("not yet published… the shape it will take at
403
+ first publish; consume it from source or a workspace link") and the `docs-content/troubleshooting.md`
404
+ "Pre-alpha (`0.0.x`), unpublished" note were factually wrong. Both now state the package is published
405
+ on npm at `0.0.1` and public, still pre-alpha on the `0.0.x`-until-first-alpha ladder, with the
406
+ `npm install @cosyte/hl7` command described as live rather than aspirational. Docs-site content only
407
+ (not in the published tarball); no runtime or public-API change.
408
+
409
+ - **`profiles.meditech` re-grounded to a public MEDITECH spec (HL7-I, ADR 0018).** The profile's
410
+ Z-segment map previously declared a `ZVI` "visit info" segment (`visitReason`/`admitSource`) that
411
+ was a **community-sourced prior with no citable public source**. No public MEDITECH spec documents
412
+ a `ZVI` segment, so, per the "encode a quirk only when a real, publicly-documented spec grounds
413
+ it" mandate, `ZVI` was **removed** and replaced with the DFT charge Z-segments MEDITECH documents
414
+ verbatim in its **publicly downloadable** [Ancillary Charges (LAB/PHA/ITS/IDM) Outbound](https://ehr.meditech.com/sites/default/files/documents/20240613/ancillary-charges-outbound-21.pdf)
415
+ spec (Version 2.1, © 2021 Medical Information Technology, Inc.): `ZF1` ("PROVIDER ENCOUNTER COPAY
416
+ DATA": `providerEncounter`, `misServiceGroup`, `serviceGroupCopay`, `visitCopay`, `copayMinimum`,
417
+ `copayMaximum`) and `ZF2` ("ENCOUNTER PROCEDURE DATA": `setId`, `providerEncounter`,
418
+ `encounterDate`, `encounterProcedure`, `encounterProcedureQuantity`, `encounterProcedureCharge`,
419
+ `prvProcedureAmountPaid`, `prvProcedureAmountDue`). The profile's `YYYYMMDDHHMM` minute-precision
420
+ timestamp format is **kept and now spec-cited**, confirmed by that spec (MSH-7 length 12; EVN-2 /
421
+ PID-7 "Format is YYYYMMDDHHMM") and by the [Admissions and Registration Outbound](https://ehr.meditech.com/sites/default/files/documents/20240613/admissions-registration-outbound-24.pdf)
422
+ spec (Version 2.4, © 2021): "Date and Time in Admissions. Format is YYYYMMDDHHMM". The vendor-shape
423
+ fixture moved from `vendor-shapes/meditech/adt-a04.hl7` (ADT + `ZVI`) to
424
+ `vendor-shapes/meditech/dft-p03.hl7` (DFT^P03 + `ZF1`/`ZF2`), synthetic and PHI-scanned. Consumers
425
+ who relied on `ZVI` from `profiles.meditech` (an ungrounded mapping) must declare it themselves
426
+ via `defineProfile({ extends: profiles.meditech, customSegments: { ZVI: … } })`. `epic`, `cerner`,
427
+ `athena`, and `genericLab` remain community-sourced priors, tracked for the same treatment.
428
+
30
429
  - **Dropped the "Dogfooded in production" claim from the README feature list.** The bullet asserted
31
430
  the package was "used internally on healthcare-integration projects"; no engagement has run it, so
32
431
  the claim was not accurate and has been removed ahead of the first public release. `README.md`
@@ -34,27 +433,40 @@ per the cosyte version ladder (`0.0.x` until first alpha).
34
433
 
35
434
  ### Fixed
36
435
 
436
+ - **`scripts/sync-version.mjs` hardened against two latent defects, and gated in CI
437
+ (SYNC-VERSION-HARDENING).** Follow-up hardening on the VERSION-SYNC script; ported byte-identically
438
+ across `hl7`, `x12`, and `mllp`. (1) The version was spliced into `src/index.ts` via
439
+ `String.prototype.replace` with a _replacement string_, which interprets `$&`, `$1`, `` $` ``, etc.,
440
+ so a version like `1.2.3-$&x` would inject the matched text and corrupt the `VERSION` constant while
441
+ exiting 0. The replacement is now a replacer _function_, whose return value is inserted literally.
442
+ (2) The declaration regex was non-global, so `.replace` silently rewrote the _first_ match; a
443
+ column-0 decoy (e.g. inside a comment) ahead of the real declaration could be edited instead. The
444
+ script now matches globally, asserts exactly one declaration, and exits non-zero loudly otherwise.
445
+ Neither defect is reachable through Changesets today and both previously failed loud rather than
446
+ shipping a lying `VERSION`, so this is hardening, not a fix for an observed break. The
447
+ `format`/`format:check` globs now cover `scripts/**/*.mjs` so the script is prettier-gated in CI (it
448
+ was matched by no glob before). Build tooling only: no runtime or public-API change.
37
449
  - **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
38
450
  `cosyte/.github` pipeline, which requests `contents`/`id-token`/`pull-requests: write`, but declared
39
- no `permissions:` of its own — so it inherited the repo default of `contents: read`. A called
451
+ no `permissions:` of its own, so it inherited the repo default of `contents: read`. A called
40
452
  workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
41
453
  workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
42
454
  way, unnoticed, because a `startup_failure` produces no logs to read. The caller job now declares
43
- the three scopes explicitly. CI-only — no runtime or API change.
455
+ the three scopes explicitly. CI-only: no runtime or API change.
44
456
 
45
457
  - **The `VERSION` export now tracks `package.json`, and the missing `version` script is restored
46
458
  (VERSION-SYNC).** Two latent release bugs, both of which would have bitten at the first publish.
47
459
  (1) `VERSION` was hardcoded `"0.0.0"` in `src/index.ts` while `changeset version` bumps only
48
- `package.json`, so a published `0.0.1` would have shipped an export reading `"0.0.0"` — every
460
+ `package.json`, so a published `0.0.1` would have shipped an export reading `"0.0.0"`. Every
49
461
  consumer asserting on or logging `VERSION` told the wrong version of the parser they were running.
50
462
  New `scripts/sync-version.mjs` rewrites the constant from `package.json` (idempotent; exits
51
463
  non-zero if the declaration is renamed rather than silently no-op'ing). (2) **No `version` script
52
- existed at all** — the shared `cosyte/.github` release workflow drives Changesets with
464
+ existed at all.** The shared `cosyte/.github` release workflow drives Changesets with
53
465
  `version: pnpm run version`, which would have failed with `ERR_PNPM_NO_SCRIPT`, so the "Version
54
466
  Packages" PR could never have been opened. `test/sanity.test.ts` now compares `VERSION` against
55
- `package.json` rather than asserting its shape against a regex — the old test would have stayed
467
+ `package.json` rather than asserting its shape against a regex. The old test would have stayed
56
468
  green through exactly this drift. Ported from `@cosyte/mllp` (MLLP-10); `hl7` is the canonical
57
- form the siblings mirror. No version bumped, nothing published — still `0.0.0`.
469
+ form the siblings mirror. No version bumped, nothing published, still `0.0.0`.
58
470
 
59
471
  - **Value reads no longer double-decode (HL7-VALUE-REDECODE). Behavior change on a rare, spec-legal
60
472
  input (pre-alpha).** The tokenizer already unescapes every subcomponent once on parse, but
@@ -69,13 +481,13 @@ per the cosyte version ladder (`0.0.x` until first alpha).
69
481
 
70
482
  ### Changed
71
483
 
72
- - **Datetime precision + timezone fidelity — the TS/DTM composite (roadmap
484
+ - **Datetime precision + timezone fidelity: the TS/DTM composite (roadmap
73
485
  Phase N). Breaking (pre-alpha).** `field.asTs()` and every helper datetime now
74
486
  return the fidelity `TS` (`DtmParts`: `{ raw, valid, precision, year, month
75
487
  (1–12, spec-native), day, hour, minute, second, fractionalSeconds (verbatim),
76
488
  hasTimezone, offsetMinutes }`) instead of the old `{ raw, date }`. HL7 v2
77
489
  Ch. 2A DTM sets a value's **precision** by how many characters are populated,
78
- and a missing offset defaults to the **sender's** local zone — not UTC. The
490
+ and a missing offset defaults to the **sender's** local zone, not UTC. The
79
491
  prior code zero-filled truncations (`|1970|` → `1970-01-01T00:00:00Z`) and
80
492
  coerced an offset-less value to UTC, which silently shifted a day-only birth
81
493
  date `|19880705|` to the previous calendar day in any negative-offset zone
@@ -87,7 +499,7 @@ hasTimezone, offsetMinutes }`) instead of the old `{ raw, date }`. HL7 v2
87
499
  `patient.dateOfBirth`, `visit.admit/dischargeDateTime`,
88
500
  `observations.observedDateTime` + `TS`/`DT` observation values,
89
501
  `allergies.onsetDate`, `diagnoses.dateTime`, `insurance.effective/expirationDate`,
90
- and `immunizations.administered/expirationDate` — each `Date → TS`. Fidelity
502
+ and `immunizations.administered/expirationDate`: each `Date → TS`. Fidelity
91
503
  only: no localization, timezone conversion, or arithmetic; `HHMM=0000` is never
92
504
  rolled to the previous day. New public exports: `parseDtm`, `formatDtm`,
93
505
  `dtmToDate`, and types `DtmParts` / `DtmPrecision` / `DtmToDateOptions`. **No
@@ -96,7 +508,7 @@ hasTimezone, offsetMinutes }`) instead of the old `{ raw, date }`. HL7 v2
96
508
 
97
509
  ### Removed
98
510
 
99
- - **`parseHl7Timestamp` (and its `ParseHl7TimestampOptions` type) — removed
511
+ - **`parseHl7Timestamp` (and its `ParseHl7TimestampOptions` type), removed
100
512
  (Phase N, breaking).** The Date-returning, UTC-assuming timestamp helper is
101
513
  replaced by the fidelity `parseDtm` + explicit `dtmToDate`, with
102
514
  `parseDtmCascade` covering the MSH-7 user-format fallback path.
@@ -109,7 +521,7 @@ hasTimezone, offsetMinutes }`) instead of the old `{ raw, date }`. HL7 v2
109
521
  `\Z..\`) and hex escapes (`\X41\`) with literal backslashes, so serialize
110
522
  **double-escaped** the former (`\H\` → `\E\H\E\`) and **decoded** the latter
111
523
  (`\X41\` → `A`, and non-canonical hex casing `\X0d\` → `\X0D\`). Emitted output
112
- was spec-clean and lossless-as-text but **not byte-verbatim** — a byte-level
524
+ was spec-clean and lossless-as-text but **not byte-verbatim**. A byte-level
113
525
  MSA-2/MSH-10 correlation (HL7 v2 §2.9.2.2) could fail. The tokenizer now
114
526
  records each affected subcomponent's **original wire bytes** in an internal
115
527
  `RawComponent.rawSubcomponents` overlay and the serializer emits from it
@@ -117,197 +529,197 @@ hasTimezone, offsetMinutes }`) instead of the old `{ raw, date }`. HL7 v2
117
529
  exact escape bytes. `buildAck`'s MSA-2 echo is byte-exact for a
118
530
  default-delimiter sender; a **custom-delimiter** sender is re-delimited
119
531
  spec-cleanly (the overlay is bypassed when the ACK's encoding differs from the
120
- inbound's — the sender's raw bytes would otherwise corrupt MSA-2's structure
121
- under default delimiters — and the decoded id re-parses back correlation-correct
532
+ inbound's, the sender's raw bytes would otherwise corrupt MSA-2's structure
533
+ under default delimiters, and the decoded id re-parses back correlation-correct
122
534
  per §2.9.2.2). **The `\X..\` policy is
123
535
  decode-on-read, re-encode-on-emit** (the decoded value surface is unchanged;
124
- the wire bytes — value and casing — are preserved). Delimiter/newline escapes
536
+ the wire bytes (value and casing) are preserved). Delimiter/newline escapes
125
537
  are unaffected (they already round-trip through `reescape`); a decoded CR still
126
- re-emits as `\X0D\` to protect wire framing. **No public-surface break** —
538
+ re-emits as `\X0D\` to protect wire framing. **No public-surface break.**
127
539
  `Field.value` and every composite/`toJSON` read the same decoded values as
128
540
  before; the overlay is `@internal` and absent from escape-free messages. See
129
541
  `docs-content/spec-notes-escapes.md`.
130
- - **PHI leak in the warning surface — `fieldWhitespaceTrimmed` and `unknownEscapeSequence` no
542
+ - **PHI leak in the warning surface: `fieldWhitespaceTrimmed` and `unknownEscapeSequence` no
131
543
  longer embed field-body content (HL7-TOKENIZE-PHI).** Both builders previously interpolated
132
544
  slices of the field VALUE directly into `Hl7ParseWarning.message`
133
545
  (`fieldWhitespaceTrimmed`'s before/after strings; `unknownEscapeSequence`'s raw escape body, and
134
546
  on an unterminated escape the **entire rest of the field**), so a clinical NTE-3/OBX-5 free-text
135
547
  value (PHI) carrying a stray `\` or surrounding whitespace could surface a payload fragment in
136
- `Hl7Message.warnings` — a leak, since every other warning builder is structural-facts-only.
548
+ `Hl7Message.warnings`, a leak, since every other warning builder is structural-facts-only.
137
549
  `fieldWhitespaceTrimmed`'s signature changed to take leading/trailing **counts** instead of the
138
550
  original/trimmed strings; `unknownEscapeSequence` now reports only the escape body's **length**
139
551
  plus, when the body's first character is a recognized HL7 escape-identifier letter (`F S T R E
140
- X C M Z H N` or a `.`-prefixed formatting escape — structural HL7 grammar, not PHI), that single
141
- letter — never the body text. A new `unterminatedEscapeSequence` factory (still
552
+ X C M Z H N` or a `.`-prefixed formatting escape, structural HL7 grammar, not PHI), that single
553
+ letter, never the body text. A new `unterminatedEscapeSequence` factory (still
142
554
  `UNKNOWN_ESCAPE_SEQUENCE`) covers the unterminated-escape case with no content and no
143
- tail-length. **No change to parse tolerance or round-trip fidelity** — the escape sequence and
555
+ tail-length. **No change to parse tolerance or round-trip fidelity.** The escape sequence and
144
556
  the whitespace are still preserved verbatim in the parsed/emitted output; only the WARNING
145
557
  message lost the body. The `FIELD_WHITESPACE_TRIMMED` / `UNKNOWN_ESCAPE_SEQUENCE` codes are
146
- unchanged and stable (see `test/warning-codes.snapshot.test.ts`) — this is a message-format fix,
558
+ unchanged and stable (see `test/warning-codes.snapshot.test.ts`). This is a message-format fix,
147
559
  not a breaking change to the code registry.
148
- - **`buildAck` now echoes the full inbound MSH-10 field into MSA-2** — the raw
560
+ - **`buildAck` now echoes the full inbound MSH-10 field into MSA-2.** The raw
149
561
  field structure is carried over whole (delimiters and repetitions included)
150
562
  instead of the component-1-only `meta.controlId` scalar. A vendor-quirk
151
563
  control id carrying an unescaped delimiter (`ID^X`) was previously truncated
152
- to `ID` silently, under a confident positive ACK — a sender correlating on
564
+ to `ID` silently, under a confident positive ACK. A sender correlating on
153
565
  the raw MSH-10 bytes (as `@cosyte/mllp`'s client does) would never match the
154
566
  ACK and resend indefinitely (HL7 v2 §2.9.2.2: an MSA-2 mismatch is a
155
567
  correlation failure). The no-correlation fail-safe now keys on the raw field
156
568
  carrying any content, so a leading-delimiter id (`^X`) correlates instead of
157
569
  being spuriously downgraded. The echo is now **byte-verbatim** across the full
158
- escape alphabet (see the escape-fidelity entry below) — the earlier
570
+ escape alphabet (see the escape-fidelity entry below). The earlier
159
571
  canonicalization limits (hex escapes decoding to their byte, preserved escapes
160
572
  re-emitting as escaped literal text) are resolved. The only remaining
161
573
  transform is structural: the ACK emits with default encoding characters and
162
574
  trailing insignificant empties canonicalize (D-02).
163
575
  - **`reescape` now emits a literal CR in decoded content as its `\X0D\` hex
164
576
  escape** instead of passing it through raw. A spec-legal `\X0D\` in any
165
- inbound field decoded to a bare CR — the HL7 segment separator — and
577
+ inbound field decoded to a bare CR (the HL7 segment separator) and
166
578
  re-serializing it corrupted the emitted message's framing (a phantom
167
579
  segment split mid-field, silently). Reachable through `buildAck`'s MSA-2
168
580
  echo among every other emit path; now structurally safe and round-trip
169
581
  stable.
170
- - **`interpretAck` surfaces MSA-2 whole** (read-side symmetry) —
582
+ - **`interpretAck` surfaces MSA-2 whole** (read-side symmetry).
171
583
  `Acknowledgment.controlId` is now the field's canonical wire text
172
584
  (`Field.text`), not its first component.
173
585
 
174
586
  ### Added
175
587
 
176
- - **Scheduling / document / charge breadth helpers — `appointments()`,
588
+ - **Scheduling / document / charge breadth helpers: `appointments()`,
177
589
  `documents()`, `charges()` (roadmap Phase Q, P2).** Three new message-family
178
590
  helpers on `Hl7Message`, each mirroring the existing extractor pattern (typed,
179
591
  frozen, never-throws, not memoized):
180
- - **`appointments()`** over **SIU** (S12/S13/S14/S15/S26) — projects each
592
+ - **`appointments()`** over **SIU** (S12/S13/S14/S15/S26), projects each
181
593
  **SCH** into a typed `Appointment`: placer/filler appointment ids (SCH-1/2),
182
594
  **SCH-25 filler status** (Table 0278, verbatim), **SCH-11** start/end timing
183
595
  (TQ.4/TQ.5, fidelity `TS`), and the **AIS/AIG/AIL/AIP** resource groups that
184
- follow it — surfaced as `AppointmentResource`s tagged `service` / `general` /
596
+ follow it, surfaced as `AppointmentResource`s tagged `service` / `general` /
185
597
  `location` / `personnel` (the personnel/provider resource also carries a
186
598
  typed `XCN`).
187
- - **`documents()`** over **MDM** (T02/T04/T06) — projects each **TXA** into a
599
+ - **`documents()`** over **MDM** (T02/T04/T06), projects each **TXA** into a
188
600
  typed `ClinicalDocument` with the OBX narrative body grouped positionally
189
601
  under it. **Completion status (TXA-17, Table 0271) and availability status
190
- (TXA-19, Table 0273) are surfaced as DISTINCT fields and NEVER conflated** — a
602
+ (TXA-19, Table 0273) are surfaced as DISTINCT fields and NEVER conflated.** A
191
603
  document can be _available_ before it is _authenticated_, and reading a
192
604
  preliminary document as final is the clinical harm this split prevents. Both
193
605
  are verbatim / provenance-only.
194
- - **`charges()`** over **DFT** (P03) — projects each **FT1** into a typed
606
+ - **`charges()`** over **DFT** (P03), projects each **FT1** into a typed
195
607
  `Charge`: FT1-6 transaction type (Table 0017), FT1-7 transaction code (the
196
608
  institution charge code, CWE), FT1-11/12 extended/unit amounts, and the
197
609
  repeating FT1-19 diagnosis linkage. **No billing logic and no
198
- money-as-float** — amounts are surfaced as their **canonical CP wire text**
610
+ money-as-float.** Amounts are surfaced as their **canonical CP wire text**
199
611
  (e.g. `"150.00^USD"`), never parsed to a `number`.
200
612
 
201
613
  New public types: `Appointment`, `AppointmentResource`, `ClinicalDocument`,
202
- `Charge`. Breadth helpers only — not a scheduling-workflow state machine,
614
+ `Charge`. Breadth helpers only: not a scheduling-workflow state machine,
203
615
  signature verification, or a claims/pricing engine (roadmap §9
204
616
  known-limitations). Never throws on malformed input (HELPERS-07); missing
205
617
  fields → omitted keys.
206
618
 
207
- - **NTE narrative grouping — notes attached to their parent by position (roadmap
619
+ - **NTE narrative grouping: notes attached to their parent by position (roadmap
208
620
  Phase P, P1).** NTE (Notes and Comments) segments are now grouped to their
209
621
  parent **by position** and surfaced on the relevant helper output. An NTE
210
622
  inherits its meaning entirely from the segment it immediately follows (HL7 v2
211
- Ch. 2 — no link field), so attachment is positional: in an ORU
623
+ Ch. 2, no link field), so attachment is positional: in an ORU
212
624
  (`{ [ORC] OBR [{NTE}] [{ [OBX] [{NTE}] }] }`, Ch. 7) a note after `OBX` lands on
213
625
  **that result** (`observation.notes`), a note after `OBR`/`ORC` on the **order**
214
- (`order.notes` — ORC notes first, then OBR), and a note after `PID` on the
626
+ (`order.notes`: ORC notes first, then OBR), and a note after `PID` on the
215
627
  **patient** (`msg.patient.notes`). A note with **no recognized preceding
216
- parent** — after `MSH`, or after an unsupported segment like `PV1`/`AL1` — is
217
- surfaced at **message level** via the new **`msg.notes()`**. **Fail-safe —
628
+ parent** (after `MSH`, or after an unsupported segment like `PV1`/`AL1`) is
629
+ surfaced at **message level** via the new **`msg.notes()`**. **Fail-safe,
218
630
  never mis-attached, never dropped:** the parent is the _nearest preceding
219
631
  non-NTE segment_ (consecutive NTEs chain; any intervening non-parent segment
220
- resets the target). Order-level notes mirror the `orders()` state machine —
632
+ resets the target). Order-level notes mirror the `orders()` state machine.
221
633
  notes after an `ORC` are buffered and flushed onto the OBR that opens the
222
634
  order, so **several `ORC`s before one `OBR` all contribute** in document order,
223
635
  not just the last. A note whose recognized parent has **no surfaced
224
- projection** — a **later `PID`** in a multi-patient ORU (patient view is the
225
- first PID), or a **trailing/dangling `ORC`** whose order never opens — is
636
+ projection** (a **later `PID`** in a multi-patient ORU (patient view is the
637
+ first PID), or a **trailing/dangling `ORC`** whose order never opens) is
226
638
  routed to `msg.notes()` rather than vanishing. Each non-empty **NTE-3 (Comment,
227
639
  FT, repeating)** repetition is one note line, with the **full** repetition text
228
640
  reassembled + HL7-unescaped, so a non-conformant raw `^`/`&` (which tokenizes
229
641
  NTE-3 into components) is preserved, not silently truncated. New optional
230
642
  `notes?: readonly string[]` on `Observation`, `Order`, and `Patient` (omitted
231
- when empty, frozen when present). **Additive only — no rename, no removal, no
643
+ when empty, frozen when present). **Additive only: no rename, no removal, no
232
644
  new warning code.** Deferred: NTE-2 (source of comment) / NTE-4 (comment type)
233
645
  interpretation, FT formatting-command rendering, and first-class
234
646
  `patient.notes` on a 2nd+ `PID`'s group. See `docs-content/spec-notes-nte.md`.
235
- - **Character-set / encoding decode — MSH-18 / Table 0211 (roadmap Phase O,
647
+ - **Character-set / encoding decode: MSH-18 / Table 0211 (roadmap Phase O,
236
648
  P1).** `parseHL7` now resolves a `Buffer` input's declared character set from
237
649
  **MSH-18** and decodes the byte stream to text **before** tokenization, through
238
650
  a frozen HL7 **Table-0211 registry** (`resolveCharset` / `canonicalCharset`,
239
- exported). MSH-18 is honoured as a **repeating** field — the **first**
240
- occurrence is the message default, a blank field is 7-bit **ASCII**. Decodes
651
+ exported). MSH-18 is honoured as a **repeating** field. The **first**
652
+ occurrence is the message default; a blank field is 7-bit **ASCII**. Decodes
241
653
  **`8859/1`** via Node's true `latin1` (byte-exact; Node's WHATWG
242
654
  `TextDecoder("iso-8859-1")` is windows-1252 and remaps the C1 range, so it is
243
655
  not used), ASCII / **UTF-8** (`UNICODE UTF-8` / `UNICODE`), and the faithfully
244
656
  decoded **ISO-8859** sets (`8859/2`–`8859/8`, `10`, `13`–`16`); `8859/9` and
245
657
  `8859/11` are **recognized but preserved verbatim** because Node's ICU aliases
246
- them to windows-1254/874 (which remap the C1 range — no faithful decoder). The
658
+ them to windows-1254/874 (which remap the C1 range, no faithful decoder). The
247
659
  multibyte / ISO-2022 East-Asian sets (`ISO IR14/87/159`, GB 18030, KS X 1001,
248
660
  CNS 11643, BIG-5) and UTF-16/32 are likewise **recognized and preserved
249
661
  verbatim** (full stateful decode is a documented non-goal). The §2.7.4 charset-switch escapes (`\Cxxyy\` / `\Mxxyyzz\`) are
250
662
  recognized by the escape layer and preserved. **Fail-safe (never guess, never
251
663
  silently corrupt):** decoding is **strict** (`fatal: true`) except byte-exact
252
664
  `8859/1`, so a byte invalid/undefined for the declared set does **not** become a
253
- silent `U+FFFD` — the bytes are read as a `latin1` 1:1 mapping and a warning
665
+ silent `U+FFFD`. The bytes are read as a `latin1` 1:1 mapping and a warning
254
666
  fires: new **`UNSUPPORTED_CHARSET`** for a recognized set that was not decoded
255
667
  (never-decoded, or a strict-decode failure), existing `UNKNOWN_CHARSET` for a
256
668
  value not in Table 0211. Recoverability is exact for single-byte content;
257
669
  multibyte content is best-effort (a code-unit byte can coincide with a structural
258
- delimiter — documented). **Behaviour change (pre-alpha):** `UNKNOWN_CHARSET` now
670
+ delimiter, documented). **Behaviour change (pre-alpha):** `UNKNOWN_CHARSET` now
259
671
  reads bytes as `latin1` instead of falling back to UTF-8. Both warnings carry the
260
- charset code only — never a field value — so no PHI is exposed. New public warning
672
+ charset code only (never a field value) so no PHI is exposed. New public warning
261
673
  code `UNSUPPORTED_CHARSET` (`WARNING_CODES` 18 → 19). See
262
674
  `docs-content/spec-notes-charset.md`.
263
675
 
264
- - **Order / medication timing — `order.timings` + `med.timings` (roadmap
676
+ - **Order / medication timing: `order.timings` + `med.timings` (roadmap
265
677
  Phase M).** `orders()` and `medications()` now surface the timing **structure**
266
678
  of an order/medication as a typed `OrderTiming[]`, read from the `TQ1` segment
267
679
  (HL7 v2.5+, Ch. 4 §4.5.4) or the **legacy embedded TQ** data type in `ORC-7` /
268
680
  `RXE-1` (pre-v2.5, Ch. 2A §2.A.81, detail withdrawn as of v2.7). Modelled:
269
681
  TQ1-2 quantity (CQ), **TQ1-3 repeat pattern (RPT, Table 0335)**, TQ1-4 explicit
270
682
  time, TQ1-6 service duration, TQ1-7/-8 start/end (DTM → the Phase N fidelity
271
- `TS`), TQ1-9 priority (CWE), and **TQ1-14 total occurrences (NM) — not TQ1-11**
683
+ `TS`), TQ1-9 priority (CWE), and **TQ1-14 total occurrences (NM), not TQ1-11**
272
684
  (the Text Instruction). The repeat pattern is surfaced **verbatim**
273
- (`repeatPattern.code`) — never normalized, resolved to clock times, or mapped
685
+ (`repeatPattern.code`), never normalized, resolved to clock times, or mapped
274
686
  to a different frequency (reading `Q6H` as "daily", or losing a `BID`, changes
275
687
  the administered dose count). A provenance-only `kind`
276
688
  (`parametric`/`named`/`unknown`) never drives a schedule; a `parametric`
277
689
  `Q<integer><unit>` template surfaces its **load-bearing integer** on
278
690
  `repeatPattern.interval`, never dropped. `TQ1` vs the legacy embedded TQ is
279
- chosen **by presence** (`source: "TQ1" | "legacy"`) — the legacy field is read
691
+ chosen **by presence** (`source: "TQ1" | "legacy"`). The legacy field is read
280
692
  only when no `TQ1` accompanies the group, so the same timing is never
281
693
  double-counted and a legacy-only timing is never dropped (the legacy source is
282
694
  `ORC-7` for an order, `RXE-1` or the preceding `ORC`'s `ORC-7` for a
283
- medication). Timings group positionally — a `TQ1` may sit either side of the
695
+ medication). Timings group positionally. A `TQ1` may sit either side of the
284
696
  order detail, an intervening `ORC` re-scopes a following `TQ1` to the next
285
697
  order, and multiple `TQ1` segments each surface (a tapering schedule). New
286
698
  public types `OrderTiming`, `RepeatPattern`,
287
699
  `RepeatPatternKind`, `TimingQuantity`; `Order` / `Medication` gain an
288
- always-present `timings` array. **Additive only** — no rename, no removal, **no
700
+ always-present `timings` array. **Additive only:** no rename, no removal, **no
289
701
  new warning code**. hl7 surfaces the timing structure only; it does not compute
290
702
  schedules, resolve "institution-specified times", or interpret sig. Non-goals:
291
703
  `TQ2` beyond segment recognition, schedule computation, 2nd+ repetitions of
292
704
  repeating timing fields. Never throws (HELPERS-07). See
293
705
  `docs-content/spec-notes-timing.md`.
294
706
 
295
- - **`splitBatch()` — batch / file envelope splitting (roadmap Phase L).**
707
+ - **`splitBatch()`: batch / file envelope splitting (roadmap Phase L).**
296
708
  Demarcates the individual `MSH`-led messages inside an HL7 v2 batch/file
297
709
  stream (`[FHS] { [BHS] { MSH… } [BTS] } [FTS]`, HL7 v2 Ch. 2 §2.10.3) and
298
- hands each one back parsed — either `{ ok: true, message }` or, for a message
710
+ hands each one back parsed: either `{ ok: true, message }` or, for a message
299
711
  that trips a Tier-3 fatal, `{ ok: false, error }`. A **malformed message
300
712
  mid-batch is isolated**, never suppressing its siblings; a **bare single
301
713
  message** (no envelope) passes straight through. Declared counts are
302
- reconciled — **BTS-1** (batch message count) against the messages in the
303
- batch, **FTS-1** (file batch count) against the batches in the file — and a
714
+ reconciled: **BTS-1** (batch message count) against the messages in the
715
+ batch, **FTS-1** (file batch count) against the batches in the file, and a
304
716
  mismatch surfaces the new Tier-2 `BATCH_COUNT_MISMATCH` warning **without ever
305
717
  dropping the tail**; an absent (`[0..1]`) or non-numeric count tolerantly
306
718
  disables reconciliation. A `BHS`/`FHS` header with no matching `BTS`/`FTS`
307
719
  raises the new `BATCH_MISSING_TRAILER` warning (`splitBatch` warns, never
308
- rejects — enforcing a mandatory envelope is the caller's call). Batch-level
720
+ rejects: enforcing a mandatory envelope is the caller's call). Batch-level
309
721
  warnings live on the returned `BatchSplitResult.warnings` (never on
310
- `Hl7Message.warnings`) and carry **counts / segment names / positions only —
722
+ `Hl7Message.warnings`) and carry **counts / segment names / positions only,
311
723
  no PHI**. A `Buffer` stream round-trips through `latin1` so each message's own
312
724
  MSH-18 charset resolution runs on its original bytes. Kept a **separate
313
725
  surface** from `parseHL7` (which rejects non-`MSH`-first input by design).
@@ -315,7 +727,7 @@ X C M Z H N` or a `.`-prefixed formatting escape — structural HL7 grammar, not
315
727
  `batchMissingTrailer` factories, and types `Batch` / `BatchSplitResult` /
316
728
  `BatchMessageEntry` / `BatchEnvelopeSegment` / `BatchEnvelopeName`. **Two
317
729
  additive Tier-2 warning codes** (`BATCH_COUNT_MISMATCH`,
318
- `BATCH_MISSING_TRAILER`) — the stable warning-code contract grows 16 → 18.
730
+ `BATCH_MISSING_TRAILER`). The stable warning-code contract grows 16 → 18.
319
731
  `batches` holds each message run delimited by a `BHS` and/or a `BTS` (a run
320
732
  with neither is still yielded in `messages`, wrapped in no batch); FTS-1
321
733
  reconciles **per-file** so concatenated files raise no false mismatch. Split-only: no
@@ -323,34 +735,34 @@ X C M Z H N` or a `.`-prefixed formatting escape — structural HL7 grammar, not
323
735
  reconciled, no transport de-framing (that is `@cosyte/mllp`). Two structural
324
736
  limits are documented: reserved envelope names are matched by name (a body
325
737
  literally containing `FHS`/`BHS`/`BTS`/`FTS` is mis-split, its tail surfaced
326
- as a `NO_MSH_SEGMENT` failure — never silently dropped), and one file envelope
738
+ as a `NO_MSH_SEGMENT` failure, never silently dropped), and one file envelope
327
739
  per stream is the modelled case. See `docs-content/spec-notes-batch.md`.
328
- - **`identityEvents()` — patient-identity / merge events (roadmap Phase K, P0
740
+ - **`identityEvents()`: patient-identity / merge events (roadmap Phase K, P0
329
741
  safety).** `msg.identityEvents()` recognizes the ADT identity-management
330
- trigger family — merges (A18/A34/A35/A36/A39/A40/A41/A42), moves (A43/A44),
331
- link/unlink (A24/A37), person add/update (A28/A31) — and surfaces every
742
+ trigger family: merges (A18/A34/A35/A36/A39/A40/A41/A42), moves (A43/A44),
743
+ link/unlink (A24/A37), person add/update (A28/A31). It surfaces every
332
744
  party **labelled by role with segment provenance**: the `surviving` party is
333
745
  only ever sourced from PID/PV1, the `prior` (non-surviving) party only ever
334
746
  from MRG, and the merge direction is the spec constant `MRG_TO_PID` (HL7 v2
335
- Ch. 3, A18: PID carries the surviving information, MRG the non-surviving) —
747
+ Ch. 3, A18: PID carries the surviving information, MRG the non-surviving),
336
748
  never inferred from content. Repeating PID+MRG groups yield one event each.
337
- Fail-safe: an incomplete MRG→PID pair (no MRG, an orphaned MRG — which is
338
- never dropped — or a PID with no surviving identifier) surfaces what is
749
+ Fail-safe: an incomplete MRG→PID pair (no MRG, an orphaned MRG (which is
750
+ never dropped), or a PID with no surviving identifier) surfaces what is
339
751
  present plus a new event-scoped Tier-2
340
752
  **`MERGE_MISSING_PRIOR_OR_SURVIVOR`** warning carrying structural facts
341
- only (never an identifier or name — no PHI). The MRG field map is
753
+ only (never an identifier or name, no PHI). The MRG field map is
342
754
  **version-scoped**: the backward-compat single-ID fields (MRG-4 / PID-2,
343
755
  withdrawn as of v2.7) are not read when MSH-12 declares v2.7+. Applying the
344
756
  merge (re-pointing data) stays the consumer's job. New public surface:
345
757
  `Hl7Message.identityEvents()`, types `IdentityEvent` / `IdentityParty` /
346
758
  `IdentityEventKind` / `IdentityRole`, the `mergeMissingPriorOrSurvivor`
347
759
  factory, and the `MERGE_MISSING_PRIOR_OR_SURVIVOR` code. Additive only.
348
- - **`MRG` is now a known segment** — parsing a merge message no longer emits
760
+ - **`MRG` is now a known segment.** Parsing a merge message no longer emits
349
761
  a spurious `UNKNOWN_SEGMENT` warning for MRG.
350
- - **`Field.text`** — the field's canonical wire text (full repetitions/
762
+ - **`Field.text`**: the field's canonical wire text (full repetitions/
351
763
  components/subcomponents re-serialized with the active delimiters), the
352
764
  whole-field counterpart to the component-1-only `Field.value`.
353
- - **`downgradePositiveAck(code)`** — the single upstream source of truth for
765
+ - **`downgradePositiveAck(code)`**: the single upstream source of truth for
354
766
  the fail-safe downgrade pair (`AA`→`AE`, `CA`→`CE`; everything else passes
355
767
  through), now exported so `@cosyte/mllp`'s `ack-from-hl7` adapter reuses it
356
768
  instead of carrying a divergent copy. **`isPositiveAck`** is exported
@@ -361,17 +773,17 @@ X C M Z H N` or a `.`-prefixed formatting escape — structural HL7 grammar, not
361
773
  - **Adopted the documentation IA spine in `docs-content/`** (umbrella DOCS-D5; reference
362
774
  exemplar for the suite). The three `spec-notes-*.md` files (coding-system provenance,
363
775
  message-type & structure awareness, version-sensitivity matrix) are now grouped under a
364
- canonical **Core Concepts** category on `docs.cosyte.com` — they previously existed in
776
+ canonical **Core Concepts** category on `docs.cosyte.com`. They previously existed in
365
777
  `docs-content/` but were not referenced from `sidebars.json` and so were unreachable from
366
778
  any rendered route. Each now carries minimal frontmatter (`id` / `title` /
367
779
  `sidebar_label`) so the sidebar text stays concise; the prose is unchanged. Also
368
780
  converted the two CommonMark autolinks in `spec-notes-coding-system.md`
369
- (`<https://…>`) to Docusaurus-MDX-safe `[text](url)` form — autolinks parse as JSX in
370
- MDX 3, which silently broke the link rendering. Doc-only — no runtime/API change.
781
+ (`<https://…>`) to Docusaurus-MDX-safe `[text](url)` form. Autolinks parse as JSX in
782
+ MDX 3, which silently broke the link rendering. Doc-only: no runtime/API change.
371
783
 
372
784
  - **Added a standard-reference primer (`spec-notes-primer.md`), first under Core Concepts.**
373
785
  The other `spec-notes-*` pages are implementation notes (what the parser does about one
374
- spec area); the primer is the spec-grounded companion they elaborate — the v2.x encoding
786
+ spec area); the primer is the spec-grounded companion they elaborate: the v2.x encoding
375
787
  model (MSH-1/MSH-2 delimiter bootstrap, the fixed `CR` terminator, the
376
788
  field→component→subcomponent hierarchy where only fields repeat), the three field
377
789
  population states (populated / not-populated / **null** `|""|`), the abstract message
@@ -381,15 +793,15 @@ X C M Z H N` or a `.`-prefixed formatting escape — structural HL7 grammar, not
381
793
  vs. `AA/AE/AR`), and the `CX`/`XPN`/`XAD`/`HD` composite layouts with their version deltas.
382
794
  Cross-links the phase notes for areas they already cover rather than duplicating them, and
383
795
  records its provenance (primary Chapter 2 / 2.A text, adversarially fact-checked; the
384
- official HL7 spec is the text of record). Doc-only — no runtime/API change.
796
+ official HL7 spec is the text of record). Doc-only: no runtime/API change.
385
797
 
386
798
  ### Security
387
799
 
388
- - **Dev-dependency advisory remediation (no runtime impact — `@cosyte/hl7`
800
+ - **Dev-dependency advisory remediation (no runtime impact, `@cosyte/hl7`
389
801
  ships zero runtime dependencies, so the published artifact is unchanged).**
390
802
  Added scoped `pnpm.overrides` pinning two transitive **dev/build-time**
391
803
  packages to their patched releases: `esbuild` (`>=0.27.3 <0.28.1` →
392
- `0.28.1`; GHSA dev-server path-traversal — not reachable here: the library
804
+ `0.28.1`; GHSA dev-server path-traversal, not reachable here: the library
393
805
  builds via `tsup`/`vitest` and never runs `esbuild serve`) and the
394
806
  `@changesets/parse` copy of `js-yaml` (`>=4.0.0 <4.2.0` → `4.2.0`;
395
807
  GHSA-h67p-54hq-rp68 merge-key DoS). The `js-yaml@3.14.2` pulled by
@@ -406,28 +818,28 @@ X C M Z H N` or a `.`-prefixed formatting escape — structural HL7 grammar, not
406
818
  `typescript-eslint`, Vitest 4, `@types/node` 22, exact-pinned dev tools, the shared
407
819
  `@cosyte/tsup-config` / `@cosyte/vitest-config`, and thin callers of the reusable `cosyte/.github`
408
820
  CI/release workflows. No public API change.
409
- - **Test bar** — added executable PHI-safety property tests
821
+ - **Test bar**: added executable PHI-safety property tests
410
822
  (`test/property/phi-safety.property.test.ts`). Locks two invariants: warning messages never echo
411
823
  field VALUES (only positional context + bounded metadata), and `Hl7ParseError.snippet` length
412
824
  stays ≤ 41 chars (40 + ellipsis) for adversarially-large inputs. Snippet **content** may carry
413
- PHI by design (see `parser/errors.ts:70-72` — the documented consumer-redaction boundary); the
414
- bound is what we lock in. Does **not** use `@cosyte/test-utils`' `assertNoSecretLeak` — that's
825
+ PHI by design (see `parser/errors.ts:70-72`, the documented consumer-redaction boundary); the
826
+ bound is what we lock in. Does **not** use `@cosyte/test-utils`' `assertNoSecretLeak`. That's
415
827
  for `Secret<T>` wrappers (the pathways credentials pattern) and is the wrong shape for parser-
416
828
  side PHI surfaces.
417
- - **Coverage policy — `src/profiles/**`coverage hole closed, global`branches`floor restored to
829
+ - **Coverage policy: `src/profiles/**`coverage hole closed, global`branches`floor restored to
418
830
  90 (roadmap Phase J, supersedes/absorbs the`H-PHI` coverage-policy entry above).** The D10
419
831
  transient relaxation is now RESOLVED: targeted tests
420
832
  (`test/profiles-merge-validate-coverage.test.ts`) closed the `mergeCustomSegments`/inheritance
421
833
  branches in `profiles/merge.ts`and the validator branches in`profiles/validate.ts`+`profiles/describe.ts`that were unreachable via the public`defineProfile()`API alone.`src/profiles/\*\*`now carries its own`>= 90`per-directory coverage gate
422
834
  (lines/branches/functions/statements) in`vitest.config.ts`, and the global `branches` floor is
423
- back to the canonical 90 — no remaining relaxation. One line
835
+ back to the canonical 90, no remaining relaxation. One line
424
836
  (`validate.ts`'s `validateUniqueFieldNames`throw branch) stays intentionally uncovered and is
425
837
  documented as provably unreachable by any legally-typed`Record<string, number>`(proven: no
426
- real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-named enumerable own
838
+ real JS object (literal,`Map`, `Proxy`, or otherwise) can hold two same-named enumerable own
427
839
  keys), matching the module's own defense-in-depth JSDoc.
428
- - **Reference test bar — fuzz target, PHI-exec surfaces, and a differential harness (roadmap Phase
840
+ - **Reference test bar: fuzz target, PHI-exec surfaces, and a differential harness (roadmap Phase
429
841
  J, "finish the reference test bar").** Three additions, no behavior change:
430
- - `test/property/fuzz.property.test.ts` — a dedicated high-run-count (1000 runs/property, fixed
842
+ - `test/property/fuzz.property.test.ts`: a dedicated high-run-count (1000 runs/property, fixed
431
843
  seed) fuzz harness on top of the existing `lenient.property.test.ts` invariant
432
844
  ("`parseHL7` either returns an `Hl7Message` or throws an `Hl7ParseError` with one of the 4
433
845
  `FATAL_CODES`, never anything else"). Adds two generator families the existing suite didn't
@@ -436,48 +848,48 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
436
848
  snapshots `FATAL_CODES`/`WARNING_CODES` registry membership and asserts a "survivor
437
849
  round-trip" property (anything that parses can be `toString()`'d and re-parsed without
438
850
  throwing).
439
- - `test/property/phi-safety-surfaces.property.test.ts` — extends
851
+ - `test/property/phi-safety-surfaces.property.test.ts`: extends
440
852
  `phi-safety.property.test.ts` (which covers the WARNING surface) to the FATAL error-message
441
853
  surface: over PHI-shaped field values that trigger each reachable fatal code, asserts
442
854
  `Hl7ParseError.message` carries only structural/positional facts, never an echoed field value.
443
855
  Separately asserts the documented `errors.ts` snippet contract (bounded length, not
444
- value-absence — snippets MAY carry PHI by design, redaction is a consumer responsibility) holds
856
+ value-absence, snippets MAY carry PHI by design, redaction is a consumer responsibility) holds
445
857
  for BOTH the direct-fatal snippet path and the strict-mode-escalation snippet path
446
858
  (`index.ts::buildSnippet`, a distinct call site). Also documents, with a round-trip test, that
447
859
  `toString()`/`toJSON()`/`prettyPrint()` legitimately contain field values (content surfaces,
448
860
  not diagnostic/log surfaces) so they're never mistaken for a leak path.
449
- - `test/differential/differential.test.ts` — an oracle-gated differential harness comparing
861
+ - `test/differential/differential.test.ts`: an oracle-gated differential harness comparing
450
862
  `@cosyte/hl7` against the external **python-hl7** parser (BSD license, PyPI package `hl7`) over
451
863
  the full canonical corpus (27 fixtures): segment count + per-segment field-text parity. Skips
452
864
  gracefully (`it.skip`) when no Python + `hl7` package is available, so `verify.sh`/CI stay green
453
865
  without Python. One real, honest divergence was found and documented: `@cosyte/hl7`'s D-02
454
866
  trailing-empty-component canonicalization (`Field.text` strips trailing empty
455
- components/subcomponents; python-hl7's `str()` preserves them verbatim) — see
867
+ components/subcomponents; python-hl7's `str()` preserves them verbatim). See
456
868
  `docs-content/spec-notes-differential.md` for the full writeup, licensing bound, and other
457
869
  plausible-but-unobserved divergence classes.
458
870
 
459
871
  ### Fixed
460
872
 
461
- - **Conformance — v2.7+ truncation char no longer rejects spec-conformant input** (roadmap Phase A,
873
+ - **Conformance: v2.7+ truncation char no longer rejects spec-conformant input** (roadmap Phase A,
462
874
  P0 correctness). `readDelimiters` previously hard-coded MSH-2 length to 4 and threw the Tier-3
463
875
  fatal `INVALID_ENCODING_CHARACTERS` on a 5-char MSH-2, so a spec-valid v2.7+ message carrying the
464
- truncation character (`^~\&#` and friends) was rejected outright — a fail-unsafe rejection of
876
+ truncation character (`^~\&#` and friends) was rejected outright, a fail-unsafe rejection of
465
877
  valid input. The parser now accepts both shapes: 4-char (v2.1–v2.6) and 5-char (v2.7+, spec
466
- §2.5.5.2 — the 5th char is the truncation character, default `#`). The `EncodingCharacters` type
878
+ §2.5.5.2, the 5th char is the truncation character, default `#`). The `EncodingCharacters` type
467
879
  gains a new optional `truncation?: string` field that is set ONLY when MSH-2 actually declared
468
880
  one, so messages that predate v2.7 round-trip with a 4-char MSH-2 unchanged. The serializer +
469
881
  builder emit the 5th char back when present; pre-v2.7 messages are unaffected.
470
- - **Conformance — standard escape sequences no longer warn as `UNKNOWN_ESCAPE_SEQUENCE`** (roadmap
882
+ - **Conformance: standard escape sequences no longer warn as `UNKNOWN_ESCAPE_SEQUENCE`** (roadmap
471
883
  Phase A). Six spec-defined escape families are now recognized:
472
- - `\P\` — truncation character. **Decoded** to `enc.truncation ?? "#"` (spec §2.5.5.2). On
884
+ - `\P\`: truncation character. **Decoded** to `enc.truncation ?? "#"` (spec §2.5.5.2). On
473
885
  serialize, the truncation character is re-escaped back to `\P\` ONLY when MSH-2 declared one,
474
886
  so pre-v2.7 messages round-trip the character literally.
475
- - `\H\` / `\N\` — highlight on / off (spec §2.7.1). **Recognized but preserved verbatim** — the
887
+ - `\H\` / `\N\`: highlight on / off (spec §2.7.1). **Recognized but preserved verbatim.** The
476
888
  parser does not pick a presentational policy; the markers stay in the decoded string for a
477
889
  downstream renderer to consume.
478
- - `\.sp\`, `\.in\`, `\.ti\`, `\.fi\`, `\.nf\`, `\.ce\` — formatting commands (spec §2.7.6).
890
+ - `\.sp\`, `\.in\`, `\.ti\`, `\.fi\`, `\.nf\`, `\.ce\`: formatting commands (spec §2.7.6).
479
891
  Recognized and preserved verbatim, same rationale as highlight.
480
- - `\Cxxyy\` (single-byte) and `\Mxxyyzz\` (multi-byte, 4 or 6 hex) — character-set switches
892
+ - `\Cxxyy\` (single-byte) and `\Mxxyyzz\` (multi-byte, 4 or 6 hex): character-set switches
481
893
  (spec §2.7.4). Recognized and preserved verbatim because byte-accurate decoding requires
482
894
  charset state this module does not own.
483
895
 
@@ -487,26 +899,26 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
487
899
  `escape-highlight.hl7`, `escape-formatting.hl7`) lock the behavior, including byte-exact
488
900
  round-trip through `toString()`.
489
901
 
490
- - **Conformance — SN (Structured Numeric) results no longer silently drop the comparator/range**
902
+ - **Conformance: SN (Structured Numeric) results no longer silently drop the comparator/range**
491
903
  (roadmap Phase B, P0 safety). An `OBX-2 = SN` value (e.g. `<^10`, `>^90`, `^100^-^200`, `^1^:^128`)
492
- previously fell through the plain-string branch, where `<^10` collapsed to the bare string `"<"` —
904
+ previously fell through the plain-string branch, where `<^10` collapsed to the bare string `"<"`,
493
905
  a misread clinical result with a documented patient-harm path (a "less-than 10" result reading as
494
906
  the operator alone). `msg.observations()` now dispatches `SN` to a typed `SN` value
495
907
  (`comparator` / `num1` / `separatorOrSuffix` / `num2`). Fail-safe by construction: `num1`/`num2` are
496
908
  strict-`Number()` parsed (`undefined`, **never `NaN`**), and the comparator is surfaced ONLY when
497
- SN.1 is a recognized operator (`>` `<` `>=` `<=` `=` `<>`) — a non-operator in the comparator slot
909
+ SN.1 is a recognized operator (`>` `<` `>=` `<=` `=` `<>`). A non-operator in the comparator slot
498
910
  is never passed off as a real relation. The comparator is preserved byte-for-byte across a
499
911
  serialize → parse round-trip. New canonical fixture `test/fixtures/canonical/oru-r01-sn-results.hl7`
500
912
  and property tests (`test/property/sn.property.test.ts`) lock the invariant over thousands of
501
913
  generated values.
502
914
 
503
- - **Types resolution from CommonJS** — the `exports` map now points the `require` condition's types at
915
+ - **Types resolution from CommonJS**: the `exports` map now points the `require` condition's types at
504
916
  `dist/index.d.cts` (was `index.d.ts`), fixing a "masquerading as ESM" (`attw` FalseESM) issue for
505
917
  CJS consumers.
506
918
 
507
919
  ### Added
508
920
 
509
- - **Version-sensitivity hardening** (roadmap Phase H, P1) — the coded-element
921
+ - **Version-sensitivity hardening** (roadmap Phase H, P1). The coded-element
510
922
  composites are now robust to **append-only component growth** across HL7
511
923
  v2.1–v2.8, with **no silent truncation**. `CWE` and `CE` gained an optional
512
924
  `extraComponents: readonly string[]` that preserves any components beyond the
@@ -515,7 +927,7 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
515
927
  7+**. An absent interior component is held as `""` so `extraComponents[i]`
516
928
  maps back to its HL7 component number; trailing empties are stripped and the
517
929
  key is omitted when there is nothing past the modeled set. This also unifies
518
- **CE↔CWE** reading — because each accessor preserves the other's extra
930
+ **CE↔CWE** reading. Because each accessor preserves the other's extra
519
931
  components, reading a CWE-shaped value through `asCe()` is no longer lossy (CE
520
932
  was deprecated at v2.5, withdrawn at v2.6 in favor of CWE). Parsing a coded
521
933
  element with an arbitrary number of trailing components **never throws**
@@ -524,9 +936,9 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
524
936
  test (no-loss / no-throw / CE↔CWE uniformity) and the supported-version matrix
525
937
  in `docs-content/spec-notes-version-matrix.md` (incl. the **TS→DTM**
526
938
  supersession and the **MSH-21** Conformance Statement ID → Message Profile
527
- Identifier rename at v2.5). Additive only — `extraComponents` is a new optional
939
+ Identifier rename at v2.5). Additive only: `extraComponents` is a new optional
528
940
  field; no rename, no removal, no new warning code.
529
- - **Message-type & structure awareness** (roadmap Phase G, P1) — a conservative
941
+ - **Message-type & structure awareness** (roadmap Phase G, P1). A conservative
530
942
  **misroute / truncation safety net**. `msg.structure` reports, for the common
531
943
  message types, whether the core segment groups the HL7 v2.5.1 abstract syntax
532
944
  marks **Required** for that trigger event are present; the parser also emits a
@@ -534,19 +946,19 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
534
946
  an `ORU^R01` with no `OBR`/`OBX` result group). Keys on the **trigger event**,
535
947
  not the message family, and models **Required anchors only** (EVN in ADT, PID
536
948
  in ORU/SIU, OBR in OML/OMG/OMI, RXA in VXU are deliberately excluded) so a
537
- conformant-but-sparse message never warns — every well-formed canonical
949
+ conformant-but-sparse message never warns. Every well-formed canonical
538
950
  fixture emits zero structural warnings. Recognized types: ADT
539
951
  (A01/A02/A03/A04/A05/A08/A11/A13), ORU^R01, ORM^O01, OML^O21, OMG^O19,
540
952
  OMP^O09, OMI^O23, SIU (S12–S26), MDM (T02/T06), DFT^P03, VXU^V04, ACK; an
541
953
  unrecognized type yields `recognized: false` and emits nothing. Warning-only
542
- (Tier-2) — lenient parse never throws, `strict` may promote; the message
954
+ (Tier-2). Lenient parse never throws, `strict` may promote; the message
543
955
  carries only structural facts (type, group, anchor names), never a field
544
956
  value (no PHI). New public surface: `Hl7Message.structure`, the
545
957
  `missingExpectedGroup` warning factory, the `MISSING_EXPECTED_GROUP` code, the
546
958
  read-only `MESSAGE_STRUCTURE_DEFINITIONS` registry, `analyzeMessageStructure`,
547
959
  and types `MessageStructure`, `StructureGroup`, `ExpectedSegmentGroup`,
548
960
  `MessageStructureDefinition` (see `docs-content/spec-notes-structure.md`).
549
- - **Coding-system provenance** (roadmap Phase F, P1) — `codingSystem(id)`,
961
+ - **Coding-system provenance** (roadmap Phase F, P1). `codingSystem(id)`,
550
962
  `codingSystemOf(coded)`, and `alternateCodingSystemOf(coded)` answer "what
551
963
  system does this code CLAIM?" off a `CWE` / `CE` (CWE.3 / CE.3 primary,
552
964
  CWE.6 / CE.6 alternate). Alias-normalized + case-insensitive
@@ -555,17 +967,17 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
555
967
  surfaced verbatim with `known: false` (never dropped, never guessed); a
556
968
  no-claim input returns `undefined`. The recognized subset is the frozen,
557
969
  read-only `KNOWN_CODING_SYSTEMS`: `LN`, `SCT`, `I10`, `I10P`, `RXN`, `NDC`,
558
- `CVX`, `MVX`, `UCUM`. **Provenance only** — no validation, lookup, network,
970
+ `CVX`, `MVX`, `UCUM`. **Provenance only**: no validation, lookup, network,
559
971
  or bundled codeset. `I10` reports the registered Table 0396 claim `ICD-10`
560
972
  (the WHO base), NOT a guessed `ICD-10-CM` (see
561
973
  `docs-content/spec-notes-coding-system.md`). New public types
562
974
  `KnownCodingSystem`, `CodingSystemInfo`, `CodedSystemFields`.
563
- - **Parser** — `parseHL7(raw, optionsOrProfile?)` with a lenient default
975
+ - **Parser**: `parseHL7(raw, optionsOrProfile?)` with a lenient default
564
976
  parser that handles vendor-quirky HL7 v2.1–v2.8 input, and a
565
977
  `{ strict: true }` mode that escalates every Tier-2 deviation to a thrown
566
978
  `Hl7ParseError`. Accepts `string` or `Buffer` input; honours MSH-18
567
979
  character set with a user `charset` override option.
568
- - **Warning system** — 14 stable Tier-2 warning codes with positional
980
+ - **Warning system**: 14 stable Tier-2 warning codes with positional
569
981
  context (`segmentIndex`, `fieldIndex`, `repetitionIndex`, `componentIndex`,
570
982
  `subcomponentIndex`): `MLLP_FRAMING_STRIPPED`, `FIELD_WHITESPACE_TRIMMED`,
571
983
  `UNKNOWN_ESCAPE_SEQUENCE`, `TIMESTAMP_FALLBACK_FORMAT`, `SEGMENT_CASE`,
@@ -573,60 +985,60 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
573
985
  `ENCODING_MISMATCH`, `MISSING_REQUIRED_FIELD`, `OUT_OF_ORDER_SEGMENT`,
574
986
  `VERSION_MISMATCH`, `UNKNOWN_CHARSET`, `ACK_NO_CORRELATION_ID`. Exposed via
575
987
  `msg.warnings` and the `onWarning` callback.
576
- - **Fatal errors** — 4 Tier-3 fatal codes always thrown as `Hl7ParseError`
988
+ - **Fatal errors**: 4 Tier-3 fatal codes always thrown as `Hl7ParseError`
577
989
  (even in lenient mode): `NO_MSH_SEGMENT`, `MSH_TOO_SHORT`,
578
990
  `INVALID_ENCODING_CHARACTERS`, `EMPTY_INPUT`. Each error carries
579
991
  `message`, `position`, and `snippet`.
580
- - **Structural model** — immutable `Hl7Message` with dot-path access
992
+ - **Structural model**: immutable `Hl7Message` with dot-path access
581
993
  (`msg.get("PID.5.1")`, `msg.get("OBX[2].5")`), typed `Segment` and
582
994
  `Field` wrappers, and `msg.segments("OBX")[0].field(3)` traversal.
583
995
  Safe-access semantics (`undefined` / `[]` for missing paths, never
584
996
  throws).
585
- - **Composite types** — parsed instances and exported TypeScript
997
+ - **Composite types**: parsed instances and exported TypeScript
586
998
  interfaces for XPN, XAD, CX, CWE, CE, XTN, PL, TS/DTM, NM, SN, HD, and XCN
587
999
  (12 types). Also available under the `HL7` namespace:
588
1000
  `import { HL7 } from "@cosyte/hl7"; type T = HL7.XPN`.
589
- - **`SN` composite + `Field.asSn()`** (roadmap Phase B) — `parseSn` /
1001
+ - **`SN` composite + `Field.asSn()`** (roadmap Phase B). `parseSn` /
590
1002
  `SN` / `HL7.SN` export the Structured Numeric datatype (comparator,
591
1003
  num1, separator/suffix, num2), and `field.asSn()` coerces an OBX-5 to it.
592
1004
  `msg.observations()` returns `{ valueType: "SN", value: SN | undefined }`
593
1005
  for `OBX-2 = SN`.
594
- - **`observation.unitsAreUcum`** (roadmap Phase B) — a boolean claim-check
1006
+ - **`observation.unitsAreUcum`** (roadmap Phase B). A boolean claim-check
595
1007
  flag, `true` iff OBX-6's coding system (CWE.3) is exactly `UCUM`
596
1008
  (HL7 Table 0396). `false` when a unit is present but not declared UCUM
597
1009
  (surfaced as-is, never coerced); omitted entirely when OBX-6 is absent.
598
- This is a claim check only — the library does not validate UCUM grammar
1010
+ This is a claim check only. The library does not validate UCUM grammar
599
1011
  or convert units.
600
- - **Named helpers** — one-line extraction for the most common HL7
1012
+ - **Named helpers**. One-line extraction for the most common HL7
601
1013
  fields: `msg.meta`, `msg.patient`, `msg.visit`, `msg.observations()`,
602
1014
  `msg.orders()`, `msg.nextOfKin()`, `msg.allergies()`, `msg.diagnoses()`,
603
1015
  `msg.insurance()`, `msg.medications()`, `msg.immunizations()`. All helpers
604
- return `undefined` / empty arrays for missing optional data — they never throw.
605
- - **`msg.medications()`** (roadmap Phase D, P0 safety) — projects every
1016
+ return `undefined` / empty arrays for missing optional data. They never throw.
1017
+ - **`msg.medications()`** (roadmap Phase D, P0 safety). Projects every
606
1018
  RXO / RXE / RXD / RXA segment into a typed `Medication` across the four
607
1019
  pharmacy contexts (`order` / `encoded` / `dispense` / `administration`),
608
1020
  grouping the RXR (route, Table 0162 + site Table 0163) and RXC
609
- (component) segments that follow each parent positionally — the same
1021
+ (component) segments that follow each parent positionally, the same
610
1022
  state-machine `orders()` uses for OBR → OBX. The give code carries its
611
1023
  own coding-system provenance (`giveCode.nameOfCodingSystem`); the give
612
1024
  **amount** (how much) and the give **strength** (concentration, RXE-25/26)
613
- are surfaced as **separate fields and are never reconciled** — a strength
1025
+ are surfaced as **separate fields and are never reconciled**. A strength
614
1026
  a coded drug (e.g. an NDC) implies never validates or overwrites the
615
1027
  explicit RXE-25 strength, so a disagreement is preserved for the caller.
616
1028
  Numerics are strict-`asNm()` parsed (absent/blank → key omitted, never
617
1029
  `NaN`); output is frozen; not memoized. Never throws (HELPERS-07). New
618
1030
  public types: `Medication`, `MedicationContext`, `MedicationAmount`,
619
1031
  `MedicationStrength`, `MedicationRoute`, `MedicationComponent`.
620
- - **`msg.immunizations()`** (roadmap Phase E, P0 safety) — projects every
1032
+ - **`msg.immunizations()`** (roadmap Phase E, P0 safety). Projects every
621
1033
  RXA segment of a VXU^V04 into a typed `Immunization`, grouping the RXR
622
1034
  (route, Table 0162 + site Table 0163) and OBX (VFC eligibility / funding
623
1035
  source) segments that follow each RXA positionally, and carrying
624
1036
  `orderControl` from the preceding ORC of the `ORC`→`RXA`→`[RXR]`→`[{OBX}]`
625
- order group — the same state machine `orders()` uses for OBR → OBX. The
1037
+ order group, the same state machine `orders()` uses for OBR → OBX. The
626
1038
  action code (RXA-21, `A`/`D`/`U`) is surfaced **verbatim and never
627
- defaulted** — a mis-key corrupts an IIS add/delete/update dedup; the
1039
+ defaulted**. A mis-key corrupts an IIS add/delete/update dedup; the
628
1040
  `recordOrigin` (administered vs historical) is derived **only** from the
629
- well-known NIP001 RXA-9.1 codes (`00`; `01`-`08`) and omitted otherwise —
1041
+ well-known NIP001 RXA-9.1 codes (`00`; `01`-`08`) and omitted otherwise,
630
1042
  never guessed, with the raw RXA-9 claim always preserved on
631
1043
  `informationSource`. The vaccine code carries its own provenance
632
1044
  (`vaccineCode.nameOfCodingSystem`, CVX, + any CVX/NDC alternate);
@@ -634,63 +1046,63 @@ real JS object — literal,`Map`, `Proxy`, or otherwise — can hold two same-na
634
1046
  surfaced as the number `999`, never coerced). Output is frozen; not
635
1047
  memoized. Never throws (HELPERS-07). New public types: `Immunization`,
636
1048
  `ImmunizationRecordOrigin`.
637
- - **Mutation** — `setField`, `addSegment`, `removeSegment` on
1049
+ - **Mutation**: `setField`, `addSegment`, `removeSegment` on
638
1050
  `Hl7Message`. Direct field mutation on unwrapped objects has no effect
639
1051
  (immutability by default).
640
- - **Serialization** — `msg.toString()` emits spec-clean HL7 regardless
1052
+ - **Serialization**: `msg.toString()` emits spec-clean HL7 regardless
641
1053
  of input quirks (Postel's Law); `msg.toJSON()` returns a structured
642
1054
  JSON tree; `msg.prettyPrint()` returns a human-readable multi-line
643
1055
  string for logs. Escape sequences are re-encoded on serialize.
644
- - **Message builder** — `buildMessage({...}).addSegment(...).toString()`
1056
+ - **Message builder**: `buildMessage({...}).addSegment(...).toString()`
645
1057
  constructs valid outbound HL7 from scratch, with helpers for control
646
1058
  IDs and HL7 timestamps.
647
- - **ACK generation + interpretation** (roadmap Phase C) — `buildAck(inbound,
1059
+ - **ACK generation + interpretation** (roadmap Phase C). `buildAck(inbound,
648
1060
  { code, error?, mode? })` produces a spec-clean `MSH`+`MSA`[+`ERR`…]
649
1061
  acknowledgment: sender/receiver swapped (full multi-component HDs
650
1062
  preserved), MSH-9 = `ACK^<trigger>^ACK`, MSA-2 echoes the inbound MSH-10,
651
1063
  and each `ERR` carries an ERL location (ERR-2), a Table 0357 condition code
652
- as a CWE (ERR-3), and a Table 0516 severity (ERR-4) — **codes and locations
1064
+ as a CWE (ERR-3), and a Table 0516 severity (ERR-4), **codes and locations
653
1065
  only, never echoed PHI**. `buildAck` is mechanical (emits the disposition it
654
1066
  is told) with one safety override: an inbound with no MSH-10 cannot be
655
1067
  correlated, so a requested positive `AA`/`CA` is downgraded to `AE`/`CE`,
656
1068
  MSA-2 is left empty, and an `ACK_NO_CORRELATION_ID` warning rides on the
657
- returned message — it never fabricates an unverifiable positive ACK.
1069
+ returned message. It never fabricates an unverifiable positive ACK.
658
1070
  `interpretAck(msg)` is the read-side: a typed `Acknowledgment` view whose
659
1071
  `accepted`/`error`/`rejected` flags are derived fail-safe from MSA-1 (all
660
1072
  three `false` on an absent or unrecognized code). `detectAckMode(inbound)`
661
1073
  exposes the spec-exact original-vs-enhanced detection (MSH-15/16). Control
662
1074
  vocabulary (Tables 0008/0357/0516/0155) ships as frozen read-only enums
663
1075
  (`ACK_CODES`, `ERR_CONDITION_CODES`, `ERR_SEVERITIES`, `ACK_CONDITIONS`).
664
- - **Profile system** — `defineProfile()` API with `extends` composition
1076
+ - **Profile system**: `defineProfile()` API with `extends` composition
665
1077
  (single parent or array), merge semantics (scalars overwrite, arrays
666
1078
  concat+dedupe, `customSegments` deep-merge per key, `onWarning` chains),
667
1079
  `profile.describe()` introspection, `profile.lineage`, and
668
1080
  `ProfileDefinitionError` with actionable messages on invalid input.
669
- - **Default profile management** — `setDefaultProfile(p)`,
1081
+ - **Default profile management**: `setDefaultProfile(p)`,
670
1082
  `getDefaultProfile()`, `setDefaultProfile(null)`. Explicit arguments
671
1083
  override; `parseHL7(raw, { profile: null })` opts out for a single call.
672
- - **Five built-in vendor profiles** — `profiles.epic`, `profiles.cerner`,
1084
+ - **Five built-in vendor profiles**: `profiles.epic`, `profiles.cerner`,
673
1085
  `profiles.meditech`, `profiles.athena`, `profiles.genericLab`. Each
674
1086
  authored through the public `defineProfile()` API with date-format
675
1087
  fallbacks and named Z-segments.
676
- - **Segment.get(name)** — resolve custom-segment fields by declared
1088
+ - **Segment.get(name)**: resolve custom-segment fields by declared
677
1089
  name (e.g., `msg.segments("ZDP")[0].get("departmentCode")`) when a
678
1090
  matching profile is applied.
679
- - **Three runnable examples** — `examples/extract-patient-info.ts`,
1091
+ - **Three runnable examples**: `examples/extract-patient-info.ts`,
680
1092
  `examples/read-lab-results.ts`, `examples/modify-and-resend.ts`, each
681
1093
  runnable via `pnpm tsx examples/<file>.ts` and smoke-tested via
682
1094
  `pnpm examples` in CI.
683
- - **Profile starter kit** — `examples/profile-starter-kit/`, a
1095
+ - **Profile starter kit**: `examples/profile-starter-kit/`, a
684
1096
  publishable template with its own CI + publish workflows,
685
1097
  `CUSTOMIZING.md` walkthrough, and placeholder tokens
686
1098
  (`{{YOUR_ORG}}` / `{{PROFILE_NAME}}`) that turn into a ready-to-publish
687
1099
  profile package in minutes.
688
- - **Documentation** — comprehensive README (value prop, quickstart,
1100
+ - **Documentation**: comprehensive README (value prop, quickstart,
689
1101
  feature list, HL7-in-90-seconds primer, three access patterns, full
690
1102
  cookbook, Profiles section, Real-World Tolerance table, Error
691
1103
  Handling, Roadmap), `CHANGELOG.md`, `CONTRIBUTING.md`, `LICENSE`
692
1104
  (MIT).
693
- - **Tooling** — strict TypeScript (`noUncheckedIndexedAccess`,
1105
+ - **Tooling**: strict TypeScript (`noUncheckedIndexedAccess`,
694
1106
  `exactOptionalPropertyTypes`), dual ESM + CJS build via tsup, Vitest
695
1107
  with ≥ 90% per-directory branch coverage on `src/parser/`, `src/model/`,
696
1108
  `src/helpers/`, `src/serialize/`, `src/builder/`. Lint, format, and