@cosyte/hl7 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +436 -147
- package/README.md +154 -91
- package/TRADEMARKS.md +37 -0
- package/dist/index.cjs +1504 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2060 -477
- package/dist/index.d.ts +2060 -477
- package/dist/index.mjs +1479 -112
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @cosyte/hl7
|
|
2
2
|
|
|
3
|
-
> Parse real-world, vendor-quirky HL7 v2 messages and extract the fields you need in one line
|
|
3
|
+
> Parse real-world, vendor-quirky HL7 v2 messages and extract the fields you need in one line, without reading the spec.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@cosyte/hl7)
|
|
6
6
|
[](https://github.com/cosyte/hl7/actions/workflows/ci.yml)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
|
|
10
|
-
A developer-focused HL7 v2 parser and utility library for Node.js and TypeScript. Optimised for the 10% of HL7 you actually use
|
|
10
|
+
A developer-focused HL7 v2 parser and utility library for Node.js and TypeScript. Optimised for the 10% of HL7 you actually use, with the other 90% still one accessor away when you need it.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ A developer-focused HL7 v2 parser and utility library for Node.js and TypeScript
|
|
|
16
16
|
Three lines of useful output after install + parse. No HL7 spec knowledge required.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
# pnpm (recommended)
|
|
19
|
+
# pnpm (recommended). Also works with: npm install @cosyte/hl7 | yarn add @cosyte/hl7
|
|
20
20
|
pnpm add @cosyte/hl7
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -26,22 +26,22 @@ import { parseHL7 } from "@cosyte/hl7";
|
|
|
26
26
|
const msg = parseHL7(rawHL7);
|
|
27
27
|
console.log(msg.patient?.fullName); // "John Q. Doe"
|
|
28
28
|
console.log(msg.patient?.mrn); // "MRN12345"
|
|
29
|
-
console.log(msg.meta.timestamp?.raw); // "20260419101500" (fidelity TS
|
|
29
|
+
console.log(msg.meta.timestamp?.raw); // "20260419101500" (fidelity TS: precision + zone preserved)
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
That's the whole pitch: no config, no schema upload, no spec lookup. The parser accepts vendor-quirky input by default, strips MLLP framing if it's there, normalises casing, and tolerates the dozen-or-so deviations real HL7 traffic routinely carries. You reach for strict mode, dot-paths, or profiles when you want them
|
|
32
|
+
That's the whole pitch: no config, no schema upload, no spec lookup. The parser accepts vendor-quirky input by default, strips MLLP framing if it's there, normalises casing, and tolerates the dozen-or-so deviations real HL7 traffic routinely carries. You reach for strict mode, dot-paths, or profiles when you want them, not before.
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
|
-
- **One-line extraction
|
|
39
|
-
- **Three access patterns
|
|
40
|
-
- **Real-world tolerance, four-tier
|
|
41
|
-
- **First-class profile system
|
|
42
|
-
- **Round-trip safe, byte-verbatim escapes
|
|
43
|
-
- **Strict TypeScript, zero runtime deps
|
|
44
|
-
- **Warnings carry stable codes + positional context
|
|
38
|
+
- **One-line extraction**: `msg.patient.mrn`, `msg.meta.timestamp`, `msg.observations()`, and friends. No segment or field numbers to memorise.
|
|
39
|
+
- **Three access patterns**: named helpers, dot-paths (`msg.get("PID.5.1")`), or structural traversal (`msg.segments("OBX")[0].field(3)`). Pick the level of ceremony you need.
|
|
40
|
+
- **Real-world tolerance, four-tier**: lenient default parses vendor-quirky messages; 18 stable warning codes flag what was tolerated; strict mode escalates every deviation for CI validators; only 4 truly-structural failures are fatal.
|
|
41
|
+
- **First-class profile system**: `defineProfile()` API, 8 built-in vendor profiles (Epic, Cerner, Meditech, athenahealth, generic lab, Visage 7 imaging/PACS, Philips Vue PACS, VA VistA Radiology/NucMed), plus a [publishable starter kit](./examples/profile-starter-kit/) you copy-and-ship.
|
|
42
|
+
- **Round-trip safe, byte-verbatim escapes**: `parse -> modify -> toString()` emits spec-clean HL7 regardless of input quirks (Postel's Law: liberal parser, conservative emitter), and a parsed field's escape sequences (`\H\`, `\X41\`, charset/vendor escapes) re-emit **byte-for-byte**. See [Escapes & round-trip](./docs-content/spec-notes-escapes.md).
|
|
43
|
+
- **Strict TypeScript, zero runtime deps**: ES2022, `noUncheckedIndexedAccess`, dual ESM + CJS, Node 18+. Every public export has JSDoc + `@example` that feeds your editor's IntelliSense.
|
|
44
|
+
- **Warnings carry stable codes + positional context**: react programmatically by `w.code`, with `segmentIndex`/`fieldIndex`/etc. attached.
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -53,11 +53,11 @@ A typical ADT message looks like this:
|
|
|
53
53
|
|
|
54
54
|
```
|
|
55
55
|
Message
|
|
56
|
-
├── MSH (header
|
|
56
|
+
├── MSH (header: sender, receiver, type, timestamp)
|
|
57
57
|
├── EVN (event details for ADT)
|
|
58
|
-
├── PID (patient identification
|
|
59
|
-
├── PV1 (visit
|
|
60
|
-
└── OBX×N (observations
|
|
58
|
+
├── PID (patient identification: name, MRN, DOB)
|
|
59
|
+
├── PV1 (visit: class, location, attending)
|
|
60
|
+
└── OBX×N (observations: repeats for labs, vitals)
|
|
61
61
|
|
|
62
62
|
Each segment = pipe-delimited (|) fields
|
|
63
63
|
Each field = caret-delimited (^) components
|
|
@@ -83,10 +83,10 @@ const msg = parseHL7(raw);
|
|
|
83
83
|
|
|
84
84
|
console.log(msg.patient?.mrn); // "MRN12345"
|
|
85
85
|
console.log(msg.patient?.fullName); // "Jane Q. Smith"
|
|
86
|
-
console.log(msg.patient?.dateOfBirth?.precision); // "day"
|
|
86
|
+
console.log(msg.patient?.dateOfBirth?.precision); // "day": a birth date is not a full instant
|
|
87
87
|
console.log(msg.meta.type); // "ADT^A01"
|
|
88
|
-
console.log(msg.meta.timestamp?.raw); // "20250102153045" (MSH-7
|
|
89
|
-
console.log(msg.visit?.location); // PL composite
|
|
88
|
+
console.log(msg.meta.timestamp?.raw); // "20250102153045" (MSH-7: fidelity TS)
|
|
89
|
+
console.log(msg.visit?.location); // PL composite: pointOfCare/room/bed
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
Use these for the 90% case. Helpers return `undefined` (not throws) when the underlying segment is absent, so optional-chaining stays idiomatic.
|
|
@@ -106,7 +106,7 @@ console.log(msg.get("OBX[2].5")); // 2nd OBX, value field
|
|
|
106
106
|
console.log(msg.getAll("NK1")); // every next-of-kin segment
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
Reach for dot-paths when you want a specific field the helpers don't surface
|
|
109
|
+
Reach for dot-paths when you want a specific field the helpers don't surface, or when you're scripting quick extractions without introducing typed imports.
|
|
110
110
|
|
|
111
111
|
### Structural traversal
|
|
112
112
|
|
|
@@ -126,7 +126,7 @@ for (const obx of msg.segments("OBX")) {
|
|
|
126
126
|
console.log(msg.allSegments().length); // total segments in the message
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
Every level (segment / field / repetition / component / subcomponent) is addressable and immutable
|
|
129
|
+
Every level (segment / field / repetition / component / subcomponent) is addressable and immutable: mutation happens through explicit methods (see the cookbook).
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
@@ -146,12 +146,12 @@ const p = msg.patient;
|
|
|
146
146
|
|
|
147
147
|
console.log(p?.mrn); // "MRN12345"
|
|
148
148
|
console.log(p?.fullName); // "Jane Q. Smith"
|
|
149
|
-
console.log(p?.dateOfBirth?.raw); // "19800115" (PID-7
|
|
149
|
+
console.log(p?.dateOfBirth?.raw); // "19800115" (PID-7: fidelity TS at day precision)
|
|
150
150
|
console.log(p?.sex); // "F"
|
|
151
151
|
console.log(p?.address?.city); // XAD composite
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
`msg.patient` is `undefined` when the message has no `PID` segment
|
|
154
|
+
`msg.patient` is `undefined` when the message has no `PID` segment. Use `?.` consistently. The `address` field is an `XAD` composite with `streetAddress`, `city`, `state`, `zip`, `country`. `dateOfBirth` (and every datetime) is a **fidelity `TS`**. See [Datetime precision & timezone fidelity](#datetime-precision--timezone-fidelity).
|
|
155
155
|
|
|
156
156
|
### Lab results
|
|
157
157
|
|
|
@@ -172,7 +172,7 @@ Observations are discriminated by `valueType` (`"NM"` -> number, `"TS"`/`"DT"` -
|
|
|
172
172
|
|
|
173
173
|
### Admit location
|
|
174
174
|
|
|
175
|
-
The `msg.visit` helper surfaces `PV1` data including the `PL` (Person Location) composite
|
|
175
|
+
The `msg.visit` helper surfaces `PV1` data including the `PL` (Person Location) composite: ward / room / bed / facility in one go.
|
|
176
176
|
|
|
177
177
|
```ts
|
|
178
178
|
import { parseHL7 } from "@cosyte/hl7";
|
|
@@ -230,32 +230,32 @@ Three breadth helpers cover the scheduling, medical-record, and financial messag
|
|
|
230
230
|
```ts
|
|
231
231
|
import { parseHL7 } from "@cosyte/hl7";
|
|
232
232
|
|
|
233
|
-
// SIU
|
|
233
|
+
// SIU, appointments(): SCH id/status/timing + AIS/AIG/AIL/AIP resources
|
|
234
234
|
for (const appt of parseHL7(raw).appointments()) {
|
|
235
235
|
console.log(appt.fillerAppointmentId, appt.fillerStatusCode?.identifier); // status verbatim
|
|
236
236
|
const provider = appt.resources.find((r) => r.kind === "personnel");
|
|
237
237
|
console.log(provider?.person?.familyName);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// MDM
|
|
240
|
+
// MDM, documents(): completion (TXA-17) and availability (TXA-19) are DISTINCT
|
|
241
241
|
for (const doc of parseHL7(raw).documents()) {
|
|
242
242
|
console.log(doc.completionStatus, doc.availabilityStatus); // e.g. "IP" (in progress) + "AV" (available)
|
|
243
|
-
// never conflated
|
|
243
|
+
// never conflated: a document can be AVAILABLE before it is AUTHENTICATED;
|
|
244
244
|
// reading a preliminary document as final is the harm this split prevents.
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
// DFT
|
|
247
|
+
// DFT, charges(): FT1 billing fields, amounts kept as canonical wire text (no money-as-float)
|
|
248
248
|
for (const charge of parseHL7(raw).charges()) {
|
|
249
249
|
console.log(charge.transactionType, charge.transactionCode?.identifier);
|
|
250
|
-
console.log(charge.amountExtended); // e.g. "150.00^USD"
|
|
250
|
+
console.log(charge.amountExtended); // e.g. "150.00^USD": a string, never a number
|
|
251
251
|
}
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
These are **breadth** helpers
|
|
254
|
+
These are **breadth** helpers: they surface the common trigger events' core fields, not a scheduling-workflow state machine, signature verification, or a claims/pricing engine (see the roadmap's known-limitations).
|
|
255
255
|
|
|
256
256
|
### Order & medication timing
|
|
257
257
|
|
|
258
|
-
Every `Order` (from `msg.orders()`) and `Medication` (from `msg.medications()`) carries a `timings` array
|
|
258
|
+
Every `Order` (from `msg.orders()`) and `Medication` (from `msg.medications()`) carries a `timings` array: the `TQ1` segment (HL7 v2.5+) or the legacy embedded TQ in `ORC-7` / `RXE-1` (pre-v2.5).
|
|
259
259
|
|
|
260
260
|
```ts
|
|
261
261
|
import { parseHL7 } from "@cosyte/hl7";
|
|
@@ -264,19 +264,19 @@ const med = parseHL7(raw).medications()[0];
|
|
|
264
264
|
|
|
265
265
|
for (const t of med?.timings ?? []) {
|
|
266
266
|
console.log(t.source); // "TQ1" or "legacy"
|
|
267
|
-
console.log(t.repeatPattern?.code); // "Q6H"
|
|
267
|
+
console.log(t.repeatPattern?.code); // "Q6H": VERBATIM, never resolved to a schedule
|
|
268
268
|
console.log(t.repeatPattern?.kind); // "parametric" (provenance only)
|
|
269
|
-
console.log(t.repeatPattern?.interval); // { count: 6, unit: "H" }
|
|
270
|
-
console.log(t.totalOccurrences); // 24
|
|
269
|
+
console.log(t.repeatPattern?.interval); // { count: 6, unit: "H" }, the load-bearing integer
|
|
270
|
+
console.log(t.totalOccurrences); // 24, from TQ1-14 (not TQ1-11)
|
|
271
271
|
console.log(t.startDateTime?.raw, t.startDateTime?.precision);
|
|
272
272
|
}
|
|
273
273
|
```
|
|
274
274
|
|
|
275
|
-
The repeat pattern (Table 0335) is surfaced **verbatim
|
|
275
|
+
The repeat pattern (Table 0335) is surfaced **verbatim**: hl7 never normalizes a sig, resolves `Q6H` to clock times, or maps it to a different frequency (reading `Q6H` as "daily", or losing a `BID`, changes the administered dose count). The `kind` flag (`parametric`/`named`/`unknown`) is provenance only and never drives a schedule; a parametric `Q<n><unit>` template's integer is never dropped. See `docs-content/spec-notes-timing.md` for the field map and non-goals.
|
|
276
276
|
|
|
277
277
|
### Patient merges and identity events
|
|
278
278
|
|
|
279
|
-
`msg.identityEvents()` recognizes the ADT identity-management trigger events
|
|
279
|
+
`msg.identityEvents()` recognizes the ADT identity-management trigger events: merges (A18/A34/A35/A36/A39/A40/A41/A42), moves (A43/A44), link/unlink (A24/A37), and person add/update (A28/A31). It surfaces every party **labelled by role**. On a merge, the MRG segment carries the _prior_ (non-surviving) identifiers and the PID carries the _surviving_ ones; the direction is the spec constant `MRG_TO_PID` and is never inferred from content.
|
|
280
280
|
|
|
281
281
|
```ts
|
|
282
282
|
import { parseHL7 } from "@cosyte/hl7";
|
|
@@ -288,12 +288,12 @@ for (const ev of msg.identityEvents()) {
|
|
|
288
288
|
// retire ev.prior.identifiers in favour of ev.surviving.identifiers
|
|
289
289
|
console.log(ev.prior.identifiers[0]?.idNumber, "->", ev.surviving.identifiers[0]?.idNumber);
|
|
290
290
|
} else if (ev.warnings.length > 0) {
|
|
291
|
-
// incomplete MRG->PID pair (MERGE_MISSING_PRIOR_OR_SURVIVOR)
|
|
291
|
+
// incomplete MRG->PID pair (MERGE_MISSING_PRIOR_OR_SURVIVOR), route for review
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
```
|
|
295
295
|
|
|
296
|
-
A mis-applied merge conflates two patients or orphans data under a retired MRN, so the helper is deliberately conservative: an incomplete pair (no MRG, an orphaned MRG, or a PID with no surviving identifier) surfaces whatever _is_ present plus a `MERGE_MISSING_PRIOR_OR_SURVIVOR` warning on the event
|
|
296
|
+
A mis-applied merge conflates two patients or orphans data under a retired MRN, so the helper is deliberately conservative: an incomplete pair (no MRG, an orphaned MRG, or a PID with no surviving identifier) surfaces whatever _is_ present plus a `MERGE_MISSING_PRIOR_OR_SURVIVOR` warning on the event: the MRG is never dropped, and the direction is never guessed. The MRG field map is version-scoped: the backward-compat single-ID fields (PID-2 / MRG-4, withdrawn as of HL7 v2.7) are not read when MSH-12 declares v2.7+. Note that `@cosyte/hl7` _surfaces_ the merge. Actually re-pointing stored data to the survivor is your integration engine's job.
|
|
297
297
|
|
|
298
298
|
### Write your first profile in 10 minutes
|
|
299
299
|
|
|
@@ -359,7 +359,7 @@ Conflicts on scalar keys (later wins), arrays concatenate+dedupe, `customSegment
|
|
|
359
359
|
|
|
360
360
|
### Publishing a profile package
|
|
361
361
|
|
|
362
|
-
Real production specs live at the integration level
|
|
362
|
+
Real production specs live at the integration level: specific EHR instances, reference labs, HIEs. The [profile starter kit](./examples/profile-starter-kit/) is a copy-and-customise template: it ships publishable as-is, with placeholders you replace with your org/profile names.
|
|
363
363
|
|
|
364
364
|
```bash
|
|
365
365
|
cp -r examples/profile-starter-kit my-profile && cd my-profile
|
|
@@ -388,11 +388,11 @@ const bare = parseHL7(raw, { profile: null }); // opt out for this call
|
|
|
388
388
|
setDefaultProfile(null); // reset
|
|
389
389
|
```
|
|
390
390
|
|
|
391
|
-
The default is scoped to the current Node process
|
|
391
|
+
The default is scoped to the current Node process: not shared across workers, not serialisable. Opt out for a specific parse with `{ profile: null }` in the options bag.
|
|
392
392
|
|
|
393
393
|
### Datetime precision & timezone fidelity
|
|
394
394
|
|
|
395
|
-
Every HL7 datetime (TS/DTM) is surfaced as a **fidelity `TS
|
|
395
|
+
Every HL7 datetime (TS/DTM) is surfaced as a **fidelity `TS`**: the raw string plus its typed parts,
|
|
396
396
|
with the stated **precision** and **timezone** preserved. It is deliberately **not** an eager JS `Date`:
|
|
397
397
|
a day-only birth date coerced to a UTC-midnight instant reads as the _previous day_ in a negative-offset
|
|
398
398
|
zone. The parser preserves what the sender wrote and lets you decide how to localize.
|
|
@@ -402,11 +402,11 @@ import { parseHL7, dtmToDate } from "@cosyte/hl7";
|
|
|
402
402
|
|
|
403
403
|
const dob = parseHL7(raw).patient?.dateOfBirth;
|
|
404
404
|
console.log(dob?.raw); // "19880705"
|
|
405
|
-
console.log(dob?.precision); // "day"
|
|
405
|
+
console.log(dob?.precision); // "day": never zero-filled to a full timestamp
|
|
406
406
|
console.log(dob?.year, dob?.month, dob?.day); // 1988 7 5 (month is 1-based, spec-native)
|
|
407
|
-
console.log(dob?.hasTimezone); // false
|
|
407
|
+
console.log(dob?.hasTimezone); // false: a missing offset is FLAGGED, never assumed to be UTC
|
|
408
408
|
|
|
409
|
-
// An absolute instant is opt-in and honest
|
|
409
|
+
// An absolute instant is opt-in and honest. It refuses to guess a zone:
|
|
410
410
|
console.log(dtmToDate(dob!)); // undefined (no offset, no assumption)
|
|
411
411
|
console.log(dtmToDate(dob!, { assumeOffsetMinutes: 0 })); // 1988-07-05T00:00:00.000Z (you chose UTC)
|
|
412
412
|
|
|
@@ -417,13 +417,13 @@ console.log(dtmToDate(ts!)?.toISOString()); // "2025-01-02T20:30:45.000Z"
|
|
|
417
417
|
```
|
|
418
418
|
|
|
419
419
|
hl7 preserves precision + timezone **fidelity**; it does **not** localize, convert, or do arithmetic on
|
|
420
|
-
timestamps
|
|
420
|
+
timestamps. A consumer needing an absolute instant applies the sender's zone via `assumeOffsetMinutes`.
|
|
421
421
|
Use `parseDtm` / `formatDtm` / `dtmToDate` directly on a raw string when you're outside the message model.
|
|
422
422
|
|
|
423
423
|
#### Non-standard timestamp formats
|
|
424
424
|
|
|
425
|
-
HL7's canonical `YYYYMMDDHHmmss` parses with zero warnings. Everything else
|
|
426
|
-
`MM/DD/YYYY`, ISO `YYYY-MM-DD`, legacy `YYYYMMDD HHmm`
|
|
425
|
+
HL7's canonical `YYYYMMDDHHmmss` parses with zero warnings. Everything else (vendor-quirky
|
|
426
|
+
`MM/DD/YYYY`, ISO `YYYY-MM-DD`, legacy `YYYYMMDD HHmm`) parses via the `dateFormats` option, which
|
|
427
427
|
tries each format in order and populates the same fidelity `TS`.
|
|
428
428
|
|
|
429
429
|
```ts
|
|
@@ -436,7 +436,7 @@ const msg = parseHL7(raw, {
|
|
|
436
436
|
console.log(msg.meta.timestamp?.matchedFormat); // e.g. "MM/DD/YYYY" (which fallback won)
|
|
437
437
|
```
|
|
438
438
|
|
|
439
|
-
When a fallback format wins, the parser emits a `TIMESTAMP_FALLBACK_FORMAT` warning with the matched format on `msg.warnings`. Built-in vendor profiles (`profiles.epic`, `profiles.genericLab`, etc.) already carry the date formats common to that vendor
|
|
439
|
+
When a fallback format wins, the parser emits a `TIMESTAMP_FALLBACK_FORMAT` warning with the matched format on `msg.warnings`. Built-in vendor profiles (`profiles.epic`, `profiles.genericLab`, etc.) already carry the date formats common to that vendor. Reach for a profile instead of hand-listing formats when one fits.
|
|
440
440
|
|
|
441
441
|
### Stripping MLLP framing
|
|
442
442
|
|
|
@@ -453,11 +453,11 @@ console.log(framed); // true if input had VT/FS/CR framing
|
|
|
453
453
|
parseHL7(raw, { stripMllpFraming: false });
|
|
454
454
|
```
|
|
455
455
|
|
|
456
|
-
Strip behaviour is idempotent
|
|
456
|
+
Strip behaviour is idempotent: calling `parseHL7` on already-stripped input is a no-op. The parser does NOT add MLLP framing on `toString()`; if you need it for transport, prepend/append the bytes in your transport layer (a future `@cosyte/hl7-mllp` package will cover network IO end to end).
|
|
457
457
|
|
|
458
458
|
### Batch files
|
|
459
459
|
|
|
460
|
-
Real lab / ELR / IIS feeds ship many messages wrapped in the HL7 v2 batch envelope (`[FHS] { [BHS] { MSH… } [BTS] } [FTS]`). `splitBatch()` demarcates the individual messages by `MSH` boundary and hands each one back **already parsed
|
|
460
|
+
Real lab / ELR / IIS feeds ship many messages wrapped in the HL7 v2 batch envelope (`[FHS] { [BHS] { MSH… } [BTS] } [FTS]`). `splitBatch()` demarcates the individual messages by `MSH` boundary and hands each one back **already parsed**: a malformed message mid-stream is isolated (returned as a typed failure), never suppressing its siblings, and the declared BTS-1 / FTS-1 counts are reconciled without ever dropping the tail.
|
|
461
461
|
|
|
462
462
|
```ts
|
|
463
463
|
import { splitBatch } from "@cosyte/hl7";
|
|
@@ -472,18 +472,41 @@ for (const entry of messages) {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
// Batch-level warnings carry counts/positions only
|
|
475
|
+
// Batch-level warnings carry counts/positions only, never PHI.
|
|
476
476
|
for (const w of warnings) {
|
|
477
|
-
// BATCH_COUNT_MISMATCH
|
|
478
|
-
// BATCH_MISSING_TRAILER
|
|
477
|
+
// BATCH_COUNT_MISMATCH: declared BTS-1/FTS-1 ≠ actual (tail still returned)
|
|
478
|
+
// BATCH_MISSING_TRAILER: a BHS/FHS header with no matching BTS/FTS
|
|
479
479
|
}
|
|
480
480
|
```
|
|
481
481
|
|
|
482
482
|
A **bare single message** (no envelope) passes straight through as a one-message result. `splitBatch` is a splitter, not an enforcer: it warns on a missing trailer but leaves accept/reject to you (a profile such as IIS may mandate the full frame), does not generate batch ACKs, and does not de-batch across transport framing (that is [`@cosyte/mllp`](https://github.com/cosyte/mllp)). The second argument is forwarded to `parseHL7` per message (profile, `strict`, `charset`). See the [batch spec notes](./docs-content/spec-notes-batch.md) for the full traceability + known limitations.
|
|
483
483
|
|
|
484
|
+
### Streaming large files (`parseStream`)
|
|
485
|
+
|
|
486
|
+
`splitBatch` needs the whole file in memory. For a feed or file too large to buffer (or a live source) `parseStream()` parses **incrementally**: it consumes a chunked source (a Node `Readable`, an async-iterable, or an iterable of `string`/`Buffer` chunks) and **yields one message per `MSH` boundary as it completes**, holding only **O(one message)** of state. A message split across chunk boundaries (mid-segment, mid-field, even mid-`MSH|^~\&`) is reassembled: feeding the same bytes in 1-byte chunks or one big chunk yields identical messages.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { createReadStream } from "node:fs";
|
|
490
|
+
import { parseStream, WARNING_CODES } from "@cosyte/hl7";
|
|
491
|
+
|
|
492
|
+
for await (const entry of parseStream(createReadStream("elr-feed.hl7"))) {
|
|
493
|
+
if (entry.ok) {
|
|
494
|
+
handle(entry.message); // released before the next is read: bounded memory
|
|
495
|
+
} else {
|
|
496
|
+
quarantine(entry.raw, entry.error.code); // isolated; the tail still streams
|
|
497
|
+
}
|
|
498
|
+
// Stream-level (not per-message) diagnostics, never PHI:
|
|
499
|
+
for (const w of entry.streamWarnings) {
|
|
500
|
+
// UNTERMINATED_STREAM_MESSAGE: the final message had no terminator (truncated feed?)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Each message is parsed by `parseHL7` (no second grammar; the same second argument is forwarded per message), so a streamed message is byte-for-byte what `splitBatch` would produce for the same bytes. A malformed message mid-stream is an isolated typed failure entry (it **never suppresses** later messages) and an unterminated final message is still yielded in full, flagged (never thrown). Batch-envelope segments (`FHS`/`BHS`/`BTS`/`FTS`) act as boundaries and are never yielded, so the yielded count equals the `MSH` count; envelope **count reconciliation** stays `splitBatch`'s job. Transport framing (MLLP) is out of scope: this consumes an already-de-framed stream; the wire is [`@cosyte/mllp`](https://github.com/cosyte/mllp)'s. See the [streaming spec notes](./docs-content/spec-notes-stream.md).
|
|
506
|
+
|
|
484
507
|
### Detect message type
|
|
485
508
|
|
|
486
|
-
`msg.meta` exposes MSH-9 pre-decomposed into its three components
|
|
509
|
+
`msg.meta` exposes MSH-9 pre-decomposed into its three components: use them instead of parsing the raw string.
|
|
487
510
|
|
|
488
511
|
```ts
|
|
489
512
|
import { parseHL7 } from "@cosyte/hl7";
|
|
@@ -504,7 +527,7 @@ Matching on `messageCode` + `triggerEvent` is more robust than string-equals on
|
|
|
504
527
|
|
|
505
528
|
### Spot a truncated or misrouted message
|
|
506
529
|
|
|
507
|
-
For the common message types, `msg.structure` reports whether the core segment groups the HL7 spec marks **Required** for that trigger event are actually present. It's a **misroute / truncation safety net**, not a conformance validator
|
|
530
|
+
For the common message types, `msg.structure` reports whether the core segment groups the HL7 spec marks **Required** for that trigger event are actually present. It's a **misroute / truncation safety net**, not a conformance validator: an `ORU^R01` that arrives with no `OBR`/`OBX` result group is almost always truncated or sent to the wrong feed.
|
|
508
531
|
|
|
509
532
|
```ts
|
|
510
533
|
import { parseHL7, WARNING_CODES } from "@cosyte/hl7";
|
|
@@ -521,11 +544,43 @@ if (msg.warnings.some((w) => w.code === WARNING_CODES.MISSING_EXPECTED_GROUP)) {
|
|
|
521
544
|
}
|
|
522
545
|
```
|
|
523
546
|
|
|
524
|
-
The check is **conservative by construction**: it only models groups that are genuinely Required (so a conformant-but-sparse message never warns), it keys on the **trigger event** (not the message family), and a type it doesn't recognize yields `recognized: false` and emits nothing. It never throws and never rewrites the message
|
|
547
|
+
The check is **conservative by construction**: it only models groups that are genuinely Required (so a conformant-but-sparse message never warns), it keys on the **trigger event** (not the message family), and a type it doesn't recognize yields `recognized: false` and emits nothing. It never throws and never rewrites the message. `strict` mode may promote the warning to an error per the usual model. Recognized types: ADT (A01/A02/A03/A04/A05/A08/A11/A13), ORU^R01, ORM^O01, OML^O21, OMG^O19, OMP^O09, OMI^O23, SIU (S12–S26), MDM (T02/T06), DFT^P03, VXU^V04, and ACK. See [`docs-content/spec-notes-structure.md`](docs-content/spec-notes-structure.md) for the per-type Required-segment sourcing.
|
|
548
|
+
|
|
549
|
+
### Validate against your own conformance profile (`validateAgainstProfile`)
|
|
550
|
+
|
|
551
|
+
`msg.structure` checks the handful of groups the base spec marks Required. When you have a **specific interface spec** ("our ADT feed requires PID-3, sex must be M/F/U, no Z-segments"), bring it as a declarative **conformance profile** and `validateAgainstProfile(msg, profile)` returns typed findings. **You author the profile and every value set; hl7 ships none**: no bundled vendor/IHE profile, no code set, no network call.
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
import { parseHL7, validateAgainstProfile, type ConformanceProfile } from "@cosyte/hl7";
|
|
555
|
+
|
|
556
|
+
// A profile YOU author: an example, NOT an attestation of conformance.
|
|
557
|
+
const profile: ConformanceProfile = {
|
|
558
|
+
name: "our-adt-intake",
|
|
559
|
+
segments: [
|
|
560
|
+
{
|
|
561
|
+
segment: "PID",
|
|
562
|
+
usage: "R",
|
|
563
|
+
fields: [
|
|
564
|
+
{ field: 3, name: "Patient Identifiers", usage: "R", cardinality: { min: 1, max: 1 } },
|
|
565
|
+
{ field: 8, name: "Administrative Sex", usage: "RE", valueSet: ["M", "F", "U"] },
|
|
566
|
+
],
|
|
567
|
+
},
|
|
568
|
+
{ segment: "ZZZ", usage: "X" }, // no local Z-segments permitted
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const { findings } = validateAgainstProfile(parseHL7(raw), profile);
|
|
573
|
+
for (const f of findings) console.log(f.severity, f.code, f.message);
|
|
574
|
+
// e.g. "error PROFILE_VALUE_NOT_IN_SET PID-8 component 1 value is not in the profile value set (3 permitted codes)."
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
The engine holds four invariants: it **never throws** (a malformed profile yields `PROFILE_MALFORMED` findings, not an exception: use `defineConformanceProfile` for a fail-fast authoring gate that _does_ throw); a **valid message yields zero findings**; **no finding carries PHI**: each names the structural locus (segment / field / component / repetition) and the rule, never the offending value; and validation is **read-only**. Usage codes are the HL7 six (`R`/`RE`/`C`/`CE`/`O`/`X`); `C`/`CE` presence is not evaluated (no predicate language, a documented boundary). Distinct from the parse-profile system (`defineProfile`/`profiles`), which shapes _how a message is parsed_.
|
|
578
|
+
|
|
579
|
+
> **"No findings" is not an attestation.** An empty result means _nothing this profile checked was violated_: never "this message is conformant." The profile only covers what you declared, and hl7 makes no conformance certification. See [`docs-content/spec-notes-conformance.md`](docs-content/spec-notes-conformance.md).
|
|
525
580
|
|
|
526
581
|
### Pretty-print for logs
|
|
527
582
|
|
|
528
|
-
`msg.prettyPrint()` returns a multi-line, labeled view of the positional tree
|
|
583
|
+
`msg.prettyPrint()` returns a multi-line, labeled view of the positional tree: useful for dev-time debugging and log snapshots.
|
|
529
584
|
|
|
530
585
|
```ts
|
|
531
586
|
import { parseHL7 } from "@cosyte/hl7";
|
|
@@ -542,13 +597,13 @@ console.log(msg.prettyPrint());
|
|
|
542
597
|
// ...
|
|
543
598
|
```
|
|
544
599
|
|
|
545
|
-
Pretty-print is **not** a replacement for `toString()
|
|
600
|
+
Pretty-print is **not** a replacement for `toString()`. It's for humans. Use `toString()` for wire output and `toJSON()` when you need a serialisable structure.
|
|
546
601
|
|
|
547
602
|
---
|
|
548
603
|
|
|
549
604
|
## Profiles
|
|
550
605
|
|
|
551
|
-
Profiles are the growth loop. Built-ins cover the common vendor patterns; real integrations (hospital-specific Epic instances, reference labs, HIEs) publish their own profile packages that extend the built-ins. The `defineProfile()` API treats built-ins and user-authored profiles as equal citizens
|
|
606
|
+
Profiles are the growth loop. Built-ins cover the common vendor patterns; real integrations (hospital-specific Epic instances, reference labs, HIEs) publish their own profile packages that extend the built-ins. The `defineProfile()` API treats built-ins and user-authored profiles as equal citizens. There's no second-class path.
|
|
552
607
|
|
|
553
608
|
### Authoring a profile
|
|
554
609
|
|
|
@@ -602,13 +657,13 @@ When `extends` resolves parents, fields are merged per-key with the following ru
|
|
|
602
657
|
- **Arrays** (`dateFormats`, `lineage`): concatenate + dedupe, preserving first-seen order.
|
|
603
658
|
- **`customSegments` map**: deep-merge per key; same-segment-name in two parents reconciles positional fields.
|
|
604
659
|
- **`onWarning` handlers**: compose into a chain, invoked in lineage order (parents before children). Errors thrown by one handler do not stop subsequent handlers.
|
|
605
|
-
- **`name`**: never inherited
|
|
660
|
+
- **`name`**: never inherited, always the profile's own.
|
|
606
661
|
|
|
607
662
|
The merge is validated post-hoc: duplicate field names across merged segments throw `ProfileDefinitionError` up front, not at parse time.
|
|
608
663
|
|
|
609
664
|
### Inspecting a profile
|
|
610
665
|
|
|
611
|
-
Every `defineProfile()` result carries a `.describe()` method and an introspectable `lineage` array
|
|
666
|
+
Every `defineProfile()` result carries a `.describe()` method and an introspectable `lineage` array: useful for debugging which layers contributed what.
|
|
612
667
|
|
|
613
668
|
```ts
|
|
614
669
|
import { defineProfile, profiles } from "@cosyte/hl7";
|
|
@@ -627,17 +682,20 @@ console.log(p.describe?.()); // multi-line summary
|
|
|
627
682
|
|
|
628
683
|
### Publishing a profile
|
|
629
684
|
|
|
630
|
-
The [profile starter kit](./examples/profile-starter-kit/) is a complete publishable npm package template. Copy the subtree, replace placeholders, swap in your Z-segments and fixtures, then `pnpm publish --access public`. See [`examples/profile-starter-kit/CUSTOMIZING.md`](./examples/profile-starter-kit/CUSTOMIZING.md) for the 5-step walkthrough
|
|
685
|
+
The [profile starter kit](./examples/profile-starter-kit/) is a complete publishable npm package template. Copy the subtree, replace placeholders, swap in your Z-segments and fixtures, then `pnpm publish --access public`. See [`examples/profile-starter-kit/CUSTOMIZING.md`](./examples/profile-starter-kit/CUSTOMIZING.md) for the 5-step walkthrough: rename, swap base profile, define Z-segments, write fixtures, publish.
|
|
631
686
|
|
|
632
687
|
### Built-in profiles
|
|
633
688
|
|
|
634
|
-
|
|
689
|
+
Eight profiles ship in the box, reachable via the `profiles` namespace:
|
|
635
690
|
|
|
636
|
-
- `profiles.epic
|
|
637
|
-
- `profiles.cerner
|
|
638
|
-
- `profiles.meditech
|
|
639
|
-
- `profiles.athena
|
|
640
|
-
- `profiles.genericLab
|
|
691
|
+
- `profiles.epic`: Epic Bridges Interconnect. Adds `MM/DD/YYYY HH:mm:ss` and `MM/DD/YYYY` date formats; declares `ZDP` (department context) and `ZRS` (result status) Z-segments.
|
|
692
|
+
- `profiles.cerner`: Cerner Millennium outbound. Handles Cerner-idiomatic date formats and common Z-segments from Millennium ADT feeds.
|
|
693
|
+
- `profiles.meditech`: MEDITECH MAGIC/6.x/Expanse. Adds the `YYYYMMDDHHMM` minute-precision timestamp format and declares the DFT charge Z-segments `ZF1` (provider-encounter copay data) and `ZF2` (encounter-procedure data). Grounded in the public [MEDITECH Ancillary Charges (LAB/PHA/ITS/IDM) Outbound](https://ehr.meditech.com/sites/default/files/documents/20240613/ancillary-charges-outbound-21.pdf) (v2.1) spec, with the date format also confirmed by the [MEDITECH Admissions and Registration Outbound](https://ehr.meditech.com/sites/default/files/documents/20240613/admissions-registration-outbound-24.pdf) (v2.4) spec.
|
|
694
|
+
- `profiles.athena`: athenahealth. Ambulatory-oriented, ISO-leaning date formats.
|
|
695
|
+
- `profiles.genericLab`: Generic reference-lab (LabCorp / Quest-style). Adds `YYYYMMDD HHmm` (ASTM-era) and `YYYY-MM-DD` (ISO date-only); declares `ZLB` (lab overrides) and `ZNT` (lab note) Z-segments.
|
|
696
|
+
- `profiles.visage`: Visage 7 imaging/PACS RIS feeds. Declares the `ZDS` Z-segment that carries the DICOM **Study Instance UID** (field 1) so an HL7 order correlates to its DICOM study, the IHE Radiology RIS/PACS bridge segment. Grounded in the public [Visage 7 HL7 Interface Specification](https://www.visageimaging.com/downloads/Visage7/Visage7_HL7InterfaceSpecification.pdf) (V23.00, Jun 2026). Dates are HL7-native, so it adds no date formats.
|
|
697
|
+
- `profiles.philips`: Philips Vue PACS ("IS Link") imaging feeds. Declares six Vue PACS Z-segments, one per filler role: `ZDS` (DICOM Study Instance UID), `ZLK` (linked studies/orders), `ZAO` (order additional details: modality, body part, transfer/acquisition status, technician + radiologist), `ZEB` (encrypted patient info), `ZAP` (patient additional details), and `ZAV` (visit additional details). Grounded in the public [Vue PACS 12.2.8 HL7 Interface Specifications](https://www.documents.philips.com/assets/Conformance%20Statements/20240409/8941f89d89aa4983aab7b14d00db578c.pdf) (Philips, doc HA1669 Rev A, §§5.11–5.16). Dates are HL7-native, so it adds no date formats.
|
|
698
|
+
- `profiles.va`: U.S. Department of Veterans Affairs VistA Radiology/Nuclear Medicine feeds (HL7 v2.4). Declares the `ZDS` Z-segment that carries the DICOM **Study Instance UID** (field 1): the same IHE Radiology RIS↔PACS bridge as `visage`/`philips`, grounded here in a distinct **federal** spec and documented on **both ORM and ORU** (result) messages. Grounded in the public [Radiology/Nuclear Medicine 5.0 HL7 Interface Specification](https://www.va.gov/vdl/documents/clinical/radiology_nuclear_med/ra5_0hl7is.pdf) (Version 3.6, Patch RA\*5.0\*203, June 2024). Dates are HL7-native, so it adds no date formats.
|
|
641
699
|
|
|
642
700
|
Use a built-in directly (`parseHL7(raw, profiles.epic)`) or as a base for your own (`defineProfile({ extends: profiles.epic, ... })`).
|
|
643
701
|
|
|
@@ -645,13 +703,13 @@ Use a built-in directly (`parseHL7(raw, profiles.epic)`) or as a base for your o
|
|
|
645
703
|
|
|
646
704
|
## Real-World Tolerance
|
|
647
705
|
|
|
648
|
-
Production HL7 traffic routinely violates the published spec
|
|
706
|
+
Production HL7 traffic routinely violates the published spec: trailing whitespace, MLLP framing, mixed-case segment names, unknown escape sequences, non-canonical timestamps. A parser that rejects those messages is useless on real integrations. Postel's Law applies: the parser is liberal, the emitter is conservative.
|
|
649
707
|
|
|
650
708
|
Every deviation the parser encounters is classified into one of four tiers:
|
|
651
709
|
|
|
652
710
|
| Tier | Behavior | When | Example codes |
|
|
653
711
|
| ---- | -------------- | ------------------------------ | ------------------------ |
|
|
654
|
-
| 0 | Silent | Spec-compliant input |
|
|
712
|
+
| 0 | Silent | Spec-compliant input | (none) |
|
|
655
713
|
| 1 | Auto-handled | Trivial deviation, no warning | Trailing whitespace tidy |
|
|
656
714
|
| 2 | Warning | Recoverable deviation | `MLLP_FRAMING_STRIPPED` |
|
|
657
715
|
| 3 | Fatal (always) | Unrecoverable structural error | `NO_MSH_SEGMENT` |
|
|
@@ -667,11 +725,11 @@ for (const w of msg.warnings) {
|
|
|
667
725
|
}
|
|
668
726
|
```
|
|
669
727
|
|
|
670
|
-
The full list of Tier-2 codes (16 entries
|
|
728
|
+
The full list of Tier-2 codes (16 entries: `MLLP_FRAMING_STRIPPED`, `FIELD_WHITESPACE_TRIMMED`, `UNKNOWN_ESCAPE_SEQUENCE`, `TIMESTAMP_FALLBACK_FORMAT`, `SEGMENT_CASE`, `EXTRA_FIELDS`, `UNKNOWN_SEGMENT`, `DUPLICATE_REQUIRED_SEGMENT`, `ENCODING_MISMATCH`, `MISSING_REQUIRED_FIELD`, `MISSING_EXPECTED_GROUP`, `OUT_OF_ORDER_SEGMENT`, `VERSION_MISMATCH`, `UNKNOWN_CHARSET`, `ACK_NO_CORRELATION_ID`, `MERGE_MISSING_PRIOR_OR_SURVIVOR`) lives in [`src/parser/warnings.ts`](./src/parser/warnings.ts). Narrow on `w.code === WARNING_CODES.UNKNOWN_SEGMENT` (and friends) for typo-free comparisons.
|
|
671
729
|
|
|
672
730
|
Need zero tolerance instead? `parseHL7(raw, { strict: true })` escalates every Tier-2 deviation to a thrown `Hl7ParseError`. Use strict in CI validators; leave it off for production ingestion.
|
|
673
731
|
|
|
674
|
-
The 4 Tier-3 fatal codes
|
|
732
|
+
The 4 Tier-3 fatal codes (`NO_MSH_SEGMENT`, `MSH_TOO_SHORT`, `INVALID_ENCODING_CHARACTERS`, `EMPTY_INPUT`) always throw regardless of mode. They represent inputs the parser can't meaningfully recover from.
|
|
675
733
|
|
|
676
734
|
---
|
|
677
735
|
|
|
@@ -702,11 +760,11 @@ if (err instanceof Hl7ParseError && err.code === FATAL_CODES.NO_MSH_SEGMENT) {
|
|
|
702
760
|
}
|
|
703
761
|
```
|
|
704
762
|
|
|
705
|
-
`Hl7ParseError.snippet` may contain PHI when parsing real clinical messages. The library does NOT redact
|
|
763
|
+
`Hl7ParseError.snippet` may contain PHI when parsing real clinical messages. The library does NOT redact. Redact at your call site if compliance demands it.
|
|
706
764
|
|
|
707
765
|
### `Hl7ParseWarning`
|
|
708
766
|
|
|
709
|
-
Tier-2 deviations
|
|
767
|
+
Tier-2 deviations: plain data, not thrown. Accumulated on `msg.warnings` and delivered to any `onWarning` callback in parse order. See the iteration example in [Real-World Tolerance](#real-world-tolerance).
|
|
710
768
|
|
|
711
769
|
```ts
|
|
712
770
|
import type { Hl7ParseWarning, WarningCode } from "@cosyte/hl7";
|
|
@@ -724,7 +782,7 @@ function label(w: Hl7ParseWarning): string {
|
|
|
724
782
|
}
|
|
725
783
|
```
|
|
726
784
|
|
|
727
|
-
Use the `WarningCode` union + `switch` for exhaustive handling
|
|
785
|
+
Use the `WarningCode` union + `switch` for exhaustive handling: the type system catches missing cases if you enable `switch-exhaustiveness-check`.
|
|
728
786
|
|
|
729
787
|
### `ProfileDefinitionError`
|
|
730
788
|
|
|
@@ -750,12 +808,12 @@ try {
|
|
|
750
808
|
|
|
751
809
|
The four cases that throw `ProfileDefinitionError`:
|
|
752
810
|
|
|
753
|
-
1. **Missing or non-string `name
|
|
754
|
-
2. **Malformed `customSegments
|
|
755
|
-
3. **Unsupported `dateFormats` tokens
|
|
756
|
-
4. **Unknown option keys
|
|
811
|
+
1. **Missing or non-string `name`**: every profile must identify itself.
|
|
812
|
+
2. **Malformed `customSegments`**: segment name must pass the Z-segment regex; each field entry must map to a positive 1-indexed integer; no duplicate field names across merged segments.
|
|
813
|
+
3. **Unsupported `dateFormats` tokens**: tokens must be drawn from `SUPPORTED_DATE_TOKENS` (re-exported from the package barrel for introspection).
|
|
814
|
+
4. **Unknown option keys**: the options bag is closed; typos throw with "did you mean" hints.
|
|
757
815
|
|
|
758
|
-
All three error types (and the `FATAL_CODES` / `WARNING_CODES` registries, and the `SUPPORTED_DATE_TOKENS` set) are top-level exports
|
|
816
|
+
All three error types (and the `FATAL_CODES` / `WARNING_CODES` registries, and the `SUPPORTED_DATE_TOKENS` set) are top-level exports: no internal reaching required.
|
|
759
817
|
|
|
760
818
|
---
|
|
761
819
|
|
|
@@ -763,32 +821,37 @@ All three error types (and the `FATAL_CODES` / `WARNING_CODES` registries, and t
|
|
|
763
821
|
|
|
764
822
|
Not in v1, but on the roadmap for v2:
|
|
765
823
|
|
|
766
|
-
- **Typed message overlays** (`msg.is("ADT^A01")` narrows to `AdtA01Message`)
|
|
767
|
-
- **Schema-aware structure validation
|
|
768
|
-
- **Streaming parser for large batch files
|
|
769
|
-
- **JSON Schema / Zod emission for `toJSON()` output
|
|
770
|
-
- **Type-safe custom-segment field names via conditional types
|
|
824
|
+
- **Typed message overlays** (`msg.is("ADT^A01")` narrows to `AdtA01Message`): message-type-aware getter narrowing.
|
|
825
|
+
- **Schema-aware structure validation**: opt-in structural validator that enforces segment ordering + cardinality against the HL7 spec.
|
|
826
|
+
- **Streaming parser for large batch files**: `createHL7Stream()` returning an iterable of messages, for multi-GB batch processing.
|
|
827
|
+
- **JSON Schema / Zod emission for `toJSON()` output**: autogenerated schemas from the internal typed model.
|
|
828
|
+
- **Type-safe custom-segment field names via conditional types**: `seg.get("departmentCode")` narrows to the profile's declared field alias, not `string | undefined`.
|
|
771
829
|
|
|
772
830
|
### Out of scope (permanently)
|
|
773
831
|
|
|
774
|
-
- **MLLP framing / network transport
|
|
775
|
-
- **HL7 v3 and CDA
|
|
776
|
-
- **FHIR conversion
|
|
777
|
-
- **Exhaustive coded-value validation
|
|
832
|
+
- **MLLP framing / network transport**: a future `@cosyte/hl7-mllp` package will cover network IO; this library is parser-only.
|
|
833
|
+
- **HL7 v3 and CDA**: different spec family entirely; not in our plans.
|
|
834
|
+
- **FHIR conversion**: a future companion package could bridge, but the conversion is non-trivial and deserves its own repo.
|
|
835
|
+
- **Exhaustive coded-value validation**: we validate structure, not every HL7 table. Integrate a domain validator (e.g. your LIS's internal code registry) for that.
|
|
778
836
|
|
|
779
837
|
---
|
|
780
838
|
|
|
781
839
|
## Contributing
|
|
782
840
|
|
|
783
|
-
Vendor-quirk fixtures, profile improvements, and standalone profile packages are all welcome. The more real-world edge cases the test suite covers, the more robust the parser gets
|
|
841
|
+
Vendor-quirk fixtures, profile improvements, and standalone profile packages are all welcome. The more real-world edge cases the test suite covers, the more robust the parser gets. Every published profile package is a signal of adoption and a contribution back.
|
|
784
842
|
|
|
785
843
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for dev setup, how to file an issue, and how to submit a PR.
|
|
786
844
|
|
|
787
845
|
---
|
|
788
846
|
|
|
847
|
+
## Trademarks
|
|
848
|
+
|
|
849
|
+
Epic, Cerner, MEDITECH, athenahealth, LabCorp, Quest Diagnostics, Visage (Visage Imaging), and Philips (Vue PACS) are trademarks of their respective owners. cosyte is not affiliated with, endorsed by, or
|
|
850
|
+
sponsored by any of them: the names identify the systems whose real-world message quirks the built-in profiles accommodate. See [TRADEMARKS.md](./TRADEMARKS.md).
|
|
851
|
+
|
|
789
852
|
## License
|
|
790
853
|
|
|
791
|
-
MIT
|
|
854
|
+
MIT: see [LICENSE](./LICENSE).
|
|
792
855
|
|
|
793
856
|
---
|
|
794
857
|
|
package/TRADEMARKS.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Trademarks
|
|
2
|
+
|
|
3
|
+
`@cosyte/hl7` is an independent open-source project. cosyte is **not affiliated with, endorsed by,
|
|
4
|
+
or sponsored by** any company named in this repository or its documentation.
|
|
5
|
+
|
|
6
|
+
## Why these names appear
|
|
7
|
+
|
|
8
|
+
Real-world HL7 v2 traffic deviates from the specification in vendor-specific ways. A parser cannot
|
|
9
|
+
tolerate those deviations (or tell you which one it tolerated) without naming the system they
|
|
10
|
+
come from.
|
|
11
|
+
|
|
12
|
+
Every reference is **descriptive**: it identifies whose real-world message quirks a profile accommodates, and nothing more. Naming a system is the only way to say
|
|
13
|
+
whether a library works with it.
|
|
14
|
+
|
|
15
|
+
## Where the profiles come from
|
|
16
|
+
|
|
17
|
+
The built-in profiles are authored through this package's own public `defineProfile()` API, from
|
|
18
|
+
publicly available specifications and published companion guides. They embed no privileged,
|
|
19
|
+
confidential, or reverse-engineered material, and they are not derived from any vendor's
|
|
20
|
+
proprietary documentation.
|
|
21
|
+
|
|
22
|
+
## Names referenced
|
|
23
|
+
|
|
24
|
+
| Name | Where it appears |
|
|
25
|
+
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| Epic | `profiles.epic`: a built-in profile name |
|
|
27
|
+
| Cerner | `profiles.cerner`: a built-in profile name |
|
|
28
|
+
| MEDITECH | `profiles.meditech`: a built-in profile name |
|
|
29
|
+
| athenahealth | `profiles.athena`: a built-in profile name |
|
|
30
|
+
| LabCorp, Quest Diagnostics | Named in the README only, to characterise `profiles.genericLab`. Neither is used as an export name. |
|
|
31
|
+
| Visage, Visage Imaging | `profiles.visage`: a built-in profile name; the ZDS quirk it accommodates is grounded in the publicly published Visage 7 HL7 Interface Specification. |
|
|
32
|
+
| Philips, Vue PACS | `profiles.philips`: a built-in profile name; the Z-segment quirks it accommodates are grounded in the publicly published Philips Vue PACS 12.2.8 HL7 Interface Specifications (doc HA1669 Rev A). |
|
|
33
|
+
| VA, VistA | `profiles.va`: a built-in profile name; the ZDS quirk it accommodates is grounded in the publicly published VA Radiology/Nuclear Medicine 5.0 HL7 Interface Specification (v3.6, June 2024), a U.S. Department of Veterans Affairs work. |
|
|
34
|
+
|
|
35
|
+
All product names, logos, and brands are the property of their respective owners. Use of a name here
|
|
36
|
+
does not imply any affiliation with, or endorsement by, its owner. If you own one of these marks and
|
|
37
|
+
would like a reference changed, please open an issue.
|