@cosyte/hl7 0.0.3 → 0.0.5

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/dist/index.d.cts CHANGED
@@ -248,7 +248,8 @@ declare function missingRequiredField(position: Hl7Position, segmentName: string
248
248
  * signature of a truncated or misrouted feed. Tier-2 and additive: lenient
249
249
  * parse never throws on it, `strict` mode may promote it. The message carries
250
250
  * only the structural fact (message type, group name, anchor segment names),
251
- * NEVER a field value, so no PHI is exposed. `position` references MSH-9.
251
+ * never a field value: the message type is shape-checked before it is
252
+ * interpolated, and withheld if it does not match. `position` references MSH-9.
252
253
  *
253
254
  * @example
254
255
  * ```ts
@@ -292,8 +293,8 @@ declare function versionMismatch(position: Hl7Position, declared: string, expect
292
293
  * Table-0211 character set**. The parser never guesses an encoding: it reads the
293
294
  * raw bytes as `latin1` (a 1:1 byte mapping) so single-byte content stays
294
295
  * recoverable, rather than corrupting it with replacement characters. The
295
- * message carries the charset code only, never a decoded field value, so no
296
- * PHI is exposed.
296
+ * message carries the charset code only, never a decoded field value: the code
297
+ * is shape-checked before it is interpolated, and withheld if it does not match.
297
298
  *
298
299
  * @example
299
300
  * ```ts
@@ -315,8 +316,8 @@ declare function unknownCharset(position: Hl7Position, requested: string): Hl7Pa
315
316
  * coincide with a structural delimiter: see the parser's known-limitations).
316
317
  * The switch escapes (`\Cxxyy\` / `\Mxxyyzz\`) are recognized and preserved by
317
318
  * the escape layer; full stateful decoding is a documented non-goal. The message
318
- * carries the charset code only, never a decoded field value, so no PHI is
319
- * exposed.
319
+ * carries the charset code only, never a decoded field value: the code is
320
+ * shape-checked before it is interpolated, and withheld if it does not match.
320
321
  *
321
322
  * @example
322
323
  * ```ts
@@ -2314,7 +2315,24 @@ declare class Field {
2314
2315
  * ```
2315
2316
  */
2316
2317
  declare class Segment {
2317
- /** Segment name (3 chars, e.g. `"PID"`, `"OBX"`, `"ZPI"`). */
2318
+ /**
2319
+ * Segment identifier: three characters with a leading letter, e.g. `"PID"`,
2320
+ * `"OBX"`, `"ZPI"`.
2321
+ *
2322
+ * **Bounded, and it is `"<withheld>"` when the raw name is not that shape.**
2323
+ * A line with no field separator has its whole content read as a segment
2324
+ * name, so an unescaped line break inside a narrative field forges a
2325
+ * "segment" whose name is clinical text. This field presents itself as a
2326
+ * structural identifier and consumers interpolate it into labels, loci and
2327
+ * reports, so it never carries that text. `""` still means absent, which is
2328
+ * a different fact from withheld.
2329
+ *
2330
+ * Use {@link Segment.raw}`.name` when you need the verbatim text: it is the
2331
+ * unbounded value, and it is what serialization emits, so the byte-verbatim
2332
+ * round-trip is unaffected. One consequence worth knowing: a segment whose
2333
+ * raw name fails the shape (a 4-character vendor Z-segment, which HL7 v2
2334
+ * Ch. 2 §2.5 does not permit) is not matched by `msg.segments(name)`.
2335
+ */
2318
2336
  readonly type: string;
2319
2337
  /** Reference to the underlying `RawSegment.fields`: 1-indexed per HL7 convention. */
2320
2338
  readonly fields: readonly RawField[];
@@ -3602,9 +3620,19 @@ interface StructureGroup {
3602
3620
  interface MessageStructure {
3603
3621
  /** `true` when a `MESSAGE_STRUCTURE_DEFINITIONS` entry matched the type. */
3604
3622
  readonly recognized: boolean;
3605
- /** MSH-9.1 message code observed on the message (empty string if absent). */
3623
+ /**
3624
+ * MSH-9.1 message code observed on the message. `""` when absent, and
3625
+ * `"<withheld>"` when `recognized` is `false` and the observed value is not
3626
+ * identifier-shaped (MSH-9 can hold a data field on a malformed message).
3627
+ */
3606
3628
  readonly messageCode: string;
3607
- /** MSH-9.2 trigger event observed on the message (empty string if absent). */
3629
+ /**
3630
+ * MSH-9.2 trigger event observed on the message. `""` when absent, and
3631
+ * `"<withheld>"` when `recognized` is `false` and the observed value is not
3632
+ * identifier-shaped. Note that on the `recognized` branch this is echoed
3633
+ * verbatim, which for a definition matching on message code alone (`ACK`)
3634
+ * means an arbitrary MSH-9.2.
3635
+ */
3608
3636
  readonly triggerEvent: string;
3609
3637
  /** Per-expected-group presence verdicts (empty when unrecognized). */
3610
3638
  readonly expectedGroups: readonly StructureGroup[];
@@ -3985,6 +4013,16 @@ declare class Hl7Message {
3985
4013
  */
3986
4014
  readonly rawSegments: readonly RawSegment[];
3987
4015
  readonly encodingCharacters: EncodingCharacters;
4016
+ /**
4017
+ * HL7 version the model asserts for this message, from MSH-12.1.1, e.g.
4018
+ * `"2.5"` or `"2.5.1"`.
4019
+ *
4020
+ * **Bounded, and it is `"<withheld>"` when MSH-12 does not hold a version.**
4021
+ * Same reasoning as {@link Segment.type}: it presents as an identifier and
4022
+ * consumers label with it. `meta.version` keeps MSH-12 exactly as it
4023
+ * arrived, and is what version-scoped field selection reads, so this bound
4024
+ * changes no parsing behaviour.
4025
+ */
3988
4026
  readonly version: string;
3989
4027
  readonly warnings: readonly Hl7ParseWarning[];
3990
4028
  readonly profile: {
@@ -4646,9 +4684,31 @@ type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
4646
4684
  * snippet of the offending input so consumers can log actionable errors.
4647
4685
  *
4648
4686
  * @remarks
4649
- * Snippets may contain PHI when parsing real clinical messages: redact at
4650
- * the call site if required by your compliance posture. The library does
4651
- * not redact snippets itself.
4687
+ * `snippet` may contain PHI when parsing real clinical messages, and the
4688
+ * library does not redact it: redact at the call site if required by your
4689
+ * compliance posture. **It is the only field carrying input verbatim and
4690
+ * unfiltered** (capped at 40 characters plus an ellipsis, but not shape-checked
4691
+ * in any way), so it is the one to redact first.
4692
+ *
4693
+ * `message` is bounded but not absolutely content-free, and the difference is
4694
+ * worth stating because `message` is what a logger prints by default and what
4695
+ * `stack` embeds. A token lifted from the input is echoed only when it matches
4696
+ * the form the spec defines for it: a three-character segment identifier, an
4697
+ * MSH-9 type, an MSH-12 version, or a charset label the closed Table 0211
4698
+ * actually contains. Anything else becomes `<withheld>`. So a message cannot
4699
+ * carry a field's value, but it can carry a residue of up to three characters
4700
+ * when a malformed line happens to look like a segment identifier. The other
4701
+ * shapes are narrower in practice than their patterns allow, because the
4702
+ * library only ever feeds them registry-matched values, but the patterns
4703
+ * themselves admit more (a message type up to 26 characters, a version up to
4704
+ * 14), which matters if you construct warnings yourself.
4705
+ *
4706
+ * Under `{ strict: true }` an escalated Tier-2 warning is thrown as this
4707
+ * error, carrying the warning's own bounded message and a `snippet` of the
4708
+ * **first 40 characters of the input**. That snippet is the head of the
4709
+ * message rather than the deviation's own segment, so it is usually the MSH
4710
+ * header; it is deliberately not re-pointed at the offending segment, since
4711
+ * doing so would move more clinical content into the unredacted field.
4652
4712
  *
4653
4713
  * @example
4654
4714
  * ```ts
package/dist/index.d.ts CHANGED
@@ -248,7 +248,8 @@ declare function missingRequiredField(position: Hl7Position, segmentName: string
248
248
  * signature of a truncated or misrouted feed. Tier-2 and additive: lenient
249
249
  * parse never throws on it, `strict` mode may promote it. The message carries
250
250
  * only the structural fact (message type, group name, anchor segment names),
251
- * NEVER a field value, so no PHI is exposed. `position` references MSH-9.
251
+ * never a field value: the message type is shape-checked before it is
252
+ * interpolated, and withheld if it does not match. `position` references MSH-9.
252
253
  *
253
254
  * @example
254
255
  * ```ts
@@ -292,8 +293,8 @@ declare function versionMismatch(position: Hl7Position, declared: string, expect
292
293
  * Table-0211 character set**. The parser never guesses an encoding: it reads the
293
294
  * raw bytes as `latin1` (a 1:1 byte mapping) so single-byte content stays
294
295
  * recoverable, rather than corrupting it with replacement characters. The
295
- * message carries the charset code only, never a decoded field value, so no
296
- * PHI is exposed.
296
+ * message carries the charset code only, never a decoded field value: the code
297
+ * is shape-checked before it is interpolated, and withheld if it does not match.
297
298
  *
298
299
  * @example
299
300
  * ```ts
@@ -315,8 +316,8 @@ declare function unknownCharset(position: Hl7Position, requested: string): Hl7Pa
315
316
  * coincide with a structural delimiter: see the parser's known-limitations).
316
317
  * The switch escapes (`\Cxxyy\` / `\Mxxyyzz\`) are recognized and preserved by
317
318
  * the escape layer; full stateful decoding is a documented non-goal. The message
318
- * carries the charset code only, never a decoded field value, so no PHI is
319
- * exposed.
319
+ * carries the charset code only, never a decoded field value: the code is
320
+ * shape-checked before it is interpolated, and withheld if it does not match.
320
321
  *
321
322
  * @example
322
323
  * ```ts
@@ -2314,7 +2315,24 @@ declare class Field {
2314
2315
  * ```
2315
2316
  */
2316
2317
  declare class Segment {
2317
- /** Segment name (3 chars, e.g. `"PID"`, `"OBX"`, `"ZPI"`). */
2318
+ /**
2319
+ * Segment identifier: three characters with a leading letter, e.g. `"PID"`,
2320
+ * `"OBX"`, `"ZPI"`.
2321
+ *
2322
+ * **Bounded, and it is `"<withheld>"` when the raw name is not that shape.**
2323
+ * A line with no field separator has its whole content read as a segment
2324
+ * name, so an unescaped line break inside a narrative field forges a
2325
+ * "segment" whose name is clinical text. This field presents itself as a
2326
+ * structural identifier and consumers interpolate it into labels, loci and
2327
+ * reports, so it never carries that text. `""` still means absent, which is
2328
+ * a different fact from withheld.
2329
+ *
2330
+ * Use {@link Segment.raw}`.name` when you need the verbatim text: it is the
2331
+ * unbounded value, and it is what serialization emits, so the byte-verbatim
2332
+ * round-trip is unaffected. One consequence worth knowing: a segment whose
2333
+ * raw name fails the shape (a 4-character vendor Z-segment, which HL7 v2
2334
+ * Ch. 2 §2.5 does not permit) is not matched by `msg.segments(name)`.
2335
+ */
2318
2336
  readonly type: string;
2319
2337
  /** Reference to the underlying `RawSegment.fields`: 1-indexed per HL7 convention. */
2320
2338
  readonly fields: readonly RawField[];
@@ -3602,9 +3620,19 @@ interface StructureGroup {
3602
3620
  interface MessageStructure {
3603
3621
  /** `true` when a `MESSAGE_STRUCTURE_DEFINITIONS` entry matched the type. */
3604
3622
  readonly recognized: boolean;
3605
- /** MSH-9.1 message code observed on the message (empty string if absent). */
3623
+ /**
3624
+ * MSH-9.1 message code observed on the message. `""` when absent, and
3625
+ * `"<withheld>"` when `recognized` is `false` and the observed value is not
3626
+ * identifier-shaped (MSH-9 can hold a data field on a malformed message).
3627
+ */
3606
3628
  readonly messageCode: string;
3607
- /** MSH-9.2 trigger event observed on the message (empty string if absent). */
3629
+ /**
3630
+ * MSH-9.2 trigger event observed on the message. `""` when absent, and
3631
+ * `"<withheld>"` when `recognized` is `false` and the observed value is not
3632
+ * identifier-shaped. Note that on the `recognized` branch this is echoed
3633
+ * verbatim, which for a definition matching on message code alone (`ACK`)
3634
+ * means an arbitrary MSH-9.2.
3635
+ */
3608
3636
  readonly triggerEvent: string;
3609
3637
  /** Per-expected-group presence verdicts (empty when unrecognized). */
3610
3638
  readonly expectedGroups: readonly StructureGroup[];
@@ -3985,6 +4013,16 @@ declare class Hl7Message {
3985
4013
  */
3986
4014
  readonly rawSegments: readonly RawSegment[];
3987
4015
  readonly encodingCharacters: EncodingCharacters;
4016
+ /**
4017
+ * HL7 version the model asserts for this message, from MSH-12.1.1, e.g.
4018
+ * `"2.5"` or `"2.5.1"`.
4019
+ *
4020
+ * **Bounded, and it is `"<withheld>"` when MSH-12 does not hold a version.**
4021
+ * Same reasoning as {@link Segment.type}: it presents as an identifier and
4022
+ * consumers label with it. `meta.version` keeps MSH-12 exactly as it
4023
+ * arrived, and is what version-scoped field selection reads, so this bound
4024
+ * changes no parsing behaviour.
4025
+ */
3988
4026
  readonly version: string;
3989
4027
  readonly warnings: readonly Hl7ParseWarning[];
3990
4028
  readonly profile: {
@@ -4646,9 +4684,31 @@ type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
4646
4684
  * snippet of the offending input so consumers can log actionable errors.
4647
4685
  *
4648
4686
  * @remarks
4649
- * Snippets may contain PHI when parsing real clinical messages: redact at
4650
- * the call site if required by your compliance posture. The library does
4651
- * not redact snippets itself.
4687
+ * `snippet` may contain PHI when parsing real clinical messages, and the
4688
+ * library does not redact it: redact at the call site if required by your
4689
+ * compliance posture. **It is the only field carrying input verbatim and
4690
+ * unfiltered** (capped at 40 characters plus an ellipsis, but not shape-checked
4691
+ * in any way), so it is the one to redact first.
4692
+ *
4693
+ * `message` is bounded but not absolutely content-free, and the difference is
4694
+ * worth stating because `message` is what a logger prints by default and what
4695
+ * `stack` embeds. A token lifted from the input is echoed only when it matches
4696
+ * the form the spec defines for it: a three-character segment identifier, an
4697
+ * MSH-9 type, an MSH-12 version, or a charset label the closed Table 0211
4698
+ * actually contains. Anything else becomes `<withheld>`. So a message cannot
4699
+ * carry a field's value, but it can carry a residue of up to three characters
4700
+ * when a malformed line happens to look like a segment identifier. The other
4701
+ * shapes are narrower in practice than their patterns allow, because the
4702
+ * library only ever feeds them registry-matched values, but the patterns
4703
+ * themselves admit more (a message type up to 26 characters, a version up to
4704
+ * 14), which matters if you construct warnings yourself.
4705
+ *
4706
+ * Under `{ strict: true }` an escalated Tier-2 warning is thrown as this
4707
+ * error, carrying the warning's own bounded message and a `snippet` of the
4708
+ * **first 40 characters of the input**. That snippet is the head of the
4709
+ * message rather than the deviation's own segment, so it is usually the MSH
4710
+ * header; it is deliberately not re-pointed at the offending segment, since
4711
+ * doing so would move more clinical content into the unredacted field.
4652
4712
  *
4653
4713
  * @example
4654
4714
  * ```ts
package/dist/index.mjs CHANGED
@@ -253,7 +253,104 @@ var KNOWN_SEGMENTS = /* @__PURE__ */ new Set([
253
253
  "CSS"
254
254
  ]);
255
255
 
256
+ // src/parser/charset.ts
257
+ function normalizeLabel(raw) {
258
+ return raw.trim().replace(/\s+/gu, " ").toUpperCase();
259
+ }
260
+ function decodeEntry(canonical, decoder) {
261
+ return { canonical, treatment: "decode", decoder };
262
+ }
263
+ function verbatimEntry(canonical) {
264
+ return { canonical, treatment: "verbatim", decoder: "" };
265
+ }
266
+ function buildTable() {
267
+ const table = /* @__PURE__ */ new Map();
268
+ const add = (aliases, entry) => {
269
+ for (const alias of aliases) table.set(normalizeLabel(alias), entry);
270
+ };
271
+ add(["ASCII", "US-ASCII", "ISO IR6"], decodeEntry("ASCII", "utf-8"));
272
+ add(["UTF-8", "UTF8", "UNICODE UTF-8", "UNICODE"], decodeEntry("UTF-8", "utf-8"));
273
+ add(["8859/1", "ISO-8859-1", "ISO 8859-1", "ISO8859-1"], decodeEntry("8859/1", "latin1"));
274
+ for (const n of [2, 3, 4, 5, 6, 7, 8, 10, 13, 14, 15, 16]) {
275
+ add(
276
+ [`8859/${n}`, `ISO-8859-${n}`, `ISO 8859-${n}`, `ISO8859-${n}`],
277
+ decodeEntry(`8859/${n}`, `iso-8859-${n}`)
278
+ );
279
+ }
280
+ add(["8859/9", "ISO-8859-9", "ISO 8859-9", "ISO8859-9"], verbatimEntry("8859/9"));
281
+ add(["8859/11", "ISO-8859-11", "ISO 8859-11", "ISO8859-11"], verbatimEntry("8859/11"));
282
+ add(["ISO IR14", "JIS X 0201"], verbatimEntry("ISO IR14"));
283
+ add(["ISO IR87", "JIS X 0208"], verbatimEntry("ISO IR87"));
284
+ add(["ISO IR159", "JIS X 0212"], verbatimEntry("ISO IR159"));
285
+ add(["GB 18030-2000", "GB18030", "GB 18030"], verbatimEntry("GB 18030-2000"));
286
+ add(["KS X 1001", "KSX1001", "KS C 5601"], verbatimEntry("KS X 1001"));
287
+ add(["CNS 11643-1992", "CNS 11643"], verbatimEntry("CNS 11643-1992"));
288
+ add(["BIG-5", "BIG5"], verbatimEntry("BIG-5"));
289
+ add(["UNICODE UTF-16", "UTF-16"], verbatimEntry("UNICODE UTF-16"));
290
+ add(["UNICODE UTF-32", "UTF-32"], verbatimEntry("UNICODE UTF-32"));
291
+ return table;
292
+ }
293
+ var CHARSET_TABLE = buildTable();
294
+ var DEFAULT_RESOLUTION = Object.freeze({
295
+ canonical: "ASCII",
296
+ treatment: "decode",
297
+ decoder: "utf-8",
298
+ recognized: true
299
+ });
300
+ function resolveCharset(raw) {
301
+ if (raw === void 0) return DEFAULT_RESOLUTION;
302
+ const key = normalizeLabel(raw);
303
+ if (key === "") return DEFAULT_RESOLUTION;
304
+ const entry = CHARSET_TABLE.get(key);
305
+ if (entry !== void 0) {
306
+ return {
307
+ canonical: entry.canonical,
308
+ treatment: entry.treatment,
309
+ decoder: entry.decoder,
310
+ recognized: true
311
+ };
312
+ }
313
+ return { canonical: key, treatment: "verbatim", decoder: "", recognized: false };
314
+ }
315
+ function canonicalCharset(raw) {
316
+ return resolveCharset(raw).canonical;
317
+ }
318
+
319
+ // src/parser/tokens.ts
320
+ var DERIVED_TOKEN_SHAPES = {
321
+ /**
322
+ * HL7 v2 Ch. 2 §2.5 segment identifier: three characters, the first
323
+ * alphabetic.
324
+ *
325
+ * Two deliberate tightenings, each measured rather than assumed. The first
326
+ * version allowed four characters as "vendor tolerance", which was invented
327
+ * rather than grounded: all 91 `KNOWN_SEGMENTS` are exactly three, and
328
+ * `defineProfile` independently validates custom Z-segments to
329
+ * `/^Z[A-Z0-9]{2}$/`, so nothing legitimate is longer. The second requires a
330
+ * leading letter, which every known segment and every legal Z-segment
331
+ * satisfies, and which excludes a **bare numeric** residue. That matters out
332
+ * of proportion to its size: a forged line reading `120` is a glucose, a
333
+ * blood pressure or a dose, which is the most clinical meaning three
334
+ * characters can carry.
335
+ */
336
+ segmentId: /^[A-Za-z][A-Za-z0-9]{2}$/,
337
+ /** MSH-9 message type / trigger, e.g. `ADT^A01` or `A40`. */
338
+ messageType: /^[A-Za-z0-9]{1,8}(?:\^[A-Za-z0-9]{1,8}){0,2}$/,
339
+ /** MSH-12 version ID, e.g. `2.5` or `2.5.1`. */
340
+ version: /^[0-9]{1,2}(?:\.[0-9]{1,3}){0,3}$/
341
+ };
342
+ function safeDerivedToken(value, kind) {
343
+ return DERIVED_TOKEN_SHAPES[kind].test(value) ? value : WITHHELD;
344
+ }
345
+ var WITHHELD = "<withheld>";
346
+ function boundedIdentifier(value, kind) {
347
+ return value === "" ? "" : safeDerivedToken(value, kind);
348
+ }
349
+
256
350
  // src/parser/warnings.ts
351
+ function safeCharsetLabel(value) {
352
+ return resolveCharset(value).recognized ? value : WITHHELD;
353
+ }
257
354
  var WARNING_CODES = {
258
355
  MLLP_FRAMING_STRIPPED: "MLLP_FRAMING_STRIPPED",
259
356
  FIELD_WHITESPACE_TRIMMED: "FIELD_WHITESPACE_TRIMMED",
@@ -338,28 +435,28 @@ function timestampFallbackFormat(position, matchedFormat) {
338
435
  function segmentCase(position, observed) {
339
436
  return {
340
437
  code: WARNING_CODES.SEGMENT_CASE,
341
- message: `Segment identifier "${observed}" is not uppercase; normalized to "${observed.toUpperCase()}".`,
438
+ message: `Segment identifier "${safeDerivedToken(observed, "segmentId")}" is not uppercase; normalized to "${safeDerivedToken(observed.toUpperCase(), "segmentId")}".`,
342
439
  position
343
440
  };
344
441
  }
345
442
  function extraFields(position, segmentName2, extraCount) {
346
443
  return {
347
444
  code: WARNING_CODES.EXTRA_FIELDS,
348
- message: `Segment "${segmentName2}" has ${String(extraCount)} more field(s) than the profile declares.`,
445
+ message: `Segment "${safeDerivedToken(segmentName2, "segmentId")}" has ${String(extraCount)} more field(s) than the profile declares.`,
349
446
  position
350
447
  };
351
448
  }
352
449
  function unknownSegment(position, segmentName2) {
353
450
  return {
354
451
  code: WARNING_CODES.UNKNOWN_SEGMENT,
355
- message: `Unknown segment "${segmentName2}": not in HL7 spec and no profile claim.`,
452
+ message: `Unknown segment "${safeDerivedToken(segmentName2, "segmentId")}": not in HL7 spec and no profile claim.`,
356
453
  position
357
454
  };
358
455
  }
359
456
  function duplicateRequiredSegment(position, segmentName2) {
360
457
  return {
361
458
  code: WARNING_CODES.DUPLICATE_REQUIRED_SEGMENT,
362
- message: `Required singleton segment "${segmentName2}" appears more than once.`,
459
+ message: `Required singleton segment "${safeDerivedToken(segmentName2, "segmentId")}" appears more than once.`,
363
460
  position
364
461
  };
365
462
  }
@@ -373,42 +470,42 @@ function encodingMismatch(position, detail) {
373
470
  function missingRequiredField(position, segmentName2, fieldIndex) {
374
471
  return {
375
472
  code: WARNING_CODES.MISSING_REQUIRED_FIELD,
376
- message: `Required field ${segmentName2}-${String(fieldIndex)} is missing or empty.`,
473
+ message: `Required field ${safeDerivedToken(segmentName2, "segmentId")}-${String(fieldIndex)} is missing or empty.`,
377
474
  position
378
475
  };
379
476
  }
380
477
  function missingExpectedGroup(position, messageType, groupName, anchorSegments) {
381
478
  return {
382
479
  code: WARNING_CODES.MISSING_EXPECTED_GROUP,
383
- message: `Message type "${messageType}" is missing its expected "${groupName}" segment group (no ${anchorSegments.join("/")}); message may be truncated or misrouted.`,
480
+ message: `Message type "${safeDerivedToken(messageType, "messageType")}" is missing its expected "${groupName}" segment group (no ${anchorSegments.join("/")}); message may be truncated or misrouted.`,
384
481
  position
385
482
  };
386
483
  }
387
484
  function outOfOrderSegment(position, segmentName2) {
388
485
  return {
389
486
  code: WARNING_CODES.OUT_OF_ORDER_SEGMENT,
390
- message: `Segment "${segmentName2}" appears out of the order declared by the active profile.`,
487
+ message: `Segment "${safeDerivedToken(segmentName2, "segmentId")}" appears out of the order declared by the active profile.`,
391
488
  position
392
489
  };
393
490
  }
394
491
  function versionMismatch(position, declared, expected) {
395
492
  return {
396
493
  code: WARNING_CODES.VERSION_MISMATCH,
397
- message: `HL7 version mismatch: message declares "${declared}", expected "${expected}".`,
494
+ message: `HL7 version mismatch: message declares "${safeDerivedToken(declared, "version")}", expected "${expected}".`,
398
495
  position
399
496
  };
400
497
  }
401
498
  function unknownCharset(position, requested) {
402
499
  return {
403
500
  code: WARNING_CODES.UNKNOWN_CHARSET,
404
- message: `Unrecognized character set "${requested}"; not decoded, bytes read as latin1.`,
501
+ message: `Unrecognized character set "${safeCharsetLabel(requested)}"; not decoded, bytes read as latin1.`,
405
502
  position
406
503
  };
407
504
  }
408
505
  function unsupportedCharset(position, code) {
409
506
  return {
410
507
  code: WARNING_CODES.UNSUPPORTED_CHARSET,
411
- message: `Character set "${code}" was not decoded; bytes read as latin1.`,
508
+ message: `Character set "${safeCharsetLabel(code)}" was not decoded; bytes read as latin1.`,
412
509
  position
413
510
  };
414
511
  }
@@ -423,7 +520,7 @@ function mergeMissingPriorOrSurvivor(position, eventType, missing) {
423
520
  const detail = missing === "prior" ? "no MRG segment carries a usable prior (non-surviving) identifier" : "no PID segment carries a usable surviving identifier";
424
521
  return {
425
522
  code: WARNING_CODES.MERGE_MISSING_PRIOR_OR_SURVIVOR,
426
- message: `Identity event "${eventType}" is missing its ${missing} role: ${detail}. Surfacing what is present; the MRG->PID merge direction is not guessed.`,
523
+ message: `Identity event "${safeDerivedToken(eventType, "messageType")}" is missing its ${missing} role: ${detail}. Surfacing what is present; the MRG->PID merge direction is not guessed.`,
427
524
  position
428
525
  };
429
526
  }
@@ -469,69 +566,6 @@ function emitIfFramed(result, emit, position) {
469
566
  }
470
567
  }
471
568
 
472
- // src/parser/charset.ts
473
- function normalizeLabel(raw) {
474
- return raw.trim().replace(/\s+/gu, " ").toUpperCase();
475
- }
476
- function decodeEntry(canonical, decoder) {
477
- return { canonical, treatment: "decode", decoder };
478
- }
479
- function verbatimEntry(canonical) {
480
- return { canonical, treatment: "verbatim", decoder: "" };
481
- }
482
- function buildTable() {
483
- const table = /* @__PURE__ */ new Map();
484
- const add = (aliases, entry) => {
485
- for (const alias of aliases) table.set(normalizeLabel(alias), entry);
486
- };
487
- add(["ASCII", "US-ASCII", "ISO IR6"], decodeEntry("ASCII", "utf-8"));
488
- add(["UTF-8", "UTF8", "UNICODE UTF-8", "UNICODE"], decodeEntry("UTF-8", "utf-8"));
489
- add(["8859/1", "ISO-8859-1", "ISO 8859-1", "ISO8859-1"], decodeEntry("8859/1", "latin1"));
490
- for (const n of [2, 3, 4, 5, 6, 7, 8, 10, 13, 14, 15, 16]) {
491
- add(
492
- [`8859/${n}`, `ISO-8859-${n}`, `ISO 8859-${n}`, `ISO8859-${n}`],
493
- decodeEntry(`8859/${n}`, `iso-8859-${n}`)
494
- );
495
- }
496
- add(["8859/9", "ISO-8859-9", "ISO 8859-9", "ISO8859-9"], verbatimEntry("8859/9"));
497
- add(["8859/11", "ISO-8859-11", "ISO 8859-11", "ISO8859-11"], verbatimEntry("8859/11"));
498
- add(["ISO IR14", "JIS X 0201"], verbatimEntry("ISO IR14"));
499
- add(["ISO IR87", "JIS X 0208"], verbatimEntry("ISO IR87"));
500
- add(["ISO IR159", "JIS X 0212"], verbatimEntry("ISO IR159"));
501
- add(["GB 18030-2000", "GB18030", "GB 18030"], verbatimEntry("GB 18030-2000"));
502
- add(["KS X 1001", "KSX1001", "KS C 5601"], verbatimEntry("KS X 1001"));
503
- add(["CNS 11643-1992", "CNS 11643"], verbatimEntry("CNS 11643-1992"));
504
- add(["BIG-5", "BIG5"], verbatimEntry("BIG-5"));
505
- add(["UNICODE UTF-16", "UTF-16"], verbatimEntry("UNICODE UTF-16"));
506
- add(["UNICODE UTF-32", "UTF-32"], verbatimEntry("UNICODE UTF-32"));
507
- return table;
508
- }
509
- var CHARSET_TABLE = buildTable();
510
- var DEFAULT_RESOLUTION = Object.freeze({
511
- canonical: "ASCII",
512
- treatment: "decode",
513
- decoder: "utf-8",
514
- recognized: true
515
- });
516
- function resolveCharset(raw) {
517
- if (raw === void 0) return DEFAULT_RESOLUTION;
518
- const key = normalizeLabel(raw);
519
- if (key === "") return DEFAULT_RESOLUTION;
520
- const entry = CHARSET_TABLE.get(key);
521
- if (entry !== void 0) {
522
- return {
523
- canonical: entry.canonical,
524
- treatment: entry.treatment,
525
- decoder: entry.decoder,
526
- recognized: true
527
- };
528
- }
529
- return { canonical: key, treatment: "verbatim", decoder: "", recognized: false };
530
- }
531
- function canonicalCharset(raw) {
532
- return resolveCharset(raw).canonical;
533
- }
534
-
535
569
  // src/parser/normalize.ts
536
570
  var CHARSET_POSITION = { segmentIndex: 0, fieldIndex: 18 };
537
571
  function preserveVerbatim(input) {
@@ -926,8 +960,8 @@ function analyzeMessageStructure(messageCode, triggerEvent, presentSegmentNames)
926
960
  if (def === void 0) {
927
961
  return Object.freeze({
928
962
  recognized: false,
929
- messageCode,
930
- triggerEvent,
963
+ messageCode: boundedIdentifier(messageCode, "segmentId"),
964
+ triggerEvent: boundedIdentifier(triggerEvent, "segmentId"),
931
965
  expectedGroups: Object.freeze([]),
932
966
  missingGroups: Object.freeze([])
933
967
  });
@@ -2159,7 +2193,24 @@ var EMPTY_REP = Object.freeze({
2159
2193
 
2160
2194
  // src/model/segment.ts
2161
2195
  var Segment = class {
2162
- /** Segment name (3 chars, e.g. `"PID"`, `"OBX"`, `"ZPI"`). */
2196
+ /**
2197
+ * Segment identifier: three characters with a leading letter, e.g. `"PID"`,
2198
+ * `"OBX"`, `"ZPI"`.
2199
+ *
2200
+ * **Bounded, and it is `"<withheld>"` when the raw name is not that shape.**
2201
+ * A line with no field separator has its whole content read as a segment
2202
+ * name, so an unescaped line break inside a narrative field forges a
2203
+ * "segment" whose name is clinical text. This field presents itself as a
2204
+ * structural identifier and consumers interpolate it into labels, loci and
2205
+ * reports, so it never carries that text. `""` still means absent, which is
2206
+ * a different fact from withheld.
2207
+ *
2208
+ * Use {@link Segment.raw}`.name` when you need the verbatim text: it is the
2209
+ * unbounded value, and it is what serialization emits, so the byte-verbatim
2210
+ * round-trip is unaffected. One consequence worth knowing: a segment whose
2211
+ * raw name fails the shape (a 4-character vendor Z-segment, which HL7 v2
2212
+ * Ch. 2 §2.5 does not permit) is not matched by `msg.segments(name)`.
2213
+ */
2163
2214
  type;
2164
2215
  /** Reference to the underlying `RawSegment.fields`: 1-indexed per HL7 convention. */
2165
2216
  fields;
@@ -2192,7 +2243,7 @@ var Segment = class {
2192
2243
  */
2193
2244
  constructor(raw, enc, absoluteIndex, customFields) {
2194
2245
  this.raw = raw;
2195
- this.type = raw.name;
2246
+ this.type = boundedIdentifier(raw.name, "segmentId");
2196
2247
  this.fields = raw.fields;
2197
2248
  this.enc = enc;
2198
2249
  this.absoluteIndex = absoluteIndex;
@@ -3883,6 +3934,16 @@ var Hl7Message = class {
3883
3934
  */
3884
3935
  rawSegments;
3885
3936
  encodingCharacters;
3937
+ /**
3938
+ * HL7 version the model asserts for this message, from MSH-12.1.1, e.g.
3939
+ * `"2.5"` or `"2.5.1"`.
3940
+ *
3941
+ * **Bounded, and it is `"<withheld>"` when MSH-12 does not hold a version.**
3942
+ * Same reasoning as {@link Segment.type}: it presents as an identifier and
3943
+ * consumers label with it. `meta.version` keeps MSH-12 exactly as it
3944
+ * arrived, and is what version-scoped field selection reads, so this bound
3945
+ * changes no parsing behaviour.
3946
+ */
3886
3947
  version;
3887
3948
  warnings;
3888
3949
  profile;
@@ -3953,7 +4014,7 @@ var Hl7Message = class {
3953
4014
  constructor(init) {
3954
4015
  this.rawSegments = init.segments;
3955
4016
  this.encodingCharacters = init.encodingCharacters;
3956
- this.version = init.version;
4017
+ this.version = boundedIdentifier(init.version, "version");
3957
4018
  this.warnings = Object.freeze(init.warnings.slice());
3958
4019
  this.profile = init.profile;
3959
4020
  if (init.customSegments !== void 0) this._customSegments = init.customSegments;
@@ -4791,7 +4852,12 @@ function resolveBufferCharset(raw, options, emit) {
4791
4852
  emit(
4792
4853
  encodingMismatch(
4793
4854
  { segmentIndex: 0 },
4794
- `options.charset="${override}" disagrees with MSH-18="${declared}"`
4855
+ // `declared` is MSH-18 read off the wire. On a message with no
4856
+ // segment terminators it is not MSH-18 at all but the 18th
4857
+ // `|`-token of the flattened message, commonly a data field, so it
4858
+ // is shape-checked before it reaches a log surface. `override` is
4859
+ // the caller's own `options.charset`, not input, and is left alone.
4860
+ `options.charset="${override}" disagrees with MSH-18="${safeCharsetLabel(declared)}"`
4795
4861
  )
4796
4862
  );
4797
4863
  }
@@ -6806,7 +6872,7 @@ async function* parseStream(source, optionsOrProfile) {
6806
6872
  }
6807
6873
 
6808
6874
  // src/index.ts
6809
- var VERSION = "0.0.3";
6875
+ var VERSION = "0.0.5";
6810
6876
 
6811
6877
  export { ACK_CODES, ACK_CONDITIONS, BUILTIN_DATE_FALLBACKS, DEFAULT_ENCODING_CHARACTERS, ERR_CONDITION_CODES, ERR_CONDITION_CODE_SYSTEM, ERR_SEVERITIES, FATAL_CODES, FINDING_CODES, Field, namespace_exports as HL7, Hl7Message, Hl7ParseError, KNOWN_CODING_SYSTEMS, KNOWN_SEGMENTS, MESSAGE_STRUCTURE_DEFINITIONS, ProfileDefinitionError, SUPPORTED_DATE_TOKENS, Segment, USAGE_CODES, VERSION, WARNING_CODES, ackNoCorrelationId, alternateCodingSystemOf, analyzeMessageStructure, batchCountMismatch, batchMissingTrailer, buildAck, buildAdt, buildMessage, buildOru, canonicalCharset, codingSystem, codingSystemOf, conformance_exports as conformance, decodeText, defineConformanceProfile, defineProfile, detectAckMode, downgradePositiveAck, dtmToDate, duplicateRequiredSegment, encodeCe, encodeComposite, encodeCompositeReps, encodeCwe, encodeCx, encodeHd, encodeNm, encodePl, encodeText, encodeTs, encodeXad, encodeXcn, encodeXpn, encodeXtn, encodingMismatch, extraFields, fieldWhitespaceTrimmed, formatDtm, getDefaultProfile, interpretAck, isPositiveAck, mergeMissingPriorOrSurvivor, missingExpectedGroup, missingRequiredField, mllpFramingStripped, outOfOrderSegment, parseCe, parseCwe, parseCx, parseDtm, parseHL7, parseHd, parseNm, parsePath, parsePl, parseSn, parseStream, parseTs, parseXad, parseXcn, parseXpn, parseXtn, pickMrn, profiles, reescape, renderText, resolveCharset, resolvePath, segmentCase, setDefaultProfile, splitBatch, text_exports as text, timestampFallbackFormat, unescape, unknownCharset, unknownEscapeSequence, unknownSegment, unsupportedCharset, unterminatedEscapeSequence, unterminatedStreamMessage, validateAgainstProfile, versionMismatch };
6812
6878
  //# sourceMappingURL=index.mjs.map