@cosyte/astm 0.0.17 → 0.0.19

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.cjs CHANGED
@@ -271,10 +271,11 @@ var WARNING_CODES = {
271
271
  ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT: "ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT",
272
272
  /**
273
273
  * Two escape alignments of the same bytes disagreed about a **field** boundary, the reading taken
274
- * kept it, and the escape character that reading resumes on **heads no sequence at all**, so the
275
- * boundary was bought with a byte this reading cannot read while the competing alignment is
276
- * exactly the reading that can. Every field after that point sits one place further right than the
277
- * competing alignment puts it.
274
+ * kept it, and the escape character that reading resumes on **heads no sequence this codec can
275
+ * interpret**, so the boundary was bought with bytes this reading cannot read while the competing
276
+ * alignment is exactly the reading that can. Every field after that point sits one place further
277
+ * right than the competing alignment puts it, **except on the one class named at the end of this
278
+ * entry**, where the two readings instead read the same number of fields in different places.
278
279
  *
279
280
  * **The shift is the harm, and on a result record it reaches the status slot.** Measured on the
280
281
  * canonical set, `R|1|^^^687|28.6&F&|&U/L||||F` reads **9** fields under the reading taken and
@@ -305,30 +306,230 @@ var WARNING_CODES = {
305
306
  * modeled *inside* a field: a Universal Test ID's four components are the LOINC-candidate slot,
306
307
  * the test name, the **coding scheme** and the **local code**, and a patient name's three are
307
308
  * last, first and middle. A gained component boundary shifts those, so a local code can be read
308
- * as a coding scheme and a given name as a middle name. Measured, and **where the earlier body is
309
- * a recognized mnemonic, reported by nothing** (only the tolerable
310
- * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER} fires, so a gate-legal profile accepts it).
311
- * An **unrecognized** earlier body raises
312
- * {@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT} there and is refused, which is the
313
- * same split this code's own exclusion takes. It is a separate, open condition rather than something this
314
- * code covers, because wiring this sink to another split is a different criterion needing its own
315
- * population measurement. The repeat role costs the **value**, which this code does not report
316
- * either.
317
- * - **The tail is weighed one construct deep.** Where the escape character the reading taken
318
- * resumes on heads a sequence this codec *recognizes*, the reading taken interprets it and the
319
- * competing alignment would leave it bare, so the bytes prefer the reading taken: under a set
320
- * naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and
321
- * escaped again, and refusing it would be an over-refusal of a well-formed stream. Where it
322
- * heads a sequence whose body is *unrecognized*, the reading taken still consumes it while the
323
- * competing alignment would leave **two** escape characters bare, so the preference is stronger
324
- * again, and this stays silent there even though the field shift is real. That last case is the
325
- * named residue: measured, not overlooked.
309
+ * as a coding scheme and a given name as a middle name. Measured, and it used to be reported by
310
+ * nothing where the earlier body was a recognized mnemonic (only the tolerable
311
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER} fired, so a gate-legal profile accepted
312
+ * it). It is now reported by
313
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS}, a separate code on the same
314
+ * tail test wired to the component split, because wiring this sink to another split is a
315
+ * different criterion needing its own population measurement. It is still not something **this**
316
+ * code covers. The repeat role costs the **value**, which this code does not report either, and
317
+ * which {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD} reports.
318
+ * - **The tail is weighed one construct deep, and exactly one tail is excluded.** Where the escape
319
+ * character the reading taken resumes on heads a sequence this codec *recognizes*, the reading
320
+ * taken interprets a construct and leaves nothing unread: under a set naming the field separator
321
+ * `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again, entirely well
322
+ * formed, and refusing it would be an over-refusal. That is the only tail on which a stream's
323
+ * escaping can be clean, which is why it is the only exclusion. Where the tail heads a sequence
324
+ * whose body is *unrecognized*, the reading taken consumes a triple it cannot read, preserved
325
+ * verbatim and never guessed at, the field shift is the same shift, and **this reports it**.
326
+ * Consuming a triple is not interpreting one.
327
+ * - **That remaining silence is a TRADE, not a claim that nothing was lost.** The gained field
328
+ * boundary is exactly as real on the excluded tail, and there it is not merely under-reported:
329
+ * it is `warnings: []`. Under the canonical set `R|1|^^^687|28.6&F&|&F&U/L||||F` reads **nine**
330
+ * fields against the competing alignment's eight and hands back a status of **`final`** with an
331
+ * empty warning list. Reporting it would refuse a stream whose escaping is working, which is
332
+ * the trade this bound makes. **Read the raw line when an escape character sits next to a
333
+ * delimiter, whether or not anything fired.**
334
+ *
335
+ * **⚠️ THE ONE CLASS WHERE NO FIELD INDEX MOVES, NAMED RATHER THAN LEFT TO BE FOUND.** Where the
336
+ * sequence past the boundary carries the **field separator itself** as its body, the reading taken
337
+ * holds that character inside an opaque atom while the competing alignment splits on it, so the
338
+ * two readings return the **same number** of fields in **different places**. Under the canonical
339
+ * set `R|1|^^^687|28.6&F&|&|&U/L||||F` reads nine fields under both, the status `F` sits in field
340
+ * 9 under both, and what differs is the units: `&|&U/L` against `&U/L`. This still fires there,
341
+ * which is over-reporting relative to the field indexes and never under-reporting, and the class
342
+ * costs no stream its disposition: that tail body is a splitting delimiter in force, so
343
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE} has already refused the record.
344
+ * What holds on **every** firing tuple is that the two readings disagree and that both consume
345
+ * every byte, so neither is forced.
326
346
  *
327
347
  * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized mnemonics,
328
348
  * and those bytes carry the reading that was taken unambiguously, so a second-generation read is
329
349
  * silent and is correct about its own bytes. A clean re-read is not evidence.
330
350
  */
331
351
  ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS: "ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS",
352
+ /**
353
+ * Two escape alignments of the same bytes disagreed about a **repeat** boundary, the reading
354
+ * taken kept it, and the escape character that reading resumes on **heads no sequence this codec
355
+ * can interpret**. Same contested position and same tail test as
356
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS}, on a different delimiter role, and
357
+ * what it costs is a different thing, which is why it is a different code.
358
+ *
359
+ * **Nothing shifts, and that is not the same as nothing being lost.** No field-indexed slot
360
+ * moves: the units and the result status are read out of the same field numbers under either
361
+ * alignment. What this reports is that **the field is read as more repeats than the competing
362
+ * alignment gives it**, which holds wherever it fires **except on the one class named at the end
363
+ * of this entry**. What that costs depends on **which** boundary was gained, and the two cases
364
+ * are stated separately rather than folded into one claim.
365
+ *
366
+ * **Where it is the FIRST boundary in the field, a modeled slot is lost**, because a field's
367
+ * modeled value and components are taken from its **first repeat alone**. Everything past the
368
+ * boundary stays on the wire and stays in `repeats`, and leaves every modeled slot:
369
+ *
370
+ * - **A value truncates.** Under `H|F^&`, `R|1|^^^687|28.6&S&F&U/L||||F` reads the value field as
371
+ * the two repeats `28.6^` and `&U/L`, so a consumer reads `28.6^` and the rest of the value is
372
+ * gone. The competing alignment reads one repeat carrying all of it.
373
+ * - **A modeled component list is deleted rather than shifted.** The same boundary inside a
374
+ * Universal Test ID leaves the components of the first repeat only: under `H|F^&`,
375
+ * `R|1|&F&F&687|28.6|U/L||||F` reads a UTID of one component holding a decoded field separator,
376
+ * with no coding scheme and no local code, where the bytes carry `687`. A patient name loses
377
+ * its given and middle names the same way. That is the half of this condition the shift report
378
+ * could not cover, because components are modeled **inside** a field.
379
+ *
380
+ * Before this code the only warnings on those streams were **tolerable** ones
381
+ * ({@link WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS} and, where the tail applies,
382
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}), so the widest gate-legal profile plus
383
+ * `{ strict: true }` accepted a truncated value and an emptied Universal Test ID.
384
+ *
385
+ * **Where it is a LATER boundary, no modeled slot moves and this still fires.** The first repeat
386
+ * is identical under both alignments, so the value, the components and every slot taken from them
387
+ * read the same either way (`R|1|^^^687|5.0F28.6&S&F&U/L||||F` under `H|F^&` reads a value of
388
+ * `5.0` under both), and what differs is the repeat structure after the first. It fires there
389
+ * deliberately: the boundary is still one the bytes do not force and a consumer reading `repeats`
390
+ * is still reading an alignment guess. Relative to the modeled slots that is **over-reporting,
391
+ * never under-reporting**. Narrowing it to the first boundary would change which streams a
392
+ * published package refuses and wants its own measurement, so the bound is written down instead.
393
+ *
394
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is identical,
395
+ * `repeats` still carries every one of them, and the value read is the value that was always
396
+ * read. Picking the other alignment would be a different guess with no more evidence behind it,
397
+ * on a published package.
398
+ *
399
+ * **Two further deliberate bounds, both stated rather than left to be found.**
400
+ * - **Only the repeat role.** The **component** role reaches a modeled slot too, and differently:
401
+ * there the components stay in the record and move one slot along, so a local code reads as a
402
+ * coding scheme and a given name as a middle name. That is measured and is now reported by
403
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS}, a third code on the same tail
404
+ * test wired to the component split. It is still not covered **here**, because wiring a sink to
405
+ * another split is another criterion, which is why it took its own population measurement.
406
+ * - **The tail is weighed one construct deep**, exactly as for the shift report, **and exactly
407
+ * one tail is excluded.** Where the escape character the reading resumes on heads a sequence
408
+ * this codec *recognizes*, it interprets a construct and the repeats are the ones the sender
409
+ * wrote: under a set naming the repeat separator `F`, `28.6&F&F&F&U/L` is that separator
410
+ * escaped, written, and escaped again, entirely well formed, and refusing it would be an
411
+ * over-refusal. That is the only tail on which a stream's escaping can be clean, which is why it
412
+ * is the only exclusion. Where the tail heads a sequence whose body is *unrecognized*, the
413
+ * reading taken consumes a triple it cannot read, preserved verbatim and never guessed at, the
414
+ * truncation is the same truncation, and **this reports it**.
415
+ * - **That remaining silence is a TRADE, not a claim that nothing was lost.** On the excluded tail
416
+ * the gained boundary is exactly as real and it is `warnings: []`: under the canonical set
417
+ * `R|1|^^^687|28.6&S&\&S&U/L|U/L||||F` reads a value of `28.6^`, and `^U/L` leaves every
418
+ * modeled slot, with an empty warning list. Reporting it would refuse a stream whose escaping
419
+ * is working.
420
+ *
421
+ * **⚠️ THE ONE CLASS WHERE THE FIELD IS NOT READ AS MORE REPEATS, NAMED RATHER THAN LEFT TO BE
422
+ * FOUND.** Where the sequence past the boundary carries the **repeat separator itself** as its
423
+ * body, the reading taken holds that character inside an opaque atom while the competing
424
+ * alignment splits on it, so the two readings return the **same number** of repeats in
425
+ * **different places** and what differs is their contents: under the canonical set
426
+ * `28.6&F&\&\&U/L` reads two repeats under both. This still fires there, which is over-reporting
427
+ * relative to the repeat count and never under-reporting, and the class costs no stream its
428
+ * disposition: that tail body is a splitting delimiter in force, so
429
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE} has already refused the record.
430
+ * What holds on **every** firing tuple is that the two readings disagree and that both consume
431
+ * every byte, so neither is forced.
432
+ *
433
+ * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized
434
+ * mnemonics, and those bytes carry the reading that was taken unambiguously, so a
435
+ * second-generation read is silent and is correct about its own bytes. A clean re-read is not
436
+ * evidence.
437
+ */
438
+ ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD: "ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD",
439
+ /**
440
+ * Two escape alignments of the same bytes disagreed about a **component** boundary, the reading
441
+ * taken kept it, and the escape character that reading resumes on **heads no sequence this codec
442
+ * can interpret**. Same contested position and same tail test as
443
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS} and
444
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD}, on the third and last splitting
445
+ * role, and what it costs is a third thing again, which is why it is a third code.
446
+ *
447
+ * **The components are neither shifted out of the record nor dropped from it: they MOVE one slot
448
+ * along.** A field's components are modeled *inside* it, so a gained component boundary shifts
449
+ * every component after it exactly as a gained field boundary shifts every later field, **except
450
+ * on the one class named at the end of this entry**, where the two readings instead read the same
451
+ * number of components in different places. Nothing leaves the record and nothing changes field
452
+ * number; what changes is which modeled slot each component lands in, and those slots are named
453
+ * things:
454
+ *
455
+ * - **A Universal Test ID's coding scheme and local code.** Under the canonical set,
456
+ * `R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads four components, so `L` is the coding scheme and
457
+ * `687` the vendor's local code. The competing alignment reads three, so `L` is the test name
458
+ * and **`687` is the coding scheme**. A local code and a code-system selector are not the same
459
+ * thing, and a consumer routing on one of them routes on the alignment.
460
+ * - **A patient's given and middle names.** `P|1||MRN-0001||DOE&F&^&JANE^A||19700101|F` reads a
461
+ * given name of `&JANE` and a middle name of `A` under the reading taken, and under the
462
+ * competing one `A` is the **given** name with no middle name at all.
463
+ *
464
+ * Before this code the only warning on those streams was the **tolerable**
465
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}, so the widest gate-legal profile plus
466
+ * `{ strict: true }` accepted a test identity and a patient name whose parts were decided by the
467
+ * alignment rather than by the sender.
468
+ *
469
+ * **Every gained boundary at or before the LAST MODELED COMPONENT INDEX moves a slot, not only
470
+ * the first, and that is the difference from the repeat role.** There the field is modeled out of
471
+ * its first repeat alone, so only the first gained boundary reaches a modeled slot; here the shift
472
+ * propagates from wherever the boundary sits to the end of the component list.
473
+ *
474
+ * **TWO bounds run the other way, and this fires inside both**, which is **over-reporting, never
475
+ * under-reporting**: the direction this package errs in. Both axes are swept on their own rather
476
+ * than left to the shared corpus, which holds them fixed, and neither is closed by narrowing the
477
+ * guard, because narrowing changes which streams a published package refuses and wants its own
478
+ * measurement.
479
+ *
480
+ * - **Past the last modeled component index, nothing NAMED moves.** A model reads a fixed number
481
+ * of components: a patient name three (last, first, middle), a Universal Test ID four. A
482
+ * contested boundary further right than that still shifts the components after it while every
483
+ * named slot reads byte-identically under both alignments.
484
+ * - **Inside a LATER repeat nothing modeled moves at all**, because `components` is the first
485
+ * repeat, so what changes is `repeats[n]` for that `n` alone.
486
+ *
487
+ * It fires in both because the boundary is still one the bytes do not force, and a consumer
488
+ * reading `components` or `repeats` is still reading an alignment guess.
489
+ *
490
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, and
491
+ * the components read are the components that were always read. Picking the other alignment would
492
+ * be a different guess with no more evidence behind it, on a published package. **Withholding the
493
+ * moved slots is a separate question and is deliberately not answered here**, for the reason the
494
+ * shift report already gives: declining to model a slot changes an extracted value for every
495
+ * consumer of a package already on the registry.
496
+ *
497
+ * **The tail is weighed one construct deep**, exactly as for the other two, **and exactly one tail
498
+ * is excluded.** Where the escape character the reading resumes on heads a sequence this codec
499
+ * *recognizes*, it interprets a construct and the components are the ones the sender wrote: under
500
+ * a set naming the component separator `F`, `GLU&F&F&F&L` is that separator escaped, written, and
501
+ * escaped again, entirely well formed, and refusing it would be an over-refusal. That is the only
502
+ * tail on which a stream's escaping can be clean, which is why it is the only exclusion. Where the
503
+ * tail heads a sequence whose body is *unrecognized*, the reading taken consumes a triple it
504
+ * cannot read, preserved verbatim and never guessed at, the slots move exactly as far, and **this
505
+ * reports it**.
506
+ *
507
+ * **That remaining silence is a TRADE, not a claim that nothing was lost.** On the excluded tail
508
+ * the gained boundary is exactly as real and it is `warnings: []`: under the canonical set
509
+ * `&F&^&F&GLU^L^687` reads one component more than the competing alignment, so `L` is the coding
510
+ * scheme and `687` the local code under one reading and `687` the **coding scheme** under the
511
+ * other, with an empty warning list. Reporting it would refuse a stream whose escaping is
512
+ * working.
513
+ *
514
+ * **⚠️ THE ONE CLASS WHERE NO COMPONENT INDEX MOVES, NAMED RATHER THAN LEFT TO BE FOUND.** It is a
515
+ * **third** bound running the other way, and unlike the two above it is about the bytes past the
516
+ * boundary rather than about where the boundary sits. Where the sequence past it carries the
517
+ * **component separator itself** as its body, the reading taken holds that character inside an
518
+ * opaque atom while the competing alignment splits on it, so the two readings return the **same
519
+ * number** of components in **different places** and what differs is their contents: under the
520
+ * canonical set `P|1||MRN-0001||DOE&F&^&^&JANE^A||19700101|F` reads three components under both,
521
+ * with `A` the middle name under both. This still fires there, and the class costs no stream its
522
+ * disposition: that tail body is a splitting delimiter in force, so
523
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE} has already refused the record.
524
+ * What holds on **every** firing tuple is that the two readings disagree and that both consume
525
+ * every byte, so neither is forced.
526
+ *
527
+ * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized
528
+ * mnemonics, and those bytes carry the reading that was taken unambiguously, so a
529
+ * second-generation read is silent and is correct about its own bytes. A clean re-read is not
530
+ * evidence.
531
+ */
532
+ ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS: "ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS",
332
533
  /** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
333
534
  ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
334
535
  /**
@@ -484,7 +685,21 @@ function ambiguousEscapeAlignment(position) {
484
685
  function alignmentShiftedFields(position) {
485
686
  return {
486
687
  code: WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS,
487
- 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.",
688
+ 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.",
689
+ position
690
+ };
691
+ }
692
+ function alignmentTruncatedField(position) {
693
+ return {
694
+ code: WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD,
695
+ 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.",
696
+ position
697
+ };
698
+ }
699
+ function alignmentShiftedComponents(position) {
700
+ return {
701
+ code: WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS,
702
+ 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.",
488
703
  position
489
704
  };
490
705
  }
@@ -1113,7 +1328,7 @@ var referenceCorpus = defineAstmProfile({
1113
1328
  tolerate: [
1114
1329
  {
1115
1330
  code: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
1116
- 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."
1331
+ 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."
1117
1332
  }
1118
1333
  ]
1119
1334
  });
@@ -1141,6 +1356,9 @@ function getDefaultAstmProfile() {
1141
1356
  function isEscapeSequenceAt(text, i, escape) {
1142
1357
  return text.charAt(i) === escape && text.charAt(i + 2) === escape;
1143
1358
  }
1359
+ function headsInterpretableSequence(text, i, escape) {
1360
+ return isEscapeSequenceAt(text, i, escape) && isMnemonicBody(text.charAt(i + 1));
1361
+ }
1144
1362
  function decodeEscapes(leaf, d, onUnknown, onUnpaired, onSwallowedDelimiter) {
1145
1363
  const esc = d.escape;
1146
1364
  if (!leaf.includes(esc)) return leaf;
@@ -1187,7 +1405,7 @@ function isMnemonicBody(body) {
1187
1405
  function escapeBody(body, d) {
1188
1406
  return isMnemonicBody(body) ? d[MNEMONIC_ROLES[body]] : void 0;
1189
1407
  }
1190
- function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShiftedFields) {
1408
+ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShiftedFields, onTruncatedField, onShiftedComponents) {
1191
1409
  if (text.length === 0) return [""];
1192
1410
  const out = [];
1193
1411
  let current = "";
@@ -1199,8 +1417,10 @@ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShift
1199
1417
  if (contested && !isMnemonicBody(text.charAt(i + 1))) {
1200
1418
  onAmbiguousAlignment?.(out.length);
1201
1419
  }
1202
- if (contested && !isEscapeSequenceAt(text, i + 4, escape)) {
1420
+ if (contested && !headsInterpretableSequence(text, i + 4, escape)) {
1203
1421
  onShiftedFields?.(out.length);
1422
+ onTruncatedField?.(out.length);
1423
+ onShiftedComponents?.(out.length);
1204
1424
  }
1205
1425
  current += text.slice(i, i + 3);
1206
1426
  i += 3;
@@ -1220,7 +1440,7 @@ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShift
1220
1440
  }
1221
1441
 
1222
1442
  // src/records/tokenize.ts
1223
- function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields) {
1443
+ function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField, onAlignmentShiftedComponents) {
1224
1444
  const rawFields = splitEscapeAware(
1225
1445
  record,
1226
1446
  d.field,
@@ -1235,11 +1455,18 @@ function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowe
1235
1455
  () => onUnknownEscape?.(fieldIndex),
1236
1456
  () => onUnpairedEscape?.(fieldIndex),
1237
1457
  () => onSwallowedDelimiter?.(fieldIndex),
1238
- () => onAmbiguousAlignment?.(fieldIndex)
1458
+ () => onAmbiguousAlignment?.(fieldIndex),
1459
+ // The repeat split runs INSIDE one field, so the index it hands back is a repeat index and is
1460
+ // discarded: what a caller needs is which field carries the contested boundary, and that is
1461
+ // the index this map is already iterating.
1462
+ () => onAlignmentTruncatedField?.(fieldIndex),
1463
+ // Likewise for the component split, which runs inside one repeat of one field: the index it
1464
+ // hands back is a component index and is discarded for the same reason.
1465
+ () => onAlignmentShiftedComponents?.(fieldIndex)
1239
1466
  )
1240
1467
  );
1241
1468
  }
1242
- function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields) {
1469
+ function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField, onAlignmentShiftedComponents) {
1243
1470
  const defEnd = record.indexOf(d.field, 2);
1244
1471
  const definition = defEnd === -1 ? record.slice(2) : record.slice(2, defEnd);
1245
1472
  const head = [opaqueField(record.slice(0, Math.min(1, record.length))), opaqueField(definition)];
@@ -1251,21 +1478,42 @@ function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowe
1251
1478
  (i) => onUnpairedEscape?.(i + 2),
1252
1479
  (i) => onSwallowedDelimiter?.(i + 2),
1253
1480
  (i) => onAmbiguousAlignment?.(i + 2),
1254
- (i) => onAlignmentShiftedFields?.(i + 2)
1481
+ (i) => onAlignmentShiftedFields?.(i + 2),
1482
+ (i) => onAlignmentTruncatedField?.(i + 2),
1483
+ (i) => onAlignmentShiftedComponents?.(i + 2)
1255
1484
  );
1256
1485
  return [...head, ...data];
1257
1486
  }
1258
1487
  function opaqueField(raw) {
1259
1488
  return { raw, components: [raw], repeats: [[raw]] };
1260
1489
  }
1261
- function toField(raw, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment) {
1262
- const rawRepeats = splitEscapeAware(raw, d.repeat, d.escape, () => {
1263
- onAmbiguousAlignment();
1264
- });
1265
- const repeats = rawRepeats.map(
1266
- (rep) => splitEscapeAware(rep, d.component, d.escape, () => {
1490
+ function toField(raw, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentTruncatedField, onAlignmentShiftedComponents) {
1491
+ const rawRepeats = splitEscapeAware(
1492
+ raw,
1493
+ d.repeat,
1494
+ d.escape,
1495
+ () => {
1267
1496
  onAmbiguousAlignment();
1268
- }).map(
1497
+ },
1498
+ void 0,
1499
+ () => {
1500
+ onAlignmentTruncatedField();
1501
+ }
1502
+ );
1503
+ const repeats = rawRepeats.map(
1504
+ (rep) => splitEscapeAware(
1505
+ rep,
1506
+ d.component,
1507
+ d.escape,
1508
+ () => {
1509
+ onAmbiguousAlignment();
1510
+ },
1511
+ void 0,
1512
+ void 0,
1513
+ () => {
1514
+ onAlignmentShiftedComponents();
1515
+ }
1516
+ ).map(
1269
1517
  (comp) => decodeEscapes(comp, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter)
1270
1518
  )
1271
1519
  );
@@ -1412,6 +1660,16 @@ function buildRecord(line2, recordIndex, d, warnings) {
1412
1660
  alignmentShiftedFields({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
1413
1661
  );
1414
1662
  };
1663
+ const onAlignmentTruncatedField = (fieldIndex) => {
1664
+ warnings.push(
1665
+ alignmentTruncatedField({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
1666
+ );
1667
+ };
1668
+ const onAlignmentShiftedComponents = (fieldIndex) => {
1669
+ warnings.push(
1670
+ alignmentShiftedComponents({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
1671
+ );
1672
+ };
1415
1673
  const fields = rawType === "H" ? tokenizeHeader(
1416
1674
  line2,
1417
1675
  d,
@@ -1419,7 +1677,9 @@ function buildRecord(line2, recordIndex, d, warnings) {
1419
1677
  onUnpairedEscape,
1420
1678
  onSwallowedDelimiter,
1421
1679
  onAmbiguousAlignment,
1422
- onAlignmentShiftedFields
1680
+ onAlignmentShiftedFields,
1681
+ onAlignmentTruncatedField,
1682
+ onAlignmentShiftedComponents
1423
1683
  ) : tokenizeRecord(
1424
1684
  line2,
1425
1685
  d,
@@ -1427,7 +1687,9 @@ function buildRecord(line2, recordIndex, d, warnings) {
1427
1687
  onUnpairedEscape,
1428
1688
  onSwallowedDelimiter,
1429
1689
  onAmbiguousAlignment,
1430
- onAlignmentShiftedFields
1690
+ onAlignmentShiftedFields,
1691
+ onAlignmentTruncatedField,
1692
+ onAlignmentShiftedComponents
1431
1693
  );
1432
1694
  if (fields.length === 1 && line2.slice(1).trim().length > 0) {
1433
1695
  warnings.push(fieldsUnseparated({ recordIndex, recordType: rawType }));
@@ -2640,7 +2902,7 @@ function concatBytes3(chunks) {
2640
2902
  }
2641
2903
 
2642
2904
  // src/index.ts
2643
- var VERSION = "0.0.17";
2905
+ var VERSION = "0.0.19";
2644
2906
 
2645
2907
  exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
2646
2908
  exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;
@@ -2664,7 +2926,9 @@ exports.SAFETY_CRITICAL_CODES = SAFETY_CRITICAL_CODES;
2664
2926
  exports.TOLERABLE_CODES = TOLERABLE_CODES;
2665
2927
  exports.VERSION = VERSION;
2666
2928
  exports.WARNING_CODES = WARNING_CODES;
2929
+ exports.alignmentShiftedComponents = alignmentShiftedComponents;
2667
2930
  exports.alignmentShiftedFields = alignmentShiftedFields;
2931
+ exports.alignmentTruncatedField = alignmentTruncatedField;
2668
2932
  exports.ambiguousEscapeAlignment = ambiguousEscapeAlignment;
2669
2933
  exports.ambiguousMessageKind = ambiguousMessageKind;
2670
2934
  exports.ambiguousValueSplit = ambiguousValueSplit;