@cosyte/astm 0.0.18 → 0.0.20

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.mjs CHANGED
@@ -269,10 +269,11 @@ var WARNING_CODES = {
269
269
  ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT: "ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT",
270
270
  /**
271
271
  * Two escape alignments of the same bytes disagreed about a **field** boundary, the reading taken
272
- * kept it, and the escape character that reading resumes on **heads no sequence at all**, so the
273
- * boundary was bought with a byte this reading cannot read while the competing alignment is
274
- * exactly the reading that can. Every field after that point sits one place further right than the
275
- * competing alignment puts it.
272
+ * kept it, and the escape character that reading resumes on **heads no sequence this codec can
273
+ * interpret**, so the boundary was bought with bytes this reading cannot read while the competing
274
+ * alignment is exactly the reading that can. Every field after that point sits one place further
275
+ * right than the competing alignment puts it, **except on the one class named at the end of this
276
+ * entry**, where the two readings instead read the same number of fields in different places.
276
277
  *
277
278
  * **The shift is the harm, and on a result record it reaches the status slot.** Measured on the
278
279
  * canonical set, `R|1|^^^687|28.6&F&|&U/L||||F` reads **9** fields under the reading taken and
@@ -303,24 +304,43 @@ var WARNING_CODES = {
303
304
  * modeled *inside* a field: a Universal Test ID's four components are the LOINC-candidate slot,
304
305
  * the test name, the **coding scheme** and the **local code**, and a patient name's three are
305
306
  * last, first and middle. A gained component boundary shifts those, so a local code can be read
306
- * as a coding scheme and a given name as a middle name. Measured, and **where the earlier body is
307
- * a recognized mnemonic, reported by nothing** (only the tolerable
308
- * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER} fires, so a gate-legal profile accepts it).
309
- * An **unrecognized** earlier body raises
310
- * {@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT} there and is refused, which is the
311
- * same split this code's own exclusion takes. It is a separate, open condition rather than something this
312
- * code covers, because wiring this sink to another split is a different criterion needing its own
313
- * population measurement. The repeat role costs the **value**, which this code does not report
314
- * either.
315
- * - **The tail is weighed one construct deep.** Where the escape character the reading taken
316
- * resumes on heads a sequence this codec *recognizes*, the reading taken interprets it and the
317
- * competing alignment would leave it bare, so the bytes prefer the reading taken: under a set
318
- * naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and
319
- * escaped again, and refusing it would be an over-refusal of a well-formed stream. Where it
320
- * heads a sequence whose body is *unrecognized*, the reading taken still consumes it while the
321
- * competing alignment would leave **two** escape characters bare, so the preference is stronger
322
- * again, and this stays silent there even though the field shift is real. That last case is the
323
- * named residue: measured, not overlooked.
307
+ * as a coding scheme and a given name as a middle name. Measured, and it used to be reported by
308
+ * nothing where the earlier body was a recognized mnemonic (only the tolerable
309
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER} fired, so a gate-legal profile accepted
310
+ * it). It is now reported by
311
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS}, a separate code on the same
312
+ * tail test wired to the component split, because wiring this sink to another split is a
313
+ * different criterion needing its own population measurement. It is still not something **this**
314
+ * code covers. The repeat role costs the **value**, which this code does not report either, and
315
+ * which {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD} reports.
316
+ * - **The tail is weighed one construct deep, and exactly one tail is excluded.** Where the escape
317
+ * character the reading taken resumes on heads a sequence this codec *recognizes*, the reading
318
+ * taken interprets a construct and leaves nothing unread: under a set naming the field separator
319
+ * `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again, entirely well
320
+ * formed, and refusing it would be an over-refusal. That is the only tail on which a stream's
321
+ * escaping can be clean, which is why it is the only exclusion. Where the tail heads a sequence
322
+ * whose body is *unrecognized*, the reading taken consumes a triple it cannot read, preserved
323
+ * verbatim and never guessed at, the field shift is the same shift, and **this reports it**.
324
+ * Consuming a triple is not interpreting one.
325
+ * - **That remaining silence is a TRADE, not a claim that nothing was lost.** The gained field
326
+ * boundary is exactly as real on the excluded tail, and there it is not merely under-reported:
327
+ * it is `warnings: []`. Under the canonical set `R|1|^^^687|28.6&F&|&F&U/L||||F` reads **nine**
328
+ * fields against the competing alignment's eight and hands back a status of **`final`** with an
329
+ * empty warning list. Reporting it would refuse a stream whose escaping is working, which is
330
+ * the trade this bound makes. **Read the raw line when an escape character sits next to a
331
+ * delimiter, whether or not anything fired.**
332
+ *
333
+ * **⚠️ THE ONE CLASS WHERE NO FIELD INDEX MOVES, NAMED RATHER THAN LEFT TO BE FOUND.** Where the
334
+ * sequence past the boundary carries the **field separator itself** as its body, the reading taken
335
+ * holds that character inside an opaque atom while the competing alignment splits on it, so the
336
+ * two readings return the **same number** of fields in **different places**. Under the canonical
337
+ * set `R|1|^^^687|28.6&F&|&|&U/L||||F` reads nine fields under both, the status `F` sits in field
338
+ * 9 under both, and what differs is the units: `&|&U/L` against `&U/L`. This still fires there,
339
+ * which is over-reporting relative to the field indexes and never under-reporting, and the class
340
+ * costs no stream its disposition: that tail body is a splitting delimiter in force, so
341
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE} has already refused the record.
342
+ * What holds on **every** firing tuple is that the two readings disagree and that both consume
343
+ * every byte, so neither is forced.
324
344
  *
325
345
  * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized mnemonics,
326
346
  * and those bytes carry the reading that was taken unambiguously, so a second-generation read is
@@ -329,16 +349,17 @@ var WARNING_CODES = {
329
349
  ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS: "ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS",
330
350
  /**
331
351
  * Two escape alignments of the same bytes disagreed about a **repeat** boundary, the reading
332
- * taken kept it, and the escape character that reading resumes on **heads no sequence at all**.
333
- * Same contested position and same tail test as
352
+ * taken kept it, and the escape character that reading resumes on **heads no sequence this codec
353
+ * can interpret**. Same contested position and same tail test as
334
354
  * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS}, on a different delimiter role, and
335
355
  * what it costs is a different thing, which is why it is a different code.
336
356
  *
337
357
  * **Nothing shifts, and that is not the same as nothing being lost.** No field-indexed slot
338
358
  * moves: the units and the result status are read out of the same field numbers under either
339
359
  * alignment. What this reports is that **the field is read as more repeats than the competing
340
- * alignment gives it**, which is true wherever it fires. What that costs depends on **which**
341
- * boundary was gained, and the two cases are stated separately rather than folded into one claim.
360
+ * alignment gives it**, which holds wherever it fires **except on the one class named at the end
361
+ * of this entry**. What that costs depends on **which** boundary was gained, and the two cases
362
+ * are stated separately rather than folded into one claim.
342
363
  *
343
364
  * **Where it is the FIRST boundary in the field, a modeled slot is lost**, because a field's
344
365
  * modeled value and components are taken from its **first repeat alone**. Everything past the
@@ -376,17 +397,36 @@ var WARNING_CODES = {
376
397
  * **Two further deliberate bounds, both stated rather than left to be found.**
377
398
  * - **Only the repeat role.** The **component** role reaches a modeled slot too, and differently:
378
399
  * there the components stay in the record and move one slot along, so a local code reads as a
379
- * coding scheme and a given name as a middle name. That is measured, reported by nothing where
380
- * the earlier body is a recognized mnemonic, and open. It is not covered here, because wiring a
381
- * sink to another split is another criterion wanting its own population measurement.
382
- * - **The tail is weighed one construct deep**, exactly as for the shift report. Where the escape
383
- * character the reading resumes on heads a sequence this codec *recognizes*, the bytes prefer
384
- * the reading taken and the repeats are the ones the sender wrote: under a set naming the
385
- * repeat separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again,
386
- * and refusing it would be an over-refusal of a well-formed stream. Where it heads a sequence
387
- * whose body is *unrecognized*, the competing alignment would leave **two** escape characters
388
- * bare, so the preference is stronger again, and this stays silent there even though the gained
389
- * boundary is real. That last case is the named residue: measured, not overlooked.
400
+ * coding scheme and a given name as a middle name. That is measured and is now reported by
401
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS}, a third code on the same tail
402
+ * test wired to the component split. It is still not covered **here**, because wiring a sink to
403
+ * another split is another criterion, which is why it took its own population measurement.
404
+ * - **The tail is weighed one construct deep**, exactly as for the shift report, **and exactly
405
+ * one tail is excluded.** Where the escape character the reading resumes on heads a sequence
406
+ * this codec *recognizes*, it interprets a construct and the repeats are the ones the sender
407
+ * wrote: under a set naming the repeat separator `F`, `28.6&F&F&F&U/L` is that separator
408
+ * escaped, written, and escaped again, entirely well formed, and refusing it would be an
409
+ * over-refusal. That is the only tail on which a stream's escaping can be clean, which is why it
410
+ * is the only exclusion. Where the tail heads a sequence whose body is *unrecognized*, the
411
+ * reading taken consumes a triple it cannot read, preserved verbatim and never guessed at, the
412
+ * truncation is the same truncation, and **this reports it**.
413
+ * - **That remaining silence is a TRADE, not a claim that nothing was lost.** On the excluded tail
414
+ * the gained boundary is exactly as real and it is `warnings: []`: under the canonical set
415
+ * `R|1|^^^687|28.6&S&\&S&U/L|U/L||||F` reads a value of `28.6^`, and `^U/L` leaves every
416
+ * modeled slot, with an empty warning list. Reporting it would refuse a stream whose escaping
417
+ * is working.
418
+ *
419
+ * **⚠️ THE ONE CLASS WHERE THE FIELD IS NOT READ AS MORE REPEATS, NAMED RATHER THAN LEFT TO BE
420
+ * FOUND.** Where the sequence past the boundary carries the **repeat separator itself** as its
421
+ * body, the reading taken holds that character inside an opaque atom while the competing
422
+ * alignment splits on it, so the two readings return the **same number** of repeats in
423
+ * **different places** and what differs is their contents: under the canonical set
424
+ * `28.6&F&\&\&U/L` reads two repeats under both. This still fires there, which is over-reporting
425
+ * relative to the repeat count and never under-reporting, and the class costs no stream its
426
+ * disposition: that tail body is a splitting delimiter in force, so
427
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE} has already refused the record.
428
+ * What holds on **every** firing tuple is that the two readings disagree and that both consume
429
+ * every byte, so neither is forced.
390
430
  *
391
431
  * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized
392
432
  * mnemonics, and those bytes carry the reading that was taken unambiguously, so a
@@ -394,6 +434,100 @@ var WARNING_CODES = {
394
434
  * evidence.
395
435
  */
396
436
  ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD: "ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD",
437
+ /**
438
+ * Two escape alignments of the same bytes disagreed about a **component** boundary, the reading
439
+ * taken kept it, and the escape character that reading resumes on **heads no sequence this codec
440
+ * can interpret**. Same contested position and same tail test as
441
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS} and
442
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD}, on the third and last splitting
443
+ * role, and what it costs is a third thing again, which is why it is a third code.
444
+ *
445
+ * **The components are neither shifted out of the record nor dropped from it: they MOVE one slot
446
+ * along.** A field's components are modeled *inside* it, so a gained component boundary shifts
447
+ * every component after it exactly as a gained field boundary shifts every later field, **except
448
+ * on the one class named at the end of this entry**, where the two readings instead read the same
449
+ * number of components in different places. Nothing leaves the record and nothing changes field
450
+ * number; what changes is which modeled slot each component lands in, and those slots are named
451
+ * things:
452
+ *
453
+ * - **A Universal Test ID's coding scheme and local code.** Under the canonical set,
454
+ * `R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads four components, so `L` is the coding scheme and
455
+ * `687` the vendor's local code. The competing alignment reads three, so `L` is the test name
456
+ * and **`687` is the coding scheme**. A local code and a code-system selector are not the same
457
+ * thing, and a consumer routing on one of them routes on the alignment.
458
+ * - **A patient's given and middle names.** `P|1||MRN-0001||DOE&F&^&JANE^A||19700101|F` reads a
459
+ * given name of `&JANE` and a middle name of `A` under the reading taken, and under the
460
+ * competing one `A` is the **given** name with no middle name at all.
461
+ *
462
+ * Before this code the only warning on those streams was the **tolerable**
463
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}, so the widest gate-legal profile plus
464
+ * `{ strict: true }` accepted a test identity and a patient name whose parts were decided by the
465
+ * alignment rather than by the sender.
466
+ *
467
+ * **Every gained boundary at or before the LAST MODELED COMPONENT INDEX moves a slot, not only
468
+ * the first, and that is the difference from the repeat role.** There the field is modeled out of
469
+ * its first repeat alone, so only the first gained boundary reaches a modeled slot; here the shift
470
+ * propagates from wherever the boundary sits to the end of the component list.
471
+ *
472
+ * **TWO bounds run the other way, and this fires inside both**, which is **over-reporting, never
473
+ * under-reporting**: the direction this package errs in. Both axes are swept on their own rather
474
+ * than left to the shared corpus, which holds them fixed, and neither is closed by narrowing the
475
+ * guard, because narrowing changes which streams a published package refuses and wants its own
476
+ * measurement.
477
+ *
478
+ * - **Past the last modeled component index, nothing NAMED moves.** A model reads a fixed number
479
+ * of components: a patient name three (last, first, middle), a Universal Test ID four. A
480
+ * contested boundary further right than that still shifts the components after it while every
481
+ * named slot reads byte-identically under both alignments.
482
+ * - **Inside a LATER repeat nothing modeled moves at all**, because `components` is the first
483
+ * repeat, so what changes is `repeats[n]` for that `n` alone.
484
+ *
485
+ * It fires in both because the boundary is still one the bytes do not force, and a consumer
486
+ * reading `components` or `repeats` is still reading an alignment guess.
487
+ *
488
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, and
489
+ * the components read are the components that were always read. Picking the other alignment would
490
+ * be a different guess with no more evidence behind it, on a published package. **Withholding the
491
+ * moved slots is a separate question and is deliberately not answered here**, for the reason the
492
+ * shift report already gives: declining to model a slot changes an extracted value for every
493
+ * consumer of a package already on the registry.
494
+ *
495
+ * **The tail is weighed one construct deep**, exactly as for the other two, **and exactly one tail
496
+ * is excluded.** Where the escape character the reading resumes on heads a sequence this codec
497
+ * *recognizes*, it interprets a construct and the components are the ones the sender wrote: under
498
+ * a set naming the component separator `F`, `GLU&F&F&F&L` is that separator escaped, written, and
499
+ * escaped again, entirely well formed, and refusing it would be an over-refusal. That is the only
500
+ * tail on which a stream's escaping can be clean, which is why it is the only exclusion. Where the
501
+ * tail heads a sequence whose body is *unrecognized*, the reading taken consumes a triple it
502
+ * cannot read, preserved verbatim and never guessed at, the slots move exactly as far, and **this
503
+ * reports it**.
504
+ *
505
+ * **That remaining silence is a TRADE, not a claim that nothing was lost.** On the excluded tail
506
+ * the gained boundary is exactly as real and it is `warnings: []`: under the canonical set
507
+ * `&F&^&F&GLU^L^687` reads one component more than the competing alignment, so `L` is the coding
508
+ * scheme and `687` the local code under one reading and `687` the **coding scheme** under the
509
+ * other, with an empty warning list. Reporting it would refuse a stream whose escaping is
510
+ * working.
511
+ *
512
+ * **⚠️ THE ONE CLASS WHERE NO COMPONENT INDEX MOVES, NAMED RATHER THAN LEFT TO BE FOUND.** It is a
513
+ * **third** bound running the other way, and unlike the two above it is about the bytes past the
514
+ * boundary rather than about where the boundary sits. Where the sequence past it carries the
515
+ * **component separator itself** as its body, the reading taken holds that character inside an
516
+ * opaque atom while the competing alignment splits on it, so the two readings return the **same
517
+ * number** of components in **different places** and what differs is their contents: under the
518
+ * canonical set `P|1||MRN-0001||DOE&F&^&^&JANE^A||19700101|F` reads three components under both,
519
+ * with `A` the middle name under both. This still fires there, and the class costs no stream its
520
+ * disposition: that tail body is a splitting delimiter in force, so
521
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE} has already refused the record.
522
+ * What holds on **every** firing tuple is that the two readings disagree and that both consume
523
+ * every byte, so neither is forced.
524
+ *
525
+ * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized
526
+ * mnemonics, and those bytes carry the reading that was taken unambiguously, so a
527
+ * second-generation read is silent and is correct about its own bytes. A clean re-read is not
528
+ * evidence.
529
+ */
530
+ ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS: "ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS",
397
531
  /** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
398
532
  ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
399
533
  /**
@@ -549,14 +683,21 @@ function ambiguousEscapeAlignment(position) {
549
683
  function alignmentShiftedFields(position) {
550
684
  return {
551
685
  code: WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS,
552
- message: "A field boundary here is one of two escape alignments the bytes carry, and the reading that took it resumes on an escape character heading no sequence, which the other alignment uses to close one. Every later field is one place further right than the other reading puts it, so a result's units and status may be read out of slots the sender did not put them in. The reading was kept and every byte is preserved.",
686
+ message: "A field boundary here is one of two escape alignments the bytes carry, and the reading that took it resumes on an escape character heading no sequence this reader can interpret, which the other alignment uses to close one. The two readings disagree, and both consume every byte, so neither is forced. Usually every later field is one place further right than the other reading puts it, so a result's units and status may be read out of slots the sender did not put them in; one measured class does not shift at all, where the sequence past the boundary carries the field separator itself and the two readings read the same number of fields with different contents. The reading was kept and every byte is preserved.",
553
687
  position
554
688
  };
555
689
  }
556
690
  function alignmentTruncatedField(position) {
557
691
  return {
558
692
  code: WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD,
559
- message: "A repeat boundary here is one of two escape alignments the bytes carry, and the reading that took it resumes on an escape character heading no sequence, which the other alignment uses to close one. This field is read as more repeats than the other reading gives it, and its modeled value and components are taken from the first repeat alone, so where that boundary is the first one the rest of the field leaves every modeled slot. The reading was kept and every byte is preserved.",
693
+ message: "A repeat boundary here is one of two escape alignments the bytes carry, and the reading that took it resumes on an escape character heading no sequence this reader can interpret, which the other alignment uses to close one. The two readings disagree, and both consume every byte, so neither is forced. Usually this field is read as more repeats than the other reading gives it, and its modeled value and components are taken from the first repeat alone, so where that boundary is the first one the rest of the field leaves every modeled slot; one measured class reads the same number of repeats, where the sequence past the boundary carries the repeat separator itself. The reading was kept and every byte is preserved.",
694
+ position
695
+ };
696
+ }
697
+ function alignmentShiftedComponents(position) {
698
+ return {
699
+ code: WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS,
700
+ message: "A component boundary here is one of two escape alignments the bytes carry, and the reading that took it resumes on an escape character heading no sequence this reader can interpret, which the other alignment uses to close one. The two readings disagree, and both consume every byte, so neither is forced. Usually every later component of this field sits one place further right than the other reading puts it, so a test identity's coding scheme and local code, or a patient's given and middle names, may be read out of slots the sender did not put them in; one measured class does not move at all, where the sequence past the boundary carries the component separator itself and the two readings read the same number of components with different contents. The reading was kept and every byte is preserved.",
560
701
  position
561
702
  };
562
703
  }
@@ -1185,7 +1326,7 @@ var referenceCorpus = defineAstmProfile({
1185
1326
  tolerate: [
1186
1327
  {
1187
1328
  code: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
1188
- rationale: "The OSS reference stack (python-astm codec.py, firsthand-verified) treats '&' as literal data and never decodes escape sequences, so streams from that ecosystem carry non-standard '&\u2026&' bodies our escape-aware tokenizer flags. The body is preserved byte-for-byte in the value. NOT always non-clinical, and this is measured rather than assumed: where the body is a delimiter, that delimiter never became a field boundary, so `28.6&|&U/L` reads as one value with no units and no final status. That subset now also raises ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE, which this profile cannot tolerate, so tolerating this code no longer hides it. The same holds for the mirror case, where a sequence ends where another could have begun and a delimiter splits that the other alignment would have held: that raises ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT, which this profile cannot tolerate either. Where that gained boundary is a field boundary and the reading taken resumes on an escape character heading no sequence, every later field shifts and a result's units and status move with it, which raises ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS, untolerable here as well. Where that same gained boundary is a repeat boundary, nothing shifts but the field is read out of its first repeat alone, so a value truncates and a test identity empties, which raises ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD, untolerable here too. Both of those carry the same tail bound and stay silent where the escape character past the boundary heads a sequence of its own, recognized or not, which is much of this corpus: tolerating this code does not quiet them, but their absence is not a statement that nothing was lost."
1329
+ rationale: "The OSS reference stack (python-astm codec.py, firsthand-verified) treats '&' as literal data and never decodes escape sequences, so streams from that ecosystem carry non-standard '&\u2026&' bodies our escape-aware tokenizer flags. The body is preserved byte-for-byte in the value. NOT always non-clinical, and this is measured rather than assumed: where the body is a delimiter, that delimiter never became a field boundary, so `28.6&|&U/L` reads as one value with no units and no final status. That subset now also raises ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE, which this profile cannot tolerate, so tolerating this code no longer hides it. The same holds for the mirror case, where a sequence ends where another could have begun and a delimiter splits that the other alignment would have held: that raises ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT, which this profile cannot tolerate either. Where that gained boundary is a field boundary and the reading taken resumes on an escape character heading no sequence this reader can interpret, every later field shifts and a result's units and status move with it, which raises ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS, untolerable here as well. Where that same gained boundary is a repeat boundary, nothing shifts but the field is read out of its first repeat alone, so a value truncates and a test identity empties, which raises ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD, untolerable here too. Where it is a component boundary, nothing leaves the record and no field number changes, and every component after it moves one slot along, so a test identity's coding scheme and local code and a patient's given and middle names are read out of positions the other alignment does not put them in, which raises ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS, untolerable here as well. Each of those three shift claims has one measured exception, where the sequence past the boundary carries the splitting delimiter itself: the two readings then read the same number of segments in different places, nothing moves index, and the record was already refused by ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE. All three carry the same tail bound and stay silent in exactly one case, where the escape character past the boundary heads a sequence this reader RECOGNIZES, which is the escape mechanism working. How often that happens on this corpus is NOT measured and must not be guessed at: the firsthand grounding above is of the DECODE path only, and a relayed stream can still carry a device-written mnemonic. Tolerating this code does not quiet those three, and their absence is not a statement that nothing was lost."
1189
1330
  }
1190
1331
  ]
1191
1332
  });
@@ -1213,6 +1354,9 @@ function getDefaultAstmProfile() {
1213
1354
  function isEscapeSequenceAt(text, i, escape) {
1214
1355
  return text.charAt(i) === escape && text.charAt(i + 2) === escape;
1215
1356
  }
1357
+ function headsInterpretableSequence(text, i, escape) {
1358
+ return isEscapeSequenceAt(text, i, escape) && isMnemonicBody(text.charAt(i + 1));
1359
+ }
1216
1360
  function decodeEscapes(leaf, d, onUnknown, onUnpaired, onSwallowedDelimiter) {
1217
1361
  const esc = d.escape;
1218
1362
  if (!leaf.includes(esc)) return leaf;
@@ -1259,7 +1403,7 @@ function isMnemonicBody(body) {
1259
1403
  function escapeBody(body, d) {
1260
1404
  return isMnemonicBody(body) ? d[MNEMONIC_ROLES[body]] : void 0;
1261
1405
  }
1262
- function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShiftedFields, onTruncatedField) {
1406
+ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShiftedFields, onTruncatedField, onShiftedComponents) {
1263
1407
  if (text.length === 0) return [""];
1264
1408
  const out = [];
1265
1409
  let current = "";
@@ -1271,9 +1415,10 @@ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShift
1271
1415
  if (contested && !isMnemonicBody(text.charAt(i + 1))) {
1272
1416
  onAmbiguousAlignment?.(out.length);
1273
1417
  }
1274
- if (contested && !isEscapeSequenceAt(text, i + 4, escape)) {
1418
+ if (contested && !headsInterpretableSequence(text, i + 4, escape)) {
1275
1419
  onShiftedFields?.(out.length);
1276
1420
  onTruncatedField?.(out.length);
1421
+ onShiftedComponents?.(out.length);
1277
1422
  }
1278
1423
  current += text.slice(i, i + 3);
1279
1424
  i += 3;
@@ -1293,7 +1438,7 @@ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShift
1293
1438
  }
1294
1439
 
1295
1440
  // src/records/tokenize.ts
1296
- function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField) {
1441
+ function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField, onAlignmentShiftedComponents) {
1297
1442
  const rawFields = splitEscapeAware(
1298
1443
  record,
1299
1444
  d.field,
@@ -1312,11 +1457,14 @@ function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowe
1312
1457
  // The repeat split runs INSIDE one field, so the index it hands back is a repeat index and is
1313
1458
  // discarded: what a caller needs is which field carries the contested boundary, and that is
1314
1459
  // the index this map is already iterating.
1315
- () => onAlignmentTruncatedField?.(fieldIndex)
1460
+ () => onAlignmentTruncatedField?.(fieldIndex),
1461
+ // Likewise for the component split, which runs inside one repeat of one field: the index it
1462
+ // hands back is a component index and is discarded for the same reason.
1463
+ () => onAlignmentShiftedComponents?.(fieldIndex)
1316
1464
  )
1317
1465
  );
1318
1466
  }
1319
- function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField) {
1467
+ function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField, onAlignmentShiftedComponents) {
1320
1468
  const defEnd = record.indexOf(d.field, 2);
1321
1469
  const definition = defEnd === -1 ? record.slice(2) : record.slice(2, defEnd);
1322
1470
  const head = [opaqueField(record.slice(0, Math.min(1, record.length))), opaqueField(definition)];
@@ -1329,14 +1477,15 @@ function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowe
1329
1477
  (i) => onSwallowedDelimiter?.(i + 2),
1330
1478
  (i) => onAmbiguousAlignment?.(i + 2),
1331
1479
  (i) => onAlignmentShiftedFields?.(i + 2),
1332
- (i) => onAlignmentTruncatedField?.(i + 2)
1480
+ (i) => onAlignmentTruncatedField?.(i + 2),
1481
+ (i) => onAlignmentShiftedComponents?.(i + 2)
1333
1482
  );
1334
1483
  return [...head, ...data];
1335
1484
  }
1336
1485
  function opaqueField(raw) {
1337
1486
  return { raw, components: [raw], repeats: [[raw]] };
1338
1487
  }
1339
- function toField(raw, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentTruncatedField) {
1488
+ function toField(raw, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentTruncatedField, onAlignmentShiftedComponents) {
1340
1489
  const rawRepeats = splitEscapeAware(
1341
1490
  raw,
1342
1491
  d.repeat,
@@ -1350,9 +1499,19 @@ function toField(raw, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter
1350
1499
  }
1351
1500
  );
1352
1501
  const repeats = rawRepeats.map(
1353
- (rep) => splitEscapeAware(rep, d.component, d.escape, () => {
1354
- onAmbiguousAlignment();
1355
- }).map(
1502
+ (rep) => splitEscapeAware(
1503
+ rep,
1504
+ d.component,
1505
+ d.escape,
1506
+ () => {
1507
+ onAmbiguousAlignment();
1508
+ },
1509
+ void 0,
1510
+ void 0,
1511
+ () => {
1512
+ onAlignmentShiftedComponents();
1513
+ }
1514
+ ).map(
1356
1515
  (comp) => decodeEscapes(comp, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter)
1357
1516
  )
1358
1517
  );
@@ -1504,6 +1663,11 @@ function buildRecord(line2, recordIndex, d, warnings) {
1504
1663
  alignmentTruncatedField({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
1505
1664
  );
1506
1665
  };
1666
+ const onAlignmentShiftedComponents = (fieldIndex) => {
1667
+ warnings.push(
1668
+ alignmentShiftedComponents({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
1669
+ );
1670
+ };
1507
1671
  const fields = rawType === "H" ? tokenizeHeader(
1508
1672
  line2,
1509
1673
  d,
@@ -1512,7 +1676,8 @@ function buildRecord(line2, recordIndex, d, warnings) {
1512
1676
  onSwallowedDelimiter,
1513
1677
  onAmbiguousAlignment,
1514
1678
  onAlignmentShiftedFields,
1515
- onAlignmentTruncatedField
1679
+ onAlignmentTruncatedField,
1680
+ onAlignmentShiftedComponents
1516
1681
  ) : tokenizeRecord(
1517
1682
  line2,
1518
1683
  d,
@@ -1521,7 +1686,8 @@ function buildRecord(line2, recordIndex, d, warnings) {
1521
1686
  onSwallowedDelimiter,
1522
1687
  onAmbiguousAlignment,
1523
1688
  onAlignmentShiftedFields,
1524
- onAlignmentTruncatedField
1689
+ onAlignmentTruncatedField,
1690
+ onAlignmentShiftedComponents
1525
1691
  );
1526
1692
  if (fields.length === 1 && line2.slice(1).trim().length > 0) {
1527
1693
  warnings.push(fieldsUnseparated({ recordIndex, recordType: rawType }));
@@ -2734,8 +2900,8 @@ function concatBytes3(chunks) {
2734
2900
  }
2735
2901
 
2736
2902
  // src/index.ts
2737
- var VERSION = "0.0.18";
2903
+ var VERSION = "0.0.20";
2738
2904
 
2739
- export { ALL_ASTM_WARNING_CODES, AMBIGUOUS_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, AstmAmbiguousStreamError, AstmFrameEncodeError, AstmFrameStrictError, AstmParseError, AstmProfileDefinitionError, AstmSerializeError, AstmStrictError, CANONICAL_DELIMITERS, FATAL_CODES, FRAME_WARNING_CODES, LIVD_WARNING_CODES, LTP_WARNING_CODES, SAFETY_CRITICAL_CODES, TOLERABLE_CODES, VERSION, WARNING_CODES, alignmentShiftedFields, alignmentTruncatedField, ambiguousEscapeAlignment, ambiguousMessageKind, ambiguousValueSplit, applyAstmProfile, applyAstmProfileToWarnings, applyLivd, astmDateToLocalISO, astmProfiles, attachComments, buildAstmMessage, classifyMessage, comments, commentsFor, composeAstmFrames, computeChecksum, decodeAstmFrames, decodeEscapes, deepFreeze, defineAstmProfile, defineLivdCatalog, delimiterRoleCollision, delimiterSwallowedByEscape, delimitersRedeclared, detectFraming, encodeComponent, fieldScalar, fieldsUnseparated, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, hasCollidingRoles, interpretAbnormalFlag, interpretResultStatus, isNonStandard, isSafetyCriticalCode, listAstmProfiles, livdAmbiguousMapping, livdUnmappedCode, lookupLivdForRecord, ltpAmbiguousTransport, ltpFrameRejected, ltpInitialState, ltpReduce, ltpUnexpectedEvent, messages, nonStandardDelimiters, orders, orphanComment, parseAstmDate, parseAstmRecords, parseChecksumHex, parseFramedAstm, parseReferenceRange, partialTimestamp, patient, primaryCode, profileQuirkApplied, query, readDelimiterDeclaration, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeHeader, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unpairedEscapeCharacter, unparseableReferenceRange, unreadableRedeclaration };
2905
+ export { ALL_ASTM_WARNING_CODES, AMBIGUOUS_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, AstmAmbiguousStreamError, AstmFrameEncodeError, AstmFrameStrictError, AstmParseError, AstmProfileDefinitionError, AstmSerializeError, AstmStrictError, CANONICAL_DELIMITERS, FATAL_CODES, FRAME_WARNING_CODES, LIVD_WARNING_CODES, LTP_WARNING_CODES, SAFETY_CRITICAL_CODES, TOLERABLE_CODES, VERSION, WARNING_CODES, alignmentShiftedComponents, alignmentShiftedFields, alignmentTruncatedField, ambiguousEscapeAlignment, ambiguousMessageKind, ambiguousValueSplit, applyAstmProfile, applyAstmProfileToWarnings, applyLivd, astmDateToLocalISO, astmProfiles, attachComments, buildAstmMessage, classifyMessage, comments, commentsFor, composeAstmFrames, computeChecksum, decodeAstmFrames, decodeEscapes, deepFreeze, defineAstmProfile, defineLivdCatalog, delimiterRoleCollision, delimiterSwallowedByEscape, delimitersRedeclared, detectFraming, encodeComponent, fieldScalar, fieldsUnseparated, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, hasCollidingRoles, interpretAbnormalFlag, interpretResultStatus, isNonStandard, isSafetyCriticalCode, listAstmProfiles, livdAmbiguousMapping, livdUnmappedCode, lookupLivdForRecord, ltpAmbiguousTransport, ltpFrameRejected, ltpInitialState, ltpReduce, ltpUnexpectedEvent, messages, nonStandardDelimiters, orders, orphanComment, parseAstmDate, parseAstmRecords, parseChecksumHex, parseFramedAstm, parseReferenceRange, partialTimestamp, patient, primaryCode, profileQuirkApplied, query, readDelimiterDeclaration, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeHeader, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unpairedEscapeCharacter, unparseableReferenceRange, unreadableRedeclaration };
2740
2906
  //# sourceMappingURL=index.mjs.map
2741
2907
  //# sourceMappingURL=index.mjs.map