@cosyte/hl7 0.0.2 → 0.0.4

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 CHANGED
@@ -15,6 +15,146 @@ per the cosyte version ladder (`0.0.x` until first alpha).
15
15
 
16
16
  ### Added
17
17
 
18
+ - **The cosyte brand banner heads `README.md` (`ASSETS-P8`, the consuming half).** A plain markdown
19
+ image on the first line of the file, above the H1, pointing at the absolute HTTPS URL published
20
+ for `hl7` in the `assets` repo's `published-urls.json` contract
21
+ (`https://cosyte.com/social/cosyte-banner-hl7-1200x300.png`, `status: live` on
22
+ `website#59 01f988b`). Re-verified `200 image/png`, 18316 bytes, immediately before the push.
23
+
24
+ **The shape is deliberate, and `hl7` sets it for the other thirteen consuming READMEs.** It is a
25
+ markdown image, **not `<img>` and not `<picture>`**: whether npm's markdown sanitizer preserves a
26
+ `<picture>` element is unverified, which is precisely why the banner artwork is self-grounded
27
+ (opaque ground, correct against a light or dark page) and depends on no such element. A markdown
28
+ image with an absolute HTTPS URL is the construct we are willing to assert renders on both npm and
29
+ GitHub, so it carries no width or height attributes either. **PNG only** is a bounded policy (the
30
+ format we assert renders on every README surface), not a demonstrated impossibility for others.
31
+ The **alt text is content, not decoration**: it names the package and its one-line purpose,
32
+ because it is what a screen reader on the npm page reads out, so "banner", "logo", or the file
33
+ name would all be a wasted line.
34
+
35
+ - **The published JSDoc is swept, and `src/` doc comments are gated
36
+ (`pnpm check:no-internal-refs` third pass; `PUBLIC-SURFACE-HYGIENE`).** Doc comments compile
37
+ into `dist/index.d.ts` and `dist/index.d.cts`, `dist` is the first entry in `files`, and every
38
+ `npm i @cosyte/hl7` receives them, so what a consumer's editor renders on hover is public
39
+ surface and the founder directive of 2026-07-27 applies to it. Re-measured with the shipped
40
+ rules before the change: **293 distinct lines** across tracked `src/*.ts` (253 phase-rule, 39
41
+ identifier-rule, 5 ADR, 1 jargon, 1 traceability marker), of which a local build carried
42
+ **164 distinct lines into `dist/index.d.ts`** (137 phase-rule, 22 identifier-rule, 5 ADR,
43
+ 1 jargon, 1 traceability). After: **0 on every rule, in `src/` doc comments and in both built
44
+ declaration files.** A consumer hovering `parseHL7`, `defineProfile`, `Medication` or
45
+ `buildMessage` no longer reads which internal phase built it. Every `Plan N` build-order
46
+ reference was swept in the same pass, by hand, and **the built declaration file now carries
47
+ none**: no rule catches them, and they contradicted the `CLAUDE.md` sentence this change adds.
48
+ For scale, the base commit's build carried 28 such lines, from 61 doc-comment lines in `src/`.
49
+
50
+ **Identifiers were translated, never deleted, and no public export lost its documentation.**
51
+ That is measured, not asserted: the built `dist/index.d.ts` carries **691 doc-comment blocks,
52
+ 633 documented declarations and 237 `@example` blocks before and after, and the sorted list of
53
+ documented declaration names is byte-identical.** Heads left behind by a front-of-line strip
54
+ were repaired: nine composite parsers wrapped `HL7-VALUE-REDECODE)` onto a continuation line,
55
+ where a naive strip leaves `). Absent / empty components are OMITTED`.
56
+
57
+ **Stripping the framing exposed a false claim on a published API, which is the argument for
58
+ doing this at all.** `defineProfile`'s doc block in `dist/index.d.ts` told consumers that
59
+ `opts.extends` was "ACCEPTED but IGNORED, resulting in `lineage === [opts.name]` regardless".
60
+ The implementation merges parents (lineage, `dateFormats`, `customSegments`, `description`,
61
+ composed `onWarning`) and re-validates the merged result. The text now describes what the
62
+ function does. Four sibling claims of the same kind were also stale and are gone: `emitMessage`,
63
+ `emitPrettyPrint`, `emitJson` and `buildMessage` were each documented as an unimplemented stub
64
+ ("Stub throws", "Implementation lives in..."), and all four have been implemented for some time.
65
+ Internal framing is where stale claims hide, which is why this is a correctness change and not
66
+ a tidying one.
67
+
68
+ **The gate now has a third pass** with its own rule array, its own extractor and its own
69
+ positive/negative self-tests written in the language of source rather than markdown. It scans
70
+ `/** */` text only, line by line and again over the block reflowed the way a hover tooltip
71
+ reflows it, because multi-token rules are blind to a violation that straddles a wrap and this
72
+ package wraps its doc comments. `//` and `/* */` comments are deliberately **not** covered and
73
+ keep their identifiers: they do not reach `dist` (checked both directions), and the convention
74
+ names source comments as a place identifiers belong. 61 such lines remain in `src/` by choice.
75
+
76
+ Every claim above was checked RED against a seeded violation rather than assumed: a single-line
77
+ violation in an exported function's JSDoc; a violation split across a wrap that a line-only
78
+ grep scores 0 on; the same bookkeeping in a `//` comment staying green; widening the source
79
+ identifier rule to the `WORD-N` shape failing its own negative self-test naming `MSH-2`; an
80
+ extractor that strips the comment leader before testing the terminator reporting 66 false hits
81
+ on a clean tree; and an extractor that yields nothing refusing to report green.
82
+
83
+ **The ceiling is stated rather than implied shut, and "0 on all six rules" is a statement about
84
+ these rules, not about the founder's rule.** Measured on the built `dist/index.d.ts`, what still
85
+ ships is **144 lines carrying `D-NN` internal decision numbers** and **51 lines carrying item
86
+ identifiers from nine prefixes the list does not hold** (`HELPERS` 14, `BIP` 15, `PROF` 8,
87
+ `SER` 6, `MODEL` 4, `PARSE`, `WR`, `TOL`, `TYPES` 1 each). `D-NN` is the deliberate
88
+ non-catch it has always been, because legacy SNOMED RT codes are axis-prefixed in exactly that
89
+ shape (`D-13000`, `T-32000`, `M-80003`). The item identifiers need new prefixes added to the
90
+ list, which is a **rule** change needing its own negative self-tests (`SER`, `WR`, `TOL` and
91
+ `BIP` are exactly the short generic tokens that made `PKG` unsafe), so it is not smuggled in on
92
+ the back of a prose sweep. Separately, `dist/` is untracked build output, so no checked-in gate
93
+ can read it: this gates dist's _source_, which holds only because the dts build copies doc text
94
+ verbatim, and the `164` figure is a hand-taken snapshot. Two "the per-X slice of Y" false
95
+ positives, where `slice` means portion, were fixed by rewording rather than by narrowing the
96
+ rule.
97
+
98
+ Documentation, tooling and CI only. No change to the published package surface, parser
99
+ behavior, or warning codes.
100
+
101
+ - **Public-surface hygiene gate in CI (`scripts/check-no-internal-refs.sh`,
102
+ `pnpm check:no-internal-refs`, `.github/workflows/no-internal-refs.yml`;
103
+ `PUBLIC-SURFACE-HYGIENE`).** Founder directive of 2026-07-27: no internal project bookkeeping on
104
+ any surface a consumer reads. The remediation half of that directive was a sweep of 149 references
105
+ across 11 repos, and a sweep regresses the first time someone writes `(CCDA-P8)` into a README, so
106
+ the deliverable here is the gate; the sweep rides along. **50 violating lines across 15
107
+ `docs-content/` pages were cleared** (re-measured with the shipped rules against the previous tree;
108
+ the backlog's count of 44 was markdown-only and predates two of the rules): phase identifiers in
109
+ page titles and headings, `HL7-N` / `HL7-J` / `HL7-ESC` / `HL7-R` / `VERIFY-AT-BUILD` item
110
+ identifiers, a citation of `operations/roadmaps/hl7.md`, bracketed spec-trace tags (`[S-NTE]`,
111
+ `[S-DTM-IMPL]`), an "Open-question #12", and review-process commentary ("All confirmed 3-0
112
+ (pass 5)", "all verified 3-0 in the research pass", "Assumption logged"). One more, in `README.md`,
113
+ was fixed by hand and is caught by no rule: a pointer to "the roadmap's known-limitations", a file
114
+ the reader does not have. Identifiers were translated at the boundary, not merely deleted, and
115
+ every head left behind by a front-of-line strip was repaired: a heading cut to `: datetime
116
+ precision` reads worse than the text it replaced. The datetime page needed more than a strip: with
117
+ its phase framing gone it read as documentation of shipped behaviour while still describing, in the
118
+ present tense, a `parseHl7TsDtm` zero-fill defect that no longer exists in the library (the API it
119
+ planned has shipped). It is now written as what the package does, its unexecuted plan and test
120
+ bookkeeping are gone, and a provenance claim that survived the strip in a stronger form than the
121
+ original ("all verified against primary Ch. 2A") is removed rather than reworded. The detection
122
+ rules are lifted
123
+ from `cosyte/.github` `scripts/release-notes.mjs`, which is already tested byte-for-byte against
124
+ the 14 published release bodies, and they are **keyed on known project prefixes, never on the
125
+ `WORD-N` shape**: `MSH-2`, `PID-3`, `OBX-5`, `TQ1-7`, `NM1-03`, `PKG-4`, `ICD-10-CM`, `HL7-V2`,
126
+ `FHIR-R4`, `DICOM-SR`, `NCPDP-SCRIPT`, `FHIR-bridge`, `HL7-defined` and the four `CSP`
127
+ Clinical Study **Phase** field names are the reference material an HL7 parser's docs exist to
128
+ provide, and a shape rule deletes them. That is asserted, not assumed: the script carries a
129
+ **negative** self-test per rule, built from that material, alongside the positive one, so widening
130
+ a rule into the `WORD-N` shape reds the gate instead of silently corrupting a segment reference on
131
+ the next sweep. Where a rule could not be guarded it was **cut** instead: `phase` after a
132
+ determiner ("the non-goals of this phase") is not detected at all, because catching it also caught
133
+ "the phase of the clinical study" and "the phase of illness", and when the collision is
134
+ with clinical reference material the reference material wins. The scan harness is the em-dash gate's,
135
+ in mllp's variant: scan list built and `./`-prefixed in one loop so the scan stays a single command
136
+ with stderr bound to all of it, no `sed -z`, no `-d skip`, no `-I`, `-e`/`--`, `-0 -r`, and a
137
+ refusal to print OK from an empty list, an unreadable input, a non-regular-file entry, or any
138
+ scanner stderr. Every one of those routes was checked RED against a seeded violation rather than
139
+ inherited. **Every rule is also applied a second time to paragraph-joined text**, because the rules
140
+ are multi-token and this repo hard-wraps markdown: a violation straddling a wrap was invisible to a
141
+ line scan and one was live (`A future phase` / `may add opt-in decode`, with the gate printing OK).
142
+ The join squeezes whitespace the way markdown does, which is load-bearing rather than cosmetic: a
143
+ verbatim join leaves the continuation line's indentation in place, and about 600 lines of
144
+ `docs-content/` start indented, so the pass would have missed the dominant wrap shape while
145
+ reporting that it ran.
146
+ It scans the public surface only (`README.md`, `TRADEMARKS.md`, `LICENSE`, `docs-content/`, and the
147
+ npm `description` and `keywords`) because the same identifier is required on the inside:
148
+ `CHANGELOG.md`, `.changeset/`, commits and PRs are where it belongs. A tripwire refuses the run if
149
+ `package.json`'s `files` starts shipping anything the gate does not cover. **Three holes are measured and disclosed rather than implied
150
+ shut:** `dist/` ships the compiled JSDoc, and the tracked `src/*.ts` files carry 253 phase-rule and
151
+ 39 identifier-rule lines, of which a local build puts 137 and 22 into `dist/index.d.ts` for every
152
+ consumer's editor to render, unswept and ungated here because remediating ~290 source doc comments
153
+ is its own reviewable change (**that change is the entry above, which swept them and added the
154
+ third pass**; `dist/` is untracked, so no checked-in gate can re-derive that figure without
155
+ building first, and that part is still true); determiner-plus-`phase` is not detected, by choice;
156
+ and the rules catch identifiers, not English sentences about our process. Documentation and CI only: no change to the
157
+ published package surface, parser behavior, or warning codes.
18
158
  - **Em-dash brand gate in CI (`scripts/check-no-emdash.sh`, `pnpm check:no-emdash`,
19
159
  `.github/workflows/no-emdash.yml`; `EMDASH-CONFORMANCE` part 1).** The founder directive of
20
160
  2026-07-24 (`knowledgebase/06-brand/voice-and-tone.md`) bans `U+2014` outright across every cosyte
@@ -310,6 +450,153 @@ kind, value)`** sets one at a field or `field[rep]` dot-path. Component values a
310
450
 
311
451
  ### Fixed
312
452
 
453
+ - **A warning or error message could carry clinical text into a log line (`HL7-WARN-MSG-PHI`).**
454
+ No options were required to reach it: plain `parseHL7(raw)` on lenient defaults.
455
+
456
+ **The mechanism.** `normalize()` rewrites every `\r\n` and `\n` in the input to `\r` before
457
+ segment splitting, unconditionally and with no field awareness. HL7 v2 Ch. 2 fixes the segment
458
+ terminator as `<CR>` and provides `\.br\` (§2.7.6) precisely because a literal line break cannot
459
+ appear inside a field, but real senders emit raw breaks inside narrative fields (`OBX-5`, notes,
460
+ embedded documents), which is exactly the class of sender violation this parser exists to
461
+ tolerate. When one occurs, the remainder of that field becomes a forged segment, and `tokenize()`
462
+ takes the whole line as its segment identifier because a line with no field separator offers no
463
+ other candidate. `UNKNOWN_SEGMENT` then interpolated that "identifier" uncapped, putting synthetic
464
+ name, date of birth and medical record number into `Hl7ParseWarning.message`. Under
465
+ `{ strict: true }` the same text reached the thrown `Hl7ParseError.message`, and therefore
466
+ `.stack`.
467
+
468
+ **The disclosure was inverted, which is what made it consequential.** Guidance said to redact
469
+ `Hl7ParseError.snippet`. On the strict path `snippet` holds the head of the message (usually the
470
+ MSH header) while `message` held the payload, so a consumer following the documented mitigation
471
+ exactly still logged the data. `message` is also the field a logger prints by default and the one
472
+ `stack` embeds.
473
+
474
+ **The fix: bound at the factory, not at the call site.** A token taken from input is echoed only
475
+ when it matches the form the spec defines for it, and becomes `<withheld>` otherwise. A length cap
476
+ was rejected because truncating still emits the first N characters of a patient name. Segment
477
+ identifiers are held to exactly three alphanumerics (HL7 v2 Ch. 2 §2.5), with equivalent forms for
478
+ MSH-9 types and MSH-12 versions. Conforming tokens render byte-identically, so `PID`, `ZZZ` and
479
+ every legitimate Z-segment name are unchanged and well-formed input sees no behaviour change:
480
+ 1653 existing tests pass untouched. Placing the check in the factories rather than at the one live
481
+ call site also covers the six factories currently exported but unreached, and consumers who
482
+ construct warnings themselves.
483
+
484
+ **Charset labels are bounded by registry membership, not by spelling, and that distinction was
485
+ earned the hard way.** The first version of this fix used a shape test for them too, and a refuter
486
+ broke it: the shape admitted all five of the PHI-shaped markers this repo's own property suite
487
+ defines, including an 18-character patient name. Table 0211 is a **closed** table and
488
+ `UNKNOWN_CHARSET` fires precisely when a label is absent from it, so at the moment of the warning
489
+ there is no spec-defined spelling left to test against, and `UNICODE UTF-8` and a patient name are
490
+ shape-siblings. No regex can separate them. A label is now echoed only when `resolveCharset`
491
+ recognises it.
492
+
493
+ **What is bounded, stated without overclaiming.** `snippet` is the only field carrying input
494
+ verbatim and unbounded, so it is the one to redact first. A message cannot carry a field's value,
495
+ but it can carry a residue of up to three characters when a malformed line happens to look like a
496
+ segment identifier. No absolute "never carries field content" claim is made anywhere in this
497
+ package, because the code cannot honour one.
498
+
499
+ **Two adjacent surfaces bounded in the same pass.** `UNKNOWN_CHARSET` and `ENCODING_MISMATCH`
500
+ interpolate a value read as MSH-18, which on a message carrying no segment terminators is not
501
+ MSH-18 at all but the 18th `|`-token of the flattened input, commonly a data field. That path is
502
+ Buffer-only, and `parseStream` and `splitBatch` re-encode each split message to a Buffer, which is
503
+ the ordinary MLLP and file ingestion shape, so it is not an exotic corner.
504
+ `MISSING_EXPECTED_GROUP` and `MERGE_MISSING_PRIOR_OR_SURVIVOR` interpolate MSH-9.
505
+
506
+ **The test gap is the real finding.** The PHI property suite could not reach this position: every
507
+ generated marker sat after a `|`, so it was always a field value to the tokenizer, while the only
508
+ position that leaks is a line's **leading** token. A green suite over an unreachable case is
509
+ indistinguishable from a correct one. New generators emit the real trigger (a field-internal `\n`
510
+ and `\r\n`) across the lenient and strict arms, asserting on `.stack` separately from `.message`
511
+ because most error reporters ship the stack off-box, and pinning the escalated code on both
512
+ strict properties so neither can pass vacuously if some other warning escalates first. A second
513
+ set covers the Buffer/MSH-18 path, which every string-input generator in the suite structurally
514
+ misses, and a third asserts the invariant on the parsed **model**. All ten new tests fail on the
515
+ code they were written against and pass on this one.
516
+
517
+ **The same lesson applied to this fix's own first draft, which is why it now bounds the model
518
+ too.** Bounding the warning message protected this package's diagnostics and nothing else.
519
+ `Segment.type` still carried the entire forged line, so a consumer that reads the model and builds
520
+ its own report from that "identifier" wrote clinical narrative into it, on lenient defaults, with
521
+ no options set. **A diagnostic-surface fix does not protect a downstream package that reads your
522
+ model and builds its own diagnostics from it. Bounding at the model protects both.**
523
+ `Segment.type`, `Hl7Message.version` and the unrecognized branch of `MessageStructure` now carry
524
+ bounded identifiers, and the shared primitive moved to `src/parser/tokens.ts` so the parser and the
525
+ model use one bound rather than two.
526
+
527
+ **One bound deliberately not taken, named rather than left implicit.** On the _recognized_ branch
528
+ of `MessageStructure`, `triggerEvent` is echoed verbatim, and a definition matching on message code
529
+ alone (`ACK`) accepts an arbitrary MSH-9.2. Left unbounded on purpose: no real sender putting
530
+ narrative in MSH-9.2 is grounded in a de-identified document, and widening the guard there is a
531
+ separate change, not a silent extension of this one. An earlier comment in that file asserted the
532
+ branch returned registry values and could not carry input, which was false; the claim is corrected
533
+ rather than the guard grown.
534
+
535
+ **What stays unbounded, deliberately, because bounding it would be a mis-parse.** The model
536
+ separates **identifiers** from **values**. `Meta.*` is the documented value view of the MSH header;
537
+ `Segment.fields`, `Segment.raw` and `Hl7Message.rawSegments` are the raw tokenization the
538
+ byte-verbatim round-trip serializes from; `toString()` / `toJSON()` / `prettyPrint()` are the
539
+ message itself. Those are content surfaces and say so. `meta.version` in particular keeps MSH-12
540
+ exactly as it arrived and is what the v2.7 MRG field-scoping reads, so bounding
541
+ `Hl7Message.version` changes no clinical field selection.
542
+
543
+ **Docs corrected in both directions.** `docs-content/troubleshooting.md` asserted that warning
544
+ messages never echo a field's value, an overclaim; it and the equivalent assurances in
545
+ `normalize.ts` and `warnings.ts` now state the check that makes it true. The `Hl7ParseError`
546
+ disclosure now says `snippet` is the field to redact first, rather than claiming a message is
547
+ clean. `README.md` called the snippet an excerpt of "the offending input" when it is always
548
+ taken from the start of the message; the wording is corrected rather than the behaviour, because
549
+ re-pointing the snippet at the deviation's own segment would move more clinical content into the
550
+ single unredacted field.
551
+
552
+ **Not a mis-parse.** Parsed values were always correct; this was a diagnostic-surface leak.
553
+ Consumers on any published version up to and including `0.0.3` should treat
554
+ `Hl7ParseWarning.message` and `Hl7ParseError.message` as potentially carrying field content for
555
+ feeds they do not control until they upgrade.
556
+
557
+ - **Stale counts in `docs-content/`, found while correcting the PHI wording.** The warning-code
558
+ count was given as 19 in three places (it is 20), and two pages quoted a published npm version
559
+ that was two releases stale. Both now point at the registry rather than restating it, which is the
560
+ only way a doc page stops drifting from it.
561
+
562
+ - **Four false claims in the README's feature list, each re-derived from source rather than from
563
+ memory (`README-CLAIM-DRIFT`).** Every number and version below was measured against the thing it
564
+ describes, in the order a consumer would have to trust it:
565
+ 1. **Node 18+ to Node 22+**, in the badge (`node-%3E%3D18`) and in the prose. `engines.node` is
566
+ `>=22.0.0` in `package.json` and in the registry (`npm view @cosyte/hl7 engines`), and CI runs
567
+ the 22 + 24 matrix. This was the one with a consumer-visible consequence: `npm i @cosyte/hl7`
568
+ on Node 18 warns on engines while the package page says 18 is supported.
569
+ 2. **ES2022 to ES2023.** `@cosyte/tsconfig/base.json` sets `"target": "ES2023"` and
570
+ `"lib": ["ES2023"]`, and this repo's `tsconfig.json` extends it without override.
571
+ 3. **18 to 20 stable warning codes.** `WARNING_CODES` in `src/parser/warnings.ts` has 20 keys, and
572
+ `test/warning-codes.snapshot.test.ts` already asserted `toHaveLength(20)`. A green test suite
573
+ and a wrong package page coexisted. **No other surface says 18, but that is not the same as the
574
+ count now being consistent**: further down `README.md`, an inline prose list of 16 code names is
575
+ still introduced as "the full list of Tier-2 codes", and `docs-content/` says 19 in three
576
+ places. Those are pre-existing, outside this feature list, and left for their own change rather
577
+ than folded in here.
578
+ 4. **"Every public export has JSDoc + `@example`" to "every public function and class."** Measured
579
+ over the 213 names in the built `dist/index.d.ts` export list: all **79 functions** and all
580
+ **5 classes** carry both, but of 17 value constants one (`ERR_CONDITION_CODE_SYSTEM`) has JSDoc
581
+ with no `@example`, and while all **109 type-only exports carry JSDoc, only 73 carry an
582
+ `@example`**. The three aliased namespace exports (`HL7`, `conformance`, `text`) fail the old
583
+ wording outright: two carry no JSDoc at all and the third has no `@example`. The narrowed
584
+ sentence is exactly true; the old one was not.
585
+
586
+ **The rest of the feature list was checked the same way and is correct**, so it is unchanged:
587
+ 4 fatal codes (`FATAL_CODES` has exactly 4 keys), 8 built-in vendor profiles (the frozen `profiles`
588
+ namespace has 8, and the eight vendor names match), zero runtime dependencies (`dependencies` is
589
+ present and empty, and there is no `peerDependencies`), `noUncheckedIndexedAccess` (true in the
590
+ inherited base config), dual ESM + CJS (the `exports` map resolves `import` to `.mjs`/`.d.ts` and
591
+ `require` to `.cjs`/`.d.cts`, and `attw` passes node10, node16-CJS, node16-ESM and bundler), and
592
+ the two linked paths (`examples/profile-starter-kit/`, `docs-content/spec-notes-escapes.md`) both
593
+ resolve.
594
+
595
+ **`CLAUDE.md` already said Node >= 22 and Target ES2023, so the repo contradicted itself and
596
+ nothing caught it.** The two files drifted in opposite directions because only one of them is read
597
+ by a consumer, and it is the one that drifted. No gate compares a README claim to the config it
598
+ describes, which is why these were found by hand.
599
+
313
600
  - **`scripts/sync-version.mjs` hardened against two latent defects, and gated in CI
314
601
  (SYNC-VERSION-HARDENING).** Follow-up hardening on the VERSION-SYNC script; ported byte-identically
315
602
  across `hl7`, `x12`, and `mllp`. (1) The version was spliced into `src/index.ts` via
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![@cosyte/hl7: parse real-world, vendor-quirky HL7 v2 messages and extract the fields you need in one line](https://cosyte.com/social/cosyte-banner-hl7-1200x300.png)
2
+
1
3
  # @cosyte/hl7
2
4
 
3
5
  > Parse real-world, vendor-quirky HL7 v2 messages and extract the fields you need in one line, without reading the spec.
@@ -5,7 +7,7 @@
5
7
  [![npm version](https://img.shields.io/npm/v/@cosyte/hl7.svg)](https://www.npmjs.com/package/@cosyte/hl7)
6
8
  [![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
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
8
- [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
10
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org)
9
11
 
10
12
  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
13
 
@@ -37,10 +39,10 @@ That's the whole pitch: no config, no schema upload, no spec lookup. The parser
37
39
 
38
40
  - **One-line extraction**: `msg.patient.mrn`, `msg.meta.timestamp`, `msg.observations()`, and friends. No segment or field numbers to memorise.
39
41
  - **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.
42
+ - **Real-world tolerance, four-tier**: lenient default parses vendor-quirky messages; 20 stable warning codes flag what was tolerated; strict mode escalates every deviation for CI validators; only 4 truly-structural failures are fatal.
41
43
  - **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
44
  - **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.
45
+ - **Strict TypeScript, zero runtime deps**: ES2023, `noUncheckedIndexedAccess`, dual ESM + CJS, Node 22+. Every public function and class has JSDoc + `@example` that feeds your editor's IntelliSense.
44
46
  - **Warnings carry stable codes + positional context**: react programmatically by `w.code`, with `segmentIndex`/`fieldIndex`/etc. attached.
45
47
 
46
48
  ---
@@ -251,7 +253,7 @@ for (const charge of parseHL7(raw).charges()) {
251
253
  }
252
254
  ```
253
255
 
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).
256
+ 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
257
 
256
258
  ### Order & medication timing
257
259
 
@@ -739,7 +741,7 @@ The library throws exactly three error types, all exported from the package barr
739
741
 
740
742
  ### `Hl7ParseError`
741
743
 
742
- Thrown by `parseHL7` when the input hits one of the 4 Tier-3 fatal codes (see above). Carries positional context plus a short snippet of the offending input.
744
+ Thrown by `parseHL7` when the input hits one of the 4 Tier-3 fatal codes (see above). Carries positional context plus a short snippet taken from the start of the input (up to 40 characters, plus an ellipsis when truncated).
743
745
 
744
746
  ```ts
745
747
  import { parseHL7, Hl7ParseError, FATAL_CODES } from "@cosyte/hl7";
@@ -760,7 +762,13 @@ if (err instanceof Hl7ParseError && err.code === FATAL_CODES.NO_MSH_SEGMENT) {
760
762
  }
761
763
  ```
762
764
 
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.
765
+ `Hl7ParseError.snippet` may contain PHI when parsing real clinical messages. The library does NOT redact it. Redact it at your call site if compliance demands it.
766
+
767
+ **`snippet` is the only field carrying input verbatim and unfiltered** (capped at 40 characters plus an ellipsis, but not shape-checked in any way), so redact it first.
768
+
769
+ `message` is bounded but not absolutely content-free, which is worth stating because `message` is what a logger prints by default and what `stack` embeds. A token taken from the input is echoed only when it matches the form the spec defines for it: a three-character segment identifier, an MSH-9 type, an MSH-12 version, or a charset label the closed Table 0211 actually contains. Anything else becomes `<withheld>`. So a message cannot carry a field's value, but it can carry a residue of up to three characters when a malformed line happens to look like a segment identifier. The other shapes are narrower in practice than their patterns allow, because the library only ever feeds them registry-matched values; the patterns themselves admit more (a message type up to 26 characters, a version up to 14), which matters only if you construct warnings yourself.
770
+
771
+ Under `{ strict: true }` an escalated Tier-2 warning is thrown as an `Hl7ParseError` carrying that same bounded message, plus a `snippet` of the first 40 characters of the input rather than of the deviation's own segment.
764
772
 
765
773
  ### `Hl7ParseWarning`
766
774