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