@cosyte/hl7 0.0.1
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 +710 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/index.cjs +5526 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5864 -0
- package/dist/index.d.ts +5864 -0
- package/dist/index.mjs +5447 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +96 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
Versions and publishing are managed with [Changesets](https://github.com/changesets/changesets);
|
|
8
|
+
this file is maintained by hand (Changesets handles the version bump and publish only).
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
The first pre-alpha release (`0.0.1`) will ship the complete v1 API surface below. An earlier
|
|
13
|
+
`0.1.0` tag was prepared but never published, so the package begins its public history at `0.0.x`,
|
|
14
|
+
per the cosyte version ladder (`0.0.x` until first alpha).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Full documentation Diátaxis spine + doc/code-agreement gate (DOCS-CONTENT-P1).** `docs-content/`
|
|
19
|
+
gains **Installation**, **Quickstart**, **Guides**, and **Troubleshooting** (with Known
|
|
20
|
+
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
|
|
22
|
+
`test/docs-content.test.ts` runs `@cosyte/vitest-config@^0.0.2`'s `docSnippetSuite()` over the
|
|
23
|
+
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.
|
|
25
|
+
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.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Dropped the "Dogfooded in production" claim from the README feature list.** The bullet asserted
|
|
31
|
+
the package was "used internally on healthcare-integration projects"; no engagement has run it, so
|
|
32
|
+
the claim was not accurate and has been removed ahead of the first public release. `README.md`
|
|
33
|
+
ships inside the published tarball, so this is a user-visible docs change. No runtime/API change.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
|
|
38
|
+
`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
|
|
40
|
+
workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
|
|
41
|
+
workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
|
|
42
|
+
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.
|
|
44
|
+
|
|
45
|
+
- **The `VERSION` export now tracks `package.json`, and the missing `version` script is restored
|
|
46
|
+
(VERSION-SYNC).** Two latent release bugs, both of which would have bitten at the first publish.
|
|
47
|
+
(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
|
|
49
|
+
consumer asserting on or logging `VERSION` told the wrong version of the parser they were running.
|
|
50
|
+
New `scripts/sync-version.mjs` rewrites the constant from `package.json` (idempotent; exits
|
|
51
|
+
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
|
|
53
|
+
`version: pnpm run version`, which would have failed with `ERR_PNPM_NO_SCRIPT`, so the "Version
|
|
54
|
+
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
|
|
56
|
+
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`.
|
|
58
|
+
|
|
59
|
+
- **Value reads no longer double-decode (HL7-VALUE-REDECODE). Behavior change on a rare, spec-legal
|
|
60
|
+
input (pre-alpha).** The tokenizer already unescapes every subcomponent once on parse, but
|
|
61
|
+
`Field.value`, the dot-path `get()`/`resolvePath`, and the composite coercions (`asXpn`/`asCwe`/`asCx`/…
|
|
62
|
+
via the shared `readSubcomponent`, plus `asNm`/`asTs`) ran a **second** `unescape` over the
|
|
63
|
+
already-decoded value. A value whose own decoded bytes look like an escape was therefore decoded
|
|
64
|
+
twice: a wire `\E\F\E\` decodes once to the literal `\F\`, which the second pass wrongly turned into
|
|
65
|
+
the field separator `|`. Readers now return the once-decoded subcomponent verbatim
|
|
66
|
+
(`field(5).value` of `A\E\F\E\B` is `A\F\B`, not `A|B`); the common single-escape case is unchanged
|
|
67
|
+
(`\F\` → `|`), and emit stays byte-verbatim (the HL7-ESC raw overlay is a separate, untouched path).
|
|
68
|
+
Found by the HL7-ESC conformance-refuter; pinned by `test/value-redecode.test.ts`.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- **Datetime precision + timezone fidelity — the TS/DTM composite (roadmap
|
|
73
|
+
Phase N). Breaking (pre-alpha).** `field.asTs()` and every helper datetime now
|
|
74
|
+
return the fidelity `TS` (`DtmParts`: `{ raw, valid, precision, year, month
|
|
75
|
+
(1–12, spec-native), day, hour, minute, second, fractionalSeconds (verbatim),
|
|
76
|
+
hasTimezone, offsetMinutes }`) instead of the old `{ raw, date }`. HL7 v2
|
|
77
|
+
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
|
|
79
|
+
prior code zero-filled truncations (`|1970|` → `1970-01-01T00:00:00Z`) and
|
|
80
|
+
coerced an offset-less value to UTC, which silently shifted a day-only birth
|
|
81
|
+
date `|19880705|` to the previous calendar day in any negative-offset zone
|
|
82
|
+
[S-DTM-IMPL]. Now precision is preserved (no zero-fill), a missing offset is
|
|
83
|
+
**flagged** (`hasTimezone: false`) and never resolved, and no eager `Date` is
|
|
84
|
+
built. An absolute instant is materialized **only on explicit request** via
|
|
85
|
+
`dtmToDate(ts, { assumeOffsetMinutes? })`, which returns `undefined` for an
|
|
86
|
+
offset-less value rather than guessing UTC. Applies to `meta.timestamp`,
|
|
87
|
+
`patient.dateOfBirth`, `visit.admit/dischargeDateTime`,
|
|
88
|
+
`observations.observedDateTime` + `TS`/`DT` observation values,
|
|
89
|
+
`allergies.onsetDate`, `diagnoses.dateTime`, `insurance.effective/expirationDate`,
|
|
90
|
+
and `immunizations.administered/expirationDate` — each `Date → TS`. Fidelity
|
|
91
|
+
only: no localization, timezone conversion, or arithmetic; `HHMM=0000` is never
|
|
92
|
+
rolled to the previous day. New public exports: `parseDtm`, `formatDtm`,
|
|
93
|
+
`dtmToDate`, and types `DtmParts` / `DtmPrecision` / `DtmToDateOptions`. **No
|
|
94
|
+
warning-code change** (a missing timezone is the structural `hasTimezone: false`
|
|
95
|
+
flag, not a warning). See `docs-content/spec-notes-datetime-precision.md`.
|
|
96
|
+
|
|
97
|
+
### Removed
|
|
98
|
+
|
|
99
|
+
- **`parseHl7Timestamp` (and its `ParseHl7TimestampOptions` type) — removed
|
|
100
|
+
(Phase N, breaking).** The Date-returning, UTC-assuming timestamp helper is
|
|
101
|
+
replaced by the fidelity `parseDtm` + explicit `dtmToDate`, with
|
|
102
|
+
`parseDtmCascade` covering the MSH-7 user-format fallback path.
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- **Escape-sequence emit is now byte-verbatim across the full alphabet
|
|
107
|
+
(HL7-ESC).** The decoded tree conflated recognize-and-preserve escapes (`\H\`,
|
|
108
|
+
`\N\`, `\.sp\`/formatting, `\Cxxyy\`/`\Mxxyyzz\` charset switches, vendor
|
|
109
|
+
`\Z..\`) and hex escapes (`\X41\`) with literal backslashes, so serialize
|
|
110
|
+
**double-escaped** the former (`\H\` → `\E\H\E\`) and **decoded** the latter
|
|
111
|
+
(`\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
|
|
113
|
+
MSA-2/MSH-10 correlation (HL7 v2 §2.9.2.2) could fail. The tokenizer now
|
|
114
|
+
records each affected subcomponent's **original wire bytes** in an internal
|
|
115
|
+
`RawComponent.rawSubcomponents` overlay and the serializer emits from it
|
|
116
|
+
verbatim, so `parseHL7(x).toString()` and `Field.text` reproduce the sender's
|
|
117
|
+
exact escape bytes. `buildAck`'s MSA-2 echo is byte-exact for a
|
|
118
|
+
default-delimiter sender; a **custom-delimiter** sender is re-delimited
|
|
119
|
+
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
|
|
122
|
+
per §2.9.2.2). **The `\X..\` policy is
|
|
123
|
+
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
|
|
125
|
+
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** —
|
|
127
|
+
`Field.value` and every composite/`toJSON` read the same decoded values as
|
|
128
|
+
before; the overlay is `@internal` and absent from escape-free messages. See
|
|
129
|
+
`docs-content/spec-notes-escapes.md`.
|
|
130
|
+
- **PHI leak in the warning surface — `fieldWhitespaceTrimmed` and `unknownEscapeSequence` no
|
|
131
|
+
longer embed field-body content (HL7-TOKENIZE-PHI).** Both builders previously interpolated
|
|
132
|
+
slices of the field VALUE directly into `Hl7ParseWarning.message`
|
|
133
|
+
(`fieldWhitespaceTrimmed`'s before/after strings; `unknownEscapeSequence`'s raw escape body, and
|
|
134
|
+
on an unterminated escape the **entire rest of the field**), so a clinical NTE-3/OBX-5 free-text
|
|
135
|
+
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.
|
|
137
|
+
`fieldWhitespaceTrimmed`'s signature changed to take leading/trailing **counts** instead of the
|
|
138
|
+
original/trimmed strings; `unknownEscapeSequence` now reports only the escape body's **length**
|
|
139
|
+
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
|
|
142
|
+
`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
|
|
144
|
+
the whitespace are still preserved verbatim in the parsed/emitted output; only the WARNING
|
|
145
|
+
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,
|
|
147
|
+
not a breaking change to the code registry.
|
|
148
|
+
- **`buildAck` now echoes the full inbound MSH-10 field into MSA-2** — the raw
|
|
149
|
+
field structure is carried over whole (delimiters and repetitions included)
|
|
150
|
+
instead of the component-1-only `meta.controlId` scalar. A vendor-quirk
|
|
151
|
+
control id carrying an unescaped delimiter (`ID^X`) was previously truncated
|
|
152
|
+
to `ID` silently, under a confident positive ACK — a sender correlating on
|
|
153
|
+
the raw MSH-10 bytes (as `@cosyte/mllp`'s client does) would never match the
|
|
154
|
+
ACK and resend indefinitely (HL7 v2 §2.9.2.2: an MSA-2 mismatch is a
|
|
155
|
+
correlation failure). The no-correlation fail-safe now keys on the raw field
|
|
156
|
+
carrying any content, so a leading-delimiter id (`^X`) correlates instead of
|
|
157
|
+
being spuriously downgraded. The echo is now **byte-verbatim** across the full
|
|
158
|
+
escape alphabet (see the escape-fidelity entry below) — the earlier
|
|
159
|
+
canonicalization limits (hex escapes decoding to their byte, preserved escapes
|
|
160
|
+
re-emitting as escaped literal text) are resolved. The only remaining
|
|
161
|
+
transform is structural: the ACK emits with default encoding characters and
|
|
162
|
+
trailing insignificant empties canonicalize (D-02).
|
|
163
|
+
- **`reescape` now emits a literal CR in decoded content as its `\X0D\` hex
|
|
164
|
+
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
|
|
166
|
+
re-serializing it corrupted the emitted message's framing (a phantom
|
|
167
|
+
segment split mid-field, silently). Reachable through `buildAck`'s MSA-2
|
|
168
|
+
echo among every other emit path; now structurally safe and round-trip
|
|
169
|
+
stable.
|
|
170
|
+
- **`interpretAck` surfaces MSA-2 whole** (read-side symmetry) —
|
|
171
|
+
`Acknowledgment.controlId` is now the field's canonical wire text
|
|
172
|
+
(`Field.text`), not its first component.
|
|
173
|
+
|
|
174
|
+
### Added
|
|
175
|
+
|
|
176
|
+
- **Scheduling / document / charge breadth helpers — `appointments()`,
|
|
177
|
+
`documents()`, `charges()` (roadmap Phase Q, P2).** Three new message-family
|
|
178
|
+
helpers on `Hl7Message`, each mirroring the existing extractor pattern (typed,
|
|
179
|
+
frozen, never-throws, not memoized):
|
|
180
|
+
- **`appointments()`** over **SIU** (S12/S13/S14/S15/S26) — projects each
|
|
181
|
+
**SCH** into a typed `Appointment`: placer/filler appointment ids (SCH-1/2),
|
|
182
|
+
**SCH-25 filler status** (Table 0278, verbatim), **SCH-11** start/end timing
|
|
183
|
+
(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` /
|
|
185
|
+
`location` / `personnel` (the personnel/provider resource also carries a
|
|
186
|
+
typed `XCN`).
|
|
187
|
+
- **`documents()`** over **MDM** (T02/T04/T06) — projects each **TXA** into a
|
|
188
|
+
typed `ClinicalDocument` with the OBX narrative body grouped positionally
|
|
189
|
+
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
|
|
191
|
+
document can be _available_ before it is _authenticated_, and reading a
|
|
192
|
+
preliminary document as final is the clinical harm this split prevents. Both
|
|
193
|
+
are verbatim / provenance-only.
|
|
194
|
+
- **`charges()`** over **DFT** (P03) — projects each **FT1** into a typed
|
|
195
|
+
`Charge`: FT1-6 transaction type (Table 0017), FT1-7 transaction code (the
|
|
196
|
+
institution charge code, CWE), FT1-11/12 extended/unit amounts, and the
|
|
197
|
+
repeating FT1-19 diagnosis linkage. **No billing logic and no
|
|
198
|
+
money-as-float** — amounts are surfaced as their **canonical CP wire text**
|
|
199
|
+
(e.g. `"150.00^USD"`), never parsed to a `number`.
|
|
200
|
+
|
|
201
|
+
New public types: `Appointment`, `AppointmentResource`, `ClinicalDocument`,
|
|
202
|
+
`Charge`. Breadth helpers only — not a scheduling-workflow state machine,
|
|
203
|
+
signature verification, or a claims/pricing engine (roadmap §9
|
|
204
|
+
known-limitations). Never throws on malformed input (HELPERS-07); missing
|
|
205
|
+
fields → omitted keys.
|
|
206
|
+
|
|
207
|
+
- **NTE narrative grouping — notes attached to their parent by position (roadmap
|
|
208
|
+
Phase P, P1).** NTE (Notes and Comments) segments are now grouped to their
|
|
209
|
+
parent **by position** and surfaced on the relevant helper output. An NTE
|
|
210
|
+
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
|
|
212
|
+
(`{ [ORC] OBR [{NTE}] [{ [OBX] [{NTE}] }] }`, Ch. 7) a note after `OBX` lands on
|
|
213
|
+
**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
|
|
215
|
+
**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 —
|
|
218
|
+
never mis-attached, never dropped:** the parent is the _nearest preceding
|
|
219
|
+
non-NTE segment_ (consecutive NTEs chain; any intervening non-parent segment
|
|
220
|
+
resets the target). Order-level notes mirror the `orders()` state machine —
|
|
221
|
+
notes after an `ORC` are buffered and flushed onto the OBR that opens the
|
|
222
|
+
order, so **several `ORC`s before one `OBR` all contribute** in document order,
|
|
223
|
+
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
|
|
226
|
+
routed to `msg.notes()` rather than vanishing. Each non-empty **NTE-3 (Comment,
|
|
227
|
+
FT, repeating)** repetition is one note line, with the **full** repetition text
|
|
228
|
+
reassembled + HL7-unescaped, so a non-conformant raw `^`/`&` (which tokenizes
|
|
229
|
+
NTE-3 into components) is preserved, not silently truncated. New optional
|
|
230
|
+
`notes?: readonly string[]` on `Observation`, `Order`, and `Patient` (omitted
|
|
231
|
+
when empty, frozen when present). **Additive only — no rename, no removal, no
|
|
232
|
+
new warning code.** Deferred: NTE-2 (source of comment) / NTE-4 (comment type)
|
|
233
|
+
interpretation, FT formatting-command rendering, and first-class
|
|
234
|
+
`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,
|
|
236
|
+
P1).** `parseHL7` now resolves a `Buffer` input's declared character set from
|
|
237
|
+
**MSH-18** and decodes the byte stream to text **before** tokenization, through
|
|
238
|
+
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
|
|
241
|
+
**`8859/1`** via Node's true `latin1` (byte-exact; Node's WHATWG
|
|
242
|
+
`TextDecoder("iso-8859-1")` is windows-1252 and remaps the C1 range, so it is
|
|
243
|
+
not used), ASCII / **UTF-8** (`UNICODE UTF-8` / `UNICODE`), and the faithfully
|
|
244
|
+
decoded **ISO-8859** sets (`8859/2`–`8859/8`, `10`, `13`–`16`); `8859/9` and
|
|
245
|
+
`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
|
|
247
|
+
multibyte / ISO-2022 East-Asian sets (`ISO IR14/87/159`, GB 18030, KS X 1001,
|
|
248
|
+
CNS 11643, BIG-5) and UTF-16/32 are likewise **recognized and preserved
|
|
249
|
+
verbatim** (full stateful decode is a documented non-goal). The §2.7.4 charset-switch escapes (`\Cxxyy\` / `\Mxxyyzz\`) are
|
|
250
|
+
recognized by the escape layer and preserved. **Fail-safe (never guess, never
|
|
251
|
+
silently corrupt):** decoding is **strict** (`fatal: true`) except byte-exact
|
|
252
|
+
`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
|
|
254
|
+
fires: new **`UNSUPPORTED_CHARSET`** for a recognized set that was not decoded
|
|
255
|
+
(never-decoded, or a strict-decode failure), existing `UNKNOWN_CHARSET` for a
|
|
256
|
+
value not in Table 0211. Recoverability is exact for single-byte content;
|
|
257
|
+
multibyte content is best-effort (a code-unit byte can coincide with a structural
|
|
258
|
+
delimiter — documented). **Behaviour change (pre-alpha):** `UNKNOWN_CHARSET` now
|
|
259
|
+
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
|
|
261
|
+
code `UNSUPPORTED_CHARSET` (`WARNING_CODES` 18 → 19). See
|
|
262
|
+
`docs-content/spec-notes-charset.md`.
|
|
263
|
+
|
|
264
|
+
- **Order / medication timing — `order.timings` + `med.timings` (roadmap
|
|
265
|
+
Phase M).** `orders()` and `medications()` now surface the timing **structure**
|
|
266
|
+
of an order/medication as a typed `OrderTiming[]`, read from the `TQ1` segment
|
|
267
|
+
(HL7 v2.5+, Ch. 4 §4.5.4) or the **legacy embedded TQ** data type in `ORC-7` /
|
|
268
|
+
`RXE-1` (pre-v2.5, Ch. 2A §2.A.81, detail withdrawn as of v2.7). Modelled:
|
|
269
|
+
TQ1-2 quantity (CQ), **TQ1-3 repeat pattern (RPT, Table 0335)**, TQ1-4 explicit
|
|
270
|
+
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**
|
|
272
|
+
(the Text Instruction). The repeat pattern is surfaced **verbatim**
|
|
273
|
+
(`repeatPattern.code`) — never normalized, resolved to clock times, or mapped
|
|
274
|
+
to a different frequency (reading `Q6H` as "daily", or losing a `BID`, changes
|
|
275
|
+
the administered dose count). A provenance-only `kind`
|
|
276
|
+
(`parametric`/`named`/`unknown`) never drives a schedule; a `parametric`
|
|
277
|
+
`Q<integer><unit>` template surfaces its **load-bearing integer** on
|
|
278
|
+
`repeatPattern.interval`, never dropped. `TQ1` vs the legacy embedded TQ is
|
|
279
|
+
chosen **by presence** (`source: "TQ1" | "legacy"`) — the legacy field is read
|
|
280
|
+
only when no `TQ1` accompanies the group, so the same timing is never
|
|
281
|
+
double-counted and a legacy-only timing is never dropped (the legacy source is
|
|
282
|
+
`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
|
|
284
|
+
order detail, an intervening `ORC` re-scopes a following `TQ1` to the next
|
|
285
|
+
order, and multiple `TQ1` segments each surface (a tapering schedule). New
|
|
286
|
+
public types `OrderTiming`, `RepeatPattern`,
|
|
287
|
+
`RepeatPatternKind`, `TimingQuantity`; `Order` / `Medication` gain an
|
|
288
|
+
always-present `timings` array. **Additive only** — no rename, no removal, **no
|
|
289
|
+
new warning code**. hl7 surfaces the timing structure only; it does not compute
|
|
290
|
+
schedules, resolve "institution-specified times", or interpret sig. Non-goals:
|
|
291
|
+
`TQ2` beyond segment recognition, schedule computation, 2nd+ repetitions of
|
|
292
|
+
repeating timing fields. Never throws (HELPERS-07). See
|
|
293
|
+
`docs-content/spec-notes-timing.md`.
|
|
294
|
+
|
|
295
|
+
- **`splitBatch()` — batch / file envelope splitting (roadmap Phase L).**
|
|
296
|
+
Demarcates the individual `MSH`-led messages inside an HL7 v2 batch/file
|
|
297
|
+
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
|
|
299
|
+
that trips a Tier-3 fatal, `{ ok: false, error }`. A **malformed message
|
|
300
|
+
mid-batch is isolated**, never suppressing its siblings; a **bare single
|
|
301
|
+
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
|
|
304
|
+
mismatch surfaces the new Tier-2 `BATCH_COUNT_MISMATCH` warning **without ever
|
|
305
|
+
dropping the tail**; an absent (`[0..1]`) or non-numeric count tolerantly
|
|
306
|
+
disables reconciliation. A `BHS`/`FHS` header with no matching `BTS`/`FTS`
|
|
307
|
+
raises the new `BATCH_MISSING_TRAILER` warning (`splitBatch` warns, never
|
|
308
|
+
rejects — enforcing a mandatory envelope is the caller's call). Batch-level
|
|
309
|
+
warnings live on the returned `BatchSplitResult.warnings` (never on
|
|
310
|
+
`Hl7Message.warnings`) and carry **counts / segment names / positions only —
|
|
311
|
+
no PHI**. A `Buffer` stream round-trips through `latin1` so each message's own
|
|
312
|
+
MSH-18 charset resolution runs on its original bytes. Kept a **separate
|
|
313
|
+
surface** from `parseHL7` (which rejects non-`MSH`-first input by design).
|
|
314
|
+
New public exports: `splitBatch`, the `batchCountMismatch` /
|
|
315
|
+
`batchMissingTrailer` factories, and types `Batch` / `BatchSplitResult` /
|
|
316
|
+
`BatchMessageEntry` / `BatchEnvelopeSegment` / `BatchEnvelopeName`. **Two
|
|
317
|
+
additive Tier-2 warning codes** (`BATCH_COUNT_MISMATCH`,
|
|
318
|
+
`BATCH_MISSING_TRAILER`) — the stable warning-code contract grows 16 → 18.
|
|
319
|
+
`batches` holds each message run delimited by a `BHS` and/or a `BTS` (a run
|
|
320
|
+
with neither is still yielded in `messages`, wrapped in no batch); FTS-1
|
|
321
|
+
reconciles **per-file** so concatenated files raise no false mismatch. Split-only: no
|
|
322
|
+
batch ACK generation, no envelope enforcement, BTS-3 totals surfaced but not
|
|
323
|
+
reconciled, no transport de-framing (that is `@cosyte/mllp`). Two structural
|
|
324
|
+
limits are documented: reserved envelope names are matched by name (a body
|
|
325
|
+
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
|
|
327
|
+
per stream is the modelled case. See `docs-content/spec-notes-batch.md`.
|
|
328
|
+
- **`identityEvents()` — patient-identity / merge events (roadmap Phase K, P0
|
|
329
|
+
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
|
|
332
|
+
party **labelled by role with segment provenance**: the `surviving` party is
|
|
333
|
+
only ever sourced from PID/PV1, the `prior` (non-surviving) party only ever
|
|
334
|
+
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) —
|
|
336
|
+
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
|
|
339
|
+
present plus a new event-scoped Tier-2
|
|
340
|
+
**`MERGE_MISSING_PRIOR_OR_SURVIVOR`** warning carrying structural facts
|
|
341
|
+
only (never an identifier or name — no PHI). The MRG field map is
|
|
342
|
+
**version-scoped**: the backward-compat single-ID fields (MRG-4 / PID-2,
|
|
343
|
+
withdrawn as of v2.7) are not read when MSH-12 declares v2.7+. Applying the
|
|
344
|
+
merge (re-pointing data) stays the consumer's job. New public surface:
|
|
345
|
+
`Hl7Message.identityEvents()`, types `IdentityEvent` / `IdentityParty` /
|
|
346
|
+
`IdentityEventKind` / `IdentityRole`, the `mergeMissingPriorOrSurvivor`
|
|
347
|
+
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
|
|
349
|
+
a spurious `UNKNOWN_SEGMENT` warning for MRG.
|
|
350
|
+
- **`Field.text`** — the field's canonical wire text (full repetitions/
|
|
351
|
+
components/subcomponents re-serialized with the active delimiters), the
|
|
352
|
+
whole-field counterpart to the component-1-only `Field.value`.
|
|
353
|
+
- **`downgradePositiveAck(code)`** — the single upstream source of truth for
|
|
354
|
+
the fail-safe downgrade pair (`AA`→`AE`, `CA`→`CE`; everything else passes
|
|
355
|
+
through), now exported so `@cosyte/mllp`'s `ack-from-hl7` adapter reuses it
|
|
356
|
+
instead of carrying a divergent copy. **`isPositiveAck`** is exported
|
|
357
|
+
alongside it.
|
|
358
|
+
|
|
359
|
+
### Documentation
|
|
360
|
+
|
|
361
|
+
- **Adopted the documentation IA spine in `docs-content/`** (umbrella DOCS-D5; reference
|
|
362
|
+
exemplar for the suite). The three `spec-notes-*.md` files (coding-system provenance,
|
|
363
|
+
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
|
|
365
|
+
`docs-content/` but were not referenced from `sidebars.json` and so were unreachable from
|
|
366
|
+
any rendered route. Each now carries minimal frontmatter (`id` / `title` /
|
|
367
|
+
`sidebar_label`) so the sidebar text stays concise; the prose is unchanged. Also
|
|
368
|
+
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.
|
|
371
|
+
|
|
372
|
+
- **Added a standard-reference primer (`spec-notes-primer.md`), first under Core Concepts.**
|
|
373
|
+
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
|
|
375
|
+
model (MSH-1/MSH-2 delimiter bootstrap, the fixed `CR` terminator, the
|
|
376
|
+
field→component→subcomponent hierarchy where only fields repeat), the three field
|
|
377
|
+
population states (populated / not-populated / **null** `|""|`), the abstract message
|
|
378
|
+
grammar (`[ ]` / `{ }` / `[{ }]`, base `OPT` vs. v2.7+ conformance `Usage` codes, and the
|
|
379
|
+
required-different-name-separator rule that makes a flat segment stream reconstructable),
|
|
380
|
+
the **three**-component `MSH-9`, the acknowledgement model (`MSH-15`/`MSH-16`; `CA/CR/CE`
|
|
381
|
+
vs. `AA/AE/AR`), and the `CX`/`XPN`/`XAD`/`HD` composite layouts with their version deltas.
|
|
382
|
+
Cross-links the phase notes for areas they already cover rather than duplicating them, and
|
|
383
|
+
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.
|
|
385
|
+
|
|
386
|
+
### Security
|
|
387
|
+
|
|
388
|
+
- **Dev-dependency advisory remediation (no runtime impact — `@cosyte/hl7`
|
|
389
|
+
ships zero runtime dependencies, so the published artifact is unchanged).**
|
|
390
|
+
Added scoped `pnpm.overrides` pinning two transitive **dev/build-time**
|
|
391
|
+
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
|
|
393
|
+
builds via `tsup`/`vitest` and never runs `esbuild serve`) and the
|
|
394
|
+
`@changesets/parse` copy of `js-yaml` (`>=4.0.0 <4.2.0` → `4.2.0`;
|
|
395
|
+
GHSA-h67p-54hq-rp68 merge-key DoS). The `js-yaml@3.14.2` pulled by
|
|
396
|
+
`read-yaml-file@1.1.0` (via `@manypkg/get-packages` → `@changesets/cli`) is
|
|
397
|
+
**intentionally left**: it calls `yaml.safeLoad`, removed/throwing in
|
|
398
|
+
js-yaml 4, so it cannot be force-upgraded without breaking the release
|
|
399
|
+
tooling, and it only parses trusted local repo YAML at release time. This is
|
|
400
|
+
the shared canonical override block, enforced suite-wide by the
|
|
401
|
+
`@cosyte/config` drift check.
|
|
402
|
+
|
|
403
|
+
### Changed
|
|
404
|
+
|
|
405
|
+
- Adopted the shared `@cosyte/*` toolchain standard: ES2023, ESLint 10 + type-checked
|
|
406
|
+
`typescript-eslint`, Vitest 4, `@types/node` 22, exact-pinned dev tools, the shared
|
|
407
|
+
`@cosyte/tsup-config` / `@cosyte/vitest-config`, and thin callers of the reusable `cosyte/.github`
|
|
408
|
+
CI/release workflows. No public API change.
|
|
409
|
+
- **Test bar** — added executable PHI-safety property tests
|
|
410
|
+
(`test/property/phi-safety.property.test.ts`). Locks two invariants: warning messages never echo
|
|
411
|
+
field VALUES (only positional context + bounded metadata), and `Hl7ParseError.snippet` length
|
|
412
|
+
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
|
|
415
|
+
for `Secret<T>` wrappers (the pathways credentials pattern) and is the wrong shape for parser-
|
|
416
|
+
side PHI surfaces.
|
|
417
|
+
- **Coverage policy — `src/profiles/**`coverage hole closed, global`branches`floor restored to
|
|
418
|
+
90 (roadmap Phase J, supersedes/absorbs the`H-PHI` coverage-policy entry above).** The D10
|
|
419
|
+
transient relaxation is now RESOLVED: targeted tests
|
|
420
|
+
(`test/profiles-merge-validate-coverage.test.ts`) closed the `mergeCustomSegments`/inheritance
|
|
421
|
+
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
|
+
(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
|
|
424
|
+
(`validate.ts`'s `validateUniqueFieldNames`throw branch) stays intentionally uncovered and is
|
|
425
|
+
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
|
|
427
|
+
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
|
|
429
|
+
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
|
|
431
|
+
seed) fuzz harness on top of the existing `lenient.property.test.ts` invariant
|
|
432
|
+
("`parseHL7` either returns an `Hl7Message` or throws an `Hl7ParseError` with one of the 4
|
|
433
|
+
`FATAL_CODES`, never anything else"). Adds two generator families the existing suite didn't
|
|
434
|
+
cover: delimiter-mutation (inject/duplicate/drop the five HL7 delimiters + `\r`) of REAL
|
|
435
|
+
canonical-corpus messages, and truncations of those same messages at every cut point. Also
|
|
436
|
+
snapshots `FATAL_CODES`/`WARNING_CODES` registry membership and asserts a "survivor
|
|
437
|
+
round-trip" property (anything that parses can be `toString()`'d and re-parsed without
|
|
438
|
+
throwing).
|
|
439
|
+
- `test/property/phi-safety-surfaces.property.test.ts` — extends
|
|
440
|
+
`phi-safety.property.test.ts` (which covers the WARNING surface) to the FATAL error-message
|
|
441
|
+
surface: over PHI-shaped field values that trigger each reachable fatal code, asserts
|
|
442
|
+
`Hl7ParseError.message` carries only structural/positional facts, never an echoed field value.
|
|
443
|
+
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
|
|
445
|
+
for BOTH the direct-fatal snippet path and the strict-mode-escalation snippet path
|
|
446
|
+
(`index.ts::buildSnippet`, a distinct call site). Also documents, with a round-trip test, that
|
|
447
|
+
`toString()`/`toJSON()`/`prettyPrint()` legitimately contain field values (content surfaces,
|
|
448
|
+
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
|
|
450
|
+
`@cosyte/hl7` against the external **python-hl7** parser (BSD license, PyPI package `hl7`) over
|
|
451
|
+
the full canonical corpus (27 fixtures): segment count + per-segment field-text parity. Skips
|
|
452
|
+
gracefully (`it.skip`) when no Python + `hl7` package is available, so `verify.sh`/CI stay green
|
|
453
|
+
without Python. One real, honest divergence was found and documented: `@cosyte/hl7`'s D-02
|
|
454
|
+
trailing-empty-component canonicalization (`Field.text` strips trailing empty
|
|
455
|
+
components/subcomponents; python-hl7's `str()` preserves them verbatim) — see
|
|
456
|
+
`docs-content/spec-notes-differential.md` for the full writeup, licensing bound, and other
|
|
457
|
+
plausible-but-unobserved divergence classes.
|
|
458
|
+
|
|
459
|
+
### Fixed
|
|
460
|
+
|
|
461
|
+
- **Conformance — v2.7+ truncation char no longer rejects spec-conformant input** (roadmap Phase A,
|
|
462
|
+
P0 correctness). `readDelimiters` previously hard-coded MSH-2 length to 4 and threw the Tier-3
|
|
463
|
+
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
|
|
465
|
+
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
|
|
467
|
+
gains a new optional `truncation?: string` field that is set ONLY when MSH-2 actually declared
|
|
468
|
+
one, so messages that predate v2.7 round-trip with a 4-char MSH-2 unchanged. The serializer +
|
|
469
|
+
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
|
|
471
|
+
Phase A). Six spec-defined escape families are now recognized:
|
|
472
|
+
- `\P\` — truncation character. **Decoded** to `enc.truncation ?? "#"` (spec §2.5.5.2). On
|
|
473
|
+
serialize, the truncation character is re-escaped back to `\P\` ONLY when MSH-2 declared one,
|
|
474
|
+
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
|
|
476
|
+
parser does not pick a presentational policy; the markers stay in the decoded string for a
|
|
477
|
+
downstream renderer to consume.
|
|
478
|
+
- `\.sp\`, `\.in\`, `\.ti\`, `\.fi\`, `\.nf\`, `\.ce\` — formatting commands (spec §2.7.6).
|
|
479
|
+
Recognized and preserved verbatim, same rationale as highlight.
|
|
480
|
+
- `\Cxxyy\` (single-byte) and `\Mxxyyzz\` (multi-byte, 4 or 6 hex) — character-set switches
|
|
481
|
+
(spec §2.7.4). Recognized and preserved verbatim because byte-accurate decoding requires
|
|
482
|
+
charset state this module does not own.
|
|
483
|
+
|
|
484
|
+
No public surface is removed or renamed; the `WARNING_CODES` registry is unchanged
|
|
485
|
+
(snapshot test asserts additions-only). `\Z..\` (vendor-specific) and genuinely-malformed bodies
|
|
486
|
+
still warn + preserve as before. New fixtures in `test/fixtures/edge-cases/` (`truncation-char-msh2.hl7`,
|
|
487
|
+
`escape-highlight.hl7`, `escape-formatting.hl7`) lock the behavior, including byte-exact
|
|
488
|
+
round-trip through `toString()`.
|
|
489
|
+
|
|
490
|
+
- **Conformance — SN (Structured Numeric) results no longer silently drop the comparator/range**
|
|
491
|
+
(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 `"<"` —
|
|
493
|
+
a misread clinical result with a documented patient-harm path (a "less-than 10" result reading as
|
|
494
|
+
the operator alone). `msg.observations()` now dispatches `SN` to a typed `SN` value
|
|
495
|
+
(`comparator` / `num1` / `separatorOrSuffix` / `num2`). Fail-safe by construction: `num1`/`num2` are
|
|
496
|
+
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
|
|
498
|
+
is never passed off as a real relation. The comparator is preserved byte-for-byte across a
|
|
499
|
+
serialize → parse round-trip. New canonical fixture `test/fixtures/canonical/oru-r01-sn-results.hl7`
|
|
500
|
+
and property tests (`test/property/sn.property.test.ts`) lock the invariant over thousands of
|
|
501
|
+
generated values.
|
|
502
|
+
|
|
503
|
+
- **Types resolution from CommonJS** — the `exports` map now points the `require` condition's types at
|
|
504
|
+
`dist/index.d.cts` (was `index.d.ts`), fixing a "masquerading as ESM" (`attw` FalseESM) issue for
|
|
505
|
+
CJS consumers.
|
|
506
|
+
|
|
507
|
+
### Added
|
|
508
|
+
|
|
509
|
+
- **Version-sensitivity hardening** (roadmap Phase H, P1) — the coded-element
|
|
510
|
+
composites are now robust to **append-only component growth** across HL7
|
|
511
|
+
v2.1–v2.8, with **no silent truncation**. `CWE` and `CE` gained an optional
|
|
512
|
+
`extraComponents: readonly string[]` that preserves any components beyond the
|
|
513
|
+
modeled set verbatim and in order: **CWE component 10+** (the v2.7
|
|
514
|
+
second-alternate triplet + coding-system / value-set OIDs) and **CE component
|
|
515
|
+
7+**. An absent interior component is held as `""` so `extraComponents[i]`
|
|
516
|
+
maps back to its HL7 component number; trailing empties are stripped and the
|
|
517
|
+
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
|
|
519
|
+
components, reading a CWE-shaped value through `asCe()` is no longer lossy (CE
|
|
520
|
+
was deprecated at v2.5, withdrawn at v2.6 in favor of CWE). Parsing a coded
|
|
521
|
+
element with an arbitrary number of trailing components **never throws**
|
|
522
|
+
(forward-compatibility with future versions). The CWE coding-system version
|
|
523
|
+
ids (CWE.7/CWE.8) were already surfaced. Added a `version-growth` property
|
|
524
|
+
test (no-loss / no-throw / CE↔CWE uniformity) and the supported-version matrix
|
|
525
|
+
in `docs-content/spec-notes-version-matrix.md` (incl. the **TS→DTM**
|
|
526
|
+
supersession and the **MSH-21** Conformance Statement ID → Message Profile
|
|
527
|
+
Identifier rename at v2.5). Additive only — `extraComponents` is a new optional
|
|
528
|
+
field; no rename, no removal, no new warning code.
|
|
529
|
+
- **Message-type & structure awareness** (roadmap Phase G, P1) — a conservative
|
|
530
|
+
**misroute / truncation safety net**. `msg.structure` reports, for the common
|
|
531
|
+
message types, whether the core segment groups the HL7 v2.5.1 abstract syntax
|
|
532
|
+
marks **Required** for that trigger event are present; the parser also emits a
|
|
533
|
+
single additive Tier-2 `MISSING_EXPECTED_GROUP` warning per absent group (e.g.
|
|
534
|
+
an `ORU^R01` with no `OBR`/`OBX` result group). Keys on the **trigger event**,
|
|
535
|
+
not the message family, and models **Required anchors only** (EVN in ADT, PID
|
|
536
|
+
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
|
|
538
|
+
fixture emits zero structural warnings. Recognized types: ADT
|
|
539
|
+
(A01/A02/A03/A04/A05/A08/A11/A13), ORU^R01, ORM^O01, OML^O21, OMG^O19,
|
|
540
|
+
OMP^O09, OMI^O23, SIU (S12–S26), MDM (T02/T06), DFT^P03, VXU^V04, ACK; an
|
|
541
|
+
unrecognized type yields `recognized: false` and emits nothing. Warning-only
|
|
542
|
+
(Tier-2) — lenient parse never throws, `strict` may promote; the message
|
|
543
|
+
carries only structural facts (type, group, anchor names), never a field
|
|
544
|
+
value (no PHI). New public surface: `Hl7Message.structure`, the
|
|
545
|
+
`missingExpectedGroup` warning factory, the `MISSING_EXPECTED_GROUP` code, the
|
|
546
|
+
read-only `MESSAGE_STRUCTURE_DEFINITIONS` registry, `analyzeMessageStructure`,
|
|
547
|
+
and types `MessageStructure`, `StructureGroup`, `ExpectedSegmentGroup`,
|
|
548
|
+
`MessageStructureDefinition` (see `docs-content/spec-notes-structure.md`).
|
|
549
|
+
- **Coding-system provenance** (roadmap Phase F, P1) — `codingSystem(id)`,
|
|
550
|
+
`codingSystemOf(coded)`, and `alternateCodingSystemOf(coded)` answer "what
|
|
551
|
+
system does this code CLAIM?" off a `CWE` / `CE` (CWE.3 / CE.3 primary,
|
|
552
|
+
CWE.6 / CE.6 alternate). Alias-normalized + case-insensitive
|
|
553
|
+
(`LOINC` → `LN`, `SNOMED` → `SCT`, `RxNorm` → `RXN`) with the original
|
|
554
|
+
spelling preserved verbatim in `claimed`; an unregistered / local id is
|
|
555
|
+
surfaced verbatim with `known: false` (never dropped, never guessed); a
|
|
556
|
+
no-claim input returns `undefined`. The recognized subset is the frozen,
|
|
557
|
+
read-only `KNOWN_CODING_SYSTEMS`: `LN`, `SCT`, `I10`, `I10P`, `RXN`, `NDC`,
|
|
558
|
+
`CVX`, `MVX`, `UCUM`. **Provenance only** — no validation, lookup, network,
|
|
559
|
+
or bundled codeset. `I10` reports the registered Table 0396 claim `ICD-10`
|
|
560
|
+
(the WHO base), NOT a guessed `ICD-10-CM` (see
|
|
561
|
+
`docs-content/spec-notes-coding-system.md`). New public types
|
|
562
|
+
`KnownCodingSystem`, `CodingSystemInfo`, `CodedSystemFields`.
|
|
563
|
+
- **Parser** — `parseHL7(raw, optionsOrProfile?)` with a lenient default
|
|
564
|
+
parser that handles vendor-quirky HL7 v2.1–v2.8 input, and a
|
|
565
|
+
`{ strict: true }` mode that escalates every Tier-2 deviation to a thrown
|
|
566
|
+
`Hl7ParseError`. Accepts `string` or `Buffer` input; honours MSH-18
|
|
567
|
+
character set with a user `charset` override option.
|
|
568
|
+
- **Warning system** — 14 stable Tier-2 warning codes with positional
|
|
569
|
+
context (`segmentIndex`, `fieldIndex`, `repetitionIndex`, `componentIndex`,
|
|
570
|
+
`subcomponentIndex`): `MLLP_FRAMING_STRIPPED`, `FIELD_WHITESPACE_TRIMMED`,
|
|
571
|
+
`UNKNOWN_ESCAPE_SEQUENCE`, `TIMESTAMP_FALLBACK_FORMAT`, `SEGMENT_CASE`,
|
|
572
|
+
`EXTRA_FIELDS`, `UNKNOWN_SEGMENT`, `DUPLICATE_REQUIRED_SEGMENT`,
|
|
573
|
+
`ENCODING_MISMATCH`, `MISSING_REQUIRED_FIELD`, `OUT_OF_ORDER_SEGMENT`,
|
|
574
|
+
`VERSION_MISMATCH`, `UNKNOWN_CHARSET`, `ACK_NO_CORRELATION_ID`. Exposed via
|
|
575
|
+
`msg.warnings` and the `onWarning` callback.
|
|
576
|
+
- **Fatal errors** — 4 Tier-3 fatal codes always thrown as `Hl7ParseError`
|
|
577
|
+
(even in lenient mode): `NO_MSH_SEGMENT`, `MSH_TOO_SHORT`,
|
|
578
|
+
`INVALID_ENCODING_CHARACTERS`, `EMPTY_INPUT`. Each error carries
|
|
579
|
+
`message`, `position`, and `snippet`.
|
|
580
|
+
- **Structural model** — immutable `Hl7Message` with dot-path access
|
|
581
|
+
(`msg.get("PID.5.1")`, `msg.get("OBX[2].5")`), typed `Segment` and
|
|
582
|
+
`Field` wrappers, and `msg.segments("OBX")[0].field(3)` traversal.
|
|
583
|
+
Safe-access semantics (`undefined` / `[]` for missing paths, never
|
|
584
|
+
throws).
|
|
585
|
+
- **Composite types** — parsed instances and exported TypeScript
|
|
586
|
+
interfaces for XPN, XAD, CX, CWE, CE, XTN, PL, TS/DTM, NM, SN, HD, and XCN
|
|
587
|
+
(12 types). Also available under the `HL7` namespace:
|
|
588
|
+
`import { HL7 } from "@cosyte/hl7"; type T = HL7.XPN`.
|
|
589
|
+
- **`SN` composite + `Field.asSn()`** (roadmap Phase B) — `parseSn` /
|
|
590
|
+
`SN` / `HL7.SN` export the Structured Numeric datatype (comparator,
|
|
591
|
+
num1, separator/suffix, num2), and `field.asSn()` coerces an OBX-5 to it.
|
|
592
|
+
`msg.observations()` returns `{ valueType: "SN", value: SN | undefined }`
|
|
593
|
+
for `OBX-2 = SN`.
|
|
594
|
+
- **`observation.unitsAreUcum`** (roadmap Phase B) — a boolean claim-check
|
|
595
|
+
flag, `true` iff OBX-6's coding system (CWE.3) is exactly `UCUM`
|
|
596
|
+
(HL7 Table 0396). `false` when a unit is present but not declared UCUM
|
|
597
|
+
(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
|
|
599
|
+
or convert units.
|
|
600
|
+
- **Named helpers** — one-line extraction for the most common HL7
|
|
601
|
+
fields: `msg.meta`, `msg.patient`, `msg.visit`, `msg.observations()`,
|
|
602
|
+
`msg.orders()`, `msg.nextOfKin()`, `msg.allergies()`, `msg.diagnoses()`,
|
|
603
|
+
`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
|
|
606
|
+
RXO / RXE / RXD / RXA segment into a typed `Medication` across the four
|
|
607
|
+
pharmacy contexts (`order` / `encoded` / `dispense` / `administration`),
|
|
608
|
+
grouping the RXR (route, Table 0162 + site Table 0163) and RXC
|
|
609
|
+
(component) segments that follow each parent positionally — the same
|
|
610
|
+
state-machine `orders()` uses for OBR → OBX. The give code carries its
|
|
611
|
+
own coding-system provenance (`giveCode.nameOfCodingSystem`); the give
|
|
612
|
+
**amount** (how much) and the give **strength** (concentration, RXE-25/26)
|
|
613
|
+
are surfaced as **separate fields and are never reconciled** — a strength
|
|
614
|
+
a coded drug (e.g. an NDC) implies never validates or overwrites the
|
|
615
|
+
explicit RXE-25 strength, so a disagreement is preserved for the caller.
|
|
616
|
+
Numerics are strict-`asNm()` parsed (absent/blank → key omitted, never
|
|
617
|
+
`NaN`); output is frozen; not memoized. Never throws (HELPERS-07). New
|
|
618
|
+
public types: `Medication`, `MedicationContext`, `MedicationAmount`,
|
|
619
|
+
`MedicationStrength`, `MedicationRoute`, `MedicationComponent`.
|
|
620
|
+
- **`msg.immunizations()`** (roadmap Phase E, P0 safety) — projects every
|
|
621
|
+
RXA segment of a VXU^V04 into a typed `Immunization`, grouping the RXR
|
|
622
|
+
(route, Table 0162 + site Table 0163) and OBX (VFC eligibility / funding
|
|
623
|
+
source) segments that follow each RXA positionally, and carrying
|
|
624
|
+
`orderControl` from the preceding ORC of the `ORC`→`RXA`→`[RXR]`→`[{OBX}]`
|
|
625
|
+
order group — the same state machine `orders()` uses for OBR → OBX. The
|
|
626
|
+
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
|
|
628
|
+
`recordOrigin` (administered vs historical) is derived **only** from the
|
|
629
|
+
well-known NIP001 RXA-9.1 codes (`00`; `01`-`08`) and omitted otherwise —
|
|
630
|
+
never guessed, with the raw RXA-9 claim always preserved on
|
|
631
|
+
`informationSource`. The vaccine code carries its own provenance
|
|
632
|
+
(`vaccineCode.nameOfCodingSystem`, CVX, + any CVX/NDC alternate);
|
|
633
|
+
`doseAmount` is strict-`asNm()` parsed (the IIS unknown-dose sentinel `999`
|
|
634
|
+
surfaced as the number `999`, never coerced). Output is frozen; not
|
|
635
|
+
memoized. Never throws (HELPERS-07). New public types: `Immunization`,
|
|
636
|
+
`ImmunizationRecordOrigin`.
|
|
637
|
+
- **Mutation** — `setField`, `addSegment`, `removeSegment` on
|
|
638
|
+
`Hl7Message`. Direct field mutation on unwrapped objects has no effect
|
|
639
|
+
(immutability by default).
|
|
640
|
+
- **Serialization** — `msg.toString()` emits spec-clean HL7 regardless
|
|
641
|
+
of input quirks (Postel's Law); `msg.toJSON()` returns a structured
|
|
642
|
+
JSON tree; `msg.prettyPrint()` returns a human-readable multi-line
|
|
643
|
+
string for logs. Escape sequences are re-encoded on serialize.
|
|
644
|
+
- **Message builder** — `buildMessage({...}).addSegment(...).toString()`
|
|
645
|
+
constructs valid outbound HL7 from scratch, with helpers for control
|
|
646
|
+
IDs and HL7 timestamps.
|
|
647
|
+
- **ACK generation + interpretation** (roadmap Phase C) — `buildAck(inbound,
|
|
648
|
+
{ code, error?, mode? })` produces a spec-clean `MSH`+`MSA`[+`ERR`…]
|
|
649
|
+
acknowledgment: sender/receiver swapped (full multi-component HDs
|
|
650
|
+
preserved), MSH-9 = `ACK^<trigger>^ACK`, MSA-2 echoes the inbound MSH-10,
|
|
651
|
+
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
|
|
653
|
+
only, never echoed PHI**. `buildAck` is mechanical (emits the disposition it
|
|
654
|
+
is told) with one safety override: an inbound with no MSH-10 cannot be
|
|
655
|
+
correlated, so a requested positive `AA`/`CA` is downgraded to `AE`/`CE`,
|
|
656
|
+
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.
|
|
658
|
+
`interpretAck(msg)` is the read-side: a typed `Acknowledgment` view whose
|
|
659
|
+
`accepted`/`error`/`rejected` flags are derived fail-safe from MSA-1 (all
|
|
660
|
+
three `false` on an absent or unrecognized code). `detectAckMode(inbound)`
|
|
661
|
+
exposes the spec-exact original-vs-enhanced detection (MSH-15/16). Control
|
|
662
|
+
vocabulary (Tables 0008/0357/0516/0155) ships as frozen read-only enums
|
|
663
|
+
(`ACK_CODES`, `ERR_CONDITION_CODES`, `ERR_SEVERITIES`, `ACK_CONDITIONS`).
|
|
664
|
+
- **Profile system** — `defineProfile()` API with `extends` composition
|
|
665
|
+
(single parent or array), merge semantics (scalars overwrite, arrays
|
|
666
|
+
concat+dedupe, `customSegments` deep-merge per key, `onWarning` chains),
|
|
667
|
+
`profile.describe()` introspection, `profile.lineage`, and
|
|
668
|
+
`ProfileDefinitionError` with actionable messages on invalid input.
|
|
669
|
+
- **Default profile management** — `setDefaultProfile(p)`,
|
|
670
|
+
`getDefaultProfile()`, `setDefaultProfile(null)`. Explicit arguments
|
|
671
|
+
override; `parseHL7(raw, { profile: null })` opts out for a single call.
|
|
672
|
+
- **Five built-in vendor profiles** — `profiles.epic`, `profiles.cerner`,
|
|
673
|
+
`profiles.meditech`, `profiles.athena`, `profiles.genericLab`. Each
|
|
674
|
+
authored through the public `defineProfile()` API with date-format
|
|
675
|
+
fallbacks and named Z-segments.
|
|
676
|
+
- **Segment.get(name)** — resolve custom-segment fields by declared
|
|
677
|
+
name (e.g., `msg.segments("ZDP")[0].get("departmentCode")`) when a
|
|
678
|
+
matching profile is applied.
|
|
679
|
+
- **Three runnable examples** — `examples/extract-patient-info.ts`,
|
|
680
|
+
`examples/read-lab-results.ts`, `examples/modify-and-resend.ts`, each
|
|
681
|
+
runnable via `pnpm tsx examples/<file>.ts` and smoke-tested via
|
|
682
|
+
`pnpm examples` in CI.
|
|
683
|
+
- **Profile starter kit** — `examples/profile-starter-kit/`, a
|
|
684
|
+
publishable template with its own CI + publish workflows,
|
|
685
|
+
`CUSTOMIZING.md` walkthrough, and placeholder tokens
|
|
686
|
+
(`{{YOUR_ORG}}` / `{{PROFILE_NAME}}`) that turn into a ready-to-publish
|
|
687
|
+
profile package in minutes.
|
|
688
|
+
- **Documentation** — comprehensive README (value prop, quickstart,
|
|
689
|
+
feature list, HL7-in-90-seconds primer, three access patterns, full
|
|
690
|
+
cookbook, Profiles section, Real-World Tolerance table, Error
|
|
691
|
+
Handling, Roadmap), `CHANGELOG.md`, `CONTRIBUTING.md`, `LICENSE`
|
|
692
|
+
(MIT).
|
|
693
|
+
- **Tooling** — strict TypeScript (`noUncheckedIndexedAccess`,
|
|
694
|
+
`exactOptionalPropertyTypes`), dual ESM + CJS build via tsup, Vitest
|
|
695
|
+
with ≥ 90% per-directory branch coverage on `src/parser/`, `src/model/`,
|
|
696
|
+
`src/helpers/`, `src/serialize/`, `src/builder/`. Lint, format, and
|
|
697
|
+
TypeScript settings come from the shared `@cosyte/*` config packages
|
|
698
|
+
(ESLint 9 + `typescript-eslint`). CI across Node 22 / 24.
|
|
699
|
+
|
|
700
|
+
### Changed
|
|
701
|
+
|
|
702
|
+
### Deprecated
|
|
703
|
+
|
|
704
|
+
### Removed
|
|
705
|
+
|
|
706
|
+
### Fixed
|
|
707
|
+
|
|
708
|
+
### Security
|
|
709
|
+
|
|
710
|
+
[Unreleased]: https://github.com/cosyte/hl7/commits/main
|