@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 +287 -0
- package/README.md +14 -6
- package/dist/index.cjs +187 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +314 -267
- package/dist/index.d.ts +314 -267
- package/dist/index.mjs +187 -122
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
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
|
+

|
|
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
|
[](https://www.npmjs.com/package/@cosyte/hl7)
|
|
6
8
|
[](https://github.com/cosyte/hl7/actions/workflows/ci.yml)
|
|
7
9
|
[](./LICENSE)
|
|
8
|
-
[](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;
|
|
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**:
|
|
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
|
|
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
|
|
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
|
|