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