@cosyte/astm 0.0.14 → 0.0.17

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
@@ -74,18 +74,170 @@ declare const WARNING_CODES: {
74
74
  * boundaries, silently, and this can happen to one record inside a run of these warnings); and a
75
75
  * set differing in the repeat, component or escape role usually splits into fields normally,
76
76
  * where a mis-split component can cost a test identity while the value survives, and where an
77
- * `&X&` sequence whose body is a delimiter is an opaque atom, so that delimiter does not split
77
+ * `&X&` sequence whose body is an unrecognized character that is itself a delimiter in force is an
78
+ * opaque atom, so that delimiter does not split
78
79
  * and every field after it shifts. The escape role's worst case has narrowed and not
79
80
  * disappeared: an escape character heading no sequence is now read as a literal and reported
80
81
  * under {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER} rather than merging the rest of the
81
- * record, but a delimiter swallowed inside an `&X&` body is reported only by
82
- * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, which is tolerable. Treat this code as a
82
+ * record, and a delimiter swallowed inside an `&X&` body now raises
83
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE} as well as the tolerable
84
+ * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, though it still splits the same way. The
85
+ * mirror of that case, where the leftmost alignment lets a delimiter split that a competing
86
+ * alignment would have held, gains a boundary rather than losing one, so the record splits into
87
+ * more fields than another reading gives and this code cannot see it either
88
+ * ({@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT} reports that). Treat this code as a
83
89
  * report that one record definitely lost its fields, never as a sweep that would have fired if
84
90
  * any had.
85
91
  */
86
92
  readonly ASTM_RECORD_FIELDS_UNSEPARATED: "ASTM_RECORD_FIELDS_UNSEPARATED";
87
93
  /** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
88
94
  readonly ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS";
95
+ /**
96
+ * A header declared **one character in two roles**, so the boundary between those two roles is
97
+ * not recoverable from the bytes. The declaration is still read and honored and no record is
98
+ * dropped: what is gone is a distinction the sender's own bytes no longer carry.
99
+ *
100
+ * The field separator is not part of this: a declaration naming it in another role is refused
101
+ * earlier (the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal on the first header,
102
+ * {@link WARNING_CODES.ASTM_RECORD_UNREADABLE_REDECLARATION} on a later one). What this code
103
+ * covers is the three unordered pairs among the rest: **repeat/component, repeat/escape,
104
+ * component/escape**.
105
+ *
106
+ * Measured on `H|^^&` (repeat and component both `^`): the field `A^B^C^D` reads back as four
107
+ * repeats of one component each, so `components` holds only `A` and a two-repeats-of-two-components
108
+ * reading cannot be recovered. Measured on `H|\&&` (component and escape both `&`): `A&B` splits
109
+ * into two components while `A&F&B` reads as the single component `A|B`, so the same character
110
+ * means two different things depending on what follows it.
111
+ *
112
+ * **It is not tolerable**, and the reason is the pair it travels with: such a set is always
113
+ * non-canonical, so before this code existed the only warning on the stream was
114
+ * {@link WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS}, which a profile may tolerate. That made a
115
+ * structurally unreadable declaration indistinguishable, to a strict consumer, from an ordinary
116
+ * vendor set. Emit refuses the same sets outright (`ASTM_EMIT_INVALID_DELIMITERS`).
117
+ *
118
+ * One warning per header that **changes** the set in force into such a set, not one per colliding
119
+ * pair. A later header restating the colliding set already in force is a no-op and warns nothing,
120
+ * on the same rule as {@link WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS}: the set it names was
121
+ * already reported when it came into force.
122
+ */
123
+ readonly ASTM_RECORD_DELIMITER_ROLE_COLLISION: "ASTM_RECORD_DELIMITER_ROLE_COLLISION";
124
+ /**
125
+ * An escape sequence whose body was **not** a recognized mnemonic held a character that is one of
126
+ * the three splitting delimiters in force, and the atom rule (an `&X&` triple is opaque) kept it
127
+ * out of the split, so a boundary the bytes carried never became one. The sequence is preserved
128
+ * verbatim in the value; nothing is dropped and nothing is re-split.
129
+ *
130
+ * Measured on the canonical set: `R|1|^^^687|28.6&|&U/L||||F` reads `value` = `28.6&|&U/L`, with
131
+ * **no units** and status `unspecified` rather than `final`.
132
+ *
133
+ * **This is the code that says a boundary was lost.** The same condition also raises
134
+ * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, which stays and stays tolerable: that code
135
+ * reports only that a body was not recognized, which is true of bodies that cost nothing. This one
136
+ * is the narrower, safety-critical half, and no profile may tolerate it.
137
+ *
138
+ * **What it does not do is repair anything.** The atom rule is unchanged (it is what keeps `&F&`
139
+ * one token under a set that names `F` as a delimiter), so the value is byte-identical to what it
140
+ * was before this code existed. It also cannot see the condition through a re-emit: emit rewrites
141
+ * the preserved sequence into recognized mnemonics, and the resulting stream says that value
142
+ * unambiguously, so a second-generation read is silent and correct about its own bytes. The place
143
+ * to catch this is the first read of the wire bytes, which is where it now refuses a strict parse.
144
+ */
145
+ readonly ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE: "ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE";
146
+ /**
147
+ * Escape sequences are matched greedily and leftmost, so the escape character that **closed** an
148
+ * unrecognized sequence could not also **open** the next one. Where it could have, and where the
149
+ * body it would have held is the delimiter that was split on, the same bytes carry two alignments
150
+ * that disagree by one boundary: under the reading taken that delimiter ends a field, repeat or
151
+ * component, and under the other it sits inside an opaque atom and ends nothing. The leftmost
152
+ * reading is kept, every byte is preserved, and nothing is re-split.
153
+ *
154
+ * **This is the mirror of {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE}, and
155
+ * the direction is the reason it needs its own code.** That one reports a boundary the reading
156
+ * **lost**; this one reports a boundary the reading may have **gained**, which is the more
157
+ * dangerous direction, because a gained boundary hands back a value the sender's bytes do not
158
+ * unambiguously carry. Measured on the canonical set: `R|1|^^^687|28.6&Z&|&U/L||||F` reads
159
+ * `value` = `28.6&Z&` and `units` = `&U/L` under the leftmost alignment, and reads as a single
160
+ * unsplit field carrying both under the other.
161
+ *
162
+ * **Two exclusions, both deliberate, and the first is wider than its own argument.** Where the
163
+ * earlier sequence's body is a **recognized** mnemonic nothing is reported, because the reading
164
+ * taken interprets a construct (`&F&` is the sender escaping a field separator, which is what the
165
+ * mechanism is for) while the competing alignment's body is a delimiter character it usually
166
+ * cannot interpret at all, so this codec's own vocabulary prefers the reading taken. That is not
167
+ * the same as the reading taken being conformant, and where the declared set names a mnemonic
168
+ * letter as a splitting delimiter both alignments interpret one construct and neither is
169
+ * preferred, yet this stays silent. Both residues are measured and recorded rather than closed by
170
+ * widening the test. And where the escape
171
+ * character after the delimiter does not itself close a sequence there is no competing alignment
172
+ * at all, so an ordinary escaped value followed by an ordinary boundary is silent.
173
+ *
174
+ * **It is a report, not a repair, and not a round-trip guard.** The reading is unchanged: picking
175
+ * the other alignment would be a different guess with no more evidence behind it. Emit then
176
+ * rewrites the preserved sequences into recognized mnemonics, and those bytes carry the reading
177
+ * that was taken unambiguously, so a second-generation read is silent and is correct about its own
178
+ * bytes. The first read of the wire bytes is the only place the ambiguity exists to be caught.
179
+ */
180
+ readonly ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT: "ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT";
181
+ /**
182
+ * Two escape alignments of the same bytes disagreed about a **field** boundary, the reading taken
183
+ * kept it, and the escape character that reading resumes on **heads no sequence at all**, so the
184
+ * boundary was bought with a byte this reading cannot read while the competing alignment is
185
+ * exactly the reading that can. Every field after that point sits one place further right than the
186
+ * competing alignment puts it.
187
+ *
188
+ * **The shift is the harm, and on a result record it reaches the status slot.** Measured on the
189
+ * canonical set, `R|1|^^^687|28.6&F&|&U/L||||F` reads **9** fields under the reading taken and
190
+ * **8** under the competing one, so the sender's trailing `F` lands in field 9 (the result status)
191
+ * under the first and in no field at all under the second. The parse hands back units `&U/L` and a
192
+ * status of **`final`**, and both are consequences of the alignment rather than values the sender
193
+ * placed in those slots. A downstream system reading `final` would act on a result the bytes do
194
+ * not say was finalised. Before this code the only warning on that stream was the **tolerable**
195
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}, so the widest gate-legal profile plus
196
+ * `{ strict: true }` accepted it.
197
+ *
198
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is identical, and
199
+ * the units and status read are the ones that were always read. Picking the other alignment would
200
+ * be a different guess with no more evidence behind it, and it would change values on a published
201
+ * package. What is new is that the shift is reported by a code no profile may tolerate.
202
+ *
203
+ * **It fires alongside {@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT}, not instead
204
+ * of it, and neither test is a widening of the other.** That code asks whether this codec's
205
+ * vocabulary prefers the reading taken *at* the contested position, and is silent where the
206
+ * earlier body is a recognized mnemonic. This one asks what the reading taken makes of the bytes
207
+ * *after* the boundary, and does not consult the earlier body at all.
208
+ *
209
+ * **Two deliberate bounds, both stated rather than left to be found.**
210
+ * - **Only the field role, and that bound is a CHOICE rather than a consequence.** A gained repeat
211
+ * or component boundary divides one field and reaches nothing outside it, so it moves no
212
+ * **field-indexed** slot: the units and the status stay where they were. **It does not follow
213
+ * that it moves no modeled slot at all, and writing that down would be false.** Components are
214
+ * modeled *inside* a field: a Universal Test ID's four components are the LOINC-candidate slot,
215
+ * the test name, the **coding scheme** and the **local code**, and a patient name's three are
216
+ * last, first and middle. A gained component boundary shifts those, so a local code can be read
217
+ * as a coding scheme and a given name as a middle name. Measured, and **where the earlier body is
218
+ * a recognized mnemonic, reported by nothing** (only the tolerable
219
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER} fires, so a gate-legal profile accepts it).
220
+ * An **unrecognized** earlier body raises
221
+ * {@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT} there and is refused, which is the
222
+ * same split this code's own exclusion takes. It is a separate, open condition rather than something this
223
+ * code covers, because wiring this sink to another split is a different criterion needing its own
224
+ * population measurement. The repeat role costs the **value**, which this code does not report
225
+ * either.
226
+ * - **The tail is weighed one construct deep.** Where the escape character the reading taken
227
+ * resumes on heads a sequence this codec *recognizes*, the reading taken interprets it and the
228
+ * competing alignment would leave it bare, so the bytes prefer the reading taken: under a set
229
+ * naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and
230
+ * escaped again, and refusing it would be an over-refusal of a well-formed stream. Where it
231
+ * heads a sequence whose body is *unrecognized*, the reading taken still consumes it while the
232
+ * competing alignment would leave **two** escape characters bare, so the preference is stronger
233
+ * again, and this stays silent there even though the field shift is real. That last case is the
234
+ * named residue: measured, not overlooked.
235
+ *
236
+ * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized mnemonics,
237
+ * and those bytes carry the reading that was taken unambiguously, so a second-generation read is
238
+ * silent and is correct about its own bytes. A clean re-read is not evidence.
239
+ */
240
+ readonly ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS: "ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS";
89
241
  /** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
90
242
  readonly ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE";
91
243
  /**
@@ -107,7 +259,8 @@ declare const WARNING_CODES: {
107
259
  * *different* escape character in the same record may still head a real three-character sequence,
108
260
  * and if that sequence's body happens to be a delimiter (`&|&` under the canonical set) the atom
109
261
  * rule means that delimiter does not split. That case is reported separately, under
110
- * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, and it can still cost a field boundary.
262
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE}, and it still costs a field
263
+ * boundary.
111
264
  */
112
265
  readonly ASTM_UNPAIRED_ESCAPE_CHARACTER: "ASTM_UNPAIRED_ESCAPE_CHARACTER";
113
266
  /**
@@ -173,7 +326,8 @@ declare const WARNING_CODES: {
173
326
  */
174
327
  readonly ASTM_RECORD_DELIMITERS_REDECLARED: "ASTM_RECORD_DELIMITERS_REDECLARED";
175
328
  /**
176
- * A later `H` (header) record could not declare a usable delimiter set: it was too short, or the
329
+ * A later `H` (header) record could not declare a usable delimiter set: it was not a header, it
330
+ * was too short, its delimiter definition held fewer than three characters, or the
177
331
  * field separator it named also appeared among the other three, leaving the four roles
178
332
  * indistinguishable. The delimiters **already in force are kept** and every record is still surfaced;
179
333
  * a set is never guessed and no record is dropped.
@@ -283,6 +437,80 @@ declare function fieldsUnseparated(position: AstmPosition): AstmRecordWarning;
283
437
  * ```
284
438
  */
285
439
  declare function nonStandardDelimiters(position: AstmPosition): AstmRecordWarning;
440
+ /**
441
+ * Build an `ASTM_RECORD_DELIMITER_ROLE_COLLISION` warning. Emitted when a header
442
+ * declares one character in two of the repeat / component / escape roles. The
443
+ * declaration is honored and no record is dropped; the message names no character,
444
+ * because a delimiter is a byte off the wire.
445
+ *
446
+ * A profile may **not** tolerate this code: it reports a distinction the bytes no
447
+ * longer carry, and the only other warning such a set raises
448
+ * ({@link WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS}) is tolerable.
449
+ *
450
+ * @example
451
+ * ```ts
452
+ * import { delimiterRoleCollision } from "@cosyte/astm";
453
+ * delimiterRoleCollision({ recordIndex: 0, recordType: "H" });
454
+ * ```
455
+ */
456
+ declare function delimiterRoleCollision(position: AstmPosition): AstmRecordWarning;
457
+ /**
458
+ * Build an `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` warning. Emitted when an
459
+ * unrecognized escape body is itself a splitting delimiter in force, so the atom
460
+ * rule kept it out of the split. The value is preserved verbatim and is identical
461
+ * with the warning and without it; what the warning reports is the boundary that
462
+ * did not happen.
463
+ *
464
+ * A profile may **not** tolerate this code. It fires alongside
465
+ * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, which remains tolerable and
466
+ * reports the strictly weaker fact that a body was not recognized.
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * import { delimiterSwallowedByEscape } from "@cosyte/astm";
471
+ * delimiterSwallowedByEscape({ recordIndex: 4, recordType: "R", fieldIndex: 4 });
472
+ * ```
473
+ */
474
+ declare function delimiterSwallowedByEscape(position: AstmPosition): AstmRecordWarning;
475
+ /**
476
+ * Build an `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` warning. Emitted when the escape
477
+ * character that closed an unrecognized escape sequence could instead have opened one
478
+ * holding the delimiter that was split on, so the bytes carry two alignments that
479
+ * disagree about that boundary. The leftmost alignment is kept and every byte is
480
+ * preserved; what the warning reports is that the boundary is a choice.
481
+ *
482
+ * A profile may **not** tolerate this code. It fires alongside
483
+ * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, which remains tolerable and
484
+ * reports the strictly weaker fact that a body was not recognized.
485
+ *
486
+ * @example
487
+ * ```ts
488
+ * import { ambiguousEscapeAlignment } from "@cosyte/astm";
489
+ * ambiguousEscapeAlignment({ recordIndex: 4, recordType: "R", fieldIndex: 4 });
490
+ * ```
491
+ */
492
+ declare function ambiguousEscapeAlignment(position: AstmPosition): AstmRecordWarning;
493
+ /**
494
+ * Build an `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` warning. Emitted when a competing escape
495
+ * alignment decided a **field** boundary and the escape character the reading taken resumes on
496
+ * heads no sequence of its own, so every field after that point sits one place further right than
497
+ * the competing alignment puts it. On a result record that reaches the units and the **result
498
+ * status**: a trailing status letter lands in field 9 under the reading taken and in no field at
499
+ * all under the competing one.
500
+ *
501
+ * A profile may **not** tolerate this code. It fires alongside
502
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}, which remains tolerable and reports the
503
+ * strictly weaker fact that one escape character was read as a literal, and alongside
504
+ * {@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT} where that also applies. The reading
505
+ * is unchanged: this reports the shift, it does not repair it.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * import { alignmentShiftedFields } from "@cosyte/astm";
510
+ * alignmentShiftedFields({ recordIndex: 2, recordType: "R", fieldIndex: 4 });
511
+ * ```
512
+ */
513
+ declare function alignmentShiftedFields(position: AstmPosition): AstmRecordWarning;
286
514
  /**
287
515
  * Build an `ASTM_RECORD_DELIMITERS_REDECLARED` warning. Emitted when a later `H`
288
516
  * record declares a delimiter set different from the one in force; the new set is
@@ -328,8 +556,10 @@ declare function unknownEscapeSequence(position: AstmPosition): AstmRecordWarnin
328
556
  *
329
557
  * **It is a statement about that one character, not about the record.** A
330
558
  * different escape character in the same record may head a real three-character
331
- * sequence, and if that sequence's body is a delimiter, that delimiter does not
332
- * split. See {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}.
559
+ * sequence, and if that sequence's body is an unrecognized character that is itself a delimiter in
560
+ * force, that delimiter does not
561
+ * split, which is reported separately by
562
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE}.
333
563
  *
334
564
  * A profile **may** tolerate this code: the value it reports is byte-identical
335
565
  * with the warning and without it, because reading the character as a literal is
@@ -528,13 +758,39 @@ interface Delimiters {
528
758
  * ```
529
759
  */
530
760
  declare const CANONICAL_DELIMITERS: Delimiters;
761
+ /**
762
+ * Why a header record could not declare a usable delimiter set. Four distinct
763
+ * conditions, kept distinct because a consumer is told which one it was and two of
764
+ * them describe records that are **not** short.
765
+ *
766
+ * - `not-a-header`: the record does not begin with an `H` type letter.
767
+ * - `record-too-short`: fewer than five characters, so `H` plus a field separator
768
+ * plus a three-character definition cannot fit.
769
+ * - `definition-truncated`: the delimiter-definition field runs to the next field
770
+ * separator (or to the end of the record) and holds fewer than three characters.
771
+ * **This is the one a full-length header reaches**, and it is what a declaration
772
+ * naming its own field separator among the other three produces, because that
773
+ * separator ends the definition where it appears.
774
+ * - `field-separator-reused`: the field separator is also the repeat, component or
775
+ * escape character.
776
+ */
777
+ type DelimiterDeclarationFault = "not-a-header" | "record-too-short" | "definition-truncated" | "field-separator-reused";
778
+ /** The result of reading delimiters from a header record: either resolved or a named failure. */
779
+ type DelimiterReadResult = {
780
+ readonly ok: true;
781
+ readonly delimiters: Delimiters;
782
+ } | {
783
+ readonly ok: false;
784
+ readonly fault: DelimiterDeclarationFault;
785
+ };
531
786
  /**
532
787
  * Read the four delimiters from a header record's raw text (a single `H`
533
788
  * record, its terminator already stripped).
534
789
  *
535
- * Returns `undefined` when the record cannot declare all four delimiters: it is
536
- * shorter than `H` + a field separator + a 3-char delimiter definition, or the
537
- * field separator it names also appears among the other three.
790
+ * Returns `undefined` when the record cannot declare all four delimiters. The four
791
+ * ways that happens are named by {@link readDelimiterDeclaration}, which is this
792
+ * function with the reason kept; use it wherever the reason is shown to a consumer,
793
+ * because "too short" is false of some of them.
538
794
  *
539
795
  * What the caller does with that depends on **which** header it is. On the first
540
796
  * header it is the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal, because there is no
@@ -543,6 +799,12 @@ declare const CANONICAL_DELIMITERS: Delimiters;
543
799
  * and an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning is raised instead; a set is
544
800
  * never guessed and no record is dropped.
545
801
  *
802
+ * **A set two of whose other three roles share a character is resolved, not
803
+ * refused**, because the stream is still readable and refusing it would drop
804
+ * records the sender did send. What it costs is the boundary between those two
805
+ * roles, which the bytes no longer carry: see {@link hasCollidingRoles}, which
806
+ * the parse path calls to report it (`ASTM_RECORD_DELIMITER_ROLE_COLLISION`).
807
+ *
546
808
  * This function does not throw: delimiter resolution and the escalation decision are
547
809
  * kept separate so the reader stays pure and testable.
548
810
  *
@@ -556,6 +818,68 @@ declare const CANONICAL_DELIMITERS: Delimiters;
556
818
  * ```
557
819
  */
558
820
  declare function readDelimiters(headerRecord: string): Delimiters | undefined;
821
+ /**
822
+ * The same read as {@link readDelimiters}, keeping **why** it failed.
823
+ *
824
+ * The reason is not cosmetic. A caller that reports every failure as "too short"
825
+ * says something false about `H||^&`, a full-length header whose definition field is
826
+ * empty because its own field separator ends it, and a consumer reading that
827
+ * diagnostic looks for the wrong thing. The two conditions are separate rules that
828
+ * happen to coincide today (see the fault list on
829
+ * {@link DelimiterDeclarationFault}), so they are named separately.
830
+ *
831
+ * **`field-separator-reused` is unreachable through this function today, and the
832
+ * check stays.** A field separator occurring in the definition positions sits at
833
+ * index 2, 3 or 4, so it ends the definition where it appears and the definition is
834
+ * under three characters: `definition-truncated` answers first, every time. The
835
+ * outcome is right either way (such a declaration does not resolve at all). The
836
+ * check is the statement of an invariant the length rule currently enforces on its
837
+ * behalf, and the two are not the same rule: a change to how the definition field is
838
+ * bounded would separate them again. Deleting it would leave the invariant
839
+ * unstated and the change silent.
840
+ *
841
+ * @param headerRecord - The raw `H` record text (no trailing CR/LF).
842
+ * @returns The resolved delimiters, or the named reason they could not be read.
843
+ * @example
844
+ * ```ts
845
+ * import { readDelimiterDeclaration } from "@cosyte/astm";
846
+ * readDelimiterDeclaration("H|\\^&").ok; // true
847
+ * readDelimiterDeclaration("H||^&"); // { ok: false, fault: "definition-truncated" }
848
+ * ```
849
+ */
850
+ declare function readDelimiterDeclaration(headerRecord: string): DelimiterReadResult;
851
+ /**
852
+ * Whether a resolved set names one character in two roles, so the boundary
853
+ * between those two roles cannot be recovered from the bytes.
854
+ *
855
+ * {@link readDelimiters} already refuses a declaration whose **field** separator
856
+ * is one of the other three (it returns `undefined`, which is the
857
+ * `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal on the first header and the
858
+ * `ASTM_RECORD_UNREADABLE_REDECLARATION` warning on a later one), so what is left
859
+ * to test here is the three unordered pairs among the remaining roles:
860
+ * **repeat/component, repeat/escape, and component/escape**. Three pairs, named
861
+ * rather than counted, because the count is only meaningful with the list.
862
+ *
863
+ * Such a declaration is read and honored (nothing is guessed, no record is
864
+ * dropped) and the loss it causes is real and was previously silent. Measured on
865
+ * the canonical-looking `H|^^&`, where the repeat and component roles are both
866
+ * `^`: the field `A^B^C^D` reads back as **four repeats of one component each**,
867
+ * so a two-repeats-of-two-components reading is unrecoverable and `components`
868
+ * holds only `A`. On `H|\&&`, where the component and escape roles are both `&`,
869
+ * the same character splits (`A&B` reads as two components) or opens an atom
870
+ * (`A&F&B` reads as the single component `A|B`) depending only on what follows
871
+ * it. Emit refuses such a set outright (`ASTM_EMIT_INVALID_DELIMITERS`).
872
+ *
873
+ * @param d - A resolved delimiter set.
874
+ * @returns `true` iff two of the repeat / component / escape roles share a character.
875
+ * @example
876
+ * ```ts
877
+ * import { hasCollidingRoles, readDelimiters } from "@cosyte/astm";
878
+ * hasCollidingRoles(readDelimiters("H|\\^&")!); // false
879
+ * hasCollidingRoles(readDelimiters("H|^^&")!); // true (repeat === component)
880
+ * ```
881
+ */
882
+ declare function hasCollidingRoles(d: Delimiters): boolean;
559
883
  /**
560
884
  * Whether a resolved delimiter set differs from the canonical `H|\^&`. A `true`
561
885
  * result is a tolerated vendor quirk, surfaced as a value-free warning so a
@@ -2042,7 +2366,10 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
2042
2366
  * set (a non-canonical source is normalized to `H|\^&` by default). Normalizing sets the
2043
2367
  * four delimiter **roles**; it does not delete a declaration's surplus characters, so a
2044
2368
  * canonically-declared header that arrived carrying surplus keeps it on the default path
2045
- * too.
2369
+ * too, **except where that surplus could not be read back as surplus, when all of it is
2370
+ * dropped** (see `declarationResidual` below). Emit re-encodes what it read, so bytes
2371
+ * can differ without anything being lost: `O&BRIEN` in a canonical stream goes out as
2372
+ * `O&E&BRIEN`.
2046
2373
  *
2047
2374
  * **One delimiter set for the whole stream.** Every record (including `H`, `M` and `S`)
2048
2375
  * is emitted against the delimiters being emitted with, and the header declares that same
@@ -2063,7 +2390,11 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
2063
2390
  * `H|\^&#` rather than losing the `#`. That holds on the **default** canonical path as
2064
2391
  * well as when a set is passed explicitly: the surplus of a canonically-declared header
2065
2392
  * survives normalization, because normalization is about the four roles and the surplus
2066
- * has none.
2393
+ * has none. It stops holding where the surplus itself could not be read back, and then
2394
+ * **the whole surplus is dropped, not the offending character alone**, on the default
2395
+ * path as much as any other: `declarationResidual` states which surpluses those are and
2396
+ * why an all-or-nothing drop is the only one of the available answers that invents
2397
+ * nothing.
2067
2398
  *
2068
2399
  * **Never break framing.** A component leaf that contains a record terminator
2069
2400
  * (`CR`/`LF`) cannot be escaped by the ASTM escape codec (only the four declared
@@ -2084,10 +2415,15 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
2084
2415
  * output, which is why they are refused at this one.
2085
2416
  *
2086
2417
  * **One position is not a modeled value and does not keep the byte:** the surplus
2087
- * of a header's delimiter declaration, where any control character is dropped
2418
+ * of a header's delimiter declaration, where a control character is dropped
2088
2419
  * silently (see `declarationResidual` below, where that disposition is argued and
2089
2420
  * where it long predates the frame-layer refusal). So "the record layer carries
2090
2421
  * these bytes" is a statement about values, not about every byte of every line.
2422
+ * Measured, one character at a time in the surplus of an otherwise spec-clean
2423
+ * `H|\^&` header: **31 of the 33 C0/`DEL` characters reach that rule and are
2424
+ * dropped**, with `warnings: []` and no error. The two that do not are `CR` and
2425
+ * `LF`, which never reach it at all, because they end the record while it is being
2426
+ * read and so are never part of any surplus the model carries.
2091
2427
  *
2092
2428
  * **Check the delimiter set before writing.** Three conditions are required for the
2093
2429
  * emitted bytes to read back as the records that produced them (each separator exactly
@@ -2104,10 +2440,18 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
2104
2440
  *
2105
2441
  * **What is still not guaranteed.** Together the two checks guarantee that every
2106
2442
  * record re-reads as its own *type*. They do not guarantee that every *field* of it
2107
- * lands where it did: an escape sequence whose body is itself a delimiter is read as
2443
+ * lands where it did: an escape sequence whose body is an unrecognized character that is itself a
2444
+ * delimiter in force is read as
2108
2445
  * one opaque atom, so that delimiter never becomes a boundary and the fields after it
2109
- * shift. That is reported on the parse side (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) and is
2110
- * not what these refusals cover.
2446
+ * shift. That is reported on the parse side, by `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`
2447
+ * (which no profile may tolerate) alongside the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, and is
2448
+ * not what these refusals cover. Nor is the mirror of it: sequences are matched leftmost, so one
2449
+ * can end where another could have begun and the delimiter between them splits where the competing
2450
+ * alignment would have held it, reported by `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`. **Neither
2451
+ * report survives this serializer**, by construction rather than by oversight: it rewrites the
2452
+ * preserved characters into recognized mnemonics, so the emitted stream carries the reading that
2453
+ * was taken with nothing ambiguous left in it, and a second-generation parse is silent and correct
2454
+ * about its own bytes. Catch both on the first read of the wire bytes.
2111
2455
  */
2112
2456
 
2113
2457
  /**
@@ -2166,10 +2510,12 @@ declare class AstmSerializeError extends Error {
2166
2510
  type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID_DELIMITERS" | "ASTM_EMIT_TYPE_LETTER_COLLISION";
2167
2511
  /**
2168
2512
  * Escape-encode one component leaf for spec-clean emit: the inverse of
2169
- * `decodeEscapes`. The **escape character itself is encoded first** (`&` → `&E&`)
2170
- * so a later delimiter substitution can never double-encode the `&` it just
2171
- * introduced; then the field / component / repeat delimiters map to their
2172
- * mnemonics.
2513
+ * `decodeEscapes`. Each character is read **once**, in one left-to-right pass, and
2514
+ * written either as itself or as a whole `escape` + mnemonic + `escape` triple
2515
+ * (`&` → `&E&`, the field / component / repeat delimiters → `&F&` / `&S&` / `&R&`).
2516
+ * Nothing this encoder writes is examined again, which is what makes it the exact
2517
+ * inverse of the decoder: the decoder reads the same output one token at a time,
2518
+ * so every triple it meets is a triple this encoder wrote.
2173
2519
  *
2174
2520
  * A `CR`/`LF` in the leaf has no escape mnemonic and would break framing, so it
2175
2521
  * is rejected with an {@link AstmSerializeError} rather than emitted raw, as is a
@@ -2199,7 +2545,11 @@ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: numb
2199
2545
  * The header (`H`) is special-cased: its delimiter-definition field is emitted as
2200
2546
  * the **literal** declaration of `d`, never escaped, escaping it would corrupt the
2201
2547
  * very declaration a reader depends on, followed by any characters the modeled
2202
- * declaration carried beyond the three a reader takes its roles from. Manufacturer
2548
+ * declaration carried beyond the three a reader takes its roles from. Those surplus
2549
+ * characters are kept unless the surplus could not be read back as surplus, when
2550
+ * **all** of it is dropped: that covers a surplus carrying the field delimiter or
2551
+ * any control character, and it is silent, because emit has no warning channel.
2552
+ * Manufacturer
2203
2553
  * (`M`) and scientific (`S`) records are reproduced **byte-identically** from their
2204
2554
  * preserved `rawLine` when they are already in `d`, and re-encoded from their fields
2205
2555
  * when they are not, so their fields never collapse into one on the next read.
@@ -2234,7 +2584,11 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
2234
2584
  * re-parsing it recovers every field. Passing `d` explicitly emits against that set
2235
2585
  * instead, and the header declares it. Normalization replaces the four delimiter
2236
2586
  * **roles**; a header declaration carrying characters beyond the three that hold a
2237
- * role keeps them, on this path as on any other, rather than being truncated.
2587
+ * role keeps them, on this path as on any other, rather than being truncated. The
2588
+ * exception is a surplus that could not be read back as one, carrying the field
2589
+ * delimiter or any control character: there the **whole** surplus is dropped, on
2590
+ * every path including the default one, and silently, because emit returns a bare
2591
+ * string with no channel to say so.
2238
2592
  *
2239
2593
  * **`d` is checked before anything is written.** Each of the four separators must be
2240
2594
  * exactly one character, none may be a record terminator, and no two may share a
@@ -2251,9 +2605,14 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
2251
2605
  * therefore checked to start with the letter the record models, and one that does
2252
2606
  * not is `ASTM_EMIT_TYPE_LETTER_COLLISION` rather than a stream that reads back as
2253
2607
  * different records. What neither check promises is that every *field* lands where
2254
- * it did: an escape sequence whose body is a delimiter is an opaque atom, so that
2608
+ * it did: an escape sequence whose body is an unrecognized character that is itself a delimiter in
2609
+ * force is an opaque atom, so that
2255
2610
  * delimiter never becomes a boundary, and the parse side reports that
2256
- * (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) rather than emit refusing it.
2611
+ * (`ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, plus the tolerable
2612
+ * `ASTM_UNKNOWN_ESCAPE_SEQUENCE`) rather than emit refusing it. The mirror case, where a sequence
2613
+ * ends where another could have begun so a delimiter splits that the competing alignment would
2614
+ * have held, is reported the same way (`ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`). Emitting
2615
+ * normalizes both away rather than preserving them, so neither reaches a second generation.
2257
2616
  *
2258
2617
  * @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
2259
2618
  * @param d - The delimiters to emit against; defaults to the canonical `H|\^&` set.
@@ -2874,6 +3233,20 @@ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageCla
2874
3233
  * unrecognized escape sequence encountered, so the caller can warn.
2875
3234
  * @param onUnpairedEscape - Called (with the 0-based field index) for each escape
2876
3235
  * character that heads no escape sequence and was therefore read as a literal.
3236
+ * @param onSwallowedDelimiter - Called (with the 0-based field index) for each
3237
+ * unrecognized escape sequence whose body is a splitting delimiter in force, so
3238
+ * that delimiter never became a boundary.
3239
+ * @param onAmbiguousAlignment - Called (with the 0-based field index) for each
3240
+ * unrecognized escape sequence whose closing escape character could instead have
3241
+ * opened one holding the delimiter that split, so the boundary is one of two
3242
+ * readings the bytes carry.
3243
+ * @param onAlignmentShiftedFields - Called (with the 0-based field index) for each
3244
+ * contested **field** boundary the reading took while resuming on an escape
3245
+ * character that heads no sequence, so every later field is one place further
3246
+ * right than the competing alignment puts it. Wired only to the field split: a
3247
+ * repeat or component boundary divides one field and so moves no field-indexed
3248
+ * slot. That is a choice and not a consequence, because components are modeled
3249
+ * inside a field; see {@link ShiftedFieldsSink}.
2877
3250
  * @returns The record's fields.
2878
3251
  * @example
2879
3252
  * ```ts
@@ -2882,7 +3255,7 @@ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageCla
2882
3255
  * fields[3].components[0]; // "28.6"
2883
3256
  * ```
2884
3257
  */
2885
- declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void): AstmField[];
3258
+ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void, onSwallowedDelimiter?: (fieldIndex: number) => void, onAmbiguousAlignment?: (fieldIndex: number) => void, onAlignmentShiftedFields?: (fieldIndex: number) => void): AstmField[];
2886
3259
  /**
2887
3260
  * Tokenize an `H` (header) record into its fields.
2888
3261
  *
@@ -2907,6 +3280,17 @@ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?:
2907
3280
  * @param onUnpairedEscape - Called with the 0-based whole-record field index for
2908
3281
  * each unpaired escape character in the data portion. The declaration itself is
2909
3282
  * opaque, so the escape character it names never reports here.
3283
+ * @param onSwallowedDelimiter - Called with the 0-based whole-record field index
3284
+ * for each unrecognized escape sequence in the data portion whose body is a
3285
+ * splitting delimiter in force. The declaration is opaque, so the delimiters it
3286
+ * names literally never report here.
3287
+ * @param onAmbiguousAlignment - Called with the 0-based whole-record field index for
3288
+ * each competing escape alignment in the data portion. The declaration is opaque,
3289
+ * so the characters it names literally never report here either.
3290
+ * @param onAlignmentShiftedFields - Called with the 0-based whole-record field index
3291
+ * for each contested field boundary in the data portion whose reading resumes on
3292
+ * an escape character heading no sequence. The declaration is opaque, so it never
3293
+ * reports here either.
2910
3294
  * @returns The header's fields.
2911
3295
  * @example
2912
3296
  * ```ts
@@ -2916,7 +3300,7 @@ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?:
2916
3300
  * fields[4].raw; // "sender"
2917
3301
  * ```
2918
3302
  */
2919
- declare function tokenizeHeader(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void): AstmField[];
3303
+ declare function tokenizeHeader(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void, onSwallowedDelimiter?: (fieldIndex: number) => void, onAmbiguousAlignment?: (fieldIndex: number) => void, onAlignmentShiftedFields?: (fieldIndex: number) => void): AstmField[];
2920
3304
  /**
2921
3305
  * The primary scalar of a field: its first repeat's first component, decoded.
2922
3306
  * Returns `undefined` for a truly empty field so callers can distinguish
@@ -2968,7 +3352,19 @@ declare const FATAL_CODES: {
2968
3352
  readonly EMPTY_INPUT: "EMPTY_INPUT";
2969
3353
  /** The first record is not an `H` (header) record: an ASTM message must lead with `H`. */
2970
3354
  readonly ASTM_RECORD_NO_HEADER: "ASTM_RECORD_NO_HEADER";
2971
- /** The `H` record is too short to declare the four delimiters (field/repeat/component/escape). */
3355
+ /**
3356
+ * The first `H` record could not declare all four delimiters (field/repeat/component/escape).
3357
+ * One code, four reasons, and the message says which: the record is not a header, it is shorter
3358
+ * than a header plus a three-character definition, its definition field holds fewer than three
3359
+ * characters before the next field separator, or its field separator is also one of the other
3360
+ * three. Only the second of those is "too short", so read the message rather than assuming it.
3361
+ *
3362
+ * **Two of the four cannot be reached on this fatal, and are not dead code.** A first record that
3363
+ * is not an `H` raises `ASTM_RECORD_NO_HEADER` before the declaration is ever read, and a field
3364
+ * separator reused among the other three ends the delimiter definition where it appears, so the
3365
+ * truncation reason answers first. The reader names all four because it is also called directly
3366
+ * and on later headers, where the same conditions are a warning rather than this fatal.
3367
+ */
2972
3368
  readonly ASTM_RECORD_UNDECLARED_DELIMITERS: "ASTM_RECORD_UNDECLARED_DELIMITERS";
2973
3369
  };
2974
3370
  /**
@@ -3043,14 +3439,53 @@ declare class AstmParseError extends Error {
3043
3439
  * next escape character with no bound, which meant a single unescaped `&` in a
3044
3440
  * value merged every field after it into that value, in silence.
3045
3441
  *
3046
- * **The atom rule is unchanged and still costs a boundary in one case.** A triple
3047
- * is opaque by design, so where its body is itself a delimiter (`&|&` under the
3048
- * canonical set) that delimiter does not split. That is deliberate: it is what
3049
- * keeps `&F&` one token under a declared set that names `F` as a delimiter. Where
3050
- * the body is not a recognized mnemonic the parse layer reports it as
3051
- * `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, so it is never silent, but that code is
3052
- * tolerable and a field boundary has still gone. Do not "fix" that by narrowing
3053
- * the atom: the narrowing would break the guarantee the atom exists for.
3442
+ * **The atom rule is unchanged and still costs a boundary in one case, which now
3443
+ * has a report of its own.** A triple is opaque by design, so where its body is
3444
+ * itself a delimiter (`&|&` under the canonical set) that delimiter does not
3445
+ * split. That is deliberate: it is what keeps `&F&` one token under a declared
3446
+ * set that names `F` as a delimiter. Where the body is not a recognized mnemonic
3447
+ * the parse layer reports it as `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, and where that
3448
+ * unrecognized body is *also* one of the three splitting roles in force it
3449
+ * reports `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` **as well**, which is the
3450
+ * code that says a boundary went missing rather than merely that a body was not
3451
+ * recognized. Do not "fix" this by narrowing the atom: the narrowing would break
3452
+ * the guarantee the atom exists for. The split is unchanged; what changed is that
3453
+ * the loss is now reported by a code no profile may tolerate.
3454
+ *
3455
+ * **The match is greedy and leftmost, and that costs a boundary in the opposite
3456
+ * direction, which also has a report of its own.** Triples are taken left to
3457
+ * right, so the escape character that *closes* one triple cannot also *open* the
3458
+ * next. Where it could have (the character after a triple is a splitting
3459
+ * delimiter and the one after that is the escape character), the bytes carry two
3460
+ * alignments that disagree about that delimiter: under the reading taken it ends
3461
+ * a field, repeat or component, and under the other it sits inside an opaque
3462
+ * atom and ends nothing. Where the earlier triple's body was **not** a
3463
+ * recognized mnemonic, the reading taken rests on a triple this codec cannot
3464
+ * interpret at all, so nothing in its own vocabulary prefers that reading to the
3465
+ * competing one, and the parse layer reports
3466
+ * `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`. The split is unchanged
3467
+ * here too: the leftmost reading is kept, and what is new is that a boundary
3468
+ * which may not be the sender's is no longer reported only by codes a profile
3469
+ * may tolerate. Where that earlier body **was** a recognized mnemonic nothing is
3470
+ * reported, and the reason is narrower than it looks, so read
3471
+ * {@link AmbiguousAlignmentSink} before widening it.
3472
+ *
3473
+ * **A second question is asked at the same position, and it is a different one,
3474
+ * which is why it is a second code and not a widening of the first.** That report
3475
+ * asks whether this codec's vocabulary prefers the reading taken *at* the contested
3476
+ * position. It says nothing about what the reading taken makes of the bytes *after*
3477
+ * the boundary, and that is where the two alignments really diverge: they resume
3478
+ * one character apart, so they disagree about the whole tail. Where the escape
3479
+ * character the reading taken resumes on heads no sequence at all, the boundary was
3480
+ * bought with a byte this reading cannot read, while the competing alignment is
3481
+ * precisely the reading that can. On the **field** separator that gained boundary
3482
+ * shifts every later field one place, so a result record's units and **result
3483
+ * status** are read out of slots the competing alignment does not put them in, and
3484
+ * a status of `final` can be a consequence of the alignment rather than something
3485
+ * the sender wrote there. That is `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`; see
3486
+ * {@link ShiftedFieldsSink}, including the tail and the two other delimiter roles it
3487
+ * deliberately stays silent on, and why neither silence is a claim that nothing was
3488
+ * lost there.
3054
3489
  *
3055
3490
  * Re-escaping (the inverse, for spec-clean emit) lives in the serializer and is
3056
3491
  * deliberately not implemented here.
@@ -3070,6 +3505,130 @@ type UnknownEscapeSink = () => void;
3070
3505
  * `ASTM_UNPAIRED_ESCAPE_CHARACTER` warning. Optional so the codec can be used purely.
3071
3506
  */
3072
3507
  type UnpairedEscapeSink = () => void;
3508
+ /**
3509
+ * A callback the codec calls when an **unrecognized** escape body is itself one of
3510
+ * the three splitting delimiters in force (field, repeat, component), so the atom
3511
+ * rule kept that character out of the split and a boundary the sender's bytes
3512
+ * carried never became one. The value is preserved verbatim either way; the
3513
+ * callback lets the parser surface a value-free
3514
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` warning. Optional so the codec can
3515
+ * be used purely.
3516
+ *
3517
+ * The **escape** character is deliberately not in that test: it is not a splitting
3518
+ * role, so `&&&` under the canonical set loses no boundary. A body that is a
3519
+ * *recognized* mnemonic is not in it either, whatever character it is: `&F&` under
3520
+ * a set naming `F` as the repeat delimiter is the escaped field delimiter the
3521
+ * sender wrote, and reading it as a swallowed repeat boundary would report the
3522
+ * escape mechanism working as a defect.
3523
+ */
3524
+ type SwallowedDelimiterSink = () => void;
3525
+ /**
3526
+ * A callback the split calls when the escape character that **closed** an
3527
+ * unrecognized escape sequence could instead have **opened** one whose body is the
3528
+ * delimiter being split on, so the two alignments of the same bytes disagree about
3529
+ * whether that delimiter ends a field, repeat or component. The leftmost reading is
3530
+ * kept and nothing is re-split; the callback lets the parser surface a value-free
3531
+ * `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` warning. Optional so the split can be
3532
+ * used purely.
3533
+ *
3534
+ * Two exclusions, both deliberate. The first is what keeps this off conformant
3535
+ * streams, and it is **wider than the case it is justified on**, so the residue is
3536
+ * named here rather than left to be found:
3537
+ *
3538
+ * - **The earlier sequence's body must not be a recognized mnemonic.** The test is
3539
+ * which alignment this codec's own vocabulary supports, not which one is tidier.
3540
+ * Where the earlier body is unrecognized the reading taken rests on a triple the
3541
+ * codec cannot interpret, while the competitor's body is the delimiter character,
3542
+ * which it usually cannot interpret either: nothing prefers one, and that is what
3543
+ * this reports. Where the earlier body is a recognized mnemonic the reading taken
3544
+ * interprets a construct (`&F&` is the sender escaping a field separator, which is
3545
+ * what the mechanism is for) and the competitor **usually** interprets none, so the
3546
+ * vocabulary usually prefers one, and reporting it would report the escape
3547
+ * mechanism working. Usually, not always: see the second residue below.
3548
+ * **What that argument does not cover, measured.** It does not follow that the
3549
+ * reading taken is *conformant*: under `28.6&F&|&U/L` it is `&F&`, a real
3550
+ * separator, and then a bare escape character, which this package reports as a
3551
+ * deviation of its own. And where the declared set names a mnemonic letter as a
3552
+ * splitting delimiter, both alignments interpret exactly one construct and
3553
+ * neither is preferred, yet the exclusion still silences it. Both residues are
3554
+ * recorded with their measurements rather than closed by widening this test: the
3555
+ * criterion that would cover them is a different one (counting what each
3556
+ * alignment interprets), and swapping criteria moves which streams a published
3557
+ * package refuses.
3558
+ * - **The following character must be the delimiter this split is taken on.** The
3559
+ * split runs once per role, so a character that is a delimiter in a *later* role
3560
+ * is reported by that role's pass, on the segment it survives into, and never
3561
+ * twice. A delimiter with no escape character two positions past it is excluded by
3562
+ * the rule that defines the condition rather than by a judgement: the sequence the
3563
+ * competing alignment would need never closes, so there is no competitor.
3564
+ *
3565
+ * @param segmentIndex - The 0-based index of the segment being accumulated when the
3566
+ * ambiguity was seen, so a caller splitting a record into fields can report which
3567
+ * field it sat in. A caller splitting one field into repeats or components
3568
+ * already knows the field index and ignores this.
3569
+ */
3570
+ type AmbiguousAlignmentSink = (segmentIndex: number) => void;
3571
+ /**
3572
+ * A callback the split calls when a competing escape alignment decided a boundary
3573
+ * **and the reading taken cannot read the byte immediately past it**: the escape
3574
+ * character the leftmost reading resumes on heads no escape sequence at all, so it
3575
+ * is kept as a bare literal (reported separately as
3576
+ * `ASTM_UNPAIRED_ESCAPE_CHARACTER`), while the competing alignment is exactly the
3577
+ * reading that gives that character a job, as the close of its own triple.
3578
+ *
3579
+ * Wired **only to the split taken on the field separator**, because that is the
3580
+ * whole of its claim: a gained *field* boundary shifts every later field one place,
3581
+ * so a record's modeled slots after it are decided by the alignment rather than by
3582
+ * the sender's own positions. On a result record that is the units slot and the
3583
+ * **result status** slot: the sender's trailing letter lands in field 9 under the
3584
+ * reading taken and in no field at all under the competing one, so a status of
3585
+ * `final` can be a consequence of the alignment rather than something the sender
3586
+ * put there. A gained *repeat* or *component* boundary divides one field and
3587
+ * reaches nothing outside it, so it moves no **field-indexed** slot and is
3588
+ * deliberately outside this. **That bound is a choice, not a consequence, and the
3589
+ * difference matters**: components are modeled *inside* a field (a Universal Test
3590
+ * ID's coding scheme and local code, a patient name's parts), so a gained component
3591
+ * boundary does move a modeled slot. That is measured, unreported by anything, and
3592
+ * open, not something this sink covers. The callback lets the parser surface a
3593
+ * value-free `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` warning. Optional so the split
3594
+ * can be used purely.
3595
+ *
3596
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is
3597
+ * identical, and the status read is the status that was always read. What is new is
3598
+ * that the shift is reported by a code no profile may tolerate, where before it was
3599
+ * covered only by tolerable ones.
3600
+ *
3601
+ * **This is independent of {@link AmbiguousAlignmentSink}, and fires alongside it
3602
+ * rather than instead of it.** That one asks whether the codec's vocabulary prefers
3603
+ * the reading taken *at* the contested position and is silent where the earlier
3604
+ * body is a recognized mnemonic; this one asks what the reading taken makes of the
3605
+ * bytes *after* the boundary and does not consult the earlier body at all. The two
3606
+ * questions are different, so neither test is widened to answer the other.
3607
+ *
3608
+ * **The tail is weighed one construct deep, and that bound is stated rather than
3609
+ * left to be found.** Two tails are excluded, with the same reason: the bytes past
3610
+ * the boundary prefer the reading taken.
3611
+ *
3612
+ * - **The escape character heads a sequence this codec recognizes.** Then the
3613
+ * reading taken interprets it and leaves nothing bare, while the competing
3614
+ * alignment would leave it bare. Under a set naming the field separator `F`,
3615
+ * `28.6&F&F&F&U/L` is the sender escaping that separator, writing it, and
3616
+ * escaping it again: entirely well formed, and refusing it is the over-refusal
3617
+ * that sank the preceding candidate criterion for this family.
3618
+ * - **The escape character heads a sequence whose body this codec does not
3619
+ * recognize.** Then the reading taken still consumes it as a sequence and carries
3620
+ * one unreadable body, while the competing alignment would leave *two* escape
3621
+ * characters bare. The preference is stronger there, not weaker.
3622
+ *
3623
+ * What that bound leaves open is the second of those tails, where the field shift
3624
+ * is real and this stays silent. It is measured and named rather than closed by
3625
+ * widening the test, because widening it would report a boundary the bytes prefer.
3626
+ *
3627
+ * @param segmentIndex - The 0-based index of the field being accumulated when the
3628
+ * shifting boundary was taken. Every field after it is one place further right
3629
+ * than the competing alignment puts it.
3630
+ */
3631
+ type ShiftedFieldsSink = (segmentIndex: number) => void;
3073
3632
  /**
3074
3633
  * Decode the four recognized ASTM escape mnemonics in a single already-split
3075
3634
  * leaf (a component string), substituting the *active* delimiters read from the
@@ -3084,6 +3643,8 @@ type UnpairedEscapeSink = () => void;
3084
3643
  * @param d - The delimiters resolved from the header.
3085
3644
  * @param onUnknown - Called once per unrecognized escape body encountered.
3086
3645
  * @param onUnpaired - Called once per escape character that heads no sequence.
3646
+ * @param onSwallowedDelimiter - Called once per unrecognized escape body that is
3647
+ * itself a splitting delimiter in force, so that delimiter never split.
3087
3648
  * @returns The decoded string.
3088
3649
  * @example
3089
3650
  * ```ts
@@ -3092,7 +3653,7 @@ type UnpairedEscapeSink = () => void;
3092
3653
  * decodeEscapes("O&Brien", CANONICAL_DELIMITERS); // "O&Brien" (literal, reported)
3093
3654
  * ```
3094
3655
  */
3095
- declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownEscapeSink, onUnpaired?: UnpairedEscapeSink): string;
3656
+ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownEscapeSink, onUnpaired?: UnpairedEscapeSink, onSwallowedDelimiter?: SwallowedDelimiterSink): string;
3096
3657
  /**
3097
3658
  * Split `text` on `delimiter`, treating an escape sequence (escape, one body
3098
3659
  * character, escape) as an opaque atom so a delimiter that appears *inside* an
@@ -3110,11 +3671,33 @@ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownE
3110
3671
  * closes is what used to merge the whole remainder of a record into one field.
3111
3672
  * Decoding the resulting leaf is what reports it, so this function stays a pure
3112
3673
  * split. Note the two rules together: a delimiter after such a character does
3113
- * split, and a delimiter sitting inside a real three-character atom does not.
3674
+ * split, and a delimiter sitting inside a real three-character atom does not. The
3675
+ * second of those is reported, from the decode step rather than from here,
3676
+ * whenever the atom's body was not a recognized mnemonic.
3677
+ *
3678
+ * **Atoms are matched greedily, leftmost first, and where that choice decides a
3679
+ * boundary it is reported from here.** The escape character closing one triple
3680
+ * cannot also open the next, so `28.6&Z&|&U/L` is read as the atom `&Z&`, then a
3681
+ * field separator that splits, and not as `&Z`, then the atom `&|&` whose field
3682
+ * separator would not have split. Both alignments are in the bytes and they
3683
+ * disagree by one boundary. The reading is **not** changed (that would only pick
3684
+ * the other alignment, with no more evidence for it); it is reported through
3685
+ * `onAmbiguousAlignment`, and only where the earlier body was unrecognized, since
3686
+ * a recognized mnemonic is a construct this codec can interpret and the
3687
+ * competitor's body usually is not. That exclusion is wider than that argument:
3688
+ * see {@link AmbiguousAlignmentSink}.
3114
3689
  *
3115
3690
  * @param text - The field or repeat string to split.
3116
3691
  * @param delimiter - The delimiter to split on.
3117
3692
  * @param escape - The active escape character.
3693
+ * @param onAmbiguousAlignment - Called once per unrecognized escape sequence whose
3694
+ * closing escape character could instead have opened a sequence holding this
3695
+ * delimiter, so the boundary taken here is not the only reading of the bytes.
3696
+ * @param onShiftedFields - Called once per contested boundary where the escape
3697
+ * character this reading resumes on heads no sequence of its own, so the boundary
3698
+ * was bought with a byte the reading cannot read. Wired only by the split taken on
3699
+ * the field separator: see {@link ShiftedFieldsSink} for why, and for the tail it
3700
+ * deliberately does not report.
3118
3701
  * @returns The raw segments, in order.
3119
3702
  * @example
3120
3703
  * ```ts
@@ -3124,7 +3707,7 @@ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownE
3124
3707
  * splitEscapeAware("O&Brien^John", "^", "&"); // ["O&Brien", "John"] (unpaired: literal)
3125
3708
  * ```
3126
3709
  */
3127
- declare function splitEscapeAware(text: string, delimiter: string, escape: string): string[];
3710
+ declare function splitEscapeAware(text: string, delimiter: string, escape: string, onAmbiguousAlignment?: AmbiguousAlignmentSink, onShiftedFields?: ShiftedFieldsSink): string[];
3128
3711
 
3129
3712
  /**
3130
3713
  * Deep-freeze helper for the immutable ASTM model.
@@ -3457,9 +4040,60 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3457
4040
  * field separator was absent outright, so it is a reader of record structure that
3458
4041
  * cannot be relied on as a sweep.
3459
4042
  *
4043
+ * **A fourth and a fifth reader of record structure landed together, and the
4044
+ * allow-list was re-derived against them.** `ASTM_RECORD_DELIMITER_ROLE_COLLISION`
4045
+ * reports a header declaring one character in two of the repeat / component /
4046
+ * escape roles, so the boundary between those two roles is not in the bytes;
4047
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` reports an unrecognized escape body
4048
+ * that is itself a splitting delimiter, so the atom rule kept it out of the split.
4049
+ * Both are safety-critical by construction, because that side is computed rather
4050
+ * than listed. What they required was re-reading the survivors below against them,
4051
+ * and two of the four needed their arguments rewritten rather than merely
4052
+ * re-checked: each was, on its own, the *only* warning a stream exhibiting one of
4053
+ * these conditions raised. That is the state this file exists to prevent, and it is
4054
+ * why the remedy in both cases was a **second, narrower, non-tolerable code**
4055
+ * rather than striking the tolerable one off. Striking it off would have changed
4056
+ * behavior for every profile naming it while still leaving the two conditions
4057
+ * reported by codes that also fire on cases costing nothing.
4058
+ *
4059
+ * **A sixth reader landed with `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, and it is
4060
+ * the mirror of the fifth.** That code reports that the escape character closing an
4061
+ * unrecognized sequence could instead have opened one holding the delimiter that
4062
+ * split, so the bytes carry two alignments disagreeing by one boundary and the
4063
+ * leftmost was taken. It is safety-critical by construction, and the direction is
4064
+ * why it could not be left to the codes below: the fifth reports a boundary the
4065
+ * reading **lost**, this one a boundary the reading may have **gained**, and a
4066
+ * gained boundary hands back a value the sender's bytes do not unambiguously carry.
4067
+ * Both of the codes that condition previously raised are on the list below, so a
4068
+ * profile naming them left `{ strict: true }` accepting it. Re-reading the survivors
4069
+ * against it changed no admission and rewrote no argument: none of the four is a
4070
+ * statement about where a boundary was taken, and the new code is not tolerable, so
4071
+ * the same "a profile re-badges the code it names and no other" reason already
4072
+ * written under the two escape entries covers it.
4073
+ *
4074
+ * **A further reader landed with `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, and it is
4075
+ * the first one that reaches a MODELED SLOT rather than a boundary.** It reports
4076
+ * that a contested alignment decided a **field** boundary while the reading taken
4077
+ * resumes on an escape character heading no sequence, so every later field sits one
4078
+ * place further right than the competing alignment puts it. On a result record that
4079
+ * moves the units and the **result status**: the sender's trailing letter is read out
4080
+ * of field 9 under the reading taken and out of no field at all under the other, so a
4081
+ * status of `final` can be a consequence of the alignment rather than something the
4082
+ * sender wrote there. Until it existed the only warning on that stream was the
4083
+ * **tolerable** `ASTM_UNPAIRED_ESCAPE_CHARACTER`, so the widest gate-legal profile
4084
+ * plus `{ strict: true }` accepted a fabricated `final` on a lab result. It is
4085
+ * safety-critical by construction and must never be admitted. Re-reading the
4086
+ * survivors against it moved no admission, and the reason is worth stating because it
4087
+ * is not last time's: `ASTM_UNPAIRED_ESCAPE_CHARACTER` stays on the list because it
4088
+ * remains true and benign of every case that costs nothing (a bare escape character
4089
+ * in a value alters no boundary at all), and the case where it accompanies a shifted
4090
+ * field is now reported by a code no profile may re-badge.
4091
+ *
3460
4092
  * **So the ones that remain are recorded with the reading each one survives**, not
3461
- * merely with the value it preserves. Each was re-derived against both readers of
3462
- * record structure above, plus the single-message / single-patient guards:
4093
+ * merely with the value it preserves. Each was re-derived against **every** reader of
4094
+ * record structure named above (this sentence deliberately names no count: it has
4095
+ * been corrected upward at every slice that landed one), plus the single-message /
4096
+ * single-patient guards:
3463
4097
  *
3464
4098
  * - `ASTM_NONSTANDARD_DELIMITERS`: the header's own declared delimiters are read
3465
4099
  * and honored either way, and this notes only that they differ from the canonical
@@ -3471,29 +4105,70 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3471
4105
  * force. A stream may be wholly non-canonical and split perfectly, and a wholly
3472
4106
  * canonical stream can still carry a record that does not split, so tolerating the
3473
4107
  * first can never quiet the second.
4108
+ *
4109
+ * **Its admission was resting on a set of declarations it does not distinguish,
4110
+ * and no longer is.** A declaration naming one character in two roles is
4111
+ * necessarily non-canonical, so this code fired on it, and until
4112
+ * `ASTM_RECORD_DELIMITER_ROLE_COLLISION` existed it was the *only* warning such a
4113
+ * stream raised: tolerating "the declared set is unusual" therefore quieted
4114
+ * "the declared set cannot express the boundary it was read with". That second
4115
+ * condition now has its own code, which is not tolerable, so what is left here is
4116
+ * the honest statement it always meant to be: the set differs from the canonical
4117
+ * one and was honored. The reason tolerating this one cannot quiet that one is the
4118
+ * profile mechanism and nothing subtler: a profile re-badges the code it names and
4119
+ * no other. It is **not** that the two conditions are independent. They are not:
4120
+ * every colliding set is non-canonical, which is exactly why this bullet needed
4121
+ * rewriting.
3474
4122
  * - `ASTM_UNKNOWN_ESCAPE_SEQUENCE`: an unrecognized escape body is **preserved
3475
4123
  * byte-for-byte** in the decoded value (the escape codec does not guess at one),
3476
- * so the value is identical with or without the profile. No reader sees it
3477
- * either: a decoded field value is never cut back into records, the type
3478
- * letter is read before decoding, and the split reader counts fields, which the
3479
- * escape-aware tokenizer has already finished dividing before any body is decoded.
3480
- *
3481
- * **This entry is on the list and is recorded as questionable, deliberately.**
3482
- * The argument above is about the *decoded value*, and it holds. What it does not
3483
- * cover is that the escape-aware split itself treats an `&X&` triple as opaque, so
3484
- * where `X` is a delimiter that delimiter never became a boundary: the split the
3485
- * argument says has "already finished dividing" divided one time too few, and this
3486
- * code is the only report of it. Measured on the canonical set:
4124
+ * so the value is identical with or without the profile. A decoded field value is
4125
+ * never cut back into records, the type letter is read before decoding, and the
4126
+ * split reader counts fields the escape-aware tokenizer has already finished
4127
+ * dividing, so none of those three sees it.
4128
+ *
4129
+ * **Part 2 acquired a reader here, and it is named rather than left to be found.**
4130
+ * `isSplittingDelimiter` in `../common/escapes.ts` runs **only** on the
4131
+ * unrecognized-body condition this code reports, and decides out of it whether to
4132
+ * raise `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`. That is a reader of the
4133
+ * reported condition, which is exactly what part 2 asks about, so this entry no
4134
+ * longer survives on "nothing reads it". It survives on a narrower and checkable
4135
+ * claim: a profile tolerating a code re-badges **that code and no other**, so
4136
+ * tolerating this one cannot quiet what the reader raises, and what it raises is
4137
+ * not tolerable in any case. If a future reader of this same condition ever
4138
+ * produces something a profile **can** tolerate, this entry stops being admissible
4139
+ * and comes off the list.
4140
+ *
4141
+ * **A second reader of the same condition landed, and it is named here for the
4142
+ * same reason.** The mnemonic test in `../common/escapes.ts` is now asked twice:
4143
+ * once by the decoder, to decide whether a body is recognized, and once by the
4144
+ * escape-aware split, which raises `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` only
4145
+ * where an **unrecognized** body sits in a sequence a competing alignment would
4146
+ * have aligned differently. That is the condition this code reports, read a second
4147
+ * time, and this entry survives it on the same narrower claim: what that reader
4148
+ * raises is a different code, which a profile cannot name and cannot tolerate.
4149
+ *
4150
+ * **This entry used to be recorded as questionable, and what made it questionable
4151
+ * is now a separate code that is not on this list.** The argument above is about
4152
+ * the *decoded value*, and it holds. What it did not cover is that the
4153
+ * escape-aware split itself treats an `&X&` triple as opaque, so where `X` is a
4154
+ * delimiter that delimiter never became a boundary: the split the argument says
4155
+ * has "already finished dividing" divided one time too few, and this code was the
4156
+ * only report of it. Measured on the canonical set:
3487
4157
  * `R|1|^^^687|28.6&|&U/L||||F` yields a value of `28.6&|&U/L`, no units, and status
3488
- * `unspecified` rather than `final`, with this as the sole warning, so a profile
3489
- * tolerating it (the shipped `referenceCorpus` does) lets `{ strict: true }` accept
3490
- * it. That fails part 1 of the two-clause test on the reading, not on the value.
3491
- * It is **left on the list on purpose**: removing it would change behavior for
3492
- * every profile naming it, and what should report a swallowed boundary instead, and
3493
- * at what severity, is a question this file cannot settle on its own. Do not read
3494
- * this bullet as an endorsement; read it as the open question it is, and do not
3495
- * close it by narrowing the atom, which is what keeps `&F&` one token under a
3496
- * delimiter set that names `F` as a delimiter.
4158
+ * `unspecified` rather than `final`, and this was the sole warning, so a profile
4159
+ * tolerating it (the shipped `referenceCorpus` does) let `{ strict: true }` accept
4160
+ * it. That failed part 1 of the two-clause test on the reading, not on the value.
4161
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` now fires **alongside** this code on
4162
+ * exactly that subset and is safety-critical, so the lost boundary is reported by
4163
+ * something no profile may tolerate and that same stream is refused under
4164
+ * `{ strict: true }`. This code was **not** removed, for two reasons: removing it
4165
+ * changes behavior for every profile naming it, and it remains true and benign of
4166
+ * every unrecognized body that is not a delimiter in force, where the sequence is
4167
+ * preserved byte-for-byte and no boundary is lost. What is scoped away here is a
4168
+ * claim, not a guard. Do not close the underlying condition by narrowing the atom,
4169
+ * which is what keeps `&F&` one token under a delimiter set that names `F` as a
4170
+ * delimiter, and do not read the new code as a repair: the value is byte-identical
4171
+ * to what it was, and only the reporting changed.
3497
4172
  * - `ASTM_UNPAIRED_ESCAPE_CHARACTER`: an escape character heading no escape
3498
4173
  * sequence, read as the **literal character it is** and kept byte-for-byte in the
3499
4174
  * decoded value, so the value is identical with or without the profile. Note what
@@ -3506,6 +4181,14 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3506
4181
  * replaced, which **would** have failed part 1: reading the same character as an
3507
4182
  * unterminated sequence merged every field after it into one, and a code reporting
3508
4183
  * that would have been a report of lost values.
4184
+ *
4185
+ * **The alignment reader is not a reader of this condition, and the distinction is
4186
+ * worth stating rather than leaving to be checked.** It asks whether the character
4187
+ * two positions past a sequence *is* the escape character, never whether that
4188
+ * character heads a sequence of its own, which is the question this code answers.
4189
+ * The two are independent in both directions, measured: `28.6&Z&|&U/L` raises this
4190
+ * code and the alignment code together, and `28.6&Z&|&U&L` raises the alignment
4191
+ * code with no unpaired character anywhere in it.
3509
4192
  * - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`: a request-information status carried
3510
4193
  * verbatim as a leaf field on a `Q` record; the code set is paywalled and is not
3511
4194
  * interpreted, profile or not. Classification reads whether a `Q` record is
@@ -3517,8 +4200,11 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3517
4200
  * undefined abnormal flag or result status, an unparseable reference range, absent
3518
4201
  * units, a mis-attached comment, a partial timestamp, a query-vs-result ambiguity,
3519
4202
  * an unrecognized record type, a record the delimiters in force could not split, a
3520
- * bad frame checksum / sequence gap / unterminated / oversize frame, an ambiguous
3521
- * transport, an unexpected protocol event, or a rejected frame: is forbidden.
4203
+ * declaration naming one character in two roles, a delimiter an unrecognized escape
4204
+ * sequence kept out of the split, a boundary a competing escape alignment disagrees
4205
+ * about, a bad frame checksum / sequence gap / unterminated
4206
+ * / oversize frame, an ambiguous transport, an unexpected protocol event, or a
4207
+ * rejected frame: is forbidden.
3522
4208
  *
3523
4209
  * **What this file cannot do for you.** Part 2 is a review obligation, not a
3524
4210
  * mechanical one. There is no honest automatic check for "nothing load-bearing
@@ -3549,6 +4235,20 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3549
4235
  * reports a record the delimiters in force could not split, so every modeled field
3550
4236
  * of that record is missing: it fails the first half of the test outright.
3551
4237
  *
4238
+ * `ASTM_RECORD_DELIMITER_ROLE_COLLISION`,
4239
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` and
4240
+ * `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` are not on this list and must not be
4241
+ * added to it. Each reports a boundary the reading cannot defend from the bytes (the
4242
+ * first two a boundary that is not in the reading, the third one that may not be in
4243
+ * the bytes), and each exists precisely because the only warnings its condition
4244
+ * previously raised were among the four below.
4245
+ *
4246
+ * `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` is not on this list and must not be added
4247
+ * to it either, and it is the strongest case of the four: it reports not merely a
4248
+ * boundary but a modeled slot changing hands, up to and including a result status
4249
+ * reading `final` that the competing alignment of the same bytes puts in no field at
4250
+ * all.
4251
+ *
3552
4252
  * @example
3553
4253
  * ```ts
3554
4254
  * import { TOLERABLE_CODES } from "@cosyte/astm";
@@ -3653,7 +4353,10 @@ declare const defaultProfile: AstmProfile;
3653
4353
  * only re-badges the *warning*: no value, flag, status, or identifier is altered
3654
4354
  * or dropped. And by the safety gate in `defineAstmProfile`, it **cannot** tolerate
3655
4355
  * a result value / flag / status / range / units, a patient or comment context, a
3656
- * message-kind ambiguity, an unrecognized record type, or any frame / LTP integrity
4356
+ * message-kind ambiguity, an unrecognized record type, a declaration naming one
4357
+ * character in two delimiter roles, a delimiter an unrecognized
4358
+ * escape sequence kept out of the split, a field boundary a competing escape
4359
+ * alignment disagrees about, or any frame / LTP integrity
3657
4360
  * warning: those always surface, profile or not. The unrecognized-record-type case
3658
4361
  * matters because message grouping reads a record's type letter, so a letter the
3659
4362
  * reader does not recognize may be a header, and a header read as something else
@@ -4890,4 +5593,4 @@ declare class AstmFrameStrictError extends Error {
4890
5593
  */
4891
5594
  declare const VERSION: string;
4892
5595
 
4893
- export { ALL_ASTM_WARNING_CODES, AMBIGUOUS_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, type AbnormalFlag, type AbnormalFlagCode, type AbnormalFlagMeaning, type AmbiguousCode, type AnyAstmWarningCode, AstmAmbiguousStreamError, type AstmDate, type AstmDatePrecision, type AstmField, type AstmFrame, AstmFrameEncodeError, type AstmFrameEncodeErrorCode, type AstmFramePosition, AstmFrameStrictError, type AstmFrameWarning, type AstmFraming, type AstmLivdWarning, type AstmLtpWarning, type AstmMessage, type AstmMessageClassification, type AstmMessageKind, AstmParseError, type AstmParseOptions, type AstmPosition, type AstmProfile, AstmProfileDefinitionError, type AstmProfileProvenance, type AstmQuirkMatch, type AstmQuirkTolerance, type AstmRecord, type AstmRecordInput, type AstmRecordWarning, AstmSerializeError, type AstmSerializeErrorCode, type AstmStreamMessage, AstmStrictError, CANONICAL_DELIMITERS, type CommentInput, type CommentRecord, type ComposeFramesOptions, type DecodeAstmFramesResult, type DefineAstmProfileOptions, type Delimiters, type DetectFramingOptions, type DetectFramingResult, FATAL_CODES, FRAME_WARNING_CODES, type FatalCode, type FrameChecksum, type FrameOptions, type FrameTerminator, type FrameWarningCode, type FramedAstmResult, type HeaderInput, type HeaderRecord, LIVD_WARNING_CODES, LTP_WARNING_CODES, type LivdAnnotation, type LivdCatalog, type LivdEntry, type LivdLookup, type LivdMapping, type LivdResult, type LivdWarningCode, type LtpAction, type LtpEvent, type LtpPhase, type LtpState, type LtpTransition, type LtpWarningCode, type ManufacturerRecord, type MessageInput, type OrderInput, type OrderRecord, type PatientInput, type PatientName, type PatientNameInput, type PatientRecord, type QueryInput, type QueryRecord, type ReferenceRange, type ReferenceRangeKind, type ResultInput, type ResultRecord, type ResultStatus, type ResultStatusCode, type ResultStatusMeaning, SAFETY_CRITICAL_CODES, type ScientificRecord, TOLERABLE_CODES, type TerminatorRecord, type UniversalTestId, type UniversalTestIdProvenance, type UnknownEscapeSink, type UnpairedEscapeSink, type UnsupportedRecord, VERSION, type VerbatimInput, WARNING_CODES, type WarningCode, ambiguousMessageKind, ambiguousValueSplit, applyAstmProfile, applyAstmProfileToWarnings, applyLivd, astmDateToLocalISO, astmProfiles, attachComments, buildAstmMessage, classifyMessage, comments, commentsFor, composeAstmFrames, computeChecksum, decodeAstmFrames, decodeEscapes, deepFreeze, defineAstmProfile, defineLivdCatalog, delimitersRedeclared, detectFraming, encodeComponent, fieldScalar, fieldsUnseparated, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, 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, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeHeader, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unpairedEscapeCharacter, unparseableReferenceRange, unreadableRedeclaration };
5596
+ export { ALL_ASTM_WARNING_CODES, AMBIGUOUS_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, type AbnormalFlag, type AbnormalFlagCode, type AbnormalFlagMeaning, type AmbiguousAlignmentSink, type AmbiguousCode, type AnyAstmWarningCode, AstmAmbiguousStreamError, type AstmDate, type AstmDatePrecision, type AstmField, type AstmFrame, AstmFrameEncodeError, type AstmFrameEncodeErrorCode, type AstmFramePosition, AstmFrameStrictError, type AstmFrameWarning, type AstmFraming, type AstmLivdWarning, type AstmLtpWarning, type AstmMessage, type AstmMessageClassification, type AstmMessageKind, AstmParseError, type AstmParseOptions, type AstmPosition, type AstmProfile, AstmProfileDefinitionError, type AstmProfileProvenance, type AstmQuirkMatch, type AstmQuirkTolerance, type AstmRecord, type AstmRecordInput, type AstmRecordWarning, AstmSerializeError, type AstmSerializeErrorCode, type AstmStreamMessage, AstmStrictError, CANONICAL_DELIMITERS, type CommentInput, type CommentRecord, type ComposeFramesOptions, type DecodeAstmFramesResult, type DefineAstmProfileOptions, type DelimiterDeclarationFault, type DelimiterReadResult, type Delimiters, type DetectFramingOptions, type DetectFramingResult, FATAL_CODES, FRAME_WARNING_CODES, type FatalCode, type FrameChecksum, type FrameOptions, type FrameTerminator, type FrameWarningCode, type FramedAstmResult, type HeaderInput, type HeaderRecord, LIVD_WARNING_CODES, LTP_WARNING_CODES, type LivdAnnotation, type LivdCatalog, type LivdEntry, type LivdLookup, type LivdMapping, type LivdResult, type LivdWarningCode, type LtpAction, type LtpEvent, type LtpPhase, type LtpState, type LtpTransition, type LtpWarningCode, type ManufacturerRecord, type MessageInput, type OrderInput, type OrderRecord, type PatientInput, type PatientName, type PatientNameInput, type PatientRecord, type QueryInput, type QueryRecord, type ReferenceRange, type ReferenceRangeKind, type ResultInput, type ResultRecord, type ResultStatus, type ResultStatusCode, type ResultStatusMeaning, SAFETY_CRITICAL_CODES, type ScientificRecord, type ShiftedFieldsSink, type SwallowedDelimiterSink, TOLERABLE_CODES, type TerminatorRecord, type UniversalTestId, type UniversalTestIdProvenance, type UnknownEscapeSink, type UnpairedEscapeSink, type UnsupportedRecord, VERSION, type VerbatimInput, WARNING_CODES, type WarningCode, alignmentShiftedFields, 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 };