@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/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 — without reading the spec.
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
  [![npm version](https://img.shields.io/npm/v/@cosyte/hl7.svg)](https://www.npmjs.com/package/@cosyte/hl7)
6
6
  [![CI](https://img.shields.io/github/actions/workflow/status/cosyte/hl7/ci.yml?branch=main&label=CI)](https://github.com/cosyte/hl7/actions/workflows/ci.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
8
8
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](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 — with the other 90% still one accessor away when you need it.
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) — also works with: npm install @cosyte/hl7 | yarn add @cosyte/hl7
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 — precision + zone preserved)
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 — not before.
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** — `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, 5 built-in vendor profiles (Epic, Cerner, Meditech, athenahealth, generic lab), 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.
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 — sender, receiver, type, timestamp)
56
+ ├── MSH (header: sender, receiver, type, timestamp)
57
57
  ├── EVN (event details for ADT)
58
- ├── PID (patient identification — name, MRN, DOB)
59
- ├── PV1 (visit — class, location, attending)
60
- └── OBX×N (observations — repeats for labs, vitals)
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" — a birth date is not a full instant
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 — fidelity TS)
89
- console.log(msg.visit?.location); // PL composite — pointOfCare/room/bed
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 — or when you're scripting quick extractions without introducing typed imports.
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 — mutation happens through explicit methods (see the cookbook).
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 — fidelity TS at day precision)
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 — 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).
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 — ward / room / bed / facility in one go.
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 — appointments(): SCH id/status/timing + AIS/AIG/AIL/AIP resources
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 — documents(): completion (TXA-17) and availability (TXA-19) are DISTINCT
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 — a document can be AVAILABLE before it is AUTHENTICATED;
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 — charges(): FT1 billing fields, amounts kept as canonical wire text (no money-as-float)
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" — a string, never a number
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 — 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).
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 known limitations in the docs).
255
255
 
256
256
  ### Order & medication timing
257
257
 
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).
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" — VERBATIM, never resolved to a schedule
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" } — the load-bearing integer
270
- console.log(t.totalOccurrences); // 24 — from TQ1-14 (not TQ1-11)
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** — 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.
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 — merges (A18/A34/A35/A36/A39/A40/A41/A42), moves (A43/A44), link/unlink (A24/A37), and person add/update (A28/A31) — and 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.
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) — route for review
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 — 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.
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 — 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.
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 — not shared across workers, not serialisable. Opt out for a specific parse with `{ profile: null }` in the options bag.
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`** — the raw string plus its typed parts,
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" — never zero-filled to a full timestamp
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 — a missing offset is FLAGGED, never assumed to be UTC
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 — it refuses to guess a zone:
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 — a consumer needing an absolute instant applies the sender's zone via `assumeOffsetMinutes`.
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 — vendor-quirky
426
- `MM/DD/YYYY`, ISO `YYYY-MM-DD`, legacy `YYYYMMDD HHmm` — parses via the `dateFormats` option, which
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 — reach for a profile instead of hand-listing formats when one fits.
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 — 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).
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** — 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.
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 — never PHI.
475
+ // Batch-level warnings carry counts/positions only, never PHI.
476
476
  for (const w of warnings) {
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
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 — use them instead of parsing the raw string.
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 — an `ORU^R01` that arrives with no `OBR`/`OBX` result group is almost always truncated or sent to the wrong feed.
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 — `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.
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 — useful for dev-time debugging and log snapshots.
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()` — it's for humans. Use `toString()` for wire output and `toJSON()` when you need a serialisable structure.
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 — there's no second-class path.
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 — always the profile's own.
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 — useful for debugging which layers contributed what.
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 — rename, swap base profile, define Z-segments, write fixtures, publish.
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
- Five profiles ship in the box, reachable via the `profiles` namespace:
689
+ Eight profiles ship in the box, reachable via the `profiles` namespace:
635
690
 
636
- - `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.
637
- - `profiles.cerner` — Cerner Millennium outbound. Handles Cerner-idiomatic date formats and common Z-segments from Millennium ADT feeds.
638
- - `profiles.meditech` — MEDITECH EXPANSE/6.x. Covers MEDITECH-specific MRN picking and known Z-segment patterns.
639
- - `profiles.athena` — athenahealth. Ambulatory-oriented, ISO-leaning date formats.
640
- - `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.
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 — 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.
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 — `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.
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 — `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.
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 — redact at your call site if compliance demands it.
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 — 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).
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 — the type system catches missing cases if you enable `switch-exhaustiveness-check`.
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`** — every profile must identify itself.
754
- 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.
755
- 3. **Unsupported `dateFormats` tokens** — tokens must be drawn from `SUPPORTED_DATE_TOKENS` (re-exported from the package barrel for introspection).
756
- 4. **Unknown option keys** — the options bag is closed; typos throw with "did you mean" hints.
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 — no internal reaching required.
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`) — message-type-aware getter narrowing.
767
- - **Schema-aware structure validation** — opt-in structural validator that enforces segment ordering + cardinality against the HL7 spec.
768
- - **Streaming parser for large batch files** — `createHL7Stream()` returning an iterable of messages, for multi-GB batch processing.
769
- - **JSON Schema / Zod emission for `toJSON()` output** — autogenerated schemas from the internal typed model.
770
- - **Type-safe custom-segment field names via conditional types** — `seg.get("departmentCode")` narrows to the profile's declared field alias, not `string | undefined`.
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** — a future `@cosyte/hl7-mllp` package will cover network IO; this library is parser-only.
775
- - **HL7 v3 and CDA** — different spec family entirely; not in our plans.
776
- - **FHIR conversion** — a future companion package could bridge, but the conversion is non-trivial and deserves its own repo.
777
- - **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.
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 — every published profile package is a signal of adoption and a contribution back.
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 — see [LICENSE](./LICENSE).
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.